diff --git a/.changeset/rotten-grapes-clap.md b/.changeset/rotten-grapes-clap.md new file mode 100644 index 00000000000..9251fabf0fc --- /dev/null +++ b/.changeset/rotten-grapes-clap.md @@ -0,0 +1,8 @@ +--- +"@wso2is/admin.server-configurations.v1": minor +"@wso2is/admin.extensions.v1": minor +"@wso2is/admin.validation.v1": minor +"@wso2is/i18n": patch +--- + +Introduce rule based password expiry diff --git a/apps/console/src/public/deployment.config.json b/apps/console/src/public/deployment.config.json index 00203c807e1..ff62fa5f4bd 100644 --- a/apps/console/src/public/deployment.config.json +++ b/apps/console/src/public/deployment.config.json @@ -692,7 +692,9 @@ "console:loginAndRegistration" ], "read": [ - "internal_governance_view" + "internal_governance_view", + "internal_group_mgt_view", + "internal_role_mgt_view" ], "update": [ "internal_config_update", diff --git a/features/admin.extensions.v1/components/password-expiry/components/password-expiry.tsx b/features/admin.extensions.v1/components/password-expiry/components/password-expiry.tsx index f7788abf7bc..66bb8eabfe9 100644 --- a/features/admin.extensions.v1/components/password-expiry/components/password-expiry.tsx +++ b/features/admin.extensions.v1/components/password-expiry/components/password-expiry.tsx @@ -20,6 +20,7 @@ import { GovernanceConnectorConstants } from "@wso2is/admin.server-configurations.v1/constants/governance-connector-constants"; import { Field } from "@wso2is/form/src"; +import { Heading } from "@wso2is/react-components"; import React, { ReactElement } from "react"; import { TFunction } from "react-i18next"; @@ -32,7 +33,9 @@ export const generatePasswordExpiry = ( ): ReactElement => { return ( <> -
{ t("extensions:manage.serverConfigurations.passwordExpiry.heading") }
+ + { t("extensions:manage.serverConfigurations.passwordExpiry.heading") } +
{ return ( <> -
{ t("extensions:manage.serverConfigurations.passwordHistoryCount.heading") }
+ + { t("extensions:manage.serverConfigurations.passwordHistoryCount.heading") } + + + { t("extensions:manage.serverConfigurations.passwordHistoryCount.message") } +
- - - { t("extensions:manage.serverConfigurations.passwordHistoryCount.message") } - - -
{ t("extensions:manage.serverConfigurations.passwordValidationHeading") }
); }; diff --git a/features/admin.extensions.v1/configs/models/server-configuration.ts b/features/admin.extensions.v1/configs/models/server-configuration.ts index 43dc65238f1..6dce8b76e87 100644 --- a/features/admin.extensions.v1/configs/models/server-configuration.ts +++ b/features/admin.extensions.v1/configs/models/server-configuration.ts @@ -95,4 +95,6 @@ export interface PasswordPoliciesInterface extends ValidationFormInterface { passwordExpiryEnabled?: boolean; passwordHistoryCount?: number | string; passwordHistoryCountEnabled?: boolean; + passwordExpiryRules?: Record; + passwordExpirySkipFallback?: boolean; } diff --git a/features/admin.extensions.v1/configs/server-configuration.tsx b/features/admin.extensions.v1/configs/server-configuration.tsx index 18939a668ef..a582e96b25f 100644 --- a/features/admin.extensions.v1/configs/server-configuration.tsx +++ b/features/admin.extensions.v1/configs/server-configuration.tsx @@ -241,6 +241,9 @@ const serverConfigurationConfig: ServerConfigurationConfig = { processPasswordPoliciesSubmitData: (data: PasswordPoliciesInterface, isLegacy: boolean) => { let passwordExpiryTime: number | undefined = parseInt((data.passwordExpiryTime as string)); const passwordExpiryEnabled: boolean | undefined = data.passwordExpiryEnabled; + const passwordExpirySkipFallback: boolean | undefined = data.passwordExpirySkipFallback || false; + const passwordExpiryRules: Record | undefined = + data?.passwordExpiryRules || {}; let passwordHistoryCount: number | undefined = parseInt((data.passwordHistoryCount as string)); const passwordHistoryCountEnabled: boolean | undefined = data.passwordHistoryCountEnabled; @@ -248,8 +251,11 @@ const serverConfigurationConfig: ServerConfigurationConfig = { delete data.passwordExpiryEnabled; delete data.passwordHistoryCount; delete data.passwordHistoryCountEnabled; + delete data.skipPasswordExpiryFallback; + delete data.passwordExpiryRules; - if (passwordExpiryEnabled && passwordExpiryTime === 0) { + // Default password expiry time. + if (passwordExpiryEnabled && !passwordExpirySkipFallback && passwordExpiryTime === 0) { passwordExpiryTime = 30; } @@ -257,6 +263,28 @@ const serverConfigurationConfig: ServerConfigurationConfig = { passwordHistoryCount = 1; } + const passwordExpiryProperties: UpdateGovernanceConnectorConfigPropertyInterface[] = [ + { + name: ServerConfigurationsConstants.PASSWORD_EXPIRY_ENABLE, + value: passwordExpiryEnabled?.toString() + }, + { + name: ServerConfigurationsConstants.PASSWORD_EXPIRY_TIME, + value: passwordExpiryTime?.toString() + }, + { + name: ServerConfigurationsConstants.PASSWORD_EXPIRY_SKIP_IF_NO_APPLICABLE_RULES, + value: passwordExpirySkipFallback?.toString() + } + ]; + + Object.entries(passwordExpiryRules as Record).forEach(([ key, value ]: [ string, string ]) => { + passwordExpiryProperties.push({ + name: key, + value: value + }); + }); + const legacyPasswordPoliciesData: { id: string, properties: UpdateGovernanceConnectorConfigPropertyInterface[] } = { id: ServerConfigurationsConstants.PASSWORD_POLICY_CONNECTOR_ID, @@ -298,16 +326,7 @@ const serverConfigurationConfig: ServerConfigurationConfig = { connectors: [ { id: ServerConfigurationsConstants.PASSWORD_EXPIRY_CONNECTOR_ID, - properties: [ - { - name: ServerConfigurationsConstants.PASSWORD_EXPIRY_ENABLE, - value: passwordExpiryEnabled?.toString() - }, - { - name: ServerConfigurationsConstants.PASSWORD_EXPIRY_TIME, - value: passwordExpiryTime?.toString() - } - ] + properties: passwordExpiryProperties }, { id: ServerConfigurationsConstants.PASSWORD_HISTORY_CONNECTOR_ID, diff --git a/features/admin.server-configurations.v1/constants/governance-connector-constants.ts b/features/admin.server-configurations.v1/constants/governance-connector-constants.ts index 2a026712e36..165a635f8a0 100644 --- a/features/admin.server-configurations.v1/constants/governance-connector-constants.ts +++ b/features/admin.server-configurations.v1/constants/governance-connector-constants.ts @@ -45,7 +45,6 @@ export class GovernanceConnectorConstants { EXPIRY_TIME_MIN_LENGTH: number; EXPIRY_TIME_MIN_VALUE: number; } = { - EXPIRY_TIME_MAX_LENGTH: 5, EXPIRY_TIME_MAX_VALUE: 10080, EXPIRY_TIME_MIN_LENGTH: 1, @@ -75,7 +74,6 @@ export class GovernanceConnectorConstants { SMS_OTP_CODE_LENGTH_MIN_LENGTH: number; SMS_OTP_CODE_LENGTH_MIN_VALUE: number; } = { - EXPIRY_TIME_MAX_LENGTH: 5, EXPIRY_TIME_MAX_VALUE: 10080, EXPIRY_TIME_MIN_LENGTH: 1, @@ -113,7 +111,6 @@ export class GovernanceConnectorConstants { FAILED_ATTEMPTS_MIN_LENGTH: number; FAILED_ATTEMPTS_MIN_VALUE: number; } = { - ACCOUNT_LOCK_INCREMENT_FACTOR_MAX_LENGTH: 2, ACCOUNT_LOCK_INCREMENT_FACTOR_MAX_VALUE: 10, ACCOUNT_LOCK_INCREMENT_FACTOR_MIN_LENGTH: 1, @@ -136,7 +133,11 @@ export class GovernanceConnectorConstants { EXPIRY_TIME_MAX_VALUE: number; EXPIRY_TIME_MIN_LENGTH: number; EXPIRY_TIME_MIN_VALUE: number; + EXPIRY_RULES_MAX_COUNT: number; + EXPIRY_RULE_MAX_VALUES_PER_RULE: number; } = { + EXPIRY_RULES_MAX_COUNT: 10, + EXPIRY_RULE_MAX_VALUES_PER_RULE: 5, EXPIRY_TIME_MAX_LENGTH: 5, EXPIRY_TIME_MAX_VALUE: 10080, EXPIRY_TIME_MIN_LENGTH: 1, diff --git a/features/admin.server-configurations.v1/constants/server-configurations-constants.ts b/features/admin.server-configurations.v1/constants/server-configurations-constants.ts index a368801ef75..ed75b690a4a 100644 --- a/features/admin.server-configurations.v1/constants/server-configurations-constants.ts +++ b/features/admin.server-configurations.v1/constants/server-configurations-constants.ts @@ -29,116 +29,116 @@ export class ServerConfigurationsConstants { * UUID of the identity governance account management policies category. * */ - public static readonly IDENTITY_GOVERNANCE_ACCOUNT_MANAGEMENT_POLICIES_ID: string = - "QWNjb3VudCBNYW5hZ2VtZW50IFBvbGljaWVz"; + public static readonly IDENTITY_GOVERNANCE_ACCOUNT_MANAGEMENT_POLICIES_ID: string = + "QWNjb3VudCBNYW5hZ2VtZW50IFBvbGljaWVz"; - /** + /** * Regex matcher to identify if the connector is deprecated. * */ - public static readonly DEPRECATION_MATCHER: string = "[Deprecated]"; + public static readonly DEPRECATION_MATCHER: string = "[Deprecated]"; - /** + /** * UUID of the identity governance self sign up connector. * */ - public static readonly SELF_SIGN_UP_CONNECTOR_ID: string = "c2VsZi1zaWduLXVw"; + public static readonly SELF_SIGN_UP_CONNECTOR_ID: string = "c2VsZi1zaWduLXVw"; - /** + /** * UUID of the identity governance light user registration connector. * */ - public static readonly LITE_USER_REGISTRATION_CONNECTOR_ID: string = "bGl0ZS11c2VyLXNpZ24tdXA"; + public static readonly LITE_USER_REGISTRATION_CONNECTOR_ID: string = "bGl0ZS11c2VyLXNpZ24tdXA"; - /** + /** * UUID of the identity governance account recovery connector. * */ - public static readonly ACCOUNT_RECOVERY_CONNECTOR_ID: string = "YWNjb3VudC1yZWNvdmVyeQ"; + public static readonly ACCOUNT_RECOVERY_CONNECTOR_ID: string = "YWNjb3VudC1yZWNvdmVyeQ"; - /** + /** * UUID of the identity governance password reset connector. * */ - public static readonly PASSWORD_RESET_CONNECTOR_ID: string = "YWRtaW4tZm9yY2VkLXBhc3N3b3JkLXJlc2V0"; + public static readonly PASSWORD_RESET_CONNECTOR_ID: string = "YWRtaW4tZm9yY2VkLXBhc3N3b3JkLXJlc2V0"; - /** + /** * UUID of the identity governance consent information connector. * */ - public static readonly CONSENT_INFO_CONNECTOR_ID: string = "cGlpLWNvbnRyb2xsZXI"; + public static readonly CONSENT_INFO_CONNECTOR_ID: string = "cGlpLWNvbnRyb2xsZXI"; - /** + /** * UUID of the identity governance analytics engine connector. * */ - public static readonly ANALYTICS_ENGINE_CONNECTOR_ID: string = "ZWxhc3RpYy1hbmFseXRpY3MtZW5naW5l"; + public static readonly ANALYTICS_ENGINE_CONNECTOR_ID: string = "ZWxhc3RpYy1hbmFseXRpY3MtZW5naW5l"; - /** + /** * UUID of the identity governance user claim update connector. * */ - public static readonly USER_CLAIM_UPDATE_CONNECTOR_ID: string = "dXNlci1jbGFpbS11cGRhdGU"; + public static readonly USER_CLAIM_UPDATE_CONNECTOR_ID: string = "dXNlci1jbGFpbS11cGRhdGU"; - /** + /** * UUID of the identity governance login policies category. * */ - public static readonly IDENTITY_GOVERNANCE_LOGIN_POLICIES_ID: string = "TG9naW4gUG9saWNpZXM"; + public static readonly IDENTITY_GOVERNANCE_LOGIN_POLICIES_ID: string = "TG9naW4gUG9saWNpZXM"; - /** + /** * UUID of the identity governance account locking connector. * */ - public static readonly ACCOUNT_LOCKING_CONNECTOR_ID: string = "YWNjb3VudC5sb2NrLmhhbmRsZXI"; + public static readonly ACCOUNT_LOCKING_CONNECTOR_ID: string = "YWNjb3VudC5sb2NrLmhhbmRsZXI"; - /** + /** * UUID of the identity governance account disabling connector. * */ - public static readonly ACCOUNT_DISABLING_CONNECTOR_ID: string = "YWNjb3VudC5kaXNhYmxlLmhhbmRsZXI"; + public static readonly ACCOUNT_DISABLING_CONNECTOR_ID: string = "YWNjb3VudC5kaXNhYmxlLmhhbmRsZXI"; - /** + /** * UUID of the identity governance captcha for sso login connector. * */ - public static readonly CAPTCHA_FOR_SSO_LOGIN_CONNECTOR_ID: string = "c3NvLmxvZ2luLnJlY2FwdGNoYQ"; + public static readonly CAPTCHA_FOR_SSO_LOGIN_CONNECTOR_ID: string = "c3NvLmxvZ2luLnJlY2FwdGNoYQ"; - /** + /** * UUID of the identity governance idle account suspend connector. * */ - public static readonly IDLE_ACCOUNT_SUSPEND_CONNECTOR_ID: string = "c3VzcGVuc2lvbi5ub3RpZmljYXRpb24"; + public static readonly IDLE_ACCOUNT_SUSPEND_CONNECTOR_ID: string = "c3VzcGVuc2lvbi5ub3RpZmljYXRpb24"; - /** + /** * UUID of the identity governance account disable connector. * */ - public static readonly ACCOUNT_DISABLE_CONNECTOR_ID: string = "YWNjb3VudC5kaXNhYmxlLmhhbmRsZXI"; + public static readonly ACCOUNT_DISABLE_CONNECTOR_ID: string = "YWNjb3VudC5kaXNhYmxlLmhhbmRsZXI"; - /** + /** * UUID of the identity governance login policies category. * */ - public static readonly IDENTITY_GOVERNANCE_PASSWORD_POLICIES_ID: string = "UGFzc3dvcmQgUG9saWNpZXM"; + public static readonly IDENTITY_GOVERNANCE_PASSWORD_POLICIES_ID: string = "UGFzc3dvcmQgUG9saWNpZXM"; - /** + /** * UUID of the identity governance captcha for sso login connector. * */ - public static readonly PASSWORD_HISTORY_CONNECTOR_ID: string = "cGFzc3dvcmRIaXN0b3J5"; + public static readonly PASSWORD_HISTORY_CONNECTOR_ID: string = "cGFzc3dvcmRIaXN0b3J5"; - /** + /** * UUID of the identity governance password expiry connector. */ - public static readonly PASSWORD_EXPIRY_CONNECTOR_ID: string = "cGFzc3dvcmRFeHBpcnk"; + public static readonly PASSWORD_EXPIRY_CONNECTOR_ID: string = "cGFzc3dvcmRFeHBpcnk"; - /** + /** * UUID of the identity governance captcha for sso login connector. * */ - public static readonly PASSWORD_POLICY_CONNECTOR_ID: string = "cGFzc3dvcmRQb2xpY3k"; + public static readonly PASSWORD_POLICY_CONNECTOR_ID: string = "cGFzc3dvcmRQb2xpY3k"; /** * Multi Attribute Login Claim List pattern regex. @@ -147,40 +147,40 @@ export class ServerConfigurationsConstants { public static readonly MULTI_ATTRIBUTE_CLAIM_LIST_REGEX_PATTERN: RegExp = new RegExp("^(?:[a-zA-Z0-9:./]+,)*[a-zA-Z0-9:./]+$"); - /** + /** * UUID of the user on boarding connector. */ - public static readonly USER_ONBOARDING_CONNECTOR_ID: string = "VXNlciBPbmJvYXJkaW5n"; + public static readonly USER_ONBOARDING_CONNECTOR_ID: string = "VXNlciBPbmJvYXJkaW5n"; - /** + /** * UUID of the email verification category. */ - public static readonly USER_EMAIL_VERIFICATION_CONNECTOR_ID: string = "dXNlci1lbWFpbC12ZXJpZmljYXRpb24"; + public static readonly USER_EMAIL_VERIFICATION_CONNECTOR_ID: string = "dXNlci1lbWFpbC12ZXJpZmljYXRpb24"; - /** + /** * UUID of the Other Settings governance connector category. */ - public static readonly OTHER_SETTINGS_CONNECTOR_CATEGORY_ID: string = "T3RoZXIgU2V0dGluZ3M"; + public static readonly OTHER_SETTINGS_CONNECTOR_CATEGORY_ID: string = "T3RoZXIgU2V0dGluZ3M"; - /** + /** * UUID of the ELK Analaytics connector. */ - public static readonly ELK_ANALYTICS_CONNECTOR_ID: string = "ZWxhc3RpYy1hbmFseXRpY3MtZW5naW5l"; + public static readonly ELK_ANALYTICS_CONNECTOR_ID: string = "ZWxhc3RpYy1hbmFseXRpY3MtZW5naW5l"; - /** + /** * UUID of the Login Attempt Security governance connector category. */ - public static readonly LOGIN_ATTEMPT_SECURITY_CONNECTOR_CATEGORY_ID: string = "TG9naW4gQXR0ZW1wdHMgU2VjdXJpdHk"; + public static readonly LOGIN_ATTEMPT_SECURITY_CONNECTOR_CATEGORY_ID: string = "TG9naW4gQXR0ZW1wdHMgU2VjdXJpdHk"; - /** + /** * UUID of the Account Management governance connector category. */ - public static readonly ACCOUNT_MANAGEMENT_CONNECTOR_CATEGORY_ID: string = "QWNjb3VudCBNYW5hZ2VtZW50"; + public static readonly ACCOUNT_MANAGEMENT_CONNECTOR_CATEGORY_ID: string = "QWNjb3VudCBNYW5hZ2VtZW50"; - /** + /** * UUID of the Multi-Factor Authenticators governance connector category. */ - public static readonly MFA_CONNECTOR_CATEGORY_ID: string = "TXVsdGkgRmFjdG9yIEF1dGhlbnRpY2F0b3Jz"; + public static readonly MFA_CONNECTOR_CATEGORY_ID: string = "TXVsdGkgRmFjdG9yIEF1dGhlbnRpY2F0b3Jz"; /** * UUID of the WSO2 Analytics Engine governance connector category. @@ -192,195 +192,199 @@ export class ServerConfigurationsConstants { */ public static readonly EMAIL_VERIFICATION_ENABLED: string = "EmailVerification.Enable"; - /** + /** * Self registration API Keyword constants. */ - public static readonly SELF_REGISTRATION_ENABLE: string = "SelfRegistration.Enable"; - public static readonly ACCOUNT_LOCK_ON_CREATION: string = "SelfRegistration.LockOnCreation"; - public static readonly SELF_SIGN_UP_NOTIFICATIONS_INTERNALLY_MANAGED: string = - "SelfRegistration.Notification.InternallyManage"; + public static readonly SELF_REGISTRATION_ENABLE: string = "SelfRegistration.Enable"; + public static readonly ACCOUNT_LOCK_ON_CREATION: string = "SelfRegistration.LockOnCreation"; + public static readonly SELF_SIGN_UP_NOTIFICATIONS_INTERNALLY_MANAGED: string = + "SelfRegistration.Notification.InternallyManage"; - public static readonly ACCOUNT_CONFIRMATION: string = "SelfRegistration.SendConfirmationOnCreation"; - public static readonly RE_CAPTCHA: string = "SelfRegistration.ReCaptcha"; - public static readonly VERIFICATION_CODE_EXPIRY_TIME: string = "SelfRegistration.VerificationCode.ExpiryTime"; - public static readonly SMS_OTP_EXPIRY_TIME: string = "SelfRegistration.VerificationCode.SMSOTP.ExpiryTime"; - public static readonly CALLBACK_REGEX: string = "SelfRegistration.CallbackRegex"; + public static readonly ACCOUNT_CONFIRMATION: string = "SelfRegistration.SendConfirmationOnCreation"; + public static readonly RE_CAPTCHA: string = "SelfRegistration.ReCaptcha"; + public static readonly VERIFICATION_CODE_EXPIRY_TIME: string = "SelfRegistration.VerificationCode.ExpiryTime"; + public static readonly SMS_OTP_EXPIRY_TIME: string = "SelfRegistration.VerificationCode.SMSOTP.ExpiryTime"; + public static readonly CALLBACK_REGEX: string = "SelfRegistration.CallbackRegex"; - /** + /** * Account recovery API Keyword constants. */ - public static readonly USERNAME_RECOVERY_ENABLE: string = "Recovery.Notification.Username.Enable"; - public static readonly USERNAME_RECOVERY_RE_CAPTCHA: string = "Recovery.ReCaptcha.Username.Enable"; - public static readonly PASSWORD_RECOVERY_NOTIFICATION_BASED_ENABLE: string = - "Recovery.Notification.Password.Enable"; + public static readonly USERNAME_RECOVERY_ENABLE: string = "Recovery.Notification.Username.Enable"; + public static readonly USERNAME_RECOVERY_RE_CAPTCHA: string = "Recovery.ReCaptcha.Username.Enable"; + public static readonly PASSWORD_RECOVERY_NOTIFICATION_BASED_ENABLE: string = + "Recovery.Notification.Password.Enable"; - public static readonly PASSWORD_RECOVERY_NOTIFICATION_BASED_RE_CAPTCHA: string = - "Recovery.ReCaptcha.Password.Enable"; + public static readonly PASSWORD_RECOVERY_NOTIFICATION_BASED_RE_CAPTCHA: string = + "Recovery.ReCaptcha.Password.Enable"; - public static readonly PASSWORD_RECOVERY_QUESTION_BASED_ENABLE: string = "Recovery.Question.Password.Enable"; - public static readonly PASSWORD_RECOVERY_QUESTION_BASED_MIN_ANSWERS: string = - "Recovery.Question.Password.MinAnswers"; + public static readonly PASSWORD_RECOVERY_QUESTION_BASED_ENABLE: string = "Recovery.Question.Password.Enable"; + public static readonly PASSWORD_RECOVERY_QUESTION_BASED_MIN_ANSWERS: string = + "Recovery.Question.Password.MinAnswers"; - public static readonly PASSWORD_RECOVERY_QUESTION_BASED_RE_CAPTCHA_ENABLE: string = - "Recovery.Question.Password.ReCaptcha.Enable"; + public static readonly PASSWORD_RECOVERY_QUESTION_BASED_RE_CAPTCHA_ENABLE: string = + "Recovery.Question.Password.ReCaptcha.Enable"; - public static readonly RE_CAPTCHA_MAX_FAILED_ATTEMPTS: string = - "Recovery.Question.Password.ReCaptcha.MaxFailedAttempts"; + public static readonly RE_CAPTCHA_MAX_FAILED_ATTEMPTS: string = + "Recovery.Question.Password.ReCaptcha.MaxFailedAttempts"; - public static readonly ACCOUNT_RECOVERY_NOTIFICATIONS_INTERNALLY_MANAGED: string = - "Recovery.Notification.InternallyManage"; + public static readonly ACCOUNT_RECOVERY_NOTIFICATIONS_INTERNALLY_MANAGED: string = + "Recovery.Notification.InternallyManage"; - public static readonly NOTIFY_RECOVERY_START: string = "Recovery.Question.Password.NotifyStart"; - public static readonly NOTIFY_SUCCESS: string = "Recovery.NotifySuccess"; - public static readonly RECOVERY_LINK_EXPIRY_TIME: string = "Recovery.ExpiryTime"; - public static readonly RECOVERY_SMS_EXPIRY_TIME: string = "Recovery.Notification.Password.ExpiryTime.smsOtp"; - public static readonly RECOVERY_CALLBACK_REGEX: string = "Recovery.CallbackRegex"; - public static readonly PASSWORD_RECOVERY_QUESTION_FORCED_ENABLE: string = - "Recovery.Question.Password.Forced.Enable"; + public static readonly NOTIFY_RECOVERY_START: string = "Recovery.Question.Password.NotifyStart"; + public static readonly NOTIFY_SUCCESS: string = "Recovery.NotifySuccess"; + public static readonly RECOVERY_LINK_EXPIRY_TIME: string = "Recovery.ExpiryTime"; + public static readonly RECOVERY_SMS_EXPIRY_TIME: string = "Recovery.Notification.Password.ExpiryTime.smsOtp"; + public static readonly RECOVERY_CALLBACK_REGEX: string = "Recovery.CallbackRegex"; + public static readonly PASSWORD_RECOVERY_QUESTION_FORCED_ENABLE: string = + "Recovery.Question.Password.Forced.Enable"; - public static readonly RECOVERY_EMAIL_LINK_ENABLE: string = "Recovery.Notification.Password.emailLink.Enable"; - public static readonly RECOVERY_SMS_OTP_ENABLE: string = "Recovery.Notification.Password.smsOtp.Enable"; - public static readonly RECOVERY_OTP_USE_UPPERCASE: string = - "Recovery.Notification.Password.OTP.UseUppercaseCharactersInOTP"; + public static readonly RECOVERY_EMAIL_LINK_ENABLE: string = "Recovery.Notification.Password.emailLink.Enable"; + public static readonly RECOVERY_SMS_OTP_ENABLE: string = "Recovery.Notification.Password.smsOtp.Enable"; + public static readonly RECOVERY_OTP_USE_UPPERCASE: string = + "Recovery.Notification.Password.OTP.UseUppercaseCharactersInOTP"; - public static readonly RECOVERY_OTP_USE_LOWERCASE: string = - "Recovery.Notification.Password.OTP.UseLowercaseCharactersInOTP"; + public static readonly RECOVERY_OTP_USE_LOWERCASE: string = + "Recovery.Notification.Password.OTP.UseLowercaseCharactersInOTP"; - public static readonly RECOVERY_OTP_USE_NUMERIC: string = "Recovery.Notification.Password.OTP.UseNumbersInOTP"; - public static readonly RECOVERY_OTP_LENGTH: string = "Recovery.Notification.Password.OTP.OTPLength"; - public static readonly RECOVERY_MAX_RESEND_COUNT: string = "Recovery.Notification.Password.MaxResendAttempts"; - public static readonly RECOVERY_MAX_FAILED_ATTEMPTS_COUNT: string = - "Recovery.Notification.Password.MaxFailedAttempts"; + public static readonly RECOVERY_OTP_USE_NUMERIC: string = "Recovery.Notification.Password.OTP.UseNumbersInOTP"; + public static readonly RECOVERY_OTP_LENGTH: string = "Recovery.Notification.Password.OTP.OTPLength"; + public static readonly RECOVERY_MAX_RESEND_COUNT: string = "Recovery.Notification.Password.MaxResendAttempts"; + public static readonly RECOVERY_MAX_FAILED_ATTEMPTS_COUNT: string = + "Recovery.Notification.Password.MaxFailedAttempts"; - /** + /** * Connector toggle constants. */ - public static readonly ACCOUNT_RECOVERY: string = "account-recovery"; - public static readonly ACCOUNT_RECOVERY_BY_USERNAME: string = "account-recovery-username"; - public static readonly ACCOUNT_LOCK_HANDLER: string = "account.lock.handler"; - public static readonly MULTI_ATTRIBUTE_LOGIN_HANDLER: string = "multiattribute.login.handler"; - public static readonly ORGANIZATION_SELF_SERVICE: string = "organization-self-service"; - public static readonly SELF_SIGNUP: string = "self-sign-up"; - public static readonly SSO_LOGIN_RECAPTCHA: string = "sso.login.recaptcha"; + public static readonly ACCOUNT_RECOVERY: string = "account-recovery"; + public static readonly ACCOUNT_RECOVERY_BY_USERNAME: string = "account-recovery-username"; + public static readonly ACCOUNT_LOCK_HANDLER: string = "account.lock.handler"; + public static readonly MULTI_ATTRIBUTE_LOGIN_HANDLER: string = "multiattribute.login.handler"; + public static readonly ORGANIZATION_SELF_SERVICE: string = "organization-self-service"; + public static readonly SELF_SIGNUP: string = "self-sign-up"; + public static readonly SSO_LOGIN_RECAPTCHA: string = "sso.login.recaptcha"; - /** + /** * Login policies - account locking API Keyword constants. */ - public static readonly ACCOUNT_LOCK_ENABLE: string = "account.lock.handler.lock.on.max.failed.attempts.enable"; - public static readonly ANALYTICS_ENGINE_ENABLE: string = "adaptive_authentication.analytics.basicAuth.enabled"; + public static readonly ACCOUNT_LOCK_ENABLE: string = "account.lock.handler.lock.on.max.failed.attempts.enable"; + public static readonly ANALYTICS_ENGINE_ENABLE: string = "adaptive_authentication.analytics.basicAuth.enabled"; - public static readonly MAX_FAILED_LOGIN_ATTEMPTS_TO_ACCOUNT_LOCK: string = - "account.lock.handler.On.Failure.Max.Attempts"; + public static readonly MAX_FAILED_LOGIN_ATTEMPTS_TO_ACCOUNT_LOCK: string = + "account.lock.handler.On.Failure.Max.Attempts"; - public static readonly ACCOUNT_LOCK_TIME: string = "account.lock.handler.Time"; - public static readonly ACCOUNT_LOCK_TIME_INCREMENT_FACTOR: string = "account.lock.handler.login.fail.timeout.ratio"; - public static readonly ACCOUNT_LOCK_INTERNAL_NOTIFICATION_MANAGEMENT: string = - "account.lock.handler.notification.manageInternally"; + public static readonly ACCOUNT_LOCK_TIME: string = "account.lock.handler.Time"; + public static readonly ACCOUNT_LOCK_TIME_INCREMENT_FACTOR: string = "account.lock.handler.login.fail.timeout.ratio"; + public static readonly ACCOUNT_LOCK_INTERNAL_NOTIFICATION_MANAGEMENT: string = + "account.lock.handler.notification.manageInternally"; - public static readonly NOTIFY_USER_ON_ACCOUNT_LOCK_INCREMENT: string = - "account.lock.handler.notification.notifyOnLockIncrement"; + public static readonly NOTIFY_USER_ON_ACCOUNT_LOCK_INCREMENT: string = + "account.lock.handler.notification.notifyOnLockIncrement"; - /** + /** * Login policies - account disabling API Keyword constants. */ - public static readonly ACCOUNT_DISABLING_ENABLE: string = "account.disable.handler.enable"; - public static readonly ACCOUNT_DISABLE_INTERNAL_NOTIFICATION_MANAGEMENT: string = - "account.disable.handler.notification.manageInternally"; + public static readonly ACCOUNT_DISABLING_ENABLE: string = "account.disable.handler.enable"; + public static readonly ACCOUNT_DISABLE_INTERNAL_NOTIFICATION_MANAGEMENT: string = + "account.disable.handler.notification.manageInternally"; - /** + /** * Login policies - captcha for sso login API Keyword constants. */ - public static readonly RE_CAPTCHA_ALWAYS_ENABLE: string = "sso.login.recaptcha.enable.always"; - public static readonly RE_CAPTCHA_AFTER_MAX_FAILED_ATTEMPTS_ENABLE: string = - "sso.login.recaptcha.enable"; + public static readonly RE_CAPTCHA_ALWAYS_ENABLE: string = "sso.login.recaptcha.enable.always"; + public static readonly RE_CAPTCHA_AFTER_MAX_FAILED_ATTEMPTS_ENABLE: string = + "sso.login.recaptcha.enable"; - public static readonly MAX_FAILED_LOGIN_ATTEMPTS_TO_RE_CAPTCHA: string = - "sso.login.recaptcha.on.max.failed.attempts"; + public static readonly MAX_FAILED_LOGIN_ATTEMPTS_TO_RE_CAPTCHA: string = + "sso.login.recaptcha.on.max.failed.attempts"; - /** + /** * Login policies - API Keyword constants. */ public static readonly PASSWORD_EXPIRY_ENABLE: string = "passwordExpiry.enablePasswordExpiry"; public static readonly PASSWORD_EXPIRY_TIME: string = "passwordExpiry.passwordExpiryInDays"; - public static readonly PASSWORD_HISTORY_ENABLE: string = "passwordHistory.enable"; - public static readonly PASSWORD_HISTORY_COUNT: string = "passwordHistory.count"; - public static readonly PASSWORD_POLICY_ENABLE: string = "passwordPolicy.enable"; - public static readonly PASSWORD_POLICY_MIN_LENGTH: string = "passwordPolicy.min.length"; - public static readonly PASSWORD_POLICY_MAX_LENGTH: string = "passwordPolicy.max.length"; - public static readonly PASSWORD_POLICY_PATTERN: string = "passwordPolicy.pattern"; - public static readonly PASSWORD_POLICY_ERROR_MESSAGE: string = "passwordPolicy.errorMsg"; - - /** + public static readonly PASSWORD_EXPIRY_SKIP_IF_NO_APPLICABLE_RULES: string = + "passwordExpiry.skipIfNoApplicableRules"; + + public static readonly PASSWORD_EXPIRY_RULES_PREFIX: string = "passwordExpiry.rule"; + public static readonly PASSWORD_HISTORY_ENABLE: string = "passwordHistory.enable"; + public static readonly PASSWORD_HISTORY_COUNT: string = "passwordHistory.count"; + public static readonly PASSWORD_POLICY_ENABLE: string = "passwordPolicy.enable"; + public static readonly PASSWORD_POLICY_MIN_LENGTH: string = "passwordPolicy.min.length"; + public static readonly PASSWORD_POLICY_MAX_LENGTH: string = "passwordPolicy.max.length"; + public static readonly PASSWORD_POLICY_PATTERN: string = "passwordPolicy.pattern"; + public static readonly PASSWORD_POLICY_ERROR_MESSAGE: string = "passwordPolicy.errorMsg"; + + /** * Real Configurations constants. */ - public static readonly HOME_REALM_IDENTIFIER: string = "homeRealmIdentifiers"; - public static readonly IDLE_SESSION_TIMEOUT_PERIOD: string = "idleSessionTimeoutPeriod"; - public static readonly REMEMBER_ME_PERIOD: string = "rememberMePeriod"; + public static readonly HOME_REALM_IDENTIFIER: string = "homeRealmIdentifiers"; + public static readonly IDLE_SESSION_TIMEOUT_PERIOD: string = "idleSessionTimeoutPeriod"; + public static readonly REMEMBER_ME_PERIOD: string = "rememberMePeriod"; - // API Errors. - public static readonly CONFIGS_FETCH_REQUEST_INVALID_STATUS_CODE_ERROR: string = "Received an invalid status " + + // API Errors. + public static readonly CONFIGS_FETCH_REQUEST_INVALID_STATUS_CODE_ERROR: string = "Received an invalid status " + "code while retrieving the configurations."; - public static readonly CONFIGS_FETCH_REQUEST_ERROR: string = "An error occurred while retrieving the " + + public static readonly CONFIGS_FETCH_REQUEST_ERROR: string = "An error occurred while retrieving the " + "configurations."; - public static readonly CONFIGS_UPDATE_REQUEST_INVALID_STATUS_CODE_ERROR: string = "Received an invalid status " + + public static readonly CONFIGS_UPDATE_REQUEST_INVALID_STATUS_CODE_ERROR: string = "Received an invalid status " + "code while updating the configurations."; - public static readonly CONFIGS_UPDATE_REQUEST_ERROR: string = "An error occurred while updating the " + + public static readonly CONFIGS_UPDATE_REQUEST_ERROR: string = "An error occurred while updating the " + "configurations."; public static readonly ADMIN_ADVISORY_BANNER_CONFIGS_UPDATE_REQUEST_ERROR: string = "An error occurred " + "while updating the admin advisory banner configurations."; - public static readonly ADMIN_ADVISORY_BANNER_CONFIGS_INVALID_INPUT_ERROR: string = "An invalid input value " + + public static readonly ADMIN_ADVISORY_BANNER_CONFIGS_INVALID_INPUT_ERROR: string = "An invalid input value " + "in the request."; - // Idle account suspend names. - public static readonly ALLOWED_IDLE_TIME_SPAN_IN_DAYS: string = "suspension.notification.account.disable.delay"; - public static readonly ALERT_SENDING_TIME_PERIODS_IN_DAYS: string = "suspension.notification.delays"; + // Idle account suspend names. + public static readonly ALLOWED_IDLE_TIME_SPAN_IN_DAYS: string = "suspension.notification.account.disable.delay"; + public static readonly ALERT_SENDING_TIME_PERIODS_IN_DAYS: string = "suspension.notification.delays"; - /** + /** * Account Management Connector Constants. */ - public static readonly ACCOUNT_MANAGEMENT_CATEGORY_ID: string = "QWNjb3VudCBNYW5hZ2VtZW50"; - public static readonly ADMIN_FORCE_PASSWORD_RESET_CONNECTOR_ID: string = "YWRtaW4tZm9yY2VkLXBhc3N3b3JkLXJlc2V0"; - public static readonly RECOVERY_LINK_PASSWORD_RESET: string = "Recovery.AdminPasswordReset.RecoveryLink"; - public static readonly OTP_PASSWORD_RESET: string = "Recovery.AdminPasswordReset.OTP"; - public static readonly OFFLINE_PASSWORD_RESET: string = "Recovery.AdminPasswordReset.Offline"; - public static readonly ADMIN_FORCED_PASSWORD_RESET_EXPIRY_TIME: string = "Recovery.AdminPasswordReset.ExpiryTime"; + public static readonly ACCOUNT_MANAGEMENT_CATEGORY_ID: string = "QWNjb3VudCBNYW5hZ2VtZW50"; + public static readonly ADMIN_FORCE_PASSWORD_RESET_CONNECTOR_ID: string = "YWRtaW4tZm9yY2VkLXBhc3N3b3JkLXJlc2V0"; + public static readonly RECOVERY_LINK_PASSWORD_RESET: string = "Recovery.AdminPasswordReset.RecoveryLink"; + public static readonly OTP_PASSWORD_RESET: string = "Recovery.AdminPasswordReset.OTP"; + public static readonly OFFLINE_PASSWORD_RESET: string = "Recovery.AdminPasswordReset.Offline"; + public static readonly ADMIN_FORCED_PASSWORD_RESET_EXPIRY_TIME: string = "Recovery.AdminPasswordReset.ExpiryTime"; public static readonly MULTI_ATTRIBUTE_CLAIM_LIST: string = "account-multiattributelogin-handler-allowedattributes"; - /** + /** * Analytics Engine Connector Constants. */ - public static readonly ANALYTICS_HOST: string = "adaptive_authentication.elastic.receiver"; - public static readonly ANALYTICS_BASIC_AUTH_ENABLE: string = "adaptive_authentication.elastic.basicAuth.enabled"; - public static readonly ANALYTICS_BASIC_AUTH_USERNAME: string = "adaptive_authentication.elastic.basicAuth.username"; - public static readonly ANALYTICS_BASIC_AUTH_PASSWORD: string = - "__secret__adaptive_authentication.elastic.basicAuth.password"; + public static readonly ANALYTICS_HOST: string = "adaptive_authentication.elastic.receiver"; + public static readonly ANALYTICS_BASIC_AUTH_ENABLE: string = "adaptive_authentication.elastic.basicAuth.enabled"; + public static readonly ANALYTICS_BASIC_AUTH_USERNAME: string = "adaptive_authentication.elastic.basicAuth.username"; + public static readonly ANALYTICS_BASIC_AUTH_PASSWORD: string = + "__secret__adaptive_authentication.elastic.basicAuth.password"; - public static readonly ANALYTICS_HTTP_CONNECTION_TIMEOUT: string = - "adaptive_authentication.elastic.HTTPConnectionTimeout"; + public static readonly ANALYTICS_HTTP_CONNECTION_TIMEOUT: string = + "adaptive_authentication.elastic.HTTPConnectionTimeout"; - public static readonly ANALYTICS_HTTP_READ_TIMEOUT: string = "adaptive_authentication.elastic.HTTPReadTimeout"; - public static readonly ANALYTICS_HTTP_CONNECTION_REQUEST_TIMEOUT: string = - "adaptive_authentication.elastic.HTTPConnectionRequestTimeout"; + public static readonly ANALYTICS_HTTP_READ_TIMEOUT: string = "adaptive_authentication.elastic.HTTPReadTimeout"; + public static readonly ANALYTICS_HTTP_CONNECTION_REQUEST_TIMEOUT: string = + "adaptive_authentication.elastic.HTTPConnectionRequestTimeout"; - public static readonly ANALYTICS_HOSTNAME_VERIFICATION: string = "adaptive_authentication.elastic.hostnameVerfier"; + public static readonly ANALYTICS_HOSTNAME_VERIFICATION: string = "adaptive_authentication.elastic.hostnameVerfier"; - /** + /** * Extensions Constants. */ - public static readonly ALL: string = "all"; + public static readonly ALL: string = "all"; - /** + /** * Custom connector IDs. */ - public static readonly SAML2_SSO_CONNECTOR_ID: string = "saml2-sso" - public static readonly SESSION_MANAGEMENT_CONNECTOR_ID: string = "session-management" - public static readonly WS_FEDERATION_CONNECTOR_ID: string = "ws-fed" + public static readonly SAML2_SSO_CONNECTOR_ID: string = "saml2-sso"; + public static readonly SESSION_MANAGEMENT_CONNECTOR_ID: string = "session-management"; + public static readonly WS_FEDERATION_CONNECTOR_ID: string = "ws-fed"; /** * Predefined connector catergory IDs. @@ -390,11 +394,11 @@ export class ServerConfigurationsConstants { public static readonly PROVISIONING_SETTINGS_CATEGORY_ID: string = "provider-settings"; public static readonly OUTBOUND_PROVISIONING_SETTINGS_CONNECTOR_ID: string = "outbound-provisioning-settings"; - /** + /** * Multi Attribute Login Constants. */ - public static readonly MULTI_ATTRIBUTE_LOGIN_CONNECTOR_ID: string = "bXVsdGlhdHRyaWJ1dGUubG9naW4uaGFuZGxlcg"; - public static readonly MULTI_ATTRIBUTE_LOGIN_ENABLE: string = "account.multiattributelogin.handler.enable"; + public static readonly MULTI_ATTRIBUTE_LOGIN_CONNECTOR_ID: string = "bXVsdGlhdHRyaWJ1dGUubG9naW4uaGFuZGxlcg"; + public static readonly MULTI_ATTRIBUTE_LOGIN_ENABLE: string = "account.multiattributelogin.handler.enable"; public static readonly ALTERNATIVE_LOGIN_IDENTIFIER: string = "alternative-login-identifier"; public static readonly USERNAME_VALIDATION: string = "username-validation"; public static readonly PASSWORD_RECOVERY: string = "password-recovery"; @@ -416,10 +420,10 @@ export class ServerConfigurationsConstants { public static readonly LOGIN_ATTEMPT_SECURITY: string = "login-attempt-security"; - /** + /** * Organization Settings Category Constants. */ - public static readonly ORGANIZATION_SETTINGS_CATEGORY_ID: string = "organization-settings"; - public static readonly EMAIL_DOMAIN_DISCOVERY: string = "ZW1haWwtZG9tYWluLWRpc2NvdmVyeQ=="; - public static readonly IMPERSONATION: string = "impersonation"; + public static readonly ORGANIZATION_SETTINGS_CATEGORY_ID: string = "organization-settings"; + public static readonly EMAIL_DOMAIN_DISCOVERY: string = "ZW1haWwtZG9tYWluLWRpc2NvdmVyeQ=="; + public static readonly IMPERSONATION: string = "impersonation"; } diff --git a/features/admin.validation.v1/components/password-expiry-rule-list.tsx b/features/admin.validation.v1/components/password-expiry-rule-list.tsx new file mode 100644 index 00000000000..85e2a3c0a13 --- /dev/null +++ b/features/admin.validation.v1/components/password-expiry-rule-list.tsx @@ -0,0 +1,559 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SelectChangeEvent } from "@mui/material/Select"; +import Alert from "@oxygen-ui/react/Alert"; +import Button from "@oxygen-ui/react/Button"; +import Checkbox from "@oxygen-ui/react/Checkbox"; +import Chip from "@oxygen-ui/react/Chip"; +import Grid from "@oxygen-ui/react/Grid/Grid"; +import IconButton from "@oxygen-ui/react/IconButton"; +import List from "@oxygen-ui/react/List"; +import ListItem from "@oxygen-ui/react/ListItem"; +import ListItemText from "@oxygen-ui/react/ListItemText"; +import MenuItem from "@oxygen-ui/react/MenuItem"; +import Select from "@oxygen-ui/react/Select"; +import TextField from "@oxygen-ui/react/TextField"; +import { ChevronDownIcon, ChevronUpIcon, PlusIcon, TrashIcon } from "@oxygen-ui/react-icons"; +import { userstoresConfig } from "@wso2is/admin.extensions.v1"; +import { GroupsInterface } from "@wso2is/admin.groups.v1"; +import { + GovernanceConnectorConstants +} from "@wso2is/admin.server-configurations.v1/constants/governance-connector-constants"; +import { RolesInterface } from "@wso2is/core/models"; +import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { PasswordExpiryRule, PasswordExpiryRuleAttribute, PasswordExpiryRuleOperator } from "../models"; + +interface PasswordExpiryRuleListProps { + componentId: string; + ruleList: PasswordExpiryRule[]; + isPasswordExpiryEnabled: boolean; + isSkipFallbackEnabled: boolean; + defaultPasswordExpiryTime: number; + rolesList: RolesInterface[]; + groupsList: GroupsInterface[]; + isReadOnly: boolean; + onSkipFallbackChange: (value: boolean) => void; + onDefaultPasswordExpiryTimeChange: (value: number) => void; + onRuleChange: (rules: PasswordExpiryRule[]) => void; + onRuleError: (hasErrors: boolean) => void; +} + +type Resource = RolesInterface | GroupsInterface; + +export const PasswordExpiryRuleList: FunctionComponent = ( + props: PasswordExpiryRuleListProps +) => { + const { + componentId, + ruleList, + isPasswordExpiryEnabled, + isSkipFallbackEnabled, + defaultPasswordExpiryTime, + rolesList, + groupsList, + isReadOnly, + onSkipFallbackChange, + onDefaultPasswordExpiryTimeChange, + onRuleChange, + onRuleError + } = props; + + const [ rules, setRules ] = useState(ruleList); + const [ hasErrors, setHasErrors ] = useState<{ [key: string]: { values: boolean; expiryDays: boolean } }>({}); + const { t } = useTranslation(); + + useEffect(() => { + setRules(ruleList); + validateRules(ruleList); + }, [ ruleList ]); + + /** + * Validate the password expiry rules. + * + * @param rulesToValidate - Password expiry rules to validate. + */ + const validateRules = (rulesToValidate: PasswordExpiryRule[]) => { + const ruleValidationErrors: { [key: string]: { values: boolean; expiryDays: boolean } } = {}; + let hasAnyError: boolean = false; + + rulesToValidate.forEach((rule: PasswordExpiryRule) => { + ruleValidationErrors[rule?.id] = { expiryDays: false, values: false }; + + if (rule?.values.length === 0) { + ruleValidationErrors[rule?.id].values = true; + hasAnyError = true; + } + + if (rule?.operator === PasswordExpiryRuleOperator.EQ + && (rule?.expiryDays < + GovernanceConnectorConstants.PASSWORD_EXPIRY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MIN_VALUE + || rule?.expiryDays > + GovernanceConnectorConstants.PASSWORD_EXPIRY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_VALUE)) { + ruleValidationErrors[rule?.id].expiryDays = true; + hasAnyError = true; + } + }); + + setHasErrors(ruleValidationErrors); + onRuleError(hasAnyError); + }; + + + /** + * Handle the rule change. + * + * @param index - index of the rule. + * @param field - field to update. + * @param value - value to update. + */ + const handleRuleChange = (index: number, field: keyof PasswordExpiryRule, value: any) => { + const updatedRules: PasswordExpiryRule[] = [ ...rules ]; + + updatedRules[index] = { ...updatedRules[index], [field]: value }; + updateRules(updatedRules); + }; + + /** + * Add a new rule. + */ + const addRule = () => { + if (rules?.length >= GovernanceConnectorConstants.PASSWORD_EXPIRY_FORM_FIELD_CONSTRAINTS. + EXPIRY_RULES_MAX_COUNT) return; + + const newRule: PasswordExpiryRule = { + attribute: PasswordExpiryRuleAttribute.ROLES, + expiryDays: 30, + id: `rule${Date.now()}`, + operator: PasswordExpiryRuleOperator.EQ, + priority: 1, + values: [] + }; + const updatedRules: PasswordExpiryRule[] = + [ newRule, ...rules.map((rule: PasswordExpiryRule) => ({ ...rule, priority: rule?.priority + 1 })) ]; + + updateRules(updatedRules); + }; + + /** + * Delete a rule. + * + * @param id - id of the rule to delete. + */ + const deleteRule = (id: string) => { + const updatedRules: PasswordExpiryRule[] = rules + .filter((rule: PasswordExpiryRule) => rule?.id !== id) + .map((rule: PasswordExpiryRule, index: number) => ({ ...rule, priority: index + 1 })); + + updateRules(updatedRules); + }; + + /** + * Update the rules. + * + * @param updatedRules - Updated rules. + */ + const updateRules = (updatedRules: PasswordExpiryRule[]) => { + setRules(updatedRules); + onRuleChange(updatedRules); + }; + + /** + * Move the priority of a rule. + * + * @param index - index of the rule. + * @param direction - direction to move the rule. + */ + const movePriority = (index: number, direction: "up" | "down") => { + if ((direction === "up" && index === 0) || (direction === "down" && index === rules.length - 1)) { + return; + } + const updatedRules: PasswordExpiryRule[] = [ ...rules ]; + const swapIndex: number = direction === "up" ? index - 1 : index + 1; + + [ updatedRules[index], updatedRules[swapIndex] ] = [ updatedRules[swapIndex], updatedRules[index] ]; + updatedRules.forEach((rule: PasswordExpiryRule, i: number) => rule.priority = i + 1); + updateRules(updatedRules); + }; + + const attributeOptions: {label: string, value: PasswordExpiryRuleAttribute}[] = [ + { label: t("validation:passwordExpiry.rules.attributes.roles"), value: PasswordExpiryRuleAttribute.ROLES }, + { label: t("validation:passwordExpiry.rules.attributes.groups"), value: PasswordExpiryRuleAttribute.GROUPS } + ]; + + const operatorOptions: { label: string, value: PasswordExpiryRuleOperator }[] = [ + { label: t("validation:passwordExpiry.rules.actions.apply"), value: PasswordExpiryRuleOperator.EQ }, + { label: t("validation:passwordExpiry.rules.actions.skip"), value: PasswordExpiryRuleOperator.NE } + ]; + + /** + * Handle the expiry days change. + * + * @param index - index of the rule. + * @param value - value to update. + */ + const handleExpiryDaysChange = (index: number, value: string) => { + const newValue: number = value === "" ? 0 : parseInt(value); + + handleRuleChange(index, "expiryDays", newValue); + }; + + /** + * Handle the rule values change. + * + * @param index - index of the rule. + * @param event - event object. + * @param isRole - is the object a role. + */ + const handleValuesChange = (index: number, event: SelectChangeEvent, isRole: boolean) => { + const { + target: { value } + } = event; + + const selectedValues: string[] = typeof value === "string" ? value.split(",") : value; + const validList: Resource[] = isRole ? rolesList : groupsList; + const validatedValues: string[] = selectedValues.filter((selectedValue: string) => + validList.some((item: Resource) => item.id === selectedValue) + ).slice(0, GovernanceConnectorConstants.PASSWORD_EXPIRY_FORM_FIELD_CONSTRAINTS.EXPIRY_RULE_MAX_VALUES_PER_RULE); + + handleRuleChange( + index, + "values", + validatedValues + ); + }; + + /** + * Get the identifier of the role or group. + * + * @param resource - Role or Group object. + * @param isRole - is the object a role. + * @returns - Identifier of the role or group. + */ + const getResourceIdentifier = (resource: Resource, isRole: boolean): string => { + if (isRole) { + const { audience } = resource as RolesInterface; + + return audience?.type === "application" + ? `application | ${audience?.display}` + : audience?.type ?? ""; + } + + const { displayName } = resource as GroupsInterface; + + return displayName?.includes("/") + ? displayName.split("/")[0] + : userstoresConfig?.primaryUserstoreName; + }; + + /** + * Get the display name of the role or group. + * + * @param resource - Role or Group object. + * @param isRole - is the object a role. + * @returns - Display name of the role or group. + */ + const getResourceDisplayName = (resource: Resource, isRole: boolean): string => { + if (isRole) { + return resource.displayName ?? ""; + } + const { displayName } = resource as GroupsInterface; + + return displayName?.includes("/") + ? displayName.split("/")[1] + : displayName ?? ""; + }; + + /** + * Render the resource (roles or groups) menu items. + * + * @param rule - password expiry rule. + */ + const renderResourceMenuItems = (rule: PasswordExpiryRule): ReactElement[] => { + const isRoleAttribute: boolean = rule?.attribute === PasswordExpiryRuleAttribute.ROLES; + const valueOptions: Resource[] = isRoleAttribute ? rolesList : groupsList; + + return ( + valueOptions.map((item: Resource) => ( + + -1 } /> + + + + )) + ); + }; + + /** + * Render the selected rule values. + * + * @param selected - selected rule values. + * @param rule - password expiry rule. + */ + const renderSelectedValues = (selected: string[], rule: PasswordExpiryRule): ReactElement[] | ReactElement => { + if (!selected || selected?.length === 0) { + return null; + } + // console.log("testing:selected:", selected, "\n", rule); + const isRoleAttribute: boolean = rule?.attribute === PasswordExpiryRuleAttribute.ROLES; + const resourceList: Resource[] = isRoleAttribute ? rolesList : groupsList; + const firstItem: Resource | undefined = + resourceList?.find((resource: Resource) => resource.id === selected[0]); + + if (!firstItem) { + return null; + } + + return ( +
+
+ + +
+ { selected?.length > 1 && ( + + ) } +
+ ); + }; + + /** + * Handle the default behavior change. + * + * @param event - event object. + */ + const handleSkipFallbackChange = (event: SelectChangeEvent) => { + const value: PasswordExpiryRuleOperator = event.target.value as PasswordExpiryRuleOperator; + + onSkipFallbackChange(value === PasswordExpiryRuleOperator.NE); + }; + + /** + * Handle the default expiry time change. + * + * @param event - event object. + */ + const handleDefaultExpiryTimeChange = (event: React.ChangeEvent) => { + const value: number = parseInt(event.target.value, 10); + + onDefaultPasswordExpiryTimeChange(value); + }; + + return ( +
+
+ + { !isSkipFallbackEnabled + ? ( +
+ + { t("validation:passwordExpiry.rules.messages.defaultRuleApplyMessage") } +
+ ) + : (" " + t("validation:passwordExpiry.rules.messages.defaultRuleSkipMessage")) + } +
+ + { t("validation:passwordExpiry.rules.messages.info") } + + + { + rules?.length > 0 && ( +
+ { t("validation:passwordExpiry.rules.messages.ifUserHas") } +
+ ) + } + + { rules?.map((rule: PasswordExpiryRule, index: number) => ( + + + +
+ movePriority(index, "up") } + disabled={ !isPasswordExpiryEnabled || index === 0 || isReadOnly } + data-componentid={ `${componentId}-move-up-${index}` } + > + + + movePriority(index, "down") } + data-componentid={ `${componentId}-move-down-${index}` } + > + + +
+
+ + + + + + + + + + { rule?.operator === PasswordExpiryRuleOperator.EQ && ( + + ) => + handleExpiryDaysChange(index, e.target.value) } + inputProps={ { + max: GovernanceConnectorConstants. + PASSWORD_EXPIRY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_VALUE, + min: GovernanceConnectorConstants. + PASSWORD_EXPIRY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MIN_VALUE, + readOnly: isReadOnly + } } + error={ hasErrors[rule?.id]?.expiryDays } + disabled={ !isPasswordExpiryEnabled } + /> + + ) } + + { rule?.operator === PasswordExpiryRuleOperator.EQ + ? t("validation:passwordExpiry.rules.messages.applyMessage") + : t("validation:passwordExpiry.rules.messages.skipMessage") } + + { rule?.operator === PasswordExpiryRuleOperator.NE && ( + +
+
+ ) } + + deleteRule(rule?.id) } + data-componentid={ `${componentId}-delete-rule-${index}` } + > + + + +
+
+ )) } +
+
+ ); +}; diff --git a/features/admin.validation.v1/models/validation-config.ts b/features/admin.validation.v1/models/validation-config.ts index 7c3c9e27713..d5cb5e8ca78 100644 --- a/features/admin.validation.v1/models/validation-config.ts +++ b/features/admin.validation.v1/models/validation-config.ts @@ -55,5 +55,24 @@ export interface ValidationFormInterface { maxConsecutiveCharacters?: string; enableValidator?: string; isAlphanumericOnly?: boolean; - [key: string]: string | boolean | number; + [key: string]: string | boolean | number | Record; +} + +export enum PasswordExpiryRuleOperator { + EQ = "eq", + NE = "ne" +} + +export enum PasswordExpiryRuleAttribute { + ROLES = "roles", + GROUPS = "groups" +} + +export interface PasswordExpiryRule { + id: string; + priority: number; + expiryDays: number; + attribute: PasswordExpiryRuleAttribute; + operator: PasswordExpiryRuleOperator; + values: string[]; } diff --git a/features/admin.validation.v1/package.json b/features/admin.validation.v1/package.json index 4e33efd26a2..1f48041f17e 100644 --- a/features/admin.validation.v1/package.json +++ b/features/admin.validation.v1/package.json @@ -34,6 +34,8 @@ "@wso2is/react-components": "^2.4.1", "@wso2is/theme": "^2.0.99", "@wso2is/validation": "^2.0.9", + "@wso2is/admin.groups.v1": "^2.20.120", + "@wso2is/admin.roles.v2": "^2.20.120", "axios": "^0.19.2", "codemirror": "^5.52.0", "country-language": "^0.1.7", diff --git a/features/admin.validation.v1/pages/password-validation-form.scss b/features/admin.validation.v1/pages/password-validation-form.scss new file mode 100644 index 00000000000..8929e37934e --- /dev/null +++ b/features/admin.validation.v1/pages/password-validation-form.scss @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.password-validation-form { + h4.ui.header.heading { + margin-top:25px; + margin-bottom: 35px; + } + + .title-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; + } + + .full-width { + width: 100%; + } + + .info-box { + margin-top: 20px; + margin-bottom: 20px; + } + + .form-container.with-max-width{ + max-width: none; + } + + .priority-arrows { + display: flex; + flex-direction: row; + align-items: center; + } + + .add-rule-btn { + margin-bottom: 10px; + } + + .heading-divider { + margin-top: 1.5rem !important; + margin-bottom: 2rem !important; + } +} + +.flex-row-gap-10 { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +} + +.select-multiple-menu { + width: 100%; +} + +.MuiPaper-root.MuiMenu-paper { + max-height: 250px; + max-width: 350px; + overflow: auto; +} + +.MuiSelect-select { + width: 100%; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} diff --git a/features/admin.validation.v1/pages/validation-config-edit.tsx b/features/admin.validation.v1/pages/validation-config-edit.tsx index e3d9cc6d44d..e2a9ee58237 100644 --- a/features/admin.validation.v1/pages/validation-config-edit.tsx +++ b/features/admin.validation.v1/pages/validation-config-edit.tsx @@ -16,8 +16,12 @@ * under the License. */ +import Switch from "@oxygen-ui/react/Switch"; +import { useRequiredScopes } from "@wso2is/access-control"; import { AppConstants, AppState, FeatureConfigInterface, history } from "@wso2is/admin.core.v1"; import { serverConfigurationConfig } from "@wso2is/admin.extensions.v1"; +import { useGroupList } from "@wso2is/admin.groups.v1/api"; +import { useRolesList } from "@wso2is/admin.roles.v2/api"; import { ConnectorPropertyInterface, GovernanceConnectorInterface, @@ -26,10 +30,10 @@ import { getConnectorDetails } from "@wso2is/admin.server-configurations.v1"; import { getConfiguration } from "@wso2is/admin.users.v1/utils/generate-password.utils"; -import { hasRequiredScopes } from "@wso2is/core/helpers"; import { AlertLevels, - IdentifiableComponentInterface + IdentifiableComponentInterface, + RolesInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { Field, Form } from "@wso2is/form"; @@ -37,6 +41,7 @@ import { ContentLoader, DocumentationLink, EmphasizedSegment, + Heading, Hint, PageLayout, useDocumentation @@ -47,7 +52,6 @@ import React, { MutableRefObject, ReactElement, useEffect, - useMemo, useRef, useState } from "react"; @@ -56,8 +60,16 @@ import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; import { Divider, Grid, Ref } from "semantic-ui-react"; import { updateValidationConfigData, useValidationConfigData } from "../api"; +import { PasswordExpiryRuleList } from "../components/password-expiry-rule-list"; import { ValidationConfigConstants } from "../constants/validation-config-constants"; -import { ValidationDataInterface, ValidationFormInterface } from "../models"; +import { + PasswordExpiryRule, + PasswordExpiryRuleAttribute, + PasswordExpiryRuleOperator, + ValidationDataInterface, + ValidationFormInterface +} from "../models"; +import "./password-validation-form.scss"; /** * Props for validation configuration page. @@ -75,7 +87,7 @@ const FORM_ID: string = "validation-config-form"; export const ValidationConfigEditPage: FunctionComponent = ( props: MyAccountSettingsEditPage ): ReactElement => { - const { [ "data-componentid" ]: componentId } = props; + const { ["data-componentid"]: componentId } = props; const dispatch: Dispatch = useDispatch(); const pageContextRef: MutableRefObject = useRef(null); @@ -83,8 +95,10 @@ export const ValidationConfigEditPage: FunctionComponent state?.config?.ui?.isPasswordInputValidationEnabled); + const disabledFeatures: string[] = useSelector((state: AppState) => + state?.config?.ui?.features?.loginAndRegistration?.disabledFeatures); + const isRuleBasedPasswordExpiryDisabled: boolean = disabledFeatures?.includes("ruleBasedPasswordExpiry"); const featureConfig: FeatureConfigInterface = useSelector((state: AppState) => state?.config?.ui?.features); - const allowedScopes: string = useSelector((state: AppState) => state?.auth?.allowedScopes); const [ isSubmitting, setSubmitting ] = useState(false); const [ initialFormValues, setInitialFormValues ] = useState< @@ -108,20 +122,21 @@ export const ValidationConfigEditPage: FunctionComponent(false); const [ passwordExpiryEnabled, setPasswordExpiryEnabled ] = useState(false); + const [ defaultPasswordExpiryTime, setDefaultPasswordExpiryTime ] = useState(30); + const [ passwordExpirySkipFallback, setPasswordExpirySkipFallback ] = useState(false); + const [ initialPasswordExpiryRules, setInitialPasswordExpiryRules ] = useState([]); + const [ passwordExpiryRules, setPasswordExpiryRules ] = useState([]); + const [ hasPasswordExpiryRuleErrors, setHasPasswordExpiryRuleErrors ] = useState(false); + + const [ allRoleList, setAllRoleList ] = useState([]); + const [ roleListOffset, setRoleListOffset ] = useState(0); + const rolesListItemLimit: number = 50; // State variables required to support legacy password policies. const [ isLegacyPasswordPolicyEnabled, setIsLegacyPasswordPolicyEnabled ] = useState(undefined); const [ legacyPasswordPolicies, setLegacyPasswordPolicies ] = useState([]); - const isReadOnly: boolean = useMemo( - () => - !hasRequiredScopes( - featureConfig?.governanceConnectors, - featureConfig?.governanceConnectors?.scopes?.update, - allowedScopes - ), - [ featureConfig, allowedScopes ] - ); + const isReadOnly: boolean = !useRequiredScopes(featureConfig?.governanceConnectors?.scopes?.update); const { data: passwordHistoryCountData, @@ -141,12 +156,136 @@ export const ValidationConfigEditPage: FunctionComponent { if (!isPasswordInputValidationEnabled) { getLegacyPasswordPolicyProperties(); } }, []); + useEffect(() => { + if (groupsListError) { + dispatch(addAlert({ + description: groupsListError?.response?.data?.description ?? groupsListError?.response?.data?.detail + ?? t("console:manage.features.groups.notifications.fetchGroups.genericError.description"), + level: AlertLevels.ERROR, + message: groupsListError?.response?.data?.message + ?? t("console:manage.features.groups.notifications.fetchGroups.genericError.message") + })); + } + if (rolesListError) { + dispatch(addAlert({ + description: rolesListError?.response?.data?.description ?? rolesListError?.response?.data?.detail + ?? t("roles:notifications.fetchRoles.genericError.description"), + level: AlertLevels.ERROR, + message: t("roles:notifications.fetchRoles.genericError.message") + })); + } + }, [ groupsListError, rolesListError ]); + + useEffect(() => { + if (!currentRoleList || !currentRoleList?.Resources) { + return; + } + setAllRoleList((prevRoles: RolesInterface[]) => [ ...prevRoles, ...currentRoleList?.Resources ]); + if (allRoleList?.length < currentRoleList?.totalResults) { + setRoleListOffset((prevOffset: number) => prevOffset + rolesListItemLimit); + } + }, [ currentRoleList ]); + + // Handle rule based password expiry related data. + useEffect(() => { + if (!passwordExpiryData || isRuleBasedPasswordExpiryDisabled) { + return; + } + + const findProperty = (name: string) => + passwordExpiryData?.properties?.find((property: ConnectorPropertyInterface) => property.name === name); + + setPasswordExpiryEnabled(findProperty(ServerConfigurationsConstants.PASSWORD_EXPIRY_ENABLE)?.value === "true"); + setDefaultPasswordExpiryTime( + parseInt(findProperty(ServerConfigurationsConstants.PASSWORD_EXPIRY_TIME)?.value, 10) || 30 + ); + setPasswordExpirySkipFallback( + findProperty(ServerConfigurationsConstants.PASSWORD_EXPIRY_SKIP_IF_NO_APPLICABLE_RULES)?.value === "true" + ); + + const rules: PasswordExpiryRule[] = passwordExpiryData?.properties + ?.filter((property: ConnectorPropertyInterface) => + property?.name?.startsWith(ServerConfigurationsConstants.PASSWORD_EXPIRY_RULES_PREFIX) + ) + .reduce((validRules: PasswordExpiryRule[], property: ConnectorPropertyInterface) => { + const rule: PasswordExpiryRule = validateAndParsePasswordExpiryRule(property); + + if (rule !== null) { + validRules.push(rule); + } + + return validRules; + }, []) + .sort((a: PasswordExpiryRule, b: PasswordExpiryRule) => a.priority - b.priority); + + setPasswordExpiryRules(rules); + setInitialPasswordExpiryRules(rules); + + }, [ passwordExpiryData ]); + + /** + * Validate and parse password expiry rule. + * + * @param property - Connector property. + * @returns Password expiry rule. + */ + const validateAndParsePasswordExpiryRule = (property: ConnectorPropertyInterface): PasswordExpiryRule | null => { + const [ priority, expiryDays, attribute, operator, ...valueArray ] = property?.value?.split(","); + + if (!priority || !expiryDays || !attribute || !operator || valueArray.length === 0) { + return null; + } + + const priorityNum: number = parseInt(priority, 10); + const expiryDaysNum: number = parseInt(expiryDays, 10); + + if (isNaN(priorityNum) || isNaN(expiryDaysNum)) { + return null; + } + if (!Object.values(PasswordExpiryRuleAttribute).includes(attribute as PasswordExpiryRuleAttribute) || + !Object.values(PasswordExpiryRuleOperator).includes(operator as PasswordExpiryRuleOperator)) { + return null; + } + + return { + attribute: attribute as PasswordExpiryRuleAttribute, + expiryDays: expiryDaysNum, + id: property?.name, + operator: operator as PasswordExpiryRuleOperator, + priority: priorityNum, + values: valueArray + }; + }; + useEffect(() => { if (!passwordHistoryCountData || !passwordExpiryData || (isPasswordInputValidationEnabled && !validationData) || @@ -429,6 +568,28 @@ export const ValidationConfigEditPage: FunctionComponent => { + const processedRules: Record = {}; + const currentRuleIds: Set = new Set(); + + passwordExpiryRules?.forEach((rule: PasswordExpiryRule) => { + if (!rule) return; + const ruleKey: string = `${ServerConfigurationsConstants.PASSWORD_EXPIRY_RULES_PREFIX}${rule?.priority}`; + + processedRules[ruleKey] = + `${rule.priority},${rule.expiryDays},${rule.attribute},${rule.operator},${rule.values?.join(",")}`; + currentRuleIds.add(ruleKey); + }); + // Handle deleted rules. + initialPasswordExpiryRules?.forEach((rule: PasswordExpiryRule) => { + if (!currentRuleIds.has(rule?.id)) { + processedRules[rule?.id] = ""; + } + }); + + return processedRules; + }; + /** * Update the My Account Portal Data. * @@ -437,7 +598,15 @@ export const ValidationConfigEditPage: FunctionComponent { - const processedFormValues: ValidationFormInterface = { ...values }; + if (hasPasswordExpiryRuleErrors) return; + + const processedFormValues: ValidationFormInterface = { + ...values, + passwordExpiryEnabled: passwordExpiryEnabled, + passwordExpiryRules: processPasswordExpiryRules(), + passwordExpirySkipFallback: passwordExpirySkipFallback, + passwordExpiryTime: defaultPasswordExpiryTime + }; const updatePasswordPolicies: Promise = serverConfigurationConfig.processPasswordPoliciesSubmitData( processedFormValues, @@ -789,9 +958,48 @@ export const ValidationConfigEditPage: FunctionComponent ReactElement = (): ReactElement => { + return ( + <> +
+ + { t("validation:passwordExpiry.heading") } + + + setPasswordExpiryEnabled(!passwordExpiryEnabled) + } /> + +
+ setDefaultPasswordExpiryTime(days) } + onSkipFallbackChange={ (skip: boolean) => setPasswordExpirySkipFallback(skip) } + onRuleChange={ (newRuleList: PasswordExpiryRule[]) => setPasswordExpiryRules(newRuleList) } + onRuleError={ (hasErrors: boolean) => setHasPasswordExpiryRuleErrors(hasErrors) } + /> + + ); + }; + const resolvePasswordValidation: () => ReactElement = (): ReactElement => { return (
+ + + { t("extensions:manage.serverConfigurations.passwordValidationHeading") } +