mirror of
https://github.com/ceph/ceph
synced 2025-03-31 16:25:56 +00:00
mgr/dashboard: Make password policy check configurable
Fixes: https://tracker.ceph.com/issues/43089 Signed-off-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
parent
90eb6733a4
commit
3684d24a43
@ -619,6 +619,56 @@ dashboard in the future.
|
||||
User and Role Management
|
||||
------------------------
|
||||
|
||||
Password Policy
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
By default the password policy feature is enabled including the following
|
||||
checks:
|
||||
|
||||
- Is the password longer than N characters?
|
||||
- Are the old and new password the same?
|
||||
|
||||
The password policy feature can be switched on or off completely::
|
||||
|
||||
$ ceph dashboard set-pwd-policy-enabled <true|false>
|
||||
|
||||
The following individual checks can be switched on or off::
|
||||
|
||||
$ ceph dashboard set-pwd-policy-check-length-enabled <true|false>
|
||||
$ ceph dashboard set-pwd-policy-check-oldpwd-enabled <true|false>
|
||||
$ ceph dashboard set-pwd-policy-check-username-enabled <true|false>
|
||||
$ ceph dashboard set-pwd-policy-check-exclusion-list-enabled <true|false>
|
||||
$ ceph dashboard set-pwd-policy-check-complexity-enabled <true|false>
|
||||
$ ceph dashboard set-pwd-policy-check-sequential-chars-enabled <true|false>
|
||||
$ ceph dashboard set-pwd-policy-check-repetitive-chars-enabled <true|false>
|
||||
|
||||
Additionally the following options are available to configure the password
|
||||
policy behaviour.
|
||||
|
||||
- The minimum password length (defaults to 8)::
|
||||
|
||||
$ ceph dashboard set-pwd-policy-min-length <N>
|
||||
|
||||
- The minimum password complexity (defaults to 10)::
|
||||
|
||||
$ ceph dashboard set-pwd-policy-min-complexity <N>
|
||||
|
||||
The password complexity is calculated by classifying each character in
|
||||
the password. The complexity count starts by 0. A character is rated by
|
||||
the following rules in the given order.
|
||||
|
||||
- Increase by 1 if the character is a digit.
|
||||
- Increase by 1 if the character is a lower case ASCII character.
|
||||
- Increase by 2 if the character is an upper case ASCII character.
|
||||
- Increase by 3 if the character is a special character like ``!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~``.
|
||||
- Increase by 5 if the character has not been classified by one of the previous rules.
|
||||
|
||||
- A list of comma separated words that are not allowed to be used in a
|
||||
password::
|
||||
|
||||
$ ceph dashboard set-pwd-policy-exclusion-list <word>[,...]
|
||||
|
||||
|
||||
User Accounts
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
|
@ -10,6 +10,27 @@ from .helper import DashboardTestCase, JObj, JLeaf
|
||||
|
||||
|
||||
class UserTest(DashboardTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(UserTest, cls).setUpClass()
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-enabled', 'true'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-length-enabled', 'true'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-oldpwd-enabled', 'true'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-username-enabled', 'true'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-exclusion-list-enabled', 'true'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-complexity-enabled', 'true'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-sequential-chars-enabled', 'true'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-repetitive-chars-enabled', 'true'])
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-username-enabled', 'false'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-exclusion-list-enabled', 'false'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-complexity-enabled', 'false'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-sequential-chars-enabled', 'false'])
|
||||
cls._ceph_cmd(['dashboard', 'set-pwd-policy-check-repetitive-chars-enabled', 'false'])
|
||||
super(UserTest, cls).tearDownClass()
|
||||
|
||||
@classmethod
|
||||
def _create_user(cls, username=None, password=None, name=None, email=None, roles=None,
|
||||
enabled=True, pwd_expiration_date=None):
|
||||
@ -220,7 +241,7 @@ class UserTest(DashboardTestCase):
|
||||
})
|
||||
self.assertStatus(400)
|
||||
self.assertError('password_policy_validation_failed', 'user',
|
||||
'Password must not contain keywords.')
|
||||
'Password must not contain the keyword "OSD".')
|
||||
self._reset_login_to_admin('test1')
|
||||
|
||||
def test_change_password_contains_sequential_characters(self):
|
||||
|
@ -35,11 +35,23 @@ class Settings(RESTController):
|
||||
def _to_native(setting):
|
||||
return setting.upper().replace('-', '_')
|
||||
|
||||
def list(self):
|
||||
return [
|
||||
self._get(name) for name in Options.__dict__
|
||||
def list(self, names=None):
|
||||
"""
|
||||
Get the list of available options.
|
||||
:param names: A comma separated list of option names that should
|
||||
be processed. Defaults to ``None``.
|
||||
:type names: None|str
|
||||
:return: A list of available options.
|
||||
:rtype: list[dict]
|
||||
"""
|
||||
option_names = [
|
||||
name for name in Options.__dict__
|
||||
if name.isupper() and not name.startswith('_')
|
||||
]
|
||||
if names:
|
||||
names = names.split(',')
|
||||
option_names = list(set(option_names) & set(names))
|
||||
return [self._get(name) for name in option_names]
|
||||
|
||||
def _get(self, name):
|
||||
with self._attribute_handler(name) as sname:
|
||||
@ -52,6 +64,13 @@ class Settings(RESTController):
|
||||
}
|
||||
|
||||
def get(self, name):
|
||||
"""
|
||||
Get the given option.
|
||||
:param name: The name of the option.
|
||||
:return: Returns a dict containing the name, type,
|
||||
default value and current value of the given option.
|
||||
:rtype: dict
|
||||
"""
|
||||
return self._get(name)
|
||||
|
||||
def set(self, name, value):
|
||||
|
@ -37,7 +37,8 @@
|
||||
<label class="cd-col-form-label"
|
||||
for="password">
|
||||
<ng-container i18n>Password</ng-container>
|
||||
<cd-helper class="text-pre"
|
||||
<cd-helper *ngIf="passwordPolicyHelpText.length > 0"
|
||||
class="text-pre"
|
||||
html="{{ passwordPolicyHelpText }}">
|
||||
</cd-helper>
|
||||
</label>
|
||||
|
@ -20,6 +20,7 @@ import { CdFormGroup } from '../../../shared/forms/cd-form-group';
|
||||
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
|
||||
import { NotificationService } from '../../../shared/services/notification.service';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { PasswordPolicyService } from './../../../shared/services/password-policy.service';
|
||||
import { UserFormComponent } from './user-form.component';
|
||||
import { UserFormModel } from './user-form.model';
|
||||
|
||||
@ -62,6 +63,7 @@ describe('UserFormComponent', () => {
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(TestBed.get(PasswordPolicyService), 'getHelpText').and.callFake(() => of(''));
|
||||
fixture = TestBed.createComponent(UserFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
form = component.userForm;
|
||||
|
@ -47,7 +47,7 @@ export class UserFormComponent implements OnInit {
|
||||
messages = new SelectMessages({ empty: this.i18n('There are no roles.') }, this.i18n);
|
||||
action: string;
|
||||
resource: string;
|
||||
passwordPolicyHelpText: string;
|
||||
passwordPolicyHelpText = '';
|
||||
passwordStrengthLevelClass: string;
|
||||
passwordValuation: string;
|
||||
icons = Icons;
|
||||
@ -79,7 +79,9 @@ export class UserFormComponent implements OnInit {
|
||||
}
|
||||
|
||||
createForm() {
|
||||
this.passwordPolicyHelpText = this.passwordPolicyService.getHelpText();
|
||||
this.passwordPolicyService.getHelpText().subscribe((helpText: string) => {
|
||||
this.passwordPolicyHelpText = helpText;
|
||||
});
|
||||
this.userForm = this.formBuilder.group(
|
||||
{
|
||||
username: ['', [Validators.required]],
|
||||
|
@ -42,7 +42,8 @@
|
||||
for="newpassword">
|
||||
<span class="required"
|
||||
i18n>New password</span>
|
||||
<cd-helper class="text-pre"
|
||||
<cd-helper *ngIf="passwordPolicyHelpText.length > 0"
|
||||
class="text-pre"
|
||||
html="{{ passwordPolicyHelpText }}">
|
||||
</cd-helper>
|
||||
</label>
|
||||
|
@ -25,7 +25,7 @@ export class UserPasswordFormComponent {
|
||||
userForm: CdFormGroup;
|
||||
action: string;
|
||||
resource: string;
|
||||
passwordPolicyHelpText: string;
|
||||
passwordPolicyHelpText = '';
|
||||
passwordStrengthLevelClass: string;
|
||||
passwordValuation: string;
|
||||
icons = Icons;
|
||||
@ -46,7 +46,9 @@ export class UserPasswordFormComponent {
|
||||
}
|
||||
|
||||
createForm() {
|
||||
this.passwordPolicyHelpText = this.passwordPolicyService.getHelpText();
|
||||
this.passwordPolicyService.getHelpText().subscribe((helpText: string) => {
|
||||
this.passwordPolicyHelpText = helpText;
|
||||
});
|
||||
this.userForm = this.formBuilder.group(
|
||||
{
|
||||
oldpassword: [
|
||||
|
@ -125,4 +125,26 @@ describe('SettingsService', () => {
|
||||
service.disableSetting(exampleUrl);
|
||||
expect(service['settings']).toEqual({ [exampleUrl]: '' });
|
||||
});
|
||||
|
||||
it('should return the specified settings (1)', () => {
|
||||
let result;
|
||||
service.getValues('foo,bar').subscribe((resp) => {
|
||||
result = resp;
|
||||
});
|
||||
const req = httpTesting.expectOne('api/settings?names=foo,bar');
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush([
|
||||
{ name: 'foo', default: '', type: 'str', value: 'test' },
|
||||
{ name: 'bar', default: 0, type: 'int', value: 2 }
|
||||
]);
|
||||
expect(result).toEqual({
|
||||
foo: 'test',
|
||||
bar: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the specified settings (2)', () => {
|
||||
service.getValues(['abc', 'xyz']).subscribe();
|
||||
httpTesting.expectOne('api/settings?names=abc,xyz');
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,20 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { CdPwdExpirationSettings } from '../models/cd-pwd-expiration-settings';
|
||||
import { ApiModule } from './api.module';
|
||||
|
||||
class SettingResponse {
|
||||
name: string;
|
||||
default: any;
|
||||
type: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: ApiModule
|
||||
})
|
||||
@ -14,6 +23,21 @@ export class SettingsService {
|
||||
|
||||
private settings: { [url: string]: string } = {};
|
||||
|
||||
getValues(names: string | string[]): Observable<{ [key: string]: any }> {
|
||||
if (_.isArray(names)) {
|
||||
names = names.join(',');
|
||||
}
|
||||
return this.http.get(`api/settings?names=${names}`).pipe(
|
||||
map((resp: SettingResponse[]) => {
|
||||
const result = {};
|
||||
_.forEach(resp, (option: SettingResponse) => {
|
||||
_.set(result, option.name, option.value);
|
||||
});
|
||||
return result;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ifSettingConfigured(url: string, fn: (value?: string) => void, elseFn?: () => void): void {
|
||||
const setting = this.settings[url];
|
||||
if (setting === undefined) {
|
||||
|
@ -0,0 +1,205 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
|
||||
import { SettingsService } from '../api/settings.service';
|
||||
import { SharedModule } from '../shared.module';
|
||||
import { PasswordPolicyService } from './password-policy.service';
|
||||
|
||||
describe('PasswordPolicyService', () => {
|
||||
let service: PasswordPolicyService;
|
||||
let settingsService: SettingsService;
|
||||
|
||||
const helpTextHelper = {
|
||||
get: (chk: string) => {
|
||||
const chkTexts: { [key: string]: string } = {
|
||||
chk_length: 'Must contain at least 10 characters',
|
||||
chk_oldpwd: 'Must not be the same as the previous one',
|
||||
chk_username: 'Cannot contain the username',
|
||||
chk_exclusion_list: 'Cannot contain any configured keyword',
|
||||
chk_repetitive: 'Cannot contain any repetitive characters e.g. "aaa"',
|
||||
chk_sequential: 'Cannot contain any sequential characters e.g. "abc"',
|
||||
chk_complexity:
|
||||
'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)'
|
||||
};
|
||||
return ['Required rules for passwords:', '- ' + chkTexts[chk]].join('\n');
|
||||
}
|
||||
};
|
||||
|
||||
configureTestBed({
|
||||
imports: [HttpClientTestingModule, SharedModule],
|
||||
providers: [i18nProviders]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.get(PasswordPolicyService);
|
||||
settingsService = TestBed.get(SettingsService);
|
||||
settingsService['settings'] = {};
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not get help text', () => {
|
||||
let helpText = '';
|
||||
spyOn(settingsService, 'getValues').and.returnValue(
|
||||
observableOf({
|
||||
PWD_POLICY_ENABLED: false
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe('');
|
||||
});
|
||||
|
||||
it('should get help text chk_length', () => {
|
||||
let helpText = '';
|
||||
const expectedHelpText = helpTextHelper.get('chk_length');
|
||||
spyOn(settingsService, 'getValues').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
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe(expectedHelpText);
|
||||
});
|
||||
|
||||
it('should get help text chk_oldpwd', () => {
|
||||
let helpText = '';
|
||||
const expectedHelpText = helpTextHelper.get('chk_oldpwd');
|
||||
spyOn(settingsService, 'getValues').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
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe(expectedHelpText);
|
||||
});
|
||||
|
||||
it('should get help text chk_username', () => {
|
||||
let helpText = '';
|
||||
const expectedHelpText = helpTextHelper.get('chk_username');
|
||||
spyOn(settingsService, 'getValues').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
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe(expectedHelpText);
|
||||
});
|
||||
|
||||
it('should get help text chk_exclusion_list', () => {
|
||||
let helpText = '';
|
||||
const expectedHelpText = helpTextHelper.get('chk_exclusion_list');
|
||||
spyOn(settingsService, 'getValues').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
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe(expectedHelpText);
|
||||
});
|
||||
|
||||
it('should get help text chk_repetitive', () => {
|
||||
let helpText = '';
|
||||
const expectedHelpText = helpTextHelper.get('chk_repetitive');
|
||||
spyOn(settingsService, 'getValues').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
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe(expectedHelpText);
|
||||
});
|
||||
|
||||
it('should get help text chk_sequential', () => {
|
||||
let helpText = '';
|
||||
const expectedHelpText = helpTextHelper.get('chk_sequential');
|
||||
spyOn(settingsService, 'getValues').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
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe(expectedHelpText);
|
||||
});
|
||||
|
||||
it('should get help text chk_complexity', () => {
|
||||
let helpText = '';
|
||||
const expectedHelpText = helpTextHelper.get('chk_complexity');
|
||||
spyOn(settingsService, 'getValues').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
|
||||
})
|
||||
);
|
||||
service.getHelpText().subscribe((text) => (helpText = text));
|
||||
expect(helpText).toBe(expectedHelpText);
|
||||
});
|
||||
|
||||
it('should get too-weak class', () => {
|
||||
expect(service.mapCreditsToCssClass(0)).toBe('too-weak');
|
||||
expect(service.mapCreditsToCssClass(9)).toBe('too-weak');
|
||||
});
|
||||
|
||||
it('should get weak class', () => {
|
||||
expect(service.mapCreditsToCssClass(10)).toBe('weak');
|
||||
expect(service.mapCreditsToCssClass(14)).toBe('weak');
|
||||
});
|
||||
|
||||
it('should get ok class', () => {
|
||||
expect(service.mapCreditsToCssClass(15)).toBe('ok');
|
||||
expect(service.mapCreditsToCssClass(19)).toBe('ok');
|
||||
});
|
||||
|
||||
it('should get strong class', () => {
|
||||
expect(service.mapCreditsToCssClass(20)).toBe('strong');
|
||||
expect(service.mapCreditsToCssClass(24)).toBe('strong');
|
||||
});
|
||||
|
||||
it('should get very-strong class', () => {
|
||||
expect(service.mapCreditsToCssClass(25)).toBe('very-strong');
|
||||
expect(service.mapCreditsToCssClass(30)).toBe('very-strong');
|
||||
});
|
||||
});
|
@ -1,27 +1,72 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { SettingsService } from '../api/settings.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PasswordPolicyService {
|
||||
constructor(private i18n: I18n) {}
|
||||
constructor(private i18n: I18n, private settingsService: SettingsService) {}
|
||||
|
||||
getHelpText() {
|
||||
return this.i18n(
|
||||
'Required rules for password complexity:\n\
|
||||
- must contain at least 8 characters\n\
|
||||
- cannot contain username\n\
|
||||
- cannot contain any keyword used in Ceph\n\
|
||||
- cannot contain any repetitive characters e.g. "aaa"\n\
|
||||
- cannot contain any sequential characters e.g. "abc"\n\
|
||||
- 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)'
|
||||
);
|
||||
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');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,9 +57,7 @@ class PasswordPolicy(object):
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.old_password = old_password
|
||||
self.forbidden_words = ['osd', 'host', 'dashboard', 'pool',
|
||||
'block', 'nfs', 'ceph', 'monitors',
|
||||
'gateway', 'logs', 'crush', 'maps']
|
||||
self.forbidden_words = Settings.PWD_POLICY_EXCLUSION_LIST.split(',')
|
||||
self.complexity_credits = 0
|
||||
|
||||
@staticmethod
|
||||
@ -67,7 +65,9 @@ class PasswordPolicy(object):
|
||||
return re.compile('(?:{0})'.format(word),
|
||||
flags=re.IGNORECASE).search(password)
|
||||
|
||||
def check_password_characters(self):
|
||||
def check_password_complexity(self):
|
||||
if not Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED:
|
||||
return Settings.PWD_POLICY_MIN_COMPLEXITY
|
||||
digit_credit = 1
|
||||
small_letter_credit = 1
|
||||
big_letter_credit = 2
|
||||
@ -88,47 +88,65 @@ class PasswordPolicy(object):
|
||||
return self.complexity_credits
|
||||
|
||||
def check_is_old_password(self):
|
||||
if not Settings.PWD_POLICY_CHECK_OLDPWD_ENABLED:
|
||||
return False
|
||||
return self.old_password and self.password == self.old_password
|
||||
|
||||
def check_if_contains_username(self):
|
||||
if not Settings.PWD_POLICY_CHECK_USERNAME_ENABLED:
|
||||
return False
|
||||
if not self.username:
|
||||
return False
|
||||
return self._check_if_contains_word(self.password, self.username)
|
||||
|
||||
def check_if_contains_forbidden_words(self):
|
||||
if not Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED:
|
||||
return False
|
||||
return self._check_if_contains_word(self.password,
|
||||
'|'.join(self.forbidden_words))
|
||||
|
||||
def check_if_sequential_characters(self):
|
||||
if not Settings.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED:
|
||||
return False
|
||||
for i in range(1, len(self.password) - 1):
|
||||
if ord(self.password[i - 1]) + 1 == ord(self.password[i])\
|
||||
== ord(self.password[i + 1]) - 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_if_repetetive_characters(self):
|
||||
def check_if_repetitive_characters(self):
|
||||
if not Settings.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED:
|
||||
return False
|
||||
for i in range(1, len(self.password) - 1):
|
||||
if self.password[i - 1] == self.password[i] == self.password[i + 1]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_password_length(self, min_length=8):
|
||||
return len(self.password) >= min_length
|
||||
def check_password_length(self):
|
||||
if not Settings.PWD_POLICY_CHECK_LENGTH_ENABLED:
|
||||
return True
|
||||
return len(self.password) >= Settings.PWD_POLICY_MIN_LENGTH
|
||||
|
||||
def check_all(self):
|
||||
"""
|
||||
Perform all password policy checks.
|
||||
:raise PasswordPolicyException: If a password policy check fails.
|
||||
"""
|
||||
if self.check_password_characters() < 10 or not self.check_password_length():
|
||||
if not Settings.PWD_POLICY_ENABLED:
|
||||
return
|
||||
if self.check_password_complexity() < Settings.PWD_POLICY_MIN_COMPLEXITY:
|
||||
raise PasswordPolicyException('Password is too weak.')
|
||||
if not self.check_password_length():
|
||||
raise PasswordPolicyException('Password is too weak.')
|
||||
if self.check_is_old_password():
|
||||
raise PasswordPolicyException('Password must not be the same as the previous one.')
|
||||
if self.check_if_contains_username():
|
||||
raise PasswordPolicyException('Password must not contain username.')
|
||||
if self.check_if_contains_forbidden_words():
|
||||
raise PasswordPolicyException('Password must not contain keywords.')
|
||||
if self.check_if_repetetive_characters():
|
||||
result = self.check_if_contains_forbidden_words()
|
||||
if result:
|
||||
raise PasswordPolicyException('Password must not contain the keyword "{}".'.format(
|
||||
result.group(0)))
|
||||
if self.check_if_repetitive_characters():
|
||||
raise PasswordPolicyException('Password must not contain repetitive characters.')
|
||||
if self.check_if_sequential_characters():
|
||||
raise PasswordPolicyException('Password must not contain sequential characters.')
|
||||
|
@ -62,6 +62,27 @@ class Options(object):
|
||||
USER_PWD_EXPIRATION_WARNING_1 = (10, int)
|
||||
USER_PWD_EXPIRATION_WARNING_2 = (5, int)
|
||||
|
||||
# Password policy
|
||||
PWD_POLICY_ENABLED = (True, bool)
|
||||
# Individual checks
|
||||
PWD_POLICY_CHECK_LENGTH_ENABLED = (True, bool)
|
||||
PWD_POLICY_CHECK_OLDPWD_ENABLED = (True, bool)
|
||||
PWD_POLICY_CHECK_USERNAME_ENABLED = (False, bool)
|
||||
PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = (False, bool)
|
||||
PWD_POLICY_CHECK_COMPLEXITY_ENABLED = (False, bool)
|
||||
PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED = (False, bool)
|
||||
PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED = (False, bool)
|
||||
# Settings
|
||||
PWD_POLICY_MIN_LENGTH = (8, int)
|
||||
PWD_POLICY_MIN_COMPLEXITY = (10, int)
|
||||
PWD_POLICY_EXCLUSION_LIST = (','.join(['osd', 'host',
|
||||
'dashboard', 'pool',
|
||||
'block', 'nfs',
|
||||
'ceph', 'monitors',
|
||||
'gateway', 'logs',
|
||||
'crush', 'maps']),
|
||||
str)
|
||||
|
||||
@staticmethod
|
||||
def has_default_value(name):
|
||||
return getattr(Settings, name, None) is None or \
|
||||
|
@ -15,6 +15,7 @@ from ..security import Scope, Permission
|
||||
from ..services.access_control import load_access_control_db, \
|
||||
password_hash, AccessControlDB, \
|
||||
SYSTEM_ROLES, PasswordPolicy
|
||||
from ..settings import Settings
|
||||
|
||||
|
||||
class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
|
||||
@ -790,54 +791,73 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
|
||||
})
|
||||
|
||||
def test_password_policy_pw_length(self):
|
||||
Settings.PWD_POLICY_CHECK_LENGTH_ENABLED = True
|
||||
Settings.PWD_POLICY_MIN_LENGTH = 3
|
||||
pw_policy = PasswordPolicy('foo')
|
||||
self.assertTrue(pw_policy.check_password_length(3))
|
||||
self.assertTrue(pw_policy.check_password_length())
|
||||
|
||||
def test_password_policy_pw_length_fail(self):
|
||||
Settings.PWD_POLICY_CHECK_LENGTH_ENABLED = True
|
||||
pw_policy = PasswordPolicy('bar')
|
||||
self.assertFalse(pw_policy.check_password_length())
|
||||
|
||||
def test_password_policy_credits_too_weak(self):
|
||||
Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
|
||||
pw_policy = PasswordPolicy('foo')
|
||||
pw_credits = pw_policy.check_password_characters()
|
||||
pw_credits = pw_policy.check_password_complexity()
|
||||
self.assertEqual(pw_credits, 3)
|
||||
|
||||
def test_password_policy_credits_weak(self):
|
||||
Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
|
||||
pw_policy = PasswordPolicy('mypassword1')
|
||||
pw_credits = pw_policy.check_password_characters()
|
||||
pw_credits = pw_policy.check_password_complexity()
|
||||
self.assertEqual(pw_credits, 11)
|
||||
|
||||
def test_password_policy_credits_ok(self):
|
||||
Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
|
||||
pw_policy = PasswordPolicy('mypassword1!@')
|
||||
pw_credits = pw_policy.check_password_characters()
|
||||
pw_credits = pw_policy.check_password_complexity()
|
||||
self.assertEqual(pw_credits, 17)
|
||||
|
||||
def test_password_policy_credits_strong(self):
|
||||
Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
|
||||
pw_policy = PasswordPolicy('testpassword0047!@')
|
||||
pw_credits = pw_policy.check_password_characters()
|
||||
pw_credits = pw_policy.check_password_complexity()
|
||||
self.assertEqual(pw_credits, 22)
|
||||
|
||||
def test_password_policy_credits_very_strong(self):
|
||||
Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED = True
|
||||
pw_policy = PasswordPolicy('testpassword#!$!@$')
|
||||
pw_credits = pw_policy.check_password_characters()
|
||||
pw_credits = pw_policy.check_password_complexity()
|
||||
self.assertEqual(pw_credits, 30)
|
||||
|
||||
def test_password_policy_forbidden_words(self):
|
||||
Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = True
|
||||
pw_policy = PasswordPolicy('!@$testdashboard#!$')
|
||||
self.assertTrue(pw_policy.check_if_contains_forbidden_words())
|
||||
|
||||
def test_password_policy_forbidden_words_custom(self):
|
||||
Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = True
|
||||
Settings.PWD_POLICY_EXCLUSION_LIST = 'foo,bar'
|
||||
pw_policy = PasswordPolicy('foo123bar')
|
||||
self.assertTrue(pw_policy.check_if_contains_forbidden_words())
|
||||
|
||||
def test_password_policy_sequential_chars(self):
|
||||
Settings.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED = True
|
||||
pw_policy = PasswordPolicy('!@$test123#!$')
|
||||
self.assertTrue(pw_policy.check_if_sequential_characters())
|
||||
|
||||
def test_password_policy_repetetive_chars(self):
|
||||
def test_password_policy_repetitive_chars(self):
|
||||
Settings.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED = True
|
||||
pw_policy = PasswordPolicy('!@$testfooo#!$')
|
||||
self.assertTrue(pw_policy.check_if_repetetive_characters())
|
||||
self.assertTrue(pw_policy.check_if_repetitive_characters())
|
||||
|
||||
def test_password_policy_contain_username(self):
|
||||
Settings.PWD_POLICY_CHECK_USERNAME_ENABLED = True
|
||||
pw_policy = PasswordPolicy('%admin135)', 'admin')
|
||||
self.assertTrue(pw_policy.check_if_contains_username())
|
||||
|
||||
def test_password_policy_is_old_pwd(self):
|
||||
Settings.PWD_POLICY_CHECK_OLDPWD_ENABLED = True
|
||||
pw_policy = PasswordPolicy('foo', old_password='foo')
|
||||
self.assertTrue(pw_policy.check_is_old_password())
|
||||
|
@ -116,6 +116,15 @@ class SettingsControllerTest(ControllerTestCase, KVStoreMockMixin):
|
||||
self.assertIn('name', data[0].keys())
|
||||
self.assertIn('value', data[0].keys())
|
||||
|
||||
def test_settings_list_filtered(self):
|
||||
self._get('/api/settings?names=GRAFANA_ENABLED,PWD_POLICY_ENABLED')
|
||||
self.assertStatus(200)
|
||||
data = self.json_body()
|
||||
self.assertTrue(len(data) == 2)
|
||||
names = [option['name'] for option in data]
|
||||
self.assertIn('GRAFANA_ENABLED', names)
|
||||
self.assertIn('PWD_POLICY_ENABLED', names)
|
||||
|
||||
def test_rgw_daemon_get(self):
|
||||
self._get('/api/settings/grafana-api-username')
|
||||
self.assertStatus(200)
|
||||
|
Loading…
Reference in New Issue
Block a user