'username': JLeaf(str),
'permissions': JObj(sub_elems={}, allow_unknown=True),
'sso': JLeaf(bool),
- 'pwdExpirationDate': JLeaf(int, none=True)
+ 'pwdExpirationDate': JLeaf(int, none=True),
+ 'pwdUpdateRequired': JLeaf(bool)
}, allow_unknown=False))
self._validate_jwt_token(data['token'], "admin", data['permissions'])
self.assertSchema(data, JObj(sub_elems={
"username": JLeaf(str),
"permissions": JObj(sub_elems={}, allow_unknown=True),
- "sso": JLeaf(bool)
+ "sso": JLeaf(bool),
+ "pwdUpdateRequired": JLeaf(bool)
}, allow_unknown=False))
self.logout()
@classmethod
def _create_user(cls, username=None, password=None, name=None, email=None, roles=None,
- enabled=True, pwd_expiration_date=None):
+ enabled=True, pwd_expiration_date=None, pwd_update_required=False):
data = {}
if username:
data['username'] = username
data['roles'] = roles
if pwd_expiration_date:
data['pwdExpirationDate'] = pwd_expiration_date
+ data['pwdUpdateRequired'] = pwd_update_required
data['enabled'] = enabled
cls._post("/api/user", data)
'roles': ['administrator'],
'lastUpdate': user['lastUpdate'],
'enabled': True,
- 'pwdExpirationDate': None
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False
})
self._put('/api/user/user1', {
'roles': ['block-manager'],
'lastUpdate': user['lastUpdate'],
'enabled': True,
- 'pwdExpirationDate': None
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False
})
self._delete('/api/user/user1')
'roles': ['administrator'],
'lastUpdate': user['lastUpdate'],
'enabled': False,
- 'pwdExpirationDate': None
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False
})
self._delete('/api/user/klara')
'roles': ['administrator'],
'lastUpdate': user['lastUpdate'],
'enabled': True,
- 'pwdExpirationDate': None
+ 'pwdExpirationDate': None,
+ 'pwdUpdateRequired': False
}])
def test_create_user_already_exists(self):
'roles': ['administrator'],
'lastUpdate': user['lastUpdate'],
'enabled': True,
- 'pwdExpirationDate': future_date
+ 'pwdExpirationDate': future_date,
+ 'pwdUpdateRequired': False
})
self._delete('/api/user/user1')
self._delete('/api/user/user1')
self._ceph_cmd(['dashboard', 'set-user-pwd-expiration-span', '0'])
+ def test_pwd_update_required(self):
+ self._create_user(username='user1',
+ password='mypassword10#',
+ name='My Name',
+ email='my@email.com',
+ roles=['administrator'],
+ pwd_update_required=True)
+ self.assertStatus(201)
+
+ user_1 = self._get('/api/user/user1')
+ self.assertStatus(200)
+ self.assertEqual(user_1['pwdUpdateRequired'], True)
+
+ self.login('user1', 'mypassword10#')
+ self.assertStatus(201)
+
+ self._get('/api/osd')
+ self.assertStatus(403)
+ self._reset_login_to_admin('user1')
+
+ def test_pwd_update_required_change_pwd(self):
+ self._create_user(username='user1',
+ password='mypassword10#',
+ name='My Name',
+ email='my@email.com',
+ roles=['administrator'],
+ pwd_update_required=True)
+ self.assertStatus(201)
+
+ self.login('user1', 'mypassword10#')
+ self._post('/api/user/user1/change_password', {
+ 'old_password': 'mypassword10#',
+ 'new_password': 'newpassword01#'
+ })
+
+ self.login('user1', 'newpassword01#')
+ user_1 = self._get('/api/user/user1')
+ self.assertStatus(200)
+ self.assertEqual(user_1['pwdUpdateRequired'], False)
+ self._get('/api/osd')
+ self.assertStatus(200)
+ self._reset_login_to_admin('user1')
+
def test_validate_password_weak(self):
self._post('/api/user/validate_password', {
'password': 'mypassword1'
def create(self, username, password):
user_data = AuthManager.authenticate(username, password)
- user_perms, pwd_expiration_date = None, None
+ user_perms, pwd_expiration_date, pwd_update_required = None, None, None
if user_data:
user_perms = user_data.get('permissions')
pwd_expiration_date = user_data.get('pwdExpirationDate')
+ pwd_update_required = user_data.get('pwdUpdateRequired')
if user_perms is not None:
logger.debug('Login successful')
'username': username,
'permissions': user_perms,
'pwdExpirationDate': pwd_expiration_date,
- 'sso': mgr.SSO_DB.protocol == 'saml2'
+ 'sso': mgr.SSO_DB.protocol == 'saml2',
+ 'pwdUpdateRequired': pwd_update_required
}
logger.debug('Login failed')
return {
'username': user.username,
'permissions': user.permissions_dict(),
- 'sso': mgr.SSO_DB.protocol == 'saml2'
+ 'sso': mgr.SSO_DB.protocol == 'saml2',
+ 'pwdUpdateRequired': user.pwd_update_required
}
return {
'login_url': self._get_login_url(),
return User._user_to_dict(user)
def create(self, username=None, password=None, name=None, email=None,
- roles=None, enabled=True, pwdExpirationDate=None):
+ roles=None, enabled=True, pwdExpirationDate=None, pwdUpdateRequired=True):
if not username:
raise DashboardException(msg='Username is required',
code='username_required',
validate_password_policy(password, username)
try:
user = mgr.ACCESS_CTRL_DB.create_user(username, password, name,
- email, enabled, pwdExpirationDate)
+ email, enabled, pwdExpirationDate,
+ pwdUpdateRequired)
except UserAlreadyExists:
raise DashboardException(msg='Username already exists',
code='username_already_exists',
mgr.ACCESS_CTRL_DB.save()
def set(self, username, password=None, name=None, email=None, roles=None,
- enabled=None, pwdExpirationDate=None):
+ enabled=None, pwdExpirationDate=None, pwdUpdateRequired=False):
if JwtManager.get_username() == username and enabled is False:
raise DashboardException(msg='You are not allowed to disable your user',
code='cannot_disable_current_user',
user.enabled = enabled
user.pwd_expiration_date = pwdExpirationDate
user.set_roles(user_roles)
+ user.pwd_update_required = pwdUpdateRequired
mgr.ACCESS_CTRL_DB.save()
return User._user_to_dict(user)
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({
UserTabsComponent,
UserListComponent,
UserFormComponent,
- UserPasswordFormComponent
+ UserPasswordFormComponent,
+ UserPasswordLoginFormComponent
]
})
export class AuthModule {}
<div class="login"
- *ngIf="isLoginActive">
+ *ngIf="isLoginActive || pwdUpdateRequired">
<header>
<nav class="navbar">
<a class="navbar-brand"></a>
<form name="loginForm"
(ngSubmit)="login()"
#loginForm="ngForm"
+ *ngIf="!pwdUpdateRequired"
novalidate>
<!-- Username -->
value="Login"
i18n-value>
</form>
+ <cd-user-password-login-form *ngIf="pwdUpdateRequired"></cd-user-password-login-form>
</div>
</div>
</div>
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,
- login.pwdExpirationDate
- );
+ this.authStorageService.set(login.username, token, login.permissions, login.sso);
this.router.navigate(['']);
}
});
}
login() {
- this.authService.login(this.model).subscribe(() => {
- this.router.navigate(['']);
+ this.authService.login(this.model).subscribe((resp: LoginResponse) => {
+ if (resp.pwdUpdateRequired) {
+ this.pwdUpdateRequired = true;
+ } else {
+ this.router.navigate(['']);
+ }
});
}
}
</div>
</div>
+ <!-- Change Password -->
+ <div class="form-group row"
+ *ngIf="!isCurrentUser()">
+ <div class="offset-sm-3 col-sm-9">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="pwdUpdateRequired"
+ name="pwdUpdateRequired"
+ formControlName="pwdUpdateRequired">
+ <label class="custom-control-label"
+ for="pwdUpdateRequired"
+ i18n>User must change password at next logon</label>
+ </div>
+ </div>
+ </div>
+
</div>
<div class="card-footer">
<div class="button-group text-right">
email: 'user0@email.com',
roles: ['administrator'],
enabled: true,
- pwdExpirationDate: undefined
+ pwdExpirationDate: undefined,
+ pwdUpdateRequired: true
};
formHelper.setMultipleValues(user);
formHelper.setValue('confirmpassword', user.password);
email: 'user1@email.com',
roles: ['administrator'],
enabled: true,
- pwdExpirationDate: undefined
+ pwdExpirationDate: undefined,
+ pwdUpdateRequired: true
};
const roles = [
{
expect(userReq.request.body).toEqual({
username: 'user1',
password: '',
+ pwdUpdateRequired: true,
name: 'User 1',
email: 'user1@email.com',
roles: ['administrator'],
pwdExpirationDate: [''],
email: ['', [CdValidators.email]],
roles: [[]],
- enabled: [true, [Validators.required]]
+ enabled: [true, [Validators.required]],
+ pwdUpdateRequired: [true]
},
{
validators: [CdValidators.match('password', 'confirmpassword')]
}
setResponse(response: UserFormModel) {
- ['username', 'name', 'email', 'roles', 'enabled'].forEach((key) =>
+ ['username', 'name', 'email', 'roles', 'enabled', 'pwdUpdateRequired'].forEach((key) =>
this.userForm.get(key).setValue(response[key])
);
const expirationDate = response['pwdExpirationDate'];
getRequest(): UserFormModel {
const userFormModel = new UserFormModel();
- ['username', 'password', 'name', 'email', 'roles', 'enabled'].forEach(
+ ['username', 'password', 'name', 'email', 'roles', 'enabled', 'pwdUpdateRequired'].forEach(
(key) => (userFormModel[key] = this.userForm.get(key).value)
);
const expirationDate = this.userForm.get('pwdExpirationDate').value;
email: string;
roles: Array<string>;
enabled: boolean;
+ pwdUpdateRequired: boolean;
}
--- /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 {}
resp.token,
resp.permissions,
resp.sso,
- resp.pwdExpirationDate
+ resp.pwdExpirationDate,
+ resp.pwdUpdateRequired
);
})
);
permissions: object;
pwdExpirationDate: number;
sso: boolean;
+ pwdUpdateRequired: boolean;
}
this.router.navigate(['/login']);
break;
case 403:
- this.router.navigate(['/403']);
+ if (this.authStorageService.getPwdUpdateRequired()) {
+ this.router.navigate(['/login']);
+ } else {
+ this.router.navigate(['/403']);
+ }
break;
default:
timeoutId = this.prepareNotification(resp);
constructor(private router: Router, private authStorageService: AuthStorageService) {}
canActivate() {
- if (this.authStorageService.isLoggedIn()) {
+ if (this.authStorageService.isLoggedIn() && !this.authStorageService.getPwdUpdateRequired()) {
return true;
}
this.router.navigate(['/login']);
token: string,
permissions = {},
sso = false,
- pwdExpirationDate: number = null
+ pwdExpirationDate: number = null,
+ pwdUpdateRequired: boolean = false
) {
localStorage.setItem('dashboard_username', username);
localStorage.setItem('access_token', token);
localStorage.setItem('dashboard_permissions', JSON.stringify(new Permissions(permissions)));
localStorage.setItem('user_pwd_expiration_date', String(pwdExpirationDate));
+ localStorage.setItem('user_pwd_update_required', String(pwdUpdateRequired));
localStorage.setItem('sso', String(sso));
}
localStorage.removeItem('access_token');
localStorage.removeItem('dashboard_username');
localStorage.removeItem('user_pwd_expiration_data');
+ localStorage.removeItem('user_pwd_update_required');
}
getToken(): string {
return Number(localStorage.getItem('user_pwd_expiration_date'));
}
+ getPwdUpdateRequired(): boolean {
+ return localStorage.getItem('user_pwd_update_required') === 'true';
+ }
+
isSSO() {
return localStorage.getItem('sso') === 'true';
}
class User(object):
def __init__(self, username, password, name=None, email=None, roles=None,
- last_update=None, enabled=True, pwd_expiration_date=None):
+ last_update=None, enabled=True, pwd_expiration_date=None,
+ pwd_update_required=False):
self.username = username
self.password = password
self.name = name
self.pwd_expiration_date = pwd_expiration_date
if self.pwd_expiration_date is None:
self.refresh_pwd_expiration_date()
+ self.pwd_update_required = pwd_update_required
def refresh_last_update(self):
self.last_update = int(time.time())
self.password = hashed_password
self.refresh_last_update()
self.refresh_pwd_expiration_date()
+ self.pwd_update_required = False
def compare_password(self, password):
"""
self.refresh_last_update()
def authorize(self, scope, permissions):
+ if self.pwd_update_required:
+ return False
+
for role in self.roles:
if role.authorize(scope, permissions):
return True
'email': self.email,
'lastUpdate': self.last_update,
'enabled': self.enabled,
- 'pwdExpirationDate': self.pwd_expiration_date
+ 'pwdExpirationDate': self.pwd_expiration_date,
+ 'pwdUpdateRequired': self.pwd_update_required
}
@classmethod
def from_dict(cls, u_dict, roles):
return User(u_dict['username'], u_dict['password'], u_dict['name'],
u_dict['email'], {roles[r] for r in u_dict['roles']},
- u_dict['lastUpdate'], u_dict['enabled'], u_dict['pwdExpirationDate'])
+ u_dict['lastUpdate'], u_dict['enabled'],
+ u_dict['pwdExpirationDate'], u_dict['pwdUpdateRequired'])
class AccessControlDB(object):
del self.roles[name]
- def create_user(self, username, password, name, email, enabled=True, pwd_expiration_date=None):
+ def create_user(self, username, password, name, email, enabled=True,
+ pwd_expiration_date=None, pwd_update_required=False):
logger.debug("creating user: username=%s", username)
with self.lock:
if username in self.users:
(pwd_expiration_date < int(time.mktime(datetime.utcnow().timetuple()))):
raise PwdExpirationDateNotValid()
user = User(username, password_hash(password), name, email, enabled=enabled,
- pwd_expiration_date=pwd_expiration_date)
+ pwd_expiration_date=pwd_expiration_date,
+ pwd_update_required=pwd_update_required)
self.users[username] = user
return user
if user.enabled and user.compare_password(password) \
and not user.is_pwd_expired():
return {'permissions': user.permissions_dict(),
- 'pwdExpirationDate': user.pwd_expiration_date}
+ 'pwdExpirationDate': user.pwd_expiration_date,
+ 'pwdUpdateRequired': user.pwd_update_required}
except UserDoesNotExist:
logger.debug("User '%s' does not exist", username)
return None