Skip to content

Commit

Permalink
Merge pull request #7457 from PasinduYeshan/mobile-patch-call
Browse files Browse the repository at this point in the history
Fix SCIM complex attribute handling logic in user profiles
  • Loading branch information
PasinduYeshan authored Jan 30, 2025
2 parents 1ba4d55 + 73b5acf commit 45f3799
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 44 deletions.
6 changes: 6 additions & 0 deletions .changeset/thirty-donuts-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@wso2is/admin.users.v1": patch
"@wso2is/myaccount": patch
---

Fix SCIM complex attribute handling logics in user profiles
95 changes: 68 additions & 27 deletions apps/myaccount/src/components/profile/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
AlertLevels,
AuthStateInterface, BasicProfileInterface, ConfigReducerStateInterface,
FeatureConfigInterface,
MultiValue,
PreferenceConnectorResponse,
PreferenceProperty,
PreferenceRequest,
Expand Down Expand Up @@ -641,9 +642,8 @@ export const Profile: FunctionComponent<ProfileProps> = (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);
}
Expand All @@ -662,16 +662,27 @@ export const Profile: FunctionComponent<ProfileProps> = (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]
}
]
Expand Down Expand Up @@ -715,6 +726,7 @@ export const Profile: FunctionComponent<ProfileProps> = (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.
Expand All @@ -733,7 +745,6 @@ export const Profile: FunctionComponent<ProfileProps> = (props: ProfileProps): R
value = {
[schemaNames[0]]: [
...attributeValues,
primaryValue,
{
type: schemaNames[1],
value: values.get(formName)
Expand Down Expand Up @@ -941,6 +952,21 @@ export const Profile: FunctionComponent<ProfileProps> = (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.
*
Expand All @@ -962,13 +988,17 @@ export const Profile: FunctionComponent<ProfileProps> = (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)) {
Expand All @@ -984,10 +1014,15 @@ export const Profile: FunctionComponent<ProfileProps> = (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
}
]
Expand Down Expand Up @@ -1060,9 +1095,8 @@ export const Profile: FunctionComponent<ProfileProps> = (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] : {
Expand All @@ -1071,10 +1105,15 @@ export const Profile: FunctionComponent<ProfileProps> = (props: ProfileProps): R
};

if (attributeValue === primaryEmail) {
const subAttributes: MultiValue[] = extractSubAttributes(EMAIL_ATTRIBUTE);

data.Operations.push({
op: "replace",
value: {
[EMAIL_ATTRIBUTE]: []
[EMAIL_ATTRIBUTE]: [
...subAttributes,
""
]
}
});
}
Expand All @@ -1084,13 +1123,20 @@ export const Profile: FunctionComponent<ProfileProps> = (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: ""
}
]
}
});
}
Expand Down Expand Up @@ -1491,18 +1537,15 @@ export const Profile: FunctionComponent<ProfileProps> = (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;

} 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;
Expand Down Expand Up @@ -1992,9 +2035,7 @@ export const Profile: FunctionComponent<ProfileProps> = (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;
Expand Down
1 change: 1 addition & 0 deletions apps/myaccount/src/constants/profile-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

/**
Expand Down
2 changes: 1 addition & 1 deletion apps/myaccount/src/models/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,5 +264,5 @@ export const createEmptyProfile = (): BasicProfileInterface => ({
*/
export type ProfilePatchOperationValue = Record<string, string
| Record<string, string | string[]>
| Array<string>
| Array<string | MultiValue>
| Array<Record<string, string>>>;
71 changes: 56 additions & 15 deletions features/admin.users.v1/components/user-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
const attributeValues: (string | string[] | SchemaAttributeValueInterface)[] = [];
const attValues: Map<string, string | string []> = 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()) {
Expand Down Expand Up @@ -1073,7 +1073,7 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
const attributeValues: (string | string[] | SchemaAttributeValueInterface)[] = [];
const attValues: Map<string, string | string []> = 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()) {
Expand Down Expand Up @@ -1534,8 +1534,7 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
};

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);

Expand All @@ -1546,10 +1545,15 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
};

if (attributeValue === primaryEmail) {
const subAttributes: Record<string, string>[] = extractSubAttributes(EMAIL_ATTRIBUTE);

data.Operations.push({
op: "replace",
value: {
[EMAIL_ATTRIBUTE]: []
[EMAIL_ATTRIBUTE]: [
...subAttributes,
""
]
}
});
}
Expand All @@ -1560,13 +1564,20 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
const primaryMobile: string = profileInfo.get(MOBILE_ATTRIBUTE);

if (attributeValue === primaryMobile) {
const filteredSubAttributes: Record<string, string>[] =
extractSubAttributes(ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS"))?.filter(
(attr: Record<string, string>) => 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: ""
}
]
}
});
}
Expand Down Expand Up @@ -1700,6 +1711,18 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
});
};

/**
* 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<string, string>[] => {

return user && user[schemaKey]?.filter(
(subAttribute: unknown) => typeof subAttribute === "object") || [];
};

/**
* Assign primary email address or mobile number the multi-valued attribute.
*
Expand All @@ -1718,13 +1741,16 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
};

if (schema.name === EMAIL_ADDRESSES_ATTRIBUTE) {
const subAttributes: Record<string, string>[] = 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(",") || [];

Expand All @@ -1741,10 +1767,15 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
}
} else if (schema.name === MOBILE_NUMBERS_ATTRIBUTE) {

const filteredSubAttributes: Record<string, string>[] =
extractSubAttributes(ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS"))?.filter(
(attr: Record<string, string>) => attr?.type !== UserManagementConstants.MOBILE);

data.Operations[0].value = {
[ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS")]: [
...filteredSubAttributes,
{
type: "mobile",
type: UserManagementConstants.MOBILE,
value: attributeValue
}
]
Expand Down Expand Up @@ -1841,10 +1872,15 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
};

if (isEmpty(existingPrimaryEmail) && !isEmpty(attributeValues)) {
const subAttributes: Record<string, string>[] = extractSubAttributes(EMAIL_ATTRIBUTE);

data.Operations.push({
op: "replace",
value: {
[EMAIL_ATTRIBUTE]: [ attributeValues[0] ]
[EMAIL_ATTRIBUTE]: [
...subAttributes,
attributeValues[0]
]
}
});
}
Expand All @@ -1862,12 +1898,17 @@ export const UserProfile: FunctionComponent<UserProfilePropsInterface> = (
};

if (isEmpty(existingPrimaryMobile) && !isEmpty(attributeValues)) {
const filteredSubAttributes: Record<string, string>[] =
extractSubAttributes(ProfileConstants.SCIM2_SCHEMA_DICTIONARY.get("PHONE_NUMBERS"))?.filter(
(attr: Record<string, string>) => 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]
}
]
Expand Down
Loading

0 comments on commit 45f3799

Please sign in to comment.