From da23fd1c5016d2c23a0dadbe8396d3434319289b Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 31 Aug 2023 11:42:32 +0200 Subject: [PATCH 01/29] init --- .../src/components/OcModal/OcModal.vue | 9 ++++++ .../components/OcTextInput/OcTextInput.vue | 30 +++++++++++-------- .../_OcTextInputPassword.vue | 26 ++++++++++++++++ .../SideBar/Shares/Links/DetailsAndEdit.vue | 5 +++- packages/web-client/src/ocs/capabilities.ts | 11 +++++++ .../composables/capability/useCapability.ts | 13 +++++++- packages/web-runtime/src/App.vue | 1 + packages/web-runtime/src/store/modal.ts | 1 + 8 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue diff --git a/packages/design-system/src/components/OcModal/OcModal.vue b/packages/design-system/src/components/OcModal/OcModal.vue index 7e4f980f775..815efa345df 100644 --- a/packages/design-system/src/components/OcModal/OcModal.vue +++ b/packages/design-system/src/components/OcModal/OcModal.vue @@ -42,6 +42,7 @@ :error-message="inputError" :label="inputLabel" :type="inputType" + :password-policy="inputPasswordPolicy" :description-message="inputDescription" :disabled="inputDisabled" :fix-message-line="true" @@ -347,6 +348,14 @@ export default defineComponent({ required: false, default: false }, + /** + * Password policy for the input + */ + inputPasswordPolicy: { + type: Object, + required: false, + default: () => {} + }, /** * Overwrite default focused element * Can be `#id, .class`. diff --git a/packages/design-system/src/components/OcTextInput/OcTextInput.vue b/packages/design-system/src/components/OcTextInput/OcTextInput.vue index 65efb422a74..b91fbde80db 100644 --- a/packages/design-system/src/components/OcTextInput/OcTextInput.vue +++ b/packages/design-system/src/components/OcTextInput/OcTextInput.vue @@ -8,7 +8,8 @@ size="small" class="oc-mt-s oc-ml-s oc-position-absolute" /> - {} } }, emits: ['change', 'update:modelValue', 'focus'], @@ -238,6 +236,11 @@ export default defineComponent({ if (this.defaultValue) { additionalAttrs['placeholder'] = this.defaultValue } + + if (this.type === 'password') { + additionalAttrs['password-policy'] = this.passwordPolicy + } + // Exclude listeners for events which are handled via methods in this component // eslint-disable-next-line no-unused-vars const { change, input, focus, class: classes, ...attrs } = this.$attrs @@ -266,6 +269,9 @@ export default defineComponent({ }, displayValue() { return this.modelValue || '' + }, + inputComponent() { + return this.type === 'password' ? 'oc-text-input-password' : 'input' } }, methods: { diff --git a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue new file mode 100644 index 00000000000..6cf1ca76652 --- /dev/null +++ b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue index ef61a516703..7ee00f43477 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue @@ -188,6 +188,7 @@ import { formatDateFromDateTime, formatRelativeDateFromDateTime } from 'web-pkg/ import { Resource, SpaceResource } from 'web-client/src/helpers' import { createFileRouteOptions } from 'web-pkg/src/helpers/router' import { OcDrop } from 'design-system/src/components' +import {useCapabilityPasswordPolicy} from "web-pkg"; export default defineComponent({ name: 'DetailsAndEdit', @@ -226,7 +227,8 @@ export default defineComponent({ setup() { return { space: inject>('space'), - resource: inject>('resource') + resource: inject>('resource'), + passwordPolicy: useCapabilityPasswordPolicy() } }, data() { @@ -483,6 +485,7 @@ export default defineComponent({ hasInput: true, confirmDisabled: true, inputLabel: this.$gettext('Password'), + inputPasswordPolicy: this.passwordPolicy, inputType: 'password', onCancel: this.hideModal, onInput: (password) => this.checkPassword(password), diff --git a/packages/web-client/src/ocs/capabilities.ts b/packages/web-client/src/ocs/capabilities.ts index b5ccaad69f5..b59cc10dde1 100644 --- a/packages/web-client/src/ocs/capabilities.ts +++ b/packages/web-client/src/ocs/capabilities.ts @@ -9,8 +9,19 @@ export interface AppProviderCapability { open_url: string version: string } + +export interface PasswordPolicyCapability { + min_characters?: number + min_lower_case_characters?: number + min_upper_case_characters?: number + min_digits?: number + min_special_characters?: number + allowed_special_characters?: string +} + export interface Capabilities { capabilities: { + password_policy?: PasswordPolicyCapability notifications: { ocs_endpoints: string[] } diff --git a/packages/web-pkg/src/composables/capability/useCapability.ts b/packages/web-pkg/src/composables/capability/useCapability.ts index 8df471becdf..354f0006c35 100644 --- a/packages/web-pkg/src/composables/capability/useCapability.ts +++ b/packages/web-pkg/src/composables/capability/useCapability.ts @@ -2,7 +2,7 @@ import { Store } from 'vuex' import get from 'lodash-es/get' import { computed, ComputedRef } from 'vue' import { useStore } from '../store' -import { AppProviderCapability } from 'web-client/src/ocs/capabilities' +import { AppProviderCapability, PasswordPolicyCapability } from 'web-client/src/ocs/capabilities' export const useCapability = ( store: Store, @@ -135,3 +135,14 @@ export const useCapabilityNotifications = createCapabilityComposable( 'notifications.ocs-endpoints', [] ) +export const useCapabilityPasswordPolicy = createCapabilityComposable( + 'password_policy', + { + min_characters: 8, + min_lower_case_characters: 2, + min_upper_case_characters: 2, + min_digits: 1, + min_special_characters: 1, + allowed_special_characters: '!"§="' + } +) diff --git a/packages/web-runtime/src/App.vue b/packages/web-runtime/src/App.vue index 510647eadb6..ea56de39673 100644 --- a/packages/web-runtime/src/App.vue +++ b/packages/web-runtime/src/App.vue @@ -14,6 +14,7 @@ :message="modal.message" :has-input="modal.hasInput" :input-description="modal.inputDescription" + :input-password-policy="modal.inputPasswordPolicy" :input-disabled="modal.inputDisabled" :input-error="modal.inputError" :input-label="modal.inputLabel" diff --git a/packages/web-runtime/src/store/modal.ts b/packages/web-runtime/src/store/modal.ts index abc0fb67812..3615cbd0156 100644 --- a/packages/web-runtime/src/store/modal.ts +++ b/packages/web-runtime/src/store/modal.ts @@ -71,6 +71,7 @@ const mutations = { state.onConfirm = modal.onConfirm state.hasInput = modal.hasInput || false state.inputValue = modal.inputValue || null + state.inputPasswordPolicy = modal.inputPasswordPolicy || {} state.inputSelectionRange = modal.inputSelectionRange state.inputDescription = modal.inputDescription || null state.inputLabel = modal.inputLabel || null From fbba5bf3c8ff8e489c01677d285665e7da1022c3 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 31 Aug 2023 11:56:25 +0200 Subject: [PATCH 02/29] Fix description fuck up --- .../src/components/OcTextInput/OcTextInput.vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/design-system/src/components/OcTextInput/OcTextInput.vue b/packages/design-system/src/components/OcTextInput/OcTextInput.vue index b91fbde80db..96bdb15af3f 100644 --- a/packages/design-system/src/components/OcTextInput/OcTextInput.vue +++ b/packages/design-system/src/components/OcTextInput/OcTextInput.vue @@ -203,12 +203,23 @@ export default defineComponent({ default: null }, /** - * The password policy object + * Determines if the input field is read only. + * + * Read only field will be visualized by a lock item and additionally behaves like a disabled field. + * Read only takes effect if the server won't allow to change the value at all, + * disabled should be used instead, if the value can't be changed in a specific context. + * + * For example: If the backend doesn't allow to set the login states for users in general, use read only. + * If it's not allowed to change for the current logged-in User, use disabled. + * */ readOnly: { type: Boolean, default: false }, + /** + * The password policy object + */ passwordPolicy: { type: Object, default: () => {} From f017992ec5c02d196835eab1c83fa6deaa2a4a3a Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 31 Aug 2023 23:48:20 +0200 Subject: [PATCH 03/29] implemented a lot --- packages/web-pkg/package.json | 3 +- .../passwordPolicyService/index.ts | 1 + .../usePasswordPolicyService.ts | 100 ++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 packages/web-pkg/src/composables/passwordPolicyService/index.ts create mode 100644 packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts diff --git a/packages/web-pkg/package.json b/packages/web-pkg/package.json index 1d61acb65eb..3f4acd38bcc 100644 --- a/packages/web-pkg/package.json +++ b/packages/web-pkg/package.json @@ -34,6 +34,7 @@ "vue3-gettext": "2.5.0-alpha.1", "vuex": "4.1.0", "web-client": "workspace:@ownclouders/web-client@*", - "web-pkg": "workspace:@ownclouders/web-pkg@*" + "web-pkg": "workspace:@ownclouders/web-pkg@*", + "password-sheriff": "^1.1.1" } } diff --git a/packages/web-pkg/src/composables/passwordPolicyService/index.ts b/packages/web-pkg/src/composables/passwordPolicyService/index.ts new file mode 100644 index 00000000000..829912b5ad9 --- /dev/null +++ b/packages/web-pkg/src/composables/passwordPolicyService/index.ts @@ -0,0 +1 @@ +export * from './usePasswordPolicyService' diff --git a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts new file mode 100644 index 00000000000..d98e88b5c51 --- /dev/null +++ b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts @@ -0,0 +1,100 @@ +import { useCapabilityPasswordPolicy } from 'web-pkg' +import { PasswordPolicy } from 'password-sheriff' +import { isObject, isNaN, isNumber } from 'lodash-es' +import { unref } from 'vue' +import { useGettext } from 'vue3-gettext' + +class AtLeastBaseRule { + protected $ngettext + protected override explain(options, password) + protected override assert(options, password) + constructor($ngettext) { + this.$ngettext = $ngettext + } + validate(options) { + if (!isObject(options)) { + throw new Error('options should be an object') + } + + if (!isNumber(options.minLength) || isNaN(options.minLength)) { + throw new Error('atLeastDigits expects minLength to be a non-zero number') + } + + return true + } + missing(options, password) { + return this.explain(options, this.assert(options, password)) + } +} + +class AtLeastCharactersRule extends AtLeastBaseRule { + constructor($gettext) { + super($gettext) + } + + explain(options, verified) { + return { + code: 'atLeastCharacters', + message: this.$ngettext( + 'At least %{param} character long', + 'At least %{param} characters long', + options.minLength + ), + format: [options.minLength], + ...(verified & { verified }) + } + } + + assert(options, password) { + return password.length >= options.minLength + } +} + +class AtLeastDigitsRule extends AtLeastBaseRule { + constructor($ngettext) { + super($ngettext) + } + + explain(options, verified) { + return { + code: 'atLeastDigits', + message: this.$ngettext( + 'At least %{param} number', + 'At least %{param} numbers', + options.minLength + ), + format: [options.minLength], + ...(verified & { verified }) + } + } + + assert(options, password) { + const digitCount = (password || '').match(/\d/g)?.length + return digitCount >= options.minLength + } +} +export function usePasswordPolicyService(): [] { + const { $ngettext } = useGettext() + const passwordPolicyCapability = unref(useCapabilityPasswordPolicy()) + const passwordPolicyRules = [] + + if (passwordPolicyCapability.min_characters) { + passwordPolicyRules.push( + new PasswordPolicy( + { atLeastCharacters: { minLength: passwordPolicyCapability.min_characters } }, + { atLeastCharacters: new AtLeastCharactersRule($ngettext) } + ) + ) + } + + if (passwordPolicyCapability.min_digits) { + passwordPolicyRules.push( + new PasswordPolicy( + { atLeastDigits: { minLength: passwordPolicyCapability.min_digits } }, + { atLeastDigits: new AtLeastDigitsRule($ngettext) } + ) + ) + } + + return passwordPolicyRules +} From 5be6a828cd7ba70eb10c1912c5cd9db250c00a80 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 31 Aug 2023 23:49:56 +0200 Subject: [PATCH 04/29] Implement a lot of stuff --- .../components/OcTextInput/OcTextInput.vue | 13 ++- .../_OcTextInputPassword.vue | 99 +++++++++++++++++-- .../SideBar/Shares/Links/DetailsAndEdit.vue | 6 +- pnpm-lock.yaml | 9 +- 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/packages/design-system/src/components/OcTextInput/OcTextInput.vue b/packages/design-system/src/components/OcTextInput/OcTextInput.vue index 96bdb15af3f..f4aefea9893 100644 --- a/packages/design-system/src/components/OcTextInput/OcTextInput.vue +++ b/packages/design-system/src/components/OcTextInput/OcTextInput.vue @@ -65,6 +65,7 @@ v-text="messageText" /> + @@ -94,7 +95,7 @@ export default defineComponent({ components: { OcIcon, OcButton, OcTextInputPassword }, status: 'ready', release: '1.0.0', - inheritAttrs: true, + inheritAttrs: false, props: { /** * The ID of the element. @@ -221,8 +222,8 @@ export default defineComponent({ * The password policy object */ passwordPolicy: { - type: Object, - default: () => {} + type: Array, + default: () => [] } }, emits: ['change', 'update:modelValue', 'focus'], @@ -348,6 +349,12 @@ export default defineComponent({ color: var(--oc-color-text-muted); } + &-success, + &-success:focus { + border-color: var(--oc-color-swatch-success-default) !important; + color: var(--oc-color-swatch-success-default) !important; + } + &-warning, &-warning:focus { border-color: var(--oc-color-swatch-warning-default) !important; diff --git a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue index 6cf1ca76652..f337b8d7319 100644 --- a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue +++ b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue @@ -1,14 +1,37 @@ + diff --git a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue index 7ee00f43477..dd5a3709e6d 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue @@ -188,7 +188,7 @@ import { formatDateFromDateTime, formatRelativeDateFromDateTime } from 'web-pkg/ import { Resource, SpaceResource } from 'web-client/src/helpers' import { createFileRouteOptions } from 'web-pkg/src/helpers/router' import { OcDrop } from 'design-system/src/components' -import {useCapabilityPasswordPolicy} from "web-pkg"; +import { usePasswordPolicyService } from 'web-pkg/src/composables/passwordPolicyService' export default defineComponent({ name: 'DetailsAndEdit', @@ -225,10 +225,12 @@ export default defineComponent({ }, emits: ['removePublicLink', 'updateLink'], setup() { + const passwordPolicy = usePasswordPolicyService() + return { space: inject>('space'), resource: inject>('resource'), - passwordPolicy: useCapabilityPasswordPolicy() + passwordPolicy } }, data() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f32b11c10a5..bf9112cb477 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -934,6 +934,9 @@ importers: mark.js: specifier: ^8.11.1 version: 8.11.1 + password-sheriff: + specifier: ^1.1.1 + version: 1.1.1 pinia: specifier: ^2.1.3 version: 2.1.3(typescript@5.0.3)(vue@3.3.4) @@ -16222,6 +16225,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /password-sheriff@1.1.1: + resolution: {integrity: sha512-bt0ptyUs97Fb2ZXUcdQP0RYrBFjzO6KhGTjq4RkmR388c6wcT3khG0U7Bvvqwq3DyShEZ9IACed9JMVyAxdaCA==} + dev: false + /path-browserify@0.0.1: resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} dev: true From 8f789351c954eb367702d3b7e7ae904a0034a0ca Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 31 Aug 2023 23:51:16 +0200 Subject: [PATCH 05/29] Lint --- .../design-system/src/components/OcTextInput/OcTextInput.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/design-system/src/components/OcTextInput/OcTextInput.vue b/packages/design-system/src/components/OcTextInput/OcTextInput.vue index f4aefea9893..409ead63df3 100644 --- a/packages/design-system/src/components/OcTextInput/OcTextInput.vue +++ b/packages/design-system/src/components/OcTextInput/OcTextInput.vue @@ -248,11 +248,9 @@ export default defineComponent({ if (this.defaultValue) { additionalAttrs['placeholder'] = this.defaultValue } - if (this.type === 'password') { additionalAttrs['password-policy'] = this.passwordPolicy } - // Exclude listeners for events which are handled via methods in this component // eslint-disable-next-line no-unused-vars const { change, input, focus, class: classes, ...attrs } = this.$attrs From a5a802337e9543b9a8b9cbd473792676793c8b2b Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 1 Sep 2023 16:19:17 +0200 Subject: [PATCH 06/29] Add more rules --- .../_OcTextInputPassword.vue | 11 +- .../SideBar/Shares/Links/DetailsAndEdit.vue | 11 +- .../usePasswordPolicyService.ts | 185 +++++++++++++++++- 3 files changed, 183 insertions(+), 24 deletions(-) diff --git a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue index f337b8d7319..48585d5d844 100644 --- a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue +++ b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue @@ -12,6 +12,7 @@
+
{ return !!(props.passwordPolicy.length && unref(passwordEntered)) }) - const onInput = () => { + const onInput = (event) => { passwordEntered.value = true + password.value = event.target.value } const getPolicyRuleMessage = (policyRule) => { @@ -63,14 +65,15 @@ export default defineComponent({ } const isPolicyRuleFulfilled = (policyRule) => { - return policyRule.check('abcjsdjd13') + return policyRule.check(password.value) } const getPolicyMessageClass = (policyRule) => { - return policyRule.check('adadsadsaasd23') ? 'oc-text-input-success' : 'oc-text-input-danger' + return policyRule.check(password.value) ? 'oc-text-input-success' : 'oc-text-input-danger' } return { + $gettext, onInput, showPassword, showPasswordPolicyInformation, diff --git a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue index dd5a3709e6d..c1a79ca200c 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue @@ -466,16 +466,7 @@ export default defineComponent({ }, checkPassword(password) { - if (password === '') { - this.setModalConfirmButtonDisabled(true) - return this.setModalInputErrorMessage(this.$gettext("Password can't be empty")) - } - if (password.length > 72) { - this.setModalConfirmButtonDisabled(true) - return this.setModalInputErrorMessage(this.$gettext("Password can't exceed 72 characters")) - } - this.setModalConfirmButtonDisabled(false) - return this.setModalInputErrorMessage(null) + //TODO Use Password Component Events }, showPasswordModal() { diff --git a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts index d98e88b5c51..462f9619672 100644 --- a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts +++ b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts @@ -4,11 +4,83 @@ import { isObject, isNaN, isNumber } from 'lodash-es' import { unref } from 'vue' import { useGettext } from 'vue3-gettext' +class MustNotBeEmptyRule { + protected $gettext + + constructor({ $gettext }: any) { + this.$gettext = $gettext + } + explain(options, verified) { + return { + code: 'mustNotBeEmpty', + message: this.$gettext('Must not be empty'), + format: [], + ...(verified & { verified }) + } + } + + assert(options, password) { + return password.length > 0 + } + validate() { + return true + } + missing(options, password) { + return this.explain(options, this.assert(options, password)) + } +} + +class AtMostBaseRule { + protected $ngettext + protected override explain(options, password) + protected override assert(options, password) + constructor({ $ngettext }) { + this.$ngettext = $ngettext + } + validate(options) { + if (!isObject(options)) { + throw new Error('options should be an object') + } + + if (!isNumber(options.maxLength) || isNaN(options.maxLength)) { + throw new Error('maxLength should be a non-zero number') + } + + return true + } + missing(options, password) { + return this.explain(options, this.assert(options, password)) + } +} + +class AtMostCharactersRule extends AtMostBaseRule { + constructor(args) { + super(args) + } + + explain(options, verified) { + return { + code: 'atMostCharacters', + message: this.$ngettext( + 'At most %{param} character long', + 'At most %{param} characters long', + options.maxLength + ), + format: [options.maxLength], + ...(verified & { verified }) + } + } + + assert(options, password) { + return password.length <= options.maxLength + } +} + class AtLeastBaseRule { protected $ngettext protected override explain(options, password) protected override assert(options, password) - constructor($ngettext) { + constructor({ $ngettext }) { this.$ngettext = $ngettext } validate(options) { @@ -17,7 +89,7 @@ class AtLeastBaseRule { } if (!isNumber(options.minLength) || isNaN(options.minLength)) { - throw new Error('atLeastDigits expects minLength to be a non-zero number') + throw new Error('minLength should be a non-zero number') } return true @@ -28,8 +100,8 @@ class AtLeastBaseRule { } class AtLeastCharactersRule extends AtLeastBaseRule { - constructor($gettext) { - super($gettext) + constructor(args) { + super(args) } explain(options, verified) { @@ -50,9 +122,57 @@ class AtLeastCharactersRule extends AtLeastBaseRule { } } +class AtLeastUppercaseCharactersRule extends AtLeastBaseRule { + constructor(args) { + super(args) + } + + explain(options, verified) { + return { + code: 'atLeastUppercaseCharacters', + message: this.$ngettext( + 'At least %{param} uppercase character', + 'At least %{param} uppercase characters', + options.minLength + ), + format: [options.minLength], + ...(verified & { verified }) + } + } + + assert(options, password) { + const uppercaseCount = (password || '').match(/[A-Z\xC0-\xD6\xD8-\xDE]/g)?.length + return uppercaseCount >= options.minLength + } +} + +class AtLeastLowercaseCharactersRule extends AtLeastBaseRule { + constructor(args) { + super(args) + } + + explain(options, verified) { + return { + code: 'atLeastLowercaseCharacters', + message: this.$ngettext( + 'At least %{param} lowercase character', + 'At least %{param} lowercase characters', + options.minLength + ), + format: [options.minLength], + ...(verified & { verified }) + } + } + + assert(options, password) { + const lowercaseCount = (password || '').match(/[a-z\xDF-\xF6\xF8-\xFF]/g)?.length + return lowercaseCount >= options.minLength + } +} + class AtLeastDigitsRule extends AtLeastBaseRule { - constructor($ngettext) { - super($ngettext) + constructor(args) { + super(args) } explain(options, verified) { @@ -73,16 +193,52 @@ class AtLeastDigitsRule extends AtLeastBaseRule { return digitCount >= options.minLength } } -export function usePasswordPolicyService(): [] { - const { $ngettext } = useGettext() +export function usePasswordPolicyService( + { useDefaultRules = true } = { useDefaultRules: Boolean } +): Array { const passwordPolicyCapability = unref(useCapabilityPasswordPolicy()) const passwordPolicyRules = [] + if (useDefaultRules && !passwordPolicyCapability.min_characters) { + passwordPolicyRules.push( + new PasswordPolicy( + { mustNotBeEmpty: {} }, + { mustNotBeEmpty: new MustNotBeEmptyRule({ ...useGettext() }) } + ) + ) + } + if (passwordPolicyCapability.min_characters) { passwordPolicyRules.push( new PasswordPolicy( { atLeastCharacters: { minLength: passwordPolicyCapability.min_characters } }, - { atLeastCharacters: new AtLeastCharactersRule($ngettext) } + { atLeastCharacters: new AtLeastCharactersRule({ ...useGettext() }) } + ) + ) + } + + if (passwordPolicyCapability.min_upper_case_characters) { + passwordPolicyRules.push( + new PasswordPolicy( + { + atLeastUppercaseCharacters: { + minLength: passwordPolicyCapability.min_upper_case_characters + } + }, + { atLeastUppercaseCharacters: new AtLeastUppercaseCharactersRule({ ...useGettext() }) } + ) + ) + } + + if (passwordPolicyCapability.min_lower_case_characters) { + passwordPolicyRules.push( + new PasswordPolicy( + { + atLeastLowercaseCharacters: { + minLength: passwordPolicyCapability.min_lower_case_characters + } + }, + { atLeastLowercaseCharacters: new AtLeastLowercaseCharactersRule({ ...useGettext() }) } ) ) } @@ -91,7 +247,16 @@ export function usePasswordPolicyService(): [] { passwordPolicyRules.push( new PasswordPolicy( { atLeastDigits: { minLength: passwordPolicyCapability.min_digits } }, - { atLeastDigits: new AtLeastDigitsRule($ngettext) } + { atLeastDigits: new AtLeastDigitsRule({ ...useGettext() }) } + ) + ) + } + + if (useDefaultRules) { + passwordPolicyRules.push( + new PasswordPolicy( + { atMostCharacters: { maxLength: 72 } }, + { atMostCharacters: new AtMostCharactersRule({ ...useGettext() }) } ) ) } From 63198a7681ce5d8389f7b80f26ceb079a1cb2a57 Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 2 Sep 2023 15:24:32 +0200 Subject: [PATCH 07/29] Add description --- .../src/components/OcTextInput/OcTextInput.vue | 7 ++++++- .../_OcTextInputPassword/_OcTextInputPassword.vue | 3 --- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/design-system/src/components/OcTextInput/OcTextInput.vue b/packages/design-system/src/components/OcTextInput/OcTextInput.vue index 409ead63df3..f81f1ac3d99 100644 --- a/packages/design-system/src/components/OcTextInput/OcTextInput.vue +++ b/packages/design-system/src/components/OcTextInput/OcTextInput.vue @@ -219,7 +219,12 @@ export default defineComponent({ default: false }, /** - * The password policy object + * Array of password policy rules, if type is password and password policy is given, + * the entered value will be checked against these rules. + * + * Password policy rules must be compliant with auth0/password-sheriff + * https://github.com/auth0/password-sheriff + * */ passwordPolicy: { type: Array, diff --git a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue index 48585d5d844..f077c2615c0 100644 --- a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue +++ b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue @@ -37,9 +37,6 @@ export default defineComponent({ release: '1.0.0', inheritAttrs: true, props: { - /** - * The ID of the element. - */ passwordPolicy: { type: Array, default: () => [] From b4915283f0f71002759fe5094f832300e1c9e4a5 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Sep 2023 07:21:36 +0200 Subject: [PATCH 08/29] Export for testing purposes --- .../usePasswordPolicyService.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts index 462f9619672..5f81317e19d 100644 --- a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts +++ b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts @@ -4,7 +4,7 @@ import { isObject, isNaN, isNumber } from 'lodash-es' import { unref } from 'vue' import { useGettext } from 'vue3-gettext' -class MustNotBeEmptyRule { +export class MustNotBeEmptyRule { protected $gettext constructor({ $gettext }: any) { @@ -30,7 +30,7 @@ class MustNotBeEmptyRule { } } -class AtMostBaseRule { +export class AtMostBaseRule { protected $ngettext protected override explain(options, password) protected override assert(options, password) @@ -53,7 +53,7 @@ class AtMostBaseRule { } } -class AtMostCharactersRule extends AtMostBaseRule { +export class AtMostCharactersRule extends AtMostBaseRule { constructor(args) { super(args) } @@ -76,7 +76,7 @@ class AtMostCharactersRule extends AtMostBaseRule { } } -class AtLeastBaseRule { +export class AtLeastBaseRule { protected $ngettext protected override explain(options, password) protected override assert(options, password) @@ -99,7 +99,7 @@ class AtLeastBaseRule { } } -class AtLeastCharactersRule extends AtLeastBaseRule { +export class AtLeastCharactersRule extends AtLeastBaseRule { constructor(args) { super(args) } @@ -122,7 +122,7 @@ class AtLeastCharactersRule extends AtLeastBaseRule { } } -class AtLeastUppercaseCharactersRule extends AtLeastBaseRule { +export class AtLeastUppercaseCharactersRule extends AtLeastBaseRule { constructor(args) { super(args) } @@ -146,7 +146,7 @@ class AtLeastUppercaseCharactersRule extends AtLeastBaseRule { } } -class AtLeastLowercaseCharactersRule extends AtLeastBaseRule { +export class AtLeastLowercaseCharactersRule extends AtLeastBaseRule { constructor(args) { super(args) } @@ -170,7 +170,7 @@ class AtLeastLowercaseCharactersRule extends AtLeastBaseRule { } } -class AtLeastDigitsRule extends AtLeastBaseRule { +export class AtLeastDigitsRule extends AtLeastBaseRule { constructor(args) { super(args) } From d71409b13bcf0e4ad974dec3afb8c221f8eeefaa Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Sep 2023 09:11:05 +0200 Subject: [PATCH 09/29] Implement mustContainRule --- .../_OcTextInputPassword.vue | 10 ++- .../usePasswordPolicyService.ts | 68 ++++++++++++++++--- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue index f077c2615c0..16f0563b837 100644 --- a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue +++ b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue @@ -58,7 +58,15 @@ export default defineComponent({ const getPolicyRuleMessage = (policyRule) => { const explained = policyRule.explain()[0] - return $gettext(explained.message, { param: explained.format[0] }) + const paramObj = {} + + for (var formatKey = 0; formatKey < explained.format.length; formatKey++) { + paramObj[`param${formatKey + 1}`] = explained.format[formatKey] + } + + console.log(paramObj) + + return $gettext(explained.message, paramObj) } const isPolicyRuleFulfilled = (policyRule) => { diff --git a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts index 5f81317e19d..6c6f896a53a 100644 --- a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts +++ b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts @@ -1,6 +1,6 @@ import { useCapabilityPasswordPolicy } from 'web-pkg' import { PasswordPolicy } from 'password-sheriff' -import { isObject, isNaN, isNumber } from 'lodash-es' +import { isObject, isNaN, isNumber, isString } from 'lodash-es' import { unref } from 'vue' import { useGettext } from 'vue3-gettext' @@ -30,6 +30,42 @@ export class MustNotBeEmptyRule { } } +export class MustContainRule { + protected $gettext + + constructor({ $gettext }: any) { + this.$gettext = $gettext + } + explain(options, verified) { + return { + code: 'mustContain', + message: this.$gettext('At least %{param1} of the special characters: %{param2}'), + format: [options.minLength, options.characters], + ...(verified & { verified }) + } + } + + assert(options, password) { + return password.length > 0 + } + validate(options) { + if (!isObject(options)) { + throw new Error('options should be an object') + } + + if (!isNumber(options.minLength) || isNaN(options.minLength)) { + throw new Error('minLength should be a non-zero number') + } + + if (!isString(options.characters)) { + throw new Error('characters should be a character sequence') + } + } + missing(options, password) { + return this.explain(options, this.assert(options, password)) + } +} + export class AtMostBaseRule { protected $ngettext protected override explain(options, password) @@ -108,8 +144,8 @@ export class AtLeastCharactersRule extends AtLeastBaseRule { return { code: 'atLeastCharacters', message: this.$ngettext( - 'At least %{param} character long', - 'At least %{param} characters long', + 'At least %{param1} character long', + 'At least %{param1} characters long', options.minLength ), format: [options.minLength], @@ -131,8 +167,8 @@ export class AtLeastUppercaseCharactersRule extends AtLeastBaseRule { return { code: 'atLeastUppercaseCharacters', message: this.$ngettext( - 'At least %{param} uppercase character', - 'At least %{param} uppercase characters', + 'At least %{param1} uppercase character', + 'At least %{param1} uppercase characters', options.minLength ), format: [options.minLength], @@ -155,8 +191,8 @@ export class AtLeastLowercaseCharactersRule extends AtLeastBaseRule { return { code: 'atLeastLowercaseCharacters', message: this.$ngettext( - 'At least %{param} lowercase character', - 'At least %{param} lowercase characters', + 'At least %{param1} lowercase character', + 'At least %{param1} lowercase characters', options.minLength ), format: [options.minLength], @@ -179,8 +215,8 @@ export class AtLeastDigitsRule extends AtLeastBaseRule { return { code: 'atLeastDigits', message: this.$ngettext( - 'At least %{param} number', - 'At least %{param} numbers', + 'At least %{param1} number', + 'At least %{param1} numbers', options.minLength ), format: [options.minLength], @@ -252,6 +288,20 @@ export function usePasswordPolicyService( ) } + if (passwordPolicyCapability.min_special_characters) { + passwordPolicyRules.push( + new PasswordPolicy( + { + mustContain: { + minLength: passwordPolicyCapability.min_special_characters, + characters: passwordPolicyCapability.allowed_special_characters + } + }, + { mustContain: new MustContainRule({ ...useGettext() }) } + ) + ) + } + if (useDefaultRules) { passwordPolicyRules.push( new PasswordPolicy( From e61164875497586ef1364cdc8af20b9aa49ac3c6 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Sep 2023 09:15:05 +0200 Subject: [PATCH 10/29] Fix string interpolation --- .../passwordPolicyService/usePasswordPolicyService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts index 6c6f896a53a..b46dd75f0c4 100644 --- a/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts +++ b/packages/web-pkg/src/composables/passwordPolicyService/usePasswordPolicyService.ts @@ -98,8 +98,8 @@ export class AtMostCharactersRule extends AtMostBaseRule { return { code: 'atMostCharacters', message: this.$ngettext( - 'At most %{param} character long', - 'At most %{param} characters long', + 'At most %{param1} character long', + 'At most %{param1} characters long', options.maxLength ), format: [options.maxLength], From 3b4b088979b62adc9b44004fb9e7d71de159bee0 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Sep 2023 09:25:52 +0200 Subject: [PATCH 11/29] Emit events --- .../_OcTextInputPassword/_OcTextInputPassword.vue | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue index 16f0563b837..3c3699ec1b6 100644 --- a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue +++ b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue @@ -26,7 +26,7 @@