mirror of https://github.com/portainer/portainer
feat(password) EE-2690 enforce strong password policy (#6751)
* feat(password) EE-2690 enforce strong password policy * feat(password) EE-2690 disable create user button if password is not valid * feat(password) EE-2690 show force password change warning only when week password is detected * feat(password) EE-2690 prevent users leave account page by clicking add access token button Co-authored-by: Simon Meng <simon.meng@portainer.io>pull/6662/head^2
parent
9ebc963082
commit
85ad4e334a
|
@ -13,6 +13,7 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/passwordutils"
|
||||
)
|
||||
|
||||
type authenticatePayload struct {
|
||||
|
@ -100,7 +101,8 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
|
|||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
forceChangePassword := !passwordutils.StrengthCheck(password)
|
||||
return handler.writeToken(w, user, forceChangePassword)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
|
@ -131,11 +133,11 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
|||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
return handler.writeToken(w, user, false)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
tokenData := composeTokenData(user)
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User, forceChangePassword bool) *httperror.HandlerError {
|
||||
tokenData := composeTokenData(user, forceChangePassword)
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData)
|
||||
}
|
||||
|
@ -206,10 +208,11 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
|
|||
return false
|
||||
}
|
||||
|
||||
func composeTokenData(user *portainer.User) *portainer.TokenData {
|
||||
func composeTokenData(user *portainer.User, forceChangePassword bool) *portainer.TokenData {
|
||||
return &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
ForceChangePassword: forceChangePassword,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,5 +110,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
|||
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
return handler.writeToken(w, user, false)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/passwordutils"
|
||||
)
|
||||
|
||||
type adminInitPayload struct {
|
||||
|
@ -57,6 +58,10 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusConflict, "Unable to create administrator user", errAdminAlreadyInitialized}
|
||||
}
|
||||
|
||||
if !passwordutils.StrengthCheck(payload.Password) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Password does not meet the requirements", nil}
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/passwordutils"
|
||||
)
|
||||
|
||||
type userCreatePayload struct {
|
||||
|
@ -94,6 +95,10 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
if !passwordutils.StrengthCheck(payload.Password) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Password does not meet the requirements", nil}
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/passwordutils"
|
||||
)
|
||||
|
||||
type userUpdatePasswordPayload struct {
|
||||
|
@ -81,6 +82,10 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Specified password do not match actual password", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
if !passwordutils.StrengthCheck(payload.NewPassword) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Password does not meet the requirements", nil}
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.NewPassword)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package passwordutils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const MinPasswordLen = 12
|
||||
|
||||
func lengthCheck(password string) bool {
|
||||
return len(password) >= MinPasswordLen
|
||||
}
|
||||
|
||||
func comboCheck(password string) bool {
|
||||
count := 0
|
||||
regexps := [4]*regexp.Regexp{
|
||||
regexp.MustCompile(`[a-z]`),
|
||||
regexp.MustCompile(`[A-Z]`),
|
||||
regexp.MustCompile(`[0-9]`),
|
||||
regexp.MustCompile(`[\W_]`),
|
||||
}
|
||||
|
||||
for _, re := range regexps {
|
||||
if re.FindString(password) != "" {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
return count >= 3
|
||||
}
|
||||
|
||||
func StrengthCheck(password string) bool {
|
||||
return lengthCheck(password) && comboCheck(password)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package passwordutils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStrengthCheck(t *testing.T) {
|
||||
type args struct {
|
||||
password string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantStrong bool
|
||||
}{
|
||||
{"Empty password", args{""}, false},
|
||||
{"Short password", args{"portainer"}, false},
|
||||
{"Short password", args{"portaienr!@#"}, false},
|
||||
{"Week password", args{"12345678!@#"}, false},
|
||||
{"Week password", args{"portaienr123"}, false},
|
||||
{"Good password", args{"Portainer123"}, true},
|
||||
{"Good password", args{"Portainer___"}, true},
|
||||
{"Good password", args{"^portainer12"}, true},
|
||||
{"Good password", args{"12%PORTAINER"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotStrong := StrengthCheck(tt.args.password); gotStrong != tt.wantStrong {
|
||||
t.Errorf("StrengthCheck() = %v, want %v", gotStrong, tt.wantStrong)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -22,10 +22,11 @@ type Service struct {
|
|||
}
|
||||
|
||||
type claims struct {
|
||||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
Scope scope `json:"scope"`
|
||||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
Scope scope `json:"scope"`
|
||||
ForceChangePassword bool `json:"forceChangePassword"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
|
@ -164,10 +165,11 @@ func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt
|
|||
}
|
||||
|
||||
cl := claims{
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
Scope: scope,
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
Scope: scope,
|
||||
ForceChangePassword: data.ForceChangePassword,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expiresAt,
|
||||
IssuedAt: time.Now().Unix(),
|
||||
|
|
|
@ -1102,9 +1102,10 @@ type (
|
|||
|
||||
// TokenData represents the data embedded in a JWT token
|
||||
TokenData struct {
|
||||
ID UserID
|
||||
Username string
|
||||
Role UserRole
|
||||
ID UserID
|
||||
Username string
|
||||
Role UserRole
|
||||
ForceChangePassword bool
|
||||
}
|
||||
|
||||
// TunnelDetails represents information associated to a tunnel
|
||||
|
|
|
@ -897,6 +897,46 @@ json-tree .branch-preview {
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ml-0 {
|
||||
margin-left: 0rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.ml-6 {
|
||||
margin-left: 1.75rem;
|
||||
}
|
||||
|
||||
.ml-7 {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.ml-8 {
|
||||
margin-left: 2.25rem;
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import { MinPasswordLen } from '../helpers/password';
|
||||
|
||||
function PasswordCombination() {
|
||||
return (
|
||||
<ul className="text-muted">
|
||||
<li className="ml-8"> Special characters </li>
|
||||
<li className="ml-8"> Lower case characters </li>
|
||||
<li className="ml-8"> Upper case characters </li>
|
||||
<li className="ml-8"> Numeric characters </li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function ForcePasswordUpdateHint() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<i
|
||||
className="fa fa-exclamation-triangle orange-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<b> Please update your password to continue </b>
|
||||
</p>
|
||||
|
||||
<p className="text-muted">
|
||||
To ensure the security of your account, please update your password to a
|
||||
stronger password using a combination of at least 3 of the following:
|
||||
</p>
|
||||
|
||||
<PasswordCombination />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PasswordCheckHint() {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-muted">
|
||||
<i className="fa fa-times red-icon space-right" aria-hidden="true">
|
||||
{' '}
|
||||
</i>
|
||||
<span>
|
||||
The password must be at least {MinPasswordLen} characters long,
|
||||
including a combination of one character of three of the below:
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<PasswordCombination />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ForcePasswordUpdateHintAngular = react2angular(
|
||||
ForcePasswordUpdateHint,
|
||||
[]
|
||||
);
|
||||
export const PasswordCheckHintAngular = react2angular(PasswordCheckHint, []);
|
|
@ -1,8 +1,14 @@
|
|||
export default class AccessTokensDatatableController {
|
||||
/* @ngInject*/
|
||||
constructor($scope, $controller, DatatableService) {
|
||||
constructor($scope, $state, $controller, DatatableService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
this.onClickAdd = () => {
|
||||
if (this.uiCanExit()) {
|
||||
$state.go('portainer.account.new-access-token');
|
||||
}
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
>
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.account.new-access-token">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add access token
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.onClickAdd()"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add access token </button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
|
|
|
@ -11,5 +11,6 @@ angular.module('portainer.app').component('accessTokensDatatable', {
|
|||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
removeAction: '<',
|
||||
uiCanExit: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import { ReactExampleAngular } from './ReactExample';
|
|||
import { TooltipAngular } from './Tip/Tooltip';
|
||||
import { beFeatureIndicatorAngular } from './BEFeatureIndicator';
|
||||
import { InformationPanelAngular } from './InformationPanel';
|
||||
import { ForcePasswordUpdateHintAngular, PasswordCheckHintAngular } from './PasswordCheckHint';
|
||||
|
||||
export default angular
|
||||
.module('portainer.app.components', [headerModule, boxSelectorModule, widgetModule, sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule])
|
||||
|
@ -21,4 +22,6 @@ export default angular
|
|||
.component('portainerTooltip', TooltipAngular)
|
||||
.component('reactExample', ReactExampleAngular)
|
||||
.component('beFeatureIndicator', beFeatureIndicatorAngular)
|
||||
.component('forcePasswordUpdateHint', ForcePasswordUpdateHintAngular)
|
||||
.component('passwordCheckHint', PasswordCheckHintAngular)
|
||||
.component('createAccessToken', CreateAccessTokenAngular).name;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
export const MinPasswordLen = 12;
|
||||
|
||||
function lengthCheck(password: string) {
|
||||
return password.length >= MinPasswordLen;
|
||||
}
|
||||
|
||||
function comboCheck(password: string) {
|
||||
let count = 0;
|
||||
const regexps = [/[a-z]/, /[A-Z]/, /[0-9]/, /[\W_]/];
|
||||
|
||||
regexps.forEach((re) => {
|
||||
if (password.match(re) != null) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return count >= 3;
|
||||
}
|
||||
|
||||
export function StrengthCheck(password: string) {
|
||||
return lengthCheck(password) && comboCheck(password);
|
||||
}
|
|
@ -101,6 +101,7 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
user.username = tokenPayload.username;
|
||||
user.ID = tokenPayload.id;
|
||||
user.role = tokenPayload.role;
|
||||
user.forceChangePassword = tokenPayload.forceChangePassword;
|
||||
await setUserTheme();
|
||||
}
|
||||
|
||||
|
|
|
@ -202,3 +202,18 @@ export function confirmChangePassword() {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function confirmForceChangePassword() {
|
||||
const box = bootbox.dialog({
|
||||
message:
|
||||
'Please update your password to a stronger password to continue using Portainer',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'OK',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
applyBoxCSS(box);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
confirmUpdate,
|
||||
confirmWebEditorDiscard,
|
||||
confirm,
|
||||
confirmForceChangePassword,
|
||||
} from './confirm';
|
||||
import {
|
||||
confirmContainerDeletion,
|
||||
|
@ -58,5 +59,6 @@ export function ModalServiceAngular() {
|
|||
selectRegistry,
|
||||
confirmContainerDeletion,
|
||||
confirmKubeconfigSelection,
|
||||
confirmForceChangePassword,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -26,17 +26,12 @@
|
|||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input type="password" class="form-control" ng-model="formValues.newPassword" id="new_password" />
|
||||
<input type="password" class="form-control" ng-model="formValues.newPassword" ng-change="onNewPasswordChange()" id="new_password" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !new-password-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<i ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[formValues.newPassword.length >= 8]" aria-hidden="true"></i>
|
||||
<span class="small text-muted">Your new password must be at least 8 characters long</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- confirm-password-input -->
|
||||
<div class="form-group">
|
||||
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
|
||||
|
@ -61,7 +56,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="(AuthenticationMethod !== 1 && userID !== 1) || !formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword"
|
||||
ng-disabled="(AuthenticationMethod !== 1 && userID !== 1) || !formValues.currentPassword || !passwordStrength || formValues.newPassword !== formValues.confirmPassword"
|
||||
ng-click="updatePassword()"
|
||||
>Update password</button
|
||||
>
|
||||
|
@ -75,6 +70,9 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<force-password-update-hint ng-if="forceChangePassword"></force-password-update-hint>
|
||||
<password-check-hint ng-if="!forceChangePassword && !passwordStrength"></password-check-hint>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
@ -85,6 +83,7 @@
|
|||
table-key="tokens"
|
||||
order-by="Description"
|
||||
remove-action="removeAction"
|
||||
ui-can-exit="uiCanExit"
|
||||
></access-tokens-datatable>
|
||||
<theme-settings></theme-settings>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { MinPasswordLen, StrengthCheck } from 'Portainer/helpers/password';
|
||||
|
||||
angular.module('portainer.app').controller('AccountController', [
|
||||
'$scope',
|
||||
'$state',
|
||||
|
@ -16,12 +18,16 @@ angular.module('portainer.app').controller('AccountController', [
|
|||
userTheme: '',
|
||||
};
|
||||
|
||||
$scope.passwordStrength = false;
|
||||
$scope.MinPasswordLen = MinPasswordLen;
|
||||
|
||||
$scope.updatePassword = async function () {
|
||||
const confirmed = await ModalService.confirmChangePassword();
|
||||
if (confirmed) {
|
||||
try {
|
||||
await UserService.updateUserPassword($scope.userID, $scope.formValues.currentPassword, $scope.formValues.newPassword);
|
||||
Notifications.success('Success', 'Password successfully updated');
|
||||
$scope.forceChangePassword = false;
|
||||
$state.go('portainer.logout');
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, err.msg);
|
||||
|
@ -29,6 +35,21 @@ angular.module('portainer.app').controller('AccountController', [
|
|||
}
|
||||
};
|
||||
|
||||
$scope.onNewPasswordChange = function () {
|
||||
$scope.passwordStrength = StrengthCheck($scope.formValues.newPassword);
|
||||
};
|
||||
|
||||
this.uiCanExit = () => {
|
||||
if ($scope.forceChangePassword) {
|
||||
ModalService.confirmForceChangePassword();
|
||||
}
|
||||
return !$scope.forceChangePassword;
|
||||
};
|
||||
|
||||
$scope.uiCanExit = () => {
|
||||
return this.uiCanExit();
|
||||
};
|
||||
|
||||
$scope.removeAction = (selectedTokens) => {
|
||||
const msg = 'Do you want to remove the selected access token(s)? Any script or application using these tokens will no longer be able to invoke the Portainer API.';
|
||||
|
||||
|
@ -77,6 +98,7 @@ angular.module('portainer.app').controller('AccountController', [
|
|||
|
||||
async function initView() {
|
||||
$scope.userID = Authentication.getUserDetails().ID;
|
||||
$scope.forceChangePassword = Authentication.getUserDetails().forceChangePassword;
|
||||
|
||||
const data = await UserService.user($scope.userID);
|
||||
|
||||
|
|
|
@ -123,6 +123,10 @@ class AuthenticationController {
|
|||
const endpoints = await this.EndpointService.endpoints(0, 1);
|
||||
const isAdmin = this.Authentication.isAdmin();
|
||||
|
||||
if (this.Authentication.getUserDetails().forceChangePassword) {
|
||||
return this.$state.go('portainer.account');
|
||||
}
|
||||
|
||||
if (endpoints.value.length === 0 && isAdmin) {
|
||||
return this.$state.go('portainer.wizard');
|
||||
} else {
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<div class="form-group">
|
||||
<label for="password" class="col-sm-4 control-label text-left">Password</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" auto-focus />
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" ng-change="onPasswordChange()" auto-focus />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !new-password-input -->
|
||||
|
@ -63,11 +63,19 @@
|
|||
<!-- !confirm-password-input -->
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
<i ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[formValues.Password.length >= 8]" aria-hidden="true"></i>
|
||||
The password must be at least 8 characters long
|
||||
</span>
|
||||
<div class="col-sm-12 text-muted" ng-if="!state.passwordStrength">
|
||||
<!-- below code is duplicated with component of <force-password-update-hint> -->
|
||||
<!-- it is a workaround for firefox that does not render component <force-password-update-hint> -->
|
||||
<p>
|
||||
<i class="fa fa-times red-icon space-right" aria-hidden="true"></i>
|
||||
<span>The password must be at least {{ MinPasswordLen }} characters long, including a combination of one character of three of the below:</span>
|
||||
</p>
|
||||
<ul>
|
||||
<li class="ml-8"> Special characters </li>
|
||||
<li class="ml-8"> Lower case characters </li>
|
||||
<li class="ml-8"> Upper case characters </li>
|
||||
<li class="ml-8"> Numeric characters </li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !note -->
|
||||
|
@ -77,7 +85,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || formValues.Password.length < 8 || formValues.Password !== formValues.ConfirmPassword"
|
||||
ng-disabled="state.actionInProgress || !state.passwordStrength || formValues.Password !== formValues.ConfirmPassword"
|
||||
ng-click="createAdminUser()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { MinPasswordLen, StrengthCheck } from 'Portainer/helpers/password';
|
||||
|
||||
angular.module('portainer.app').controller('InitAdminController', [
|
||||
'$scope',
|
||||
'$state',
|
||||
|
@ -25,10 +27,17 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
actionInProgress: false,
|
||||
showInitPassword: true,
|
||||
showRestorePortainer: false,
|
||||
passwordStrength: false,
|
||||
};
|
||||
|
||||
$scope.MinPasswordLen = MinPasswordLen;
|
||||
|
||||
createAdministratorFlow();
|
||||
|
||||
$scope.onPasswordChange = function () {
|
||||
$scope.state.passwordStrength = StrengthCheck($scope.formValues.Password);
|
||||
};
|
||||
|
||||
$scope.togglePanel = function () {
|
||||
$scope.state.showInitPassword = !$scope.state.showInitPassword;
|
||||
$scope.state.showRestorePortainer = !$scope.state.showRestorePortainer;
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" data-cy="user-passwordInput" />
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" ng-change="onPasswordChange()" data-cy="user-passwordInput" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,6 +68,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !confirm-password-input -->
|
||||
|
||||
<!-- password-check-hint -->
|
||||
<div class="form-group" ng-if="AuthenticationMethod === 1 && !state.passwordStrength">
|
||||
<div class="col-sm-3 col-lg-2"></div>
|
||||
<div class="col-sm-8">
|
||||
<password-check-hint></password-check-hint>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! password-check-hint -->
|
||||
|
||||
<!-- admin-checkbox -->
|
||||
<div class="form-group" ng-if="isAdmin">
|
||||
<div class="col-sm-12">
|
||||
|
@ -120,7 +130,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || (AuthenticationMethod === 1 && formValues.Password === '') || (AuthenticationMethod === 1 && formValues.Password !== formValues.ConfirmPassword)"
|
||||
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || (AuthenticationMethod === 1 && !state.passwordStrength) || (AuthenticationMethod === 1 && formValues.Password !== formValues.ConfirmPassword)"
|
||||
ng-click="addUser()"
|
||||
button-spinner="state.actionInProgress"
|
||||
data-cy="user-createUserButton"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
import { StrengthCheck } from 'Portainer/helpers/password';
|
||||
|
||||
angular.module('portainer.app').controller('UsersController', [
|
||||
'$q',
|
||||
|
@ -16,6 +17,7 @@ angular.module('portainer.app').controller('UsersController', [
|
|||
userCreationError: '',
|
||||
validUsername: false,
|
||||
actionInProgress: false,
|
||||
passwordStrength: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
|
@ -26,6 +28,10 @@ angular.module('portainer.app').controller('UsersController', [
|
|||
Teams: [],
|
||||
};
|
||||
|
||||
$scope.onPasswordChange = function () {
|
||||
$scope.state.passwordStrength = StrengthCheck($scope.formValues.Password);
|
||||
};
|
||||
|
||||
$scope.checkUsernameValidity = function () {
|
||||
var valid = true;
|
||||
for (var i = 0; i < $scope.users.length; i++) {
|
||||
|
|
Loading…
Reference in New Issue