diff --git a/changelog/unreleased/enhancement-add-password-policy-compatibility b/changelog/unreleased/enhancement-add-password-policy-compatibility new file mode 100644 index 00000000000..ece0964fe92 --- /dev/null +++ b/changelog/unreleased/enhancement-add-password-policy-compatibility @@ -0,0 +1,9 @@ +Enhancement: Add password policy compatibility + +We consume password policy rules from the server and test public link passwords against those. +Additionally we added a show/hide toggle button to password input field + +https://github.com/owncloud/web/pull/9682 +https://github.com/owncloud/web/pull/9634 +https://github.com/owncloud/web/issues/9638 +https://github.com/owncloud/web/issues/9657 diff --git a/packages/web-client/src/ocs/capabilities.ts b/packages/web-client/src/ocs/capabilities.ts index 410ac89202e..4f5fd4680ee 100644 --- a/packages/web-client/src/ocs/capabilities.ts +++ b/packages/web-client/src/ocs/capabilities.ts @@ -13,11 +13,10 @@ export interface AppProviderCapability { export interface PasswordPolicyCapability { min_characters?: number max_characters?: number - min_lower_case_characters?: number - min_upper_case_characters?: number + min_lowercase_characters?: number + min_uppercase_characters?: number min_digits?: number min_special_characters?: number - special_characters?: string } export interface Capabilities { diff --git a/packages/web-pkg/src/services/passwordPolicy/passwordPolicy.ts b/packages/web-pkg/src/services/passwordPolicy/passwordPolicy.ts index dac86de30c6..25c584c1b98 100644 --- a/packages/web-pkg/src/services/passwordPolicy/passwordPolicy.ts +++ b/packages/web-pkg/src/services/passwordPolicy/passwordPolicy.ts @@ -36,21 +36,28 @@ export class PasswordPolicyService { } const rules = {} as any + // add default rule + if ( + Object.keys(this.capability).length === 0 || + (Object.keys(this.capability).length === 1 && + Object.keys(this.capability)[0] === 'max_characters') + ) { + rules.mustNotBeEmpty = {} + } + if (this.capability.min_characters) { rules.atLeastCharacters = { minLength: this.capability.min_characters } - } else { - rules.mustNotBeEmpty = {} } - if (this.capability.min_upper_case_characters) { + if (this.capability.min_uppercase_characters) { rules.atLeastUppercaseCharacters = { - minLength: this.capability.min_upper_case_characters + minLength: this.capability.min_uppercase_characters } } - if (this.capability.min_lower_case_characters) { + if (this.capability.min_lowercase_characters) { rules.atLeastLowercaseCharacters = { - minLength: this.capability.min_lower_case_characters + minLength: this.capability.min_lowercase_characters } } @@ -61,7 +68,7 @@ export class PasswordPolicyService { if (this.capability.min_special_characters) { rules.mustContain = { minLength: this.capability.min_special_characters, - characters: this.capability.special_characters + characters: ' "!#\\$%&\'()*+,-./:;<=>?@[\\]^_`{|}~"' } } diff --git a/packages/web-pkg/tests/unit/services/passwordPolicy.spec.ts b/packages/web-pkg/tests/unit/services/passwordPolicy.spec.ts new file mode 100644 index 00000000000..efd08270cf6 --- /dev/null +++ b/packages/web-pkg/tests/unit/services/passwordPolicy.spec.ts @@ -0,0 +1,127 @@ +import { PasswordPolicyService } from '../../../src/services' +import { createStore, defaultStoreMockOptions } from 'web-test-helpers' +import { Language } from 'vue3-gettext' +import { PasswordPolicyCapability } from 'web-client/src/ocs/capabilities' + +describe('PasswordPolicyService', () => { + describe('policy', () => { + describe('contains the rules according to the capability', () => { + it.each([ + [{} as PasswordPolicyCapability, ['mustNotBeEmpty']], + [{ min_characters: 2 } as PasswordPolicyCapability, ['atLeastCharacters']], + [ + { min_lowercase_characters: 2 } as PasswordPolicyCapability, + ['atLeastLowercaseCharacters'] + ], + [ + { min_uppercase_characters: 2 } as PasswordPolicyCapability, + ['atLeastUppercaseCharacters'] + ], + [{ min_digits: 2 } as PasswordPolicyCapability, ['atLeastDigits']], + [{ min_digits: 2 } as PasswordPolicyCapability, ['atLeastDigits']], + [{ min_special_characters: 2 } as PasswordPolicyCapability, ['mustContain']], + [ + { max_characters: 72 } as PasswordPolicyCapability, + ['mustNotBeEmpty', 'atMostCharacters'] + ], + [ + { + min_characters: 2, + min_lowercase_characters: 2, + min_uppercase_characters: 2, + min_digits: 2, + min_special_characters: 2, + max_characters: 72 + } as PasswordPolicyCapability, + [ + 'atLeastCharacters', + 'atLeastUppercaseCharacters', + 'atLeastLowercaseCharacters', + 'atLeastDigits', + 'mustContain', + 'atMostCharacters' + ] + ] + ])('capability "%s"', (capability: PasswordPolicyCapability, expected: Array) => { + const { passwordPolicyService } = getWrapper(capability) + expect(Object.keys((passwordPolicyService.getPolicy() as any).rules)).toEqual(expected) + }) + }) + describe('method "check"', () => { + describe('test the password correctly against te defined rules', () => { + it.each([ + [{} as PasswordPolicyCapability, ['', 'o'], [false, true]], + [ + { min_characters: 2 } as PasswordPolicyCapability, + ['', 'o', 'ow', 'ownCloud'], + [false, false, true, true] + ], + [ + { min_lowercase_characters: 2 } as PasswordPolicyCapability, + ['', 'o', 'oWNCLOUD', 'ownCloud'], + [false, false, false, true] + ], + [ + { min_uppercase_characters: 2 } as PasswordPolicyCapability, + ['', 'o', 'ownCloud', 'ownCLoud'], + [false, false, false, true] + ], + [ + { min_digits: 2 } as PasswordPolicyCapability, + ['', '1', 'ownCloud1', 'ownCloud12'], + [false, false, false, true] + ], + [ + { min_special_characters: 2 } as PasswordPolicyCapability, + ['', '!', 'ownCloud!', 'ownCloud!#'], + [false, false, false, true] + ], + [ + { max_characters: 2 } as PasswordPolicyCapability, + ['ownCloud', 'ownC', 'ow', 'o'], + [false, false, true, true] + ], + [ + { + min_characters: 8, + min_lowercase_characters: 2, + min_uppercase_characters: 2, + min_digits: 2, + min_special_characters: 2, + max_characters: 72 + } as PasswordPolicyCapability, + ['öwnCloud', 'öwnCloudää', 'öwnCloudää12', 'öwnCloudäÄ12#!'], + [false, false, false, true] + ] + ])( + 'capability "%s, passwords "%s"', + ( + capability: PasswordPolicyCapability, + passwords: Array, + expected: Array + ) => { + const { passwordPolicyService } = getWrapper(capability) + const policy = passwordPolicyService.getPolicy() + for (let i = 0; i < passwords.length; i++) { + expect((policy as any).check(passwords[i])).toEqual(expected[i]) + } + } + ) + }) + }) + }) +}) + +const getWrapper = (capability: PasswordPolicyCapability) => { + const storeOptions = defaultStoreMockOptions + storeOptions.getters.capabilities.mockReturnValue({ + password_policy: capability + }) + const store = createStore(storeOptions) + return { + passwordPolicyService: new PasswordPolicyService({ + store, + language: { current: 'en' } as Language + }) + } +}