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 0f717df4295..b3966a691a9 100644
--- a/apps/console/src/public/deployment.config.json
+++ b/apps/console/src/public/deployment.config.json
@@ -691,7 +691,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) => (
+
+ ))
+ );
+ };
+
+ /**
+ * 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 a522f8576e1..2d0fcdb28c5 100644
--- a/features/admin.validation.v1/package.json
+++ b/features/admin.validation.v1/package.json
@@ -34,6 +34,8 @@
"@wso2is/react-components": "^2.4.6",
"@wso2is/theme": "^2.1.2",
"@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") }
+