diff --git a/.changeset/thirty-donuts-march.md b/.changeset/thirty-donuts-march.md new file mode 100644 index 00000000000..2e5bac57f43 --- /dev/null +++ b/.changeset/thirty-donuts-march.md @@ -0,0 +1,6 @@ +--- +"@wso2is/admin.users.v1": patch +"@wso2is/myaccount": patch +--- + +Fix SCIM complex attribute handling logics in user profiles diff --git a/apps/myaccount/src/components/profile/profile.tsx b/apps/myaccount/src/components/profile/profile.tsx index 68e384cc801..18cde66426e 100644 --- a/apps/myaccount/src/components/profile/profile.tsx +++ b/apps/myaccount/src/components/profile/profile.tsx @@ -89,6 +89,7 @@ import { AlertLevels, AuthStateInterface, BasicProfileInterface, ConfigReducerStateInterface, FeatureConfigInterface, + MultiValue, PreferenceConnectorResponse, PreferenceProperty, PreferenceRequest, @@ -641,9 +642,8 @@ export const Profile: FunctionComponent = (props: ProfileProps): R let primaryValue: string | null; if (schema.name === EMAIL_ADDRESSES_ATTRIBUTE) { - primaryValue = profileDetails?.profileInfo?.emails?.length > 0 - ? profileDetails?.profileInfo?.emails[0] - : null; + primaryValue = profileDetails.profileInfo?.emails?.find( + (subAttribute: string) => typeof subAttribute === "string"); } else if (schema.name === MOBILE_NUMBERS_ATTRIBUTE) { primaryValue = profileInfo.get(MOBILE_ATTRIBUTE); } @@ -662,16 +662,27 @@ export const Profile: FunctionComponent = (props: ProfileProps): R // If no primary value is set, set the first value as the primary value. if (isEmpty(primaryValue) && !isEmpty(currentValues)) { if (schema.name === EMAIL_ADDRESSES_ATTRIBUTE) { + const subAttributes: MultiValue[] = extractSubAttributes(EMAIL_ATTRIBUTE); + value = { ...value, - [EMAIL_ATTRIBUTE]: [ currentValues[0] ] + [EMAIL_ATTRIBUTE]: [ + ...subAttributes, + currentValues[0] + ] }; } else if (schema.name === MOBILE_NUMBERS_ATTRIBUTE) { + + const filteredSubAttributes: MultiValue[] = extractSubAttributes( + ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")) + .filter((attr: MultiValue) => attr?.type !== MyAccountProfileConstants.MOBILE); + value = { ...value, [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ + ...filteredSubAttributes, { - type: "mobile", + type: MyAccountProfileConstants.MOBILE, value: currentValues[0] } ] @@ -715,6 +726,7 @@ export const Profile: FunctionComponent = (props: ProfileProps): R primaryValue = profileDetails.profileInfo[schemaNames[0]] && profileDetails.profileInfo[schemaNames[0]] .find((subAttribute: string) => typeof subAttribute === "string"); + attributeValues.push(primaryValue); } // List of sub attributes. @@ -733,7 +745,6 @@ export const Profile: FunctionComponent = (props: ProfileProps): R value = { [schemaNames[0]]: [ ...attributeValues, - primaryValue, { type: schemaNames[1], value: values.get(formName) @@ -941,6 +952,21 @@ export const Profile: FunctionComponent = (props: ProfileProps): R }); }; + /** + * Extracts sub-attributes (objects) from the profile details. + * + * @param schemaKey - The attribute key to extract sub-attributes from. + * @returns Array of sub-attributes (objects). + */ + const extractSubAttributes = (schemaKey: string): MultiValue[] => { + + const attributes: MultiValue[] = profileDetails?.profileInfo?.[schemaKey]; + + return Array.isArray(attributes) + ? attributes.filter((subAttribute: unknown) => typeof subAttribute === "object") + : []; + }; + /** * Assign primary email address or mobile number the multi-valued attribute. * @@ -962,13 +988,17 @@ export const Profile: FunctionComponent = (props: ProfileProps): R if (schema.name === EMAIL_ADDRESSES_ATTRIBUTE) { + const subAttributes: MultiValue[] = extractSubAttributes(EMAIL_ATTRIBUTE); + data.Operations[0].value = { - [EMAIL_ATTRIBUTE]: [ attributeValue ] + [EMAIL_ATTRIBUTE]: [ + ...subAttributes, + attributeValue + ] }; - const existingPrimaryEmail: string = profileDetails?.profileInfo?.emails?.length > 0 - ? profileDetails?.profileInfo?.emails[0] - : null; + const existingPrimaryEmail: string = profileDetails.profileInfo?.emails?.find( + (subAttribute: string) => typeof subAttribute === "string"); const existingEmailList: string[] = profileInfo?.get(EMAIL_ADDRESSES_ATTRIBUTE)?.split(",") || []; if (existingPrimaryEmail && !existingEmailList.includes(existingPrimaryEmail)) { @@ -984,10 +1014,15 @@ export const Profile: FunctionComponent = (props: ProfileProps): R } } else if (schema.name === MOBILE_NUMBERS_ATTRIBUTE) { + const filteredSubAttributes: MultiValue[] = extractSubAttributes( + ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")) + .filter((attr: MultiValue) => attr?.type !== MyAccountProfileConstants.MOBILE); + data.Operations[0].value = { [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ + ...filteredSubAttributes, { - type: "mobile", + type: MyAccountProfileConstants.MOBILE, value: attributeValue } ] @@ -1060,9 +1095,8 @@ export const Profile: FunctionComponent = (props: ProfileProps): R if (schema.name === EMAIL_ADDRESSES_ATTRIBUTE) { const emailList: string[] = profileInfo?.get(EMAIL_ADDRESSES_ATTRIBUTE)?.split(",") || []; const updatedEmailList: string[] = emailList.filter((email: string) => email !== attributeValue); - const primaryEmail: string = profileDetails?.profileInfo?.emails?.length > 0 - ? profileDetails?.profileInfo?.emails[0] - : null; + const primaryEmail: string = profileDetails?.profileInfo?.emails?.find( + (subAttribute: string) => typeof subAttribute === "string"); data.Operations[0].value = { [schema.schemaId] : { @@ -1071,10 +1105,15 @@ export const Profile: FunctionComponent = (props: ProfileProps): R }; if (attributeValue === primaryEmail) { + const subAttributes: MultiValue[] = extractSubAttributes(EMAIL_ATTRIBUTE); + data.Operations.push({ op: "replace", value: { - [EMAIL_ATTRIBUTE]: [] + [EMAIL_ATTRIBUTE]: [ + ...subAttributes, + "" + ] } }); } @@ -1084,13 +1123,20 @@ export const Profile: FunctionComponent = (props: ProfileProps): R const primaryMobile: string = profileInfo.get(MOBILE_ATTRIBUTE); if (attributeValue === primaryMobile) { + const filteredSubAttributes: MultiValue[] = extractSubAttributes( + ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")) + .filter((attr: MultiValue) => attr?.type !== MyAccountProfileConstants.MOBILE); + data.Operations.push({ op: "replace", value: { - [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ { - type: "mobile", - value: "" - } ] + [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ + ...filteredSubAttributes, + { + type: MyAccountProfileConstants.MOBILE, + value: "" + } + ] } }); } @@ -1491,10 +1537,7 @@ export const Profile: FunctionComponent = (props: ProfileProps): R pendingEmailAddress = profileDetails?.profileInfo?.pendingEmails?.length > 0 ? profileDetails?.profileInfo?.pendingEmails[0]?.value : null; - primaryAttributeValue = profileDetails?.profileInfo?.emails?.length > 0 - ? profileDetails?.profileInfo?.emails[0] - : null; - primaryAttributeValue = profileDetails?.profileInfo?.emails[0]; + primaryAttributeValue = profileInfo.get(EMAIL_ATTRIBUTE); verificationEnabled = isEmailVerificationEnabled; primaryAttributeSchema = getSchemaFromName(EMAIL_ATTRIBUTE); maxAllowedLimit = ProfileConstants.MAX_EMAIL_ADDRESSES_ALLOWED; @@ -1502,7 +1545,7 @@ export const Profile: FunctionComponent = (props: ProfileProps): R } else if (schema.name === MOBILE_NUMBERS_ATTRIBUTE) { attributeValueList = profileInfo?.get(MOBILE_NUMBERS_ATTRIBUTE)?.split(",") ?? []; verifiedAttributeValueList = profileInfo?.get(VERIFIED_MOBILE_NUMBERS_ATTRIBUTE)?.split(",") ?? []; - primaryAttributeValue = profileInfo?.get(ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("MOBILE")); + primaryAttributeValue = profileInfo?.get(MOBILE_ATTRIBUTE); verificationEnabled = isMobileVerificationEnabled; primaryAttributeSchema = getSchemaFromName(MOBILE_ATTRIBUTE); maxAllowedLimit = ProfileConstants.MAX_MOBILE_NUMBERS_ALLOWED; @@ -1992,9 +2035,7 @@ export const Profile: FunctionComponent = (props: ProfileProps): R pendingEmailAddress = profileDetails?.profileInfo?.pendingEmails?.length > 0 ? profileDetails?.profileInfo?.pendingEmails[0]?.value : null; - primaryAttributeValue = profileDetails?.profileInfo?.emails?.length > 0 - ? profileDetails?.profileInfo?.emails[0] - : null; + primaryAttributeValue = profileInfo.get(EMAIL_ATTRIBUTE); } else if (schema.name === MOBILE_NUMBERS_ATTRIBUTE) { verificationEnabled = isMobileVerificationEnabled; diff --git a/apps/myaccount/src/constants/profile-constants.ts b/apps/myaccount/src/constants/profile-constants.ts index 6026437320f..46df818b612 100644 --- a/apps/myaccount/src/constants/profile-constants.ts +++ b/apps/myaccount/src/constants/profile-constants.ts @@ -41,6 +41,7 @@ export class ProfileConstants { "personalInfo.distinct.attribute.profiles"; public static readonly USERNAME_CLAIM_NAME: string = "userName"; + public static readonly MOBILE: string = "mobile"; } /** diff --git a/apps/myaccount/src/models/profile.ts b/apps/myaccount/src/models/profile.ts index 3fd964eaf45..ff77268822f 100644 --- a/apps/myaccount/src/models/profile.ts +++ b/apps/myaccount/src/models/profile.ts @@ -264,5 +264,5 @@ export const createEmptyProfile = (): BasicProfileInterface => ({ */ export type ProfilePatchOperationValue = Record - | Array + | Array | Array>>; diff --git a/features/admin.users.v1/components/user-profile.tsx b/features/admin.users.v1/components/user-profile.tsx index f8f0e0ef711..c30d0fba9fa 100644 --- a/features/admin.users.v1/components/user-profile.tsx +++ b/features/admin.users.v1/components/user-profile.tsx @@ -912,7 +912,7 @@ export const UserProfile: FunctionComponent = ( const attributeValues: (string | string[] | SchemaAttributeValueInterface)[] = []; const attValues: Map = new Map(); - if (schemaNames.length === 1 || schema.name === "phoneNumbers.mobile") { + if (schemaNames.length === 1 || schemaNames.length === 2) { // Extract the sub attributes from the form values. for (const value of values.keys()) { @@ -1073,7 +1073,7 @@ export const UserProfile: FunctionComponent = ( const attributeValues: (string | string[] | SchemaAttributeValueInterface)[] = []; const attValues: Map = new Map(); - if (schemaNames.length === 1 || schema.name === "phoneNumbers.mobile") { + if (schemaNames.length === 1 || schemaNames.length === 2) { // Extract the sub attributes from the form values. for (const value of values.keys()) { @@ -1534,8 +1534,7 @@ export const UserProfile: FunctionComponent = ( }; if (schema.name === EMAIL_ADDRESSES_ATTRIBUTE) { - const emailList: string[] = profileInfo?.get(ProfileConstants.SCIM2_SCHEMA_DICTIONARY. - get("EMAIL_ADDRESSES"))?.split(",") || []; + const emailList: string[] = profileInfo?.get(EMAIL_ADDRESSES_ATTRIBUTE)?.split(",") || []; const updatedEmailList: string[] = emailList.filter((email: string) => email !== attributeValue); const primaryEmail: string = profileInfo?.get(EMAIL_ATTRIBUTE); @@ -1546,10 +1545,15 @@ export const UserProfile: FunctionComponent = ( }; if (attributeValue === primaryEmail) { + const subAttributes: Record[] = extractSubAttributes(EMAIL_ATTRIBUTE); + data.Operations.push({ op: "replace", value: { - [EMAIL_ATTRIBUTE]: [] + [EMAIL_ATTRIBUTE]: [ + ...subAttributes, + "" + ] } }); } @@ -1560,13 +1564,20 @@ export const UserProfile: FunctionComponent = ( const primaryMobile: string = profileInfo.get(MOBILE_ATTRIBUTE); if (attributeValue === primaryMobile) { + const filteredSubAttributes: Record[] = + extractSubAttributes(ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS"))?.filter( + (attr: Record) => attr?.type !== UserManagementConstants.MOBILE); + data.Operations.push({ op: "replace", value: { - [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ { - type: "mobile", - value: "" - } ] + [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ + ...filteredSubAttributes, + { + type: UserManagementConstants.MOBILE, + value: "" + } + ] } }); } @@ -1700,6 +1711,18 @@ export const UserProfile: FunctionComponent = ( }); }; + /** + * Extracts sub-attributes (objects) from the profile details. + * + * @param schemaKey - The attribute key to extract sub-attributes from. + * @returns Array of sub-attributes (objects). + */ + const extractSubAttributes = (schemaKey: string): Record[] => { + + return user && user[schemaKey]?.filter( + (subAttribute: unknown) => typeof subAttribute === "object") || []; + }; + /** * Assign primary email address or mobile number the multi-valued attribute. * @@ -1718,13 +1741,16 @@ export const UserProfile: FunctionComponent = ( }; if (schema.name === EMAIL_ADDRESSES_ATTRIBUTE) { + const subAttributes: Record[] = extractSubAttributes(EMAIL_ATTRIBUTE); data.Operations[0].value = { - [EMAIL_ATTRIBUTE]: [ attributeValue ] + [EMAIL_ATTRIBUTE]: [ + ...subAttributes, + attributeValue + ] }; - const existingPrimaryEmail: string = - profileInfo?.get(EMAIL_ATTRIBUTE); + const existingPrimaryEmail: string = profileInfo?.get(EMAIL_ATTRIBUTE); const existingEmailList: string[] = profileInfo?.get( EMAIL_ADDRESSES_ATTRIBUTE)?.split(",") || []; @@ -1741,10 +1767,15 @@ export const UserProfile: FunctionComponent = ( } } else if (schema.name === MOBILE_NUMBERS_ATTRIBUTE) { + const filteredSubAttributes: Record[] = + extractSubAttributes(ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS"))?.filter( + (attr: Record) => attr?.type !== UserManagementConstants.MOBILE); + data.Operations[0].value = { [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ + ...filteredSubAttributes, { - type: "mobile", + type: UserManagementConstants.MOBILE, value: attributeValue } ] @@ -1841,10 +1872,15 @@ export const UserProfile: FunctionComponent = ( }; if (isEmpty(existingPrimaryEmail) && !isEmpty(attributeValues)) { + const subAttributes: Record[] = extractSubAttributes(EMAIL_ATTRIBUTE); + data.Operations.push({ op: "replace", value: { - [EMAIL_ATTRIBUTE]: [ attributeValues[0] ] + [EMAIL_ATTRIBUTE]: [ + ...subAttributes, + attributeValues[0] + ] } }); } @@ -1862,12 +1898,17 @@ export const UserProfile: FunctionComponent = ( }; if (isEmpty(existingPrimaryMobile) && !isEmpty(attributeValues)) { + const filteredSubAttributes: Record[] = + extractSubAttributes(ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS"))?.filter( + (attr: Record) => attr?.type !== UserManagementConstants.MOBILE); + data.Operations.push({ op: "replace", value: { [ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [ + ...filteredSubAttributes, { - type: "mobile", + type: UserManagementConstants.MOBILE, value: attributeValues[0] } ] diff --git a/features/admin.users.v1/constants/user-management-constants.ts b/features/admin.users.v1/constants/user-management-constants.ts index 614f7f363ef..78c9c9dbf5c 100644 --- a/features/admin.users.v1/constants/user-management-constants.ts +++ b/features/admin.users.v1/constants/user-management-constants.ts @@ -128,6 +128,7 @@ export class UserManagementConstants { public static readonly ROLES: string = "roles"; public static readonly GROUPS: string = "groups"; + public static readonly MOBILE: string = "mobile"; public static readonly SCIM_USER_PATH: string = "/Users"; public static readonly SCIM_GROUP_PATH: string = "/Groups"; public static readonly SCIM_V2_ROLE_PATH: string = "/v2/Roles"; diff --git a/features/admin.users.v1/models/user.ts b/features/admin.users.v1/models/user.ts index ed54ebfa038..81e41fd0162 100644 --- a/features/admin.users.v1/models/user.ts +++ b/features/admin.users.v1/models/user.ts @@ -399,7 +399,7 @@ export interface PatchUserAddOpInterface { */ export type PatchUserOperationValue = Record - | Array + | Array> | Array>>; /**