consists of a username, a password (stored in encrypted form using ``bcrypt``),
an optional name, and an optional email address.
+If a new user is created via Web UI, it is possible to set an option that this
+user must assign a new password when they log in for the first time.
+
User accounts are stored in MON's configuration database, and are globally
shared across all ceph-mgr instances.
self._delete('/api/user/user1')
self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '0'])
- def test_pwd_update_required(self):
+ def test_pwd_update_required(self):
self._create_user(username='user1',
password='mypassword10#',
name='My Name',
:rtype: str|dict[str, str]
"""
if isinstance(name, dict):
- result = {self._to_native(key): value
- for key, value in name.items()}
+ result = {
+ self._to_native(key): value
+ for key, value in name.items()
+ }
else:
result = self._to_native(name)
@UiApiController('/standard_settings')
class StandardSettings(RESTController):
def list(self):
+ """
+ Get various Dashboard related settings.
+ :return: Returns a dictionary containing various Dashboard
+ settings.
+ :rtype: dict
+ """
return {
- 'user_pwd_expiration_span': SettingsModule.USER_PWD_EXPIRATION_SPAN,
- 'user_pwd_expiration_warning_1': SettingsModule.USER_PWD_EXPIRATION_WARNING_1,
- 'user_pwd_expiration_warning_2': SettingsModule.USER_PWD_EXPIRATION_WARNING_2
+ 'user_pwd_expiration_span':
+ SettingsModule.USER_PWD_EXPIRATION_SPAN,
+ 'user_pwd_expiration_warning_1':
+ SettingsModule.USER_PWD_EXPIRATION_WARNING_1,
+ 'user_pwd_expiration_warning_2':
+ SettingsModule.USER_PWD_EXPIRATION_WARNING_2,
+ 'pwd_policy_enabled':
+ SettingsModule.PWD_POLICY_ENABLED,
+ 'pwd_policy_min_length':
+ SettingsModule.PWD_POLICY_MIN_LENGTH,
+ 'pwd_policy_check_length_enabled':
+ SettingsModule.PWD_POLICY_CHECK_LENGTH_ENABLED,
+ 'pwd_policy_check_oldpwd_enabled':
+ SettingsModule.PWD_POLICY_CHECK_OLDPWD_ENABLED,
+ 'pwd_policy_check_username_enabled':
+ SettingsModule.PWD_POLICY_CHECK_USERNAME_ENABLED,
+ 'pwd_policy_check_exclusion_list_enabled':
+ SettingsModule.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED,
+ 'pwd_policy_check_repetitive_chars_enabled':
+ SettingsModule.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED,
+ 'pwd_policy_check_sequential_chars_enabled':
+ SettingsModule.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED,
+ 'pwd_policy_check_complexity_enabled':
+ SettingsModule.PWD_POLICY_CHECK_COMPLEXITY_ENABLED
}
import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component';
import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component';
import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component';
+import { LoginPasswordFormComponent } from './core/auth/login-password-form/login-password-form.component';
import { LoginComponent } from './core/auth/login/login.component';
import { SsoNotFoundComponent } from './core/auth/sso/sso-not-found/sso-not-found.component';
import { UserPasswordFormComponent } from './core/auth/user-password-form/user-password-form.component';
import { ForbiddenComponent } from './core/forbidden/forbidden.component';
import { BlankLayoutComponent } from './core/layouts/blank-layout/blank-layout.component';
+import { LoginLayoutComponent } from './core/layouts/login-layout/login-layout.component';
import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component';
import { NotFoundComponent } from './core/not-found/not-found.component';
import { ActionLabels, URLVerbs } from './shared/constants/app.constants';
import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs';
import { AuthGuardService } from './shared/services/auth-guard.service';
+import { ChangePasswordGuardService } from './shared/services/change-password-guard.service';
import { FeatureTogglesGuardService } from './shared/services/feature-toggles-guard.service';
import { ModuleStatusGuardService } from './shared/services/module-status-guard.service';
import { NoSsoGuardService } from './shared/services/no-sso-guard.service';
{
path: '',
component: WorkbenchLayoutComponent,
- canActivate: [AuthGuardService],
- canActivateChild: [AuthGuardService],
+ canActivate: [AuthGuardService, ChangePasswordGuardService],
+ canActivateChild: [AuthGuardService, ChangePasswordGuardService],
children: [
{ path: 'dashboard', component: DashboardComponent },
// Cluster
}
]
},
+ {
+ path: '',
+ component: LoginLayoutComponent,
+ children: [
+ { path: 'login', component: LoginComponent },
+ {
+ path: 'login-change-password',
+ component: LoginPasswordFormComponent,
+ canActivate: [NoSsoGuardService]
+ },
+ { path: 'logout', children: [] }
+ ]
+ },
{
path: '',
component: BlankLayoutComponent,
// Single Sign-On (SSO)
{ path: 'sso/404', component: SsoNotFoundComponent },
// System
- { path: 'login', component: LoginComponent },
- { path: 'logout', children: [] },
{ path: '403', component: ForbiddenComponent },
{ path: '404', component: NotFoundComponent },
{ path: '**', redirectTo: '/404' }
import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
import { ButtonsModule } from 'ngx-bootstrap/buttons';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
-import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { ActionLabels, URLVerbs } from '../../shared/constants/app.constants';
import { SharedModule } from '../../shared/shared.module';
+import { LoginPasswordFormComponent } from './login-password-form/login-password-form.component';
import { LoginComponent } from './login/login.component';
import { RoleDetailsComponent } from './role-details/role-details.component';
import { RoleFormComponent } from './role-form/role-form.component';
import { UserFormComponent } from './user-form/user-form.component';
import { UserListComponent } from './user-list/user-list.component';
import { UserPasswordFormComponent } from './user-password-form/user-password-form.component';
-import { UserPasswordLoginFormComponent } from './user-password-login-form/user-password-login-form.component';
import { UserTabsComponent } from './user-tabs/user-tabs.component';
@NgModule({
imports: [
- BsDropdownModule.forRoot(),
ButtonsModule.forRoot(),
CommonModule,
FormsModule,
],
declarations: [
LoginComponent,
+ LoginPasswordFormComponent,
RoleDetailsComponent,
RoleFormComponent,
RoleListComponent,
UserTabsComponent,
UserListComponent,
UserFormComponent,
- UserPasswordFormComponent,
- UserPasswordLoginFormComponent
+ UserPasswordFormComponent
]
})
export class AuthModule {}
--- /dev/null
+<div>
+ <h1 i18n>Please set a new password.</h1>
+ <h4 i18n>You will be redirected to the login page afterwards.</h4>
+ <form #frm="ngForm"
+ [formGroup]="userForm"
+ novalidate>
+
+ <!-- Old password -->
+ <div class="form-group has-feedback">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="Old password..."
+ id="oldpassword"
+ formControlName="oldpassword"
+ autocomplete="new-password"
+ autofocus>
+ <span class="input-group-append">
+ <button class="btn btn-outline-light btn-password"
+ cdPasswordButton="oldpassword">
+ </button>
+ </span>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('oldpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('oldpassword', frm, 'notmatch')"
+ i18n>The old and new passwords must be different.</span>
+ </div>
+
+ <!-- New password -->
+ <div class="form-group has-feedback">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ placeholder="New password..."
+ id="newpassword"
+ autocomplete="new-password"
+ formControlName="newpassword">
+ <span class="input-group-append">
+ <button type="button"
+ class="btn btn-outline-light btn-password"
+ cdPasswordButton="newpassword">
+ </button>
+ </span>
+ </div>
+ <div class="password-strength-level">
+ <div class="{{ passwordStrengthLevelClass }}"
+ data-toggle="tooltip"
+ title="{{ passwordValuation }}">
+ </div>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'notmatch')"
+ i18n>The old and new passwords must be different.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('newpassword', frm, 'passwordPolicy')">
+ {{ passwordValuation }}
+ </span>
+ </div>
+
+ <!-- Confirm new password -->
+ <div class="form-group has-feedback">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ autocomplete="new-password"
+ placeholder="Confirm new password..."
+ id="confirmnewpassword"
+ formControlName="confirmnewpassword">
+ <span class="input-group-append">
+ <button class="btn btn-outline-light btn-password"
+ cdPasswordButton="confirmnewpassword">
+ </button>
+ </span>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmnewpassword', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmnewpassword', frm, 'match')"
+ i18n>Password confirmation doesn't match the new password.</span>
+ </div>
+ </form>
+ <div class="form-footer">
+ <cd-submit-button class="full-width"
+ btnClass="btn-block"
+ (submitAction)="onSubmit()"
+ [form]="userForm"
+ i18n="form action button|Example: Create Pool@@formActionButton">
+ {{ action | titlecase }} {{ resource | upperFirst }}
+ </cd-submit-button>
+ <button class="btn btn-light"
+ (click)="onCancel()">
+ <ng-container i18n>Cancel</ng-container>
+ </button>
+ </div>
+</div>
--- /dev/null
+@import 'defaults';
+
+::ng-deep cd-login-password-form {
+ h4 {
+ margin: 0 0 30px 0;
+ }
+
+ .btn-password,
+ .btn-password:focus,
+ .form-control,
+ .form-control:focus {
+ color: $color-password-toggle-text;
+ background-color: $color-password-toggle-bg;
+ }
+
+ .form-control::placeholder {
+ color: $color-password-toggle-placeholder-text;
+ }
+
+ .btn-password:focus {
+ outline-color: $color-password-toggle-focus;
+ }
+
+ button.btn:not(:first-child) {
+ margin-left: 5px;
+ }
+}
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { configureTestBed, FormHelper, i18nProviders } from '../../../../testing/unit-test-helper';
+import { AuthService } from '../../../shared/api/auth.service';
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { LoginPasswordFormComponent } from './login-password-form.component';
+
+describe('LoginPasswordFormComponent', () => {
+ let component: LoginPasswordFormComponent;
+ let fixture: ComponentFixture<LoginPasswordFormComponent>;
+ let form: CdFormGroup;
+ let formHelper: FormHelper;
+ let httpTesting: HttpTestingController;
+ let router: Router;
+ let authStorageService: AuthStorageService;
+ let authService: AuthService;
+
+ configureTestBed(
+ {
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ComponentsModule,
+ ToastrModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [LoginPasswordFormComponent],
+ providers: i18nProviders
+ },
+ true
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginPasswordFormComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.get(HttpTestingController);
+ router = TestBed.get(Router);
+ authStorageService = TestBed.get(AuthStorageService);
+ authService = TestBed.get(AuthService);
+ spyOn(router, 'navigate');
+ fixture.detectChanges();
+ form = component.userForm;
+ formHelper = new FormHelper(form);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should submit', () => {
+ spyOn(component, 'onPasswordChange').and.callThrough();
+ spyOn(authService, 'logout');
+ spyOn(authStorageService, 'getUsername').and.returnValue('test1');
+ formHelper.setMultipleValues({
+ oldpassword: 'foo',
+ newpassword: 'bar'
+ });
+ formHelper.setValue('confirmnewpassword', 'bar', true);
+ component.onSubmit();
+ const request = httpTesting.expectOne('api/user/test1/change_password');
+ request.flush({});
+ expect(component.onPasswordChange).toHaveBeenCalled();
+ expect(authService.logout).toHaveBeenCalled();
+ });
+
+ it('should cancel', () => {
+ spyOn(authService, 'logout');
+ component.onCancel();
+ expect(authService.logout).toHaveBeenCalled();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+
+import { AuthService } from '../../../shared/api/auth.service';
+import { UserService } from '../../../shared/api/user.service';
+import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
+import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { NotificationService } from '../../../shared/services/notification.service';
+import { PasswordPolicyService } from '../../../shared/services/password-policy.service';
+import { UserPasswordFormComponent } from '../user-password-form/user-password-form.component';
+
+@Component({
+ selector: 'cd-login-password-form',
+ templateUrl: './login-password-form.component.html',
+ styleUrls: ['./login-password-form.component.scss']
+})
+export class LoginPasswordFormComponent extends UserPasswordFormComponent {
+ constructor(
+ public i18n: I18n,
+ public actionLabels: ActionLabelsI18n,
+ public notificationService: NotificationService,
+ public userService: UserService,
+ public authStorageService: AuthStorageService,
+ public formBuilder: CdFormBuilder,
+ public router: Router,
+ public passwordPolicyService: PasswordPolicyService,
+ public authService: AuthService
+ ) {
+ super(
+ i18n,
+ actionLabels,
+ notificationService,
+ userService,
+ authStorageService,
+ formBuilder,
+ router,
+ passwordPolicyService
+ );
+ }
+
+ onPasswordChange() {
+ // Logout here because changing the password will change the
+ // session token which will finally lead to a 401 when calling
+ // the REST API the next time. The API HTTP inteceptor will
+ // then also redirect to the login page immediately.
+ this.authService.logout();
+ }
+
+ onCancel() {
+ this.authService.logout();
+ }
+}
-<div class="login"
- *ngIf="isLoginActive || pwdUpdateRequired">
- <header>
- <nav class="navbar">
- <a class="navbar-brand"></a>
- <div class="form-inline">
- <cd-language-selector></cd-language-selector>
- </div>
- </nav>
- </header>
-
-
- <section>
- <div class="container">
- <div class="row full-height vertical-align">
- <div class="col-sm-6 d-none d-sm-block">
- <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
- alt="Ceph"
- class="float-right img-fluid">
- </div>
-
- <div class="col-12 col-sm-6 col-xl-5">
- <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
- <form name="loginForm"
- (ngSubmit)="login()"
- #loginForm="ngForm"
- *ngIf="!pwdUpdateRequired"
- novalidate>
+<div *ngIf="isLoginActive">
+ <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
+ <form name="loginForm"
+ (ngSubmit)="login()"
+ #loginForm="ngForm"
+ novalidate>
- <!-- Username -->
- <div class="form-group has-feedback">
- <input name="username"
- [(ngModel)]="model.username"
- #username="ngModel"
- type="text"
- placeholder="Enter your username..."
- class="form-control"
- required
- autofocus>
- <div class="invalid-feedback"
- *ngIf="(loginForm.submitted || username.dirty) && username.invalid"
- i18n>Username is required</div>
- </div>
-
- <!-- Password -->
- <div class="form-group has-feedback">
- <div class="input-group">
- <input id="password"
- name="password"
- [(ngModel)]="model.password"
- #password="ngModel"
- type="password"
- placeholder="Enter your password..."
- class="form-control"
- required>
- <span class="input-group-append">
- <button type="button"
- class="btn btn-outline-light btn-password"
- cdPasswordButton="password">
- </button>
- </span>
- </div>
- <div class="invalid-feedback"
- *ngIf="(loginForm.submitted || password.dirty) && password.invalid"
- i18n>Password is required</div>
- </div>
+ <!-- Username -->
+ <div class="form-group has-feedback">
+ <input name="username"
+ [(ngModel)]="model.username"
+ #username="ngModel"
+ type="text"
+ placeholder="Enter your username..."
+ class="form-control"
+ required
+ autofocus>
+ <div class="invalid-feedback"
+ *ngIf="(loginForm.submitted || username.dirty) && username.invalid"
+ i18n>Username is required</div>
+ </div>
- <input type="submit"
- class="btn btn-secondary btn-block"
- [disabled]="loginForm.invalid"
- value="Login"
- i18n-value>
- </form>
- <cd-user-password-login-form *ngIf="pwdUpdateRequired"></cd-user-password-login-form>
- </div>
+ <!-- Password -->
+ <div class="form-group has-feedback">
+ <div class="input-group">
+ <input id="password"
+ name="password"
+ [(ngModel)]="model.password"
+ #password="ngModel"
+ type="password"
+ placeholder="Enter your password..."
+ class="form-control"
+ required>
+ <span class="input-group-append">
+ <button type="button"
+ class="btn btn-outline-light btn-password"
+ cdPasswordButton="password">
+ </button>
+ </span>
</div>
+ <div class="invalid-feedback"
+ *ngIf="(loginForm.submitted || password.dirty) && password.invalid"
+ i18n>Password is required</div>
</div>
- </section>
+
+ <input type="submit"
+ class="btn btn-secondary btn-block"
+ [disabled]="loginForm.invalid"
+ value="Login"
+ i18n-value>
+ </form>
</div>
@import 'defaults';
-::ng-deep .login {
- height: 100vh;
- color: $color-login-row-text;
- background-color: $color-login-row-bg;
-
- header {
- position: absolute;
- width: 100vw;
-
- .navbar {
- padding: 1rem 2rem;
-
- .dropdown-menu {
- margin-top: 0.2rem;
-
- li a {
- &:hover {
- background-color: $color-brand-teal;
- }
- }
- }
- }
+::ng-deep cd-login {
+ h1 {
+ margin: 0 0 30px 0;
}
- section {
- display: inline-flex;
- width: 100vw;
- min-height: 100vh;
-
- h1 {
- margin: 0 0 30px 0;
- }
+ .btn-password,
+ .btn-password:focus,
+ .form-control,
+ .form-control:focus {
+ color: $color-password-toggle-text;
+ background-color: $color-password-toggle-bg;
+ }
- .btn-password,
- .form-control {
- color: $color-password-toggle-text;
- background-color: $color-password-toggle-bg;
- }
+ .form-control::placeholder {
+ color: $color-password-toggle-placeholder-text;
+ }
- .btn-password:focus {
- outline-color: $color-password-toggle-focus;
- }
+ .btn-password:focus {
+ outline-color: $color-password-toggle-focus;
}
}
import { AuthService } from '../../../shared/api/auth.service';
import { Credentials } from '../../../shared/models/credentials';
-import { LoginResponse } from '../../../shared/models/login-response';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
@Component({
export class LoginComponent implements OnInit {
model = new Credentials();
isLoginActive = false;
- pwdUpdateRequired = false;
constructor(
private authService: AuthService,
ngOnInit() {
if (this.authStorageService.isLoggedIn()) {
- this.pwdUpdateRequired = this.authStorageService.getPwdUpdateRequired();
this.router.navigate(['']);
} else {
// Make sure all open modal dialogs are closed. This might be
window.location.replace(login.login_url);
}
} else {
- this.authStorageService.set(login.username, token, login.permissions, login.sso);
+ this.authStorageService.set(
+ login.username,
+ token,
+ login.permissions,
+ login.sso,
+ login.pwdExpirationDate
+ );
this.router.navigate(['']);
}
});
}
login() {
- this.authService.login(this.model).subscribe((resp: LoginResponse) => {
- if (resp.pwdUpdateRequired) {
- this.pwdUpdateRequired = true;
- } else {
- this.router.navigate(['']);
- }
+ this.authService.login(this.model).subscribe(() => {
+ this.router.navigate(['']);
});
}
}
i18n>Password confirmation doesn't match the password.</span>
</div>
</div>
+
<!-- Password expiration date -->
<div class="form-group row"
*ngIf="!authStorageService.isSSO()">
</div>
</div>
- <!-- Change Password -->
+ <!-- Force change password -->
<div class="form-group row"
- *ngIf="!isCurrentUser()">
- <div class="offset-sm-3 col-sm-9">
+ *ngIf="!isCurrentUser() && !authStorageService.isSSO()">
+ <div class="cd-col-form-offset">
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
}
}
];
- const pwdExpirationSettings = {
- user_pwd_expiration_warning_1: 10,
- user_pwd_expiration_warning_2: 5,
- user_pwd_expiration_span: 90
- };
beforeEach(() => {
spyOn(userService, 'get').and.callFake(() => of(user));
spyOn(TestBed.get(RoleService), 'list').and.callFake(() => of(roles));
setUrl('/user-management/users/edit/user1');
- spyOn(TestBed.get(SettingsService), 'pwdExpirationSettings').and.callFake(() =>
- of(pwdExpirationSettings)
+ spyOn(TestBed.get(SettingsService), 'getStandardSettings').and.callFake(() =>
+ of({
+ user_pwd_expiration_warning_1: 10,
+ user_pwd_expiration_warning_2: 5,
+ user_pwd_expiration_span: 90
+ })
);
component.ngOnInit();
const req = httpTesting.expectOne('api/role');
}
this.minDate = new Date();
- const observables = [this.roleService.list(), this.settingsService.pwdExpirationSettings()];
+ const observables = [this.roleService.list(), this.settingsService.getStandardSettings()];
observableForkJoin(observables).subscribe(
(result: [UserFormRoleModel[], CdPwdExpirationSettings]) => {
this.allRoles = _.map(result[0], (role) => {
placeholder="Old password..."
id="oldpassword"
formControlName="oldpassword"
- autocomplete="off"
+ autocomplete="new-password"
autofocus>
<span class="input-group-append">
<button class="btn btn-light"
<div class="input-group">
<input class="form-control"
type="password"
- autocomplete="off"
+ autocomplete="new-password"
placeholder="Confirm new password..."
id="confirmnewpassword"
formControlName="confirmnewpassword">
});
it('should submit', () => {
+ spyOn(component, 'onPasswordChange').and.callThrough();
spyOn(authStorageService, 'getUsername').and.returnValue('xyz');
formHelper.setMultipleValues({
oldpassword: 'foo',
new_password: 'bar'
});
request.flush({});
+ expect(component.onPasswordChange).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/logout']);
});
});
icons = Icons;
constructor(
- private i18n: I18n,
+ public i18n: I18n,
public actionLabels: ActionLabelsI18n,
- private notificationService: NotificationService,
- private userService: UserService,
- private authStorageService: AuthStorageService,
- private formBuilder: CdFormBuilder,
- private router: Router,
- private passwordPolicyService: PasswordPolicyService
+ public notificationService: NotificationService,
+ public userService: UserService,
+ public authStorageService: AuthStorageService,
+ public formBuilder: CdFormBuilder,
+ public router: Router,
+ public passwordPolicyService: PasswordPolicyService
) {
this.action = this.actionLabels.CHANGE;
this.resource = this.i18n('password');
const oldPassword = this.userForm.getValue('oldpassword');
const newPassword = this.userForm.getValue('newpassword');
this.userService.changePassword(username, oldPassword, newPassword).subscribe(
- () => {
- this.notificationService.show(
- NotificationType.success,
- this.i18n('Updated user password"')
- );
- // Theoretically it is not necessary to navigate to '/logout' because
- // the auth token gets invalid after changing the password in the
- // backend, thus the user would be automatically logged out after the
- // next periodically API request is executed.
- this.router.navigate(['/logout']);
- },
+ () => this.onPasswordChange(),
() => {
this.userForm.setErrors({ cdSubmitButton: true });
}
);
}
+
+ /**
+ * The function that is called after the password has been changed.
+ * Override this in derived classes to change the behaviour.
+ */
+ onPasswordChange() {
+ this.notificationService.show(NotificationType.success, this.i18n('Updated user password"'));
+ // Theoretically it is not necessary to navigate to '/logout' because
+ // the auth token gets invalid after changing the password in the
+ // backend, thus the user would be automatically logged out after the
+ // next periodically API request is executed.
+ this.router.navigate(['/logout']);
+ }
}
+++ /dev/null
-<form #frm="ngForm"
- [formGroup]="userForm"
- (ngSubmit)="onSubmit()"
- novalidate>
- <div class="form-group has-feedback">
- <div class="input-group">
- <input class="form-control"
- type="password"
- placeholder="Old password..."
- id="oldpassword"
- formControlName="oldpassword"
- autocomplete="off"
- autofocus>
- <span class="input-group-append">
- <button class="btn btn-outline-light btn-password"
- cdPasswordButton="oldpassword">
- </button>
- </span>
- </div>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('oldpassword', frm, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('oldpassword', frm, 'notmatch')"
- i18n>The old and new passwords must be different.</span>
- </div>
- <div class="form-group has-feedback">
- <div class="input-group">
- <input class="form-control"
- type="password"
- placeholder="Password..."
- id="newpassword"
- autocomplete="new-password"
- formControlName="newpassword">
- <span class="input-group-append">
- <button type="button"
- class="btn btn-outline-light btn-password"
- cdPasswordButton="newpassword">
- </button>
- </span>
- </div>
- <div class="passwordStrengthLevel">
- <div class="{{ passwordStrengthLevel }}"
- data-toggle="tooltip"
- title="{{ passwordStrengthDescription }}">
- </div>
- </div>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('newpassword', frm, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('newpassword', frm, 'notmatch')"
- i18n>The old and new passwords must be different.</span>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('newpassword', frm, 'checkPassword')"
- i18n>Too weak</span>
- </div>
- <div class="form-group has-feedback">
- <div class="input-group">
- <input class="form-control"
- type="password"
- autocomplete="off"
- placeholder="Confirm new password..."
- id="confirmnewpassword"
- formControlName="confirmnewpassword">
- <span class="input-group-append">
- <button class="btn btn-outline-light btn-password"
- cdPasswordButton="confirmnewpassword">
- </button>
- </span>
- </div>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('confirmnewpassword', frm, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('confirmnewpassword', frm, 'match')"
- i18n>Password confirmation doesn't match the new password.</span>
- </div>
-
- <input type="submit"
- class="btn btn-secondary btn-block"
- [disabled]="userForm.invalid"
- value="Change password"
- i18n-value>
-</form>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ReactiveFormsModule } from '@angular/forms';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { ToastrModule } from 'ngx-toastr';
-
-import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
-import { SharedModule } from '../../../shared/shared.module';
-import { UserPasswordLoginFormComponent } from './user-password-login-form.component';
-
-describe('UserPasswordLoginFormComponent', () => {
- let component: UserPasswordLoginFormComponent;
- let fixture: ComponentFixture<UserPasswordLoginFormComponent>;
-
- configureTestBed(
- {
- imports: [
- HttpClientTestingModule,
- RouterTestingModule,
- ReactiveFormsModule,
- ToastrModule.forRoot(),
- SharedModule
- ],
- declarations: [UserPasswordLoginFormComponent],
- providers: i18nProviders
- },
- true
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(UserPasswordLoginFormComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component } from '@angular/core';
-
-import { UserPasswordFormComponent } from '../user-password-form/user-password-form.component';
-
-@Component({
- selector: 'cd-user-password-login-form',
- templateUrl: './user-password-login-form.component.html',
- styleUrls: ['./user-password-login-form.component.scss']
-})
-export class UserPasswordLoginFormComponent extends UserPasswordFormComponent {}
import { SharedModule } from '../shared/shared.module';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { BlankLayoutComponent } from './layouts/blank-layout/blank-layout.component';
+import { LoginLayoutComponent } from './layouts/login-layout/login-layout.component';
import { WorkbenchLayoutComponent } from './layouts/workbench-layout/workbench-layout.component';
import { NavigationModule } from './navigation/navigation.module';
import { NotFoundComponent } from './not-found/not-found.component';
NotFoundComponent,
ForbiddenComponent,
WorkbenchLayoutComponent,
- BlankLayoutComponent
+ BlankLayoutComponent,
+ LoginLayoutComponent
]
})
export class CoreModule {}
--- /dev/null
+<div class="login full-height">
+ <header>
+ <nav class="navbar">
+ <a class="navbar-brand"></a>
+ <div class="form-inline">
+ <cd-language-selector></cd-language-selector>
+ </div>
+ </nav>
+ </header>
+ <section>
+ <div class="container">
+ <div class="row full-height vertical-align">
+ <div class="col-sm-6 d-none d-sm-block">
+ <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
+ alt="Ceph"
+ class="float-right img-fluid">
+ </div>
+ <div class="col-12 col-sm-6 col-xl-5">
+ <router-outlet></router-outlet>
+ </div>
+ </div>
+ </div>
+ </section>
+</div>
--- /dev/null
+@import 'defaults';
+
+::ng-deep .login {
+ color: $color-login-row-text;
+ background-color: $color-login-row-bg;
+
+ header {
+ position: absolute;
+ width: 100vw;
+
+ .navbar {
+ padding: 1rem 2rem;
+
+ .dropdown-menu {
+ margin-top: 0.2rem;
+
+ li a {
+ &:hover {
+ background-color: $color-brand-teal;
+ }
+ }
+ }
+ }
+ }
+
+ section {
+ display: inline-flex;
+ width: 100vw;
+ min-height: 100vh;
+ }
+}
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+import { RouterTestingModule } from '@angular/router/testing';
+import { SharedModule } from '../../../shared/shared.module';
+import { LoginLayoutComponent } from './login-layout.component';
+
+describe('LoginLayoutComponent', () => {
+ let component: LoginLayoutComponent;
+ let fixture: ComponentFixture<LoginLayoutComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [LoginLayoutComponent],
+ imports: [
+ BsDropdownModule.forRoot(),
+ BsDatepickerModule.forRoot(),
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginLayoutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-login-layout',
+ templateUrl: './login-layout.component.html',
+ styleUrls: ['./login-layout.component.scss']
+})
+export class LoginLayoutComponent {}
it('should return the specified settings (2)', () => {
service.getValues(['abc', 'xyz']).subscribe();
- httpTesting.expectOne('api/settings?names=abc,xyz');
+ const req = httpTesting.expectOne('api/settings?names=abc,xyz');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should return standard settings', () => {
+ service.getStandardSettings().subscribe();
+ const req = httpTesting.expectOne('ui-api/standard_settings');
+ expect(req.request.method).toBe('GET');
});
});
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
-import { CdPwdExpirationSettings } from '../models/cd-pwd-expiration-settings';
import { ApiModule } from './api.module';
class SettingResponse {
return this.http.get(`api/grafana/validation/${uid}`);
}
- pwdExpirationSettings(): Observable<CdPwdExpirationSettings> {
- return this.http.get<CdPwdExpirationSettings>('ui-api/standard_settings');
+ getStandardSettings(): Observable<{ [key: string]: any }> {
+ return this.http.get('ui-api/standard_settings');
}
}
authStorageService = TestBed.get(AuthStorageService);
settingsService = TestBed.get(SettingsService);
spyOn(authStorageService, 'getPwdExpirationDate').and.returnValue(1645488000);
- spyOn(settingsService, 'pwdExpirationSettings').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
user_pwd_expiration_warning_1: 10,
user_pwd_expiration_warning_2: 5,
) {}
ngOnInit() {
- this.settingsService.pwdExpirationSettings().subscribe((pwdExpirationSettings) => {
+ this.settingsService.getStandardSettings().subscribe((pwdExpirationSettings) => {
this.pwdExpirationSettings = new CdPwdExpirationSettings(pwdExpirationSettings);
const pwdExpirationDate = this.authStorageService.getPwdExpirationDate();
if (pwdExpirationDate) {
<button [type]="type"
class="btn btn-secondary tc_submitButton"
+ [ngClass]="btnClass"
[disabled]="loading || disabled"
(click)="submit($event)">
<ng-content></ng-content>
export class SubmitButtonComponent implements OnInit {
@Input()
form: FormGroup | NgForm;
+
@Input()
type = 'submit';
- @Output()
- submitAction = new EventEmitter();
+
@Input()
disabled = false;
+ // A CSS class string to apply to the button's main element.
+ @Input()
+ btnClass: string;
+
+ @Output()
+ submitAction = new EventEmitter();
+
loading = false;
icons = Icons;
pwdExpirationWarning1: number;
pwdExpirationWarning2: number;
- constructor(data: any) {
- this.pwdExpirationSpan = data.user_pwd_expiration_span;
- this.pwdExpirationWarning1 = data.user_pwd_expiration_warning_1;
- this.pwdExpirationWarning2 = data.user_pwd_expiration_warning_2;
+ constructor(settings: { [key: string]: any }) {
+ this.pwdExpirationSpan = settings.user_pwd_expiration_span;
+ this.pwdExpirationWarning1 = settings.user_pwd_expiration_warning_1;
+ this.pwdExpirationWarning2 = settings.user_pwd_expiration_warning_2;
}
}
--- /dev/null
+export class CdPwdPolicySettings {
+ pwdPolicyEnabled: boolean;
+ pwdPolicyMinLength: number;
+ pwdPolicyCheckLengthEnabled: boolean;
+ pwdPolicyCheckOldpwdEnabled: boolean;
+ pwdPolicyCheckUsernameEnabled: boolean;
+ pwdPolicyCheckExclusionListEnabled: boolean;
+ pwdPolicyCheckRepetitiveCharsEnabled: boolean;
+ pwdPolicyCheckSequentialCharsEnabled: boolean;
+ pwdPolicyCheckComplexityEnabled: boolean;
+
+ constructor(settings: { [key: string]: any }) {
+ this.pwdPolicyEnabled = settings.pwd_policy_enabled;
+ this.pwdPolicyMinLength = settings.pwd_policy_min_length;
+ this.pwdPolicyCheckLengthEnabled = settings.pwd_policy_check_length_enabled;
+ this.pwdPolicyCheckOldpwdEnabled = settings.pwd_policy_check_oldpwd_enabled;
+ this.pwdPolicyCheckUsernameEnabled = settings.pwd_policy_check_username_enabled;
+ this.pwdPolicyCheckExclusionListEnabled = settings.pwd_policy_check_exclusion_list_enabled;
+ this.pwdPolicyCheckRepetitiveCharsEnabled = settings.pwd_policy_check_repetitive_chars_enabled;
+ this.pwdPolicyCheckSequentialCharsEnabled = settings.pwd_policy_check_sequential_chars_enabled;
+ this.pwdPolicyCheckComplexityEnabled = settings.pwd_policy_check_complexity_enabled;
+ }
+}
this.router.navigate(['/login']);
break;
case 403:
- if (this.authStorageService.getPwdUpdateRequired()) {
- this.router.navigate(['/login']);
- } else {
- this.router.navigate(['/403']);
- }
+ this.router.navigate(['/403']);
break;
default:
timeoutId = this.prepareNotification(resp);
constructor(private router: Router, private authStorageService: AuthStorageService) {}
canActivate() {
- if (this.authStorageService.isLoggedIn() && !this.authStorageService.getPwdUpdateRequired()) {
+ if (this.authStorageService.isLoggedIn()) {
return true;
}
this.router.navigate(['/login']);
--- /dev/null
+import { Component, NgZone } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { AuthStorageService } from './auth-storage.service';
+import { ChangePasswordGuardService } from './change-password-guard.service';
+
+describe('ChangePasswordGuardService', () => {
+ let service: ChangePasswordGuardService;
+ let authStorageService: AuthStorageService;
+ let ngZone: NgZone;
+
+ @Component({ selector: 'cd-login-password-form', template: '' })
+ class LoginPasswordFormComponent {}
+
+ const routes: Routes = [{ path: 'login-change-password', component: LoginPasswordFormComponent }];
+
+ configureTestBed({
+ imports: [RouterTestingModule.withRoutes(routes)],
+ providers: [ChangePasswordGuardService, AuthStorageService],
+ declarations: [LoginPasswordFormComponent]
+ });
+
+ beforeEach(() => {
+ service = TestBed.get(ChangePasswordGuardService);
+ authStorageService = TestBed.get(AuthStorageService);
+ ngZone = TestBed.get(NgZone);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should do nothing (not logged in)', () => {
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(false);
+ expect(service.canActivate()).toBeTruthy();
+ });
+
+ it('should do nothing (SSO enabled)', () => {
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+ spyOn(authStorageService, 'isSSO').and.returnValue(true);
+ expect(service.canActivate()).toBeTruthy();
+ });
+
+ it('should do nothing (no update pwd required)', () => {
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+ spyOn(authStorageService, 'getPwdUpdateRequired').and.returnValue(false);
+ expect(service.canActivate()).toBeTruthy();
+ });
+
+ it('should redirect to change password page', fakeAsync(() => {
+ spyOn(authStorageService, 'isLoggedIn').and.returnValue(true);
+ spyOn(authStorageService, 'isSSO').and.returnValue(false);
+ spyOn(authStorageService, 'getPwdUpdateRequired').and.returnValue(true);
+ const router = TestBed.get(Router);
+ ngZone.run(() => {
+ expect(service.canActivate()).toBeFalsy();
+ });
+ tick();
+ expect(router.url).toBe('/login-change-password');
+ }));
+});
--- /dev/null
+import { Injectable } from '@angular/core';
+import { CanActivate, CanActivateChild, Router } from '@angular/router';
+
+import { AuthStorageService } from './auth-storage.service';
+
+/**
+ * This service guard checks if a user must be redirected to a special
+ * page at '/login-change-password' to set a new password.
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class ChangePasswordGuardService implements CanActivate, CanActivateChild {
+ constructor(private router: Router, private authStorageService: AuthStorageService) {}
+
+ canActivate() {
+ // Redirect to '/login-change-password' when the following constraints
+ // are fulfilled:
+ // - The user must be logged in.
+ // - SSO must be disabled.
+ // - The flag 'User must change password at next logon' must be set.
+ if (
+ this.authStorageService.isLoggedIn() &&
+ !this.authStorageService.isSSO() &&
+ this.authStorageService.getPwdUpdateRequired()
+ ) {
+ this.router.navigate(['/login-change-password']);
+ return false;
+ }
+ return true;
+ }
+
+ canActivateChild(): boolean {
+ return this.canActivate();
+ }
+}
it('should not get help text', () => {
let helpText = '';
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: false
+ pwd_policy_enabled: false
})
);
service.getHelpText().subscribe((text) => (helpText = text));
it('should get help text chk_length', () => {
let helpText = '';
const expectedHelpText = helpTextHelper.get('chk_length');
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: true,
- PWD_POLICY_MIN_LENGTH: 10,
- PWD_POLICY_CHECK_LENGTH_ENABLED: true,
- PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
- PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: false,
- PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+ user_pwd_expiration_warning_1: 10,
+ user_pwd_expiration_warning_2: 5,
+ user_pwd_expiration_span: 90,
+ pwd_policy_enabled: true,
+ pwd_policy_min_length: 10,
+ pwd_policy_check_length_enabled: true,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_sequential_chars_enabled: false,
+ pwd_policy_check_complexity_enabled: false
})
);
service.getHelpText().subscribe((text) => (helpText = text));
it('should get help text chk_oldpwd', () => {
let helpText = '';
const expectedHelpText = helpTextHelper.get('chk_oldpwd');
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: true,
- PWD_POLICY_CHECK_OLDPWD_ENABLED: true,
- PWD_POLICY_CHECK_USERNAME_ENABLED: false,
- PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
- PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+ pwd_policy_enabled: true,
+ pwd_policy_check_oldpwd_enabled: true,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_complexity_enabled: false
})
);
service.getHelpText().subscribe((text) => (helpText = text));
it('should get help text chk_username', () => {
let helpText = '';
const expectedHelpText = helpTextHelper.get('chk_username');
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: true,
- PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
- PWD_POLICY_CHECK_USERNAME_ENABLED: true,
- PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false
+ pwd_policy_enabled: true,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_username_enabled: true,
+ pwd_policy_check_exclusion_list_enabled: false
})
);
service.getHelpText().subscribe((text) => (helpText = text));
it('should get help text chk_exclusion_list', () => {
let helpText = '';
const expectedHelpText = helpTextHelper.get('chk_exclusion_list');
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: true,
- PWD_POLICY_CHECK_USERNAME_ENABLED: false,
- PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: true,
- PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: false
+ pwd_policy_enabled: true,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: true,
+ pwd_policy_check_repetitive_chars_enabled: false
})
);
service.getHelpText().subscribe((text) => (helpText = text));
it('should get help text chk_repetitive', () => {
let helpText = '';
const expectedHelpText = helpTextHelper.get('chk_repetitive');
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: true,
- PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
- PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
- PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: true,
- PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: false,
- PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+ user_pwd_expiration_warning_1: 10,
+ pwd_policy_enabled: true,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_repetitive_chars_enabled: true,
+ pwd_policy_check_sequential_chars_enabled: false,
+ pwd_policy_check_complexity_enabled: false
})
);
service.getHelpText().subscribe((text) => (helpText = text));
it('should get help text chk_sequential', () => {
let helpText = '';
const expectedHelpText = helpTextHelper.get('chk_sequential');
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: true,
- PWD_POLICY_MIN_LENGTH: 8,
- PWD_POLICY_CHECK_LENGTH_ENABLED: false,
- PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
- PWD_POLICY_CHECK_USERNAME_ENABLED: false,
- PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
- PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: false,
- PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: true,
- PWD_POLICY_CHECK_COMPLEXITY_ENABLED: false
+ pwd_policy_enabled: true,
+ pwd_policy_min_length: 8,
+ pwd_policy_check_length_enabled: false,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_repetitive_chars_enabled: false,
+ pwd_policy_check_sequential_chars_enabled: true,
+ pwd_policy_check_complexity_enabled: false
})
);
service.getHelpText().subscribe((text) => (helpText = text));
it('should get help text chk_complexity', () => {
let helpText = '';
const expectedHelpText = helpTextHelper.get('chk_complexity');
- spyOn(settingsService, 'getValues').and.returnValue(
+ spyOn(settingsService, 'getStandardSettings').and.returnValue(
observableOf({
- PWD_POLICY_ENABLED: true,
- PWD_POLICY_MIN_LENGTH: 8,
- PWD_POLICY_CHECK_LENGTH_ENABLED: false,
- PWD_POLICY_CHECK_OLDPWD_ENABLED: false,
- PWD_POLICY_CHECK_USERNAME_ENABLED: false,
- PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: false,
- PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: false,
- PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: false,
- PWD_POLICY_CHECK_COMPLEXITY_ENABLED: true
+ pwd_policy_enabled: true,
+ pwd_policy_min_length: 8,
+ pwd_policy_check_length_enabled: false,
+ pwd_policy_check_oldpwd_enabled: false,
+ pwd_policy_check_username_enabled: false,
+ pwd_policy_check_exclusion_list_enabled: false,
+ pwd_policy_check_repetitive_chars_enabled: false,
+ pwd_policy_check_sequential_chars_enabled: false,
+ pwd_policy_check_complexity_enabled: true
})
);
service.getHelpText().subscribe((text) => (helpText = text));
import { Injectable } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
+import * as _ from 'lodash';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SettingsService } from '../api/settings.service';
+import { CdPwdPolicySettings } from '../models/cd-pwd-policy-settings';
@Injectable({
providedIn: 'root'
constructor(private i18n: I18n, private settingsService: SettingsService) {}
getHelpText(): Observable<string> {
- return this.settingsService
- .getValues([
- 'PWD_POLICY_ENABLED',
- 'PWD_POLICY_MIN_LENGTH',
- 'PWD_POLICY_CHECK_LENGTH_ENABLED',
- 'PWD_POLICY_CHECK_OLDPWD_ENABLED',
- 'PWD_POLICY_CHECK_USERNAME_ENABLED',
- 'PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED',
- 'PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED',
- 'PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED',
- 'PWD_POLICY_CHECK_COMPLEXITY_ENABLED'
- ])
- .pipe(
- map((resp: Object[]) => {
- let helpText: string[] = [];
- if (resp['PWD_POLICY_ENABLED']) {
- helpText.push(this.i18n('Required rules for passwords:'));
- const i18nHelp: { [key: string]: string } = {
- PWD_POLICY_CHECK_LENGTH_ENABLED: this.i18n(
- 'Must contain at least {{length}} characters',
- {
- length: resp['PWD_POLICY_MIN_LENGTH']
- }
- ),
- PWD_POLICY_CHECK_OLDPWD_ENABLED: this.i18n(
- 'Must not be the same as the previous one'
- ),
- PWD_POLICY_CHECK_USERNAME_ENABLED: this.i18n('Cannot contain the username'),
- PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: this.i18n(
- 'Cannot contain any configured keyword'
- ),
- PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: this.i18n(
- 'Cannot contain any repetitive characters e.g. "aaa"'
- ),
- PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: this.i18n(
- 'Cannot contain any sequential characters e.g. "abc"'
- ),
- PWD_POLICY_CHECK_COMPLEXITY_ENABLED: this.i18n(
- 'Must consist of characters from the following groups:\n' +
- ' * Alphabetic a-z, A-Z\n' +
- ' * Numbers 0-9\n' +
- ' * Special chars: !"#$%& \'()*+,-./:;<=>?@[\\]^_`{{|}}~\n' +
- ' * Any other characters (signs)'
- )
- };
- helpText = helpText.concat(
- Object.keys(i18nHelp)
- .filter((key) => resp[key])
- .map((key) => '- ' + i18nHelp[key])
- );
- }
- return helpText.join('\n');
- })
- );
+ return this.settingsService.getStandardSettings().pipe(
+ map((resp: { [key: string]: any }) => {
+ const settings = new CdPwdPolicySettings(resp);
+ let helpText: string[] = [];
+ if (settings.pwdPolicyEnabled) {
+ helpText.push(this.i18n('Required rules for passwords:'));
+ const i18nHelp: { [key: string]: string } = {
+ pwdPolicyCheckLengthEnabled: this.i18n('Must contain at least {{length}} characters', {
+ length: settings.pwdPolicyMinLength
+ }),
+ pwdPolicyCheckOldpwdEnabled: this.i18n('Must not be the same as the previous one'),
+ pwdPolicyCheckUsernameEnabled: this.i18n('Cannot contain the username'),
+ pwdPolicyCheckExclusionListEnabled: this.i18n('Cannot contain any configured keyword'),
+ pwdPolicyCheckRepetitiveCharsEnabled: this.i18n(
+ 'Cannot contain any repetitive characters e.g. "aaa"'
+ ),
+ pwdPolicyCheckSequentialCharsEnabled: this.i18n(
+ 'Cannot contain any sequential characters e.g. "abc"'
+ ),
+ pwdPolicyCheckComplexityEnabled: this.i18n(
+ 'Must consist of characters from the following groups:\n' +
+ ' * Alphabetic a-z, A-Z\n' +
+ ' * Numbers 0-9\n' +
+ ' * Special chars: !"#$%& \'()*+,-./:;<=>?@[\\]^_`{{|}}~\n' +
+ ' * Any other characters (signs)'
+ )
+ };
+ helpText = helpText.concat(
+ _.keys(i18nHelp)
+ .filter((key) => _.get(settings, key))
+ .map((key) => '- ' + _.get(i18nHelp, key))
+ );
+ }
+ return helpText.join('\n');
+ })
+ );
}
/**
}
refresh() {
- if (this.router.url !== '/login') {
+ if (!_.includes(['/login', '/login-change-password'], this.router.url)) {
this.http.get('api/summary').subscribe((data) => {
this.summaryDataSource.next(data);
});
.full-height {
height: 100vh;
}
+.full-width {
+ width: 100vw;
+}
.vertical-align {
display: flex;
align-items: center;
padding-left: 4px;
}
+.form-footer {
+ width: 100%;
+ display: flex;
+}
+
.form-control {
display: table-cell;
$color-login-row-text: $color-solid-white !default;
$color-login-row-bg: $color-secondary !default;
$color-password-toggle-text: $color-solid-white !default;
+$color-password-toggle-placeholder-text: $color-blue-gray !default;
$color-password-toggle-bg: $color-solid-gray !default;
$color-password-toggle-focus: $color-primary !default;
// $color-login-active-row-bg: $color-light-yellow !default;
for user, _ in v1_db['users'].items():
v1_db['users'][user]['enabled'] = True
v1_db['users'][user]['pwdExpirationDate'] = None
+ v1_db['users'][user]['pwdUpdateRequired'] = False
self.roles = {rn: Role.from_dict(r) for rn, r in v1_db.get('roles', {}).items()}
self.users = {un: User.from_dict(u, dict(self.roles, **SYSTEM_ROLES))
'username': username,
'password': pass_hash,
'pwdExpirationDate': pwdExpirationDate,
+ 'pwdUpdateRequired': False,
'lastUpdate': user['lastUpdate'],
'name': '{} User'.format(username),
'email': '{}@user.com'.format(username),
'lastUpdate': user['lastUpdate'],
'password': pass_hash,
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': 'admin User',
'email': 'admin@user.com',
'roles': ['block-manager', 'pool-manager'],
'username': 'admin',
'password': pass_hash,
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': 'Admin Name',
'email': 'admin@admin.com',
'lastUpdate': user['lastUpdate'],
'username': 'admin',
'password': pass_hash,
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': 'admin User',
'email': 'admin@user.com',
'lastUpdate': user['lastUpdate'],
'username': 'admin',
'password': pass_hash,
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': 'admin User',
'email': 'admin@user.com',
'lastUpdate': user['lastUpdate'],
'username': 'admin',
'password': pass_hash,
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': None,
'email': None,
'lastUpdate': user['lastUpdate'],
'username': 'admin',
'password': pass_hash,
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': 'admin User',
'email': 'admin@user.com',
'lastUpdate': user['lastUpdate'],
'password':
"$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': 'admin User',
'email': 'admin@user.com',
'roles': ['block-manager', 'test_role'],
"password":
"$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
"pwdExpirationDate": null,
+ "pwdUpdateRequired": false,
"roles": ["block-manager", "test_role"],
"name": "admin User",
"email": "admin@user.com",
'password':
"$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': 'admin User',
'email': 'admin@user.com',
'roles': ['block-manager', 'test_role'],
'password':
"$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False,
'name': None,
'email': None,
'roles': ['administrator'],