Skip to content

Commit

Permalink
feat(api): updateUserAttributes full support
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Dec 11, 2021
1 parent b3b116c commit 308c9c2
Show file tree
Hide file tree
Showing 7 changed files with 647 additions and 73 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog
| UpdateGroup ||
| UpdateIdentityProvider ||
| UpdateResourceServer ||
| UpdateUserAttributes | |
| UpdateUserAttributes | |
| UpdateUserPool ||
| UpdateUserPoolClient ||
| UpdateUserPoolDomain ||
Expand Down
93 changes: 93 additions & 0 deletions integration-tests/aws-sdk/updateUserAttributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Pino from "pino";
import { UUID } from "../../src/__tests__/patterns";
import { withCognitoSdk } from "./setup";

describe(
"CognitoIdentityServiceProvider.updateUserAttributes",
withCognitoSdk((Cognito) => {
it("updates a user's attributes", async () => {
const client = Cognito();

const pool = await client
.createUserPool({
PoolName: "test",
AutoVerifiedAttributes: ["email"],
})
.promise();
const userPoolId = pool.UserPool?.Id as string;

const upc = await client
.createUserPoolClient({
UserPoolId: userPoolId,
ClientName: "test",
})
.promise();

await client
.adminCreateUser({
UserAttributes: [
{ Name: "email", Value: "[email protected]" },
{ Name: "phone_number", Value: "0400000000" },
],
Username: "abc",
UserPoolId: userPoolId,
TemporaryPassword: "def",
})
.promise();

await client
.adminConfirmSignUp({
UserPoolId: userPoolId,
Username: "abc",
})
.promise();

// login as the user
const initiateAuthResponse = await client
.initiateAuth({
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: "abc",
PASSWORD: "def",
},
ClientId: upc.UserPoolClient?.ClientId as string,
})
.promise();

let user = await client
.adminGetUser({
UserPoolId: userPoolId,
Username: "abc",
})
.promise();

expect(user.UserAttributes).toEqual([
{ Name: "sub", Value: expect.stringMatching(UUID) },
{ Name: "email", Value: "[email protected]" },
{ Name: "phone_number", Value: "0400000000" },
]);

await client
.updateUserAttributes({
AccessToken: initiateAuthResponse.AuthenticationResult
?.AccessToken as string,
UserAttributes: [{ Name: "email", Value: "[email protected]" }],
})
.promise();

user = await client
.adminGetUser({
UserPoolId: userPoolId,
Username: "abc",
})
.promise();

expect(user.UserAttributes).toEqual([
{ Name: "sub", Value: expect.stringMatching(UUID) },
{ Name: "email", Value: "[email protected]" },
{ Name: "phone_number", Value: "0400000000" },
{ Name: "email_verified", Value: "false" },
]);
});
})
);
66 changes: 66 additions & 0 deletions src/services/userPoolService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
AttributeListType,
AttributeType,
MFAOptionListType,
SchemaAttributesListType,
UserPoolType,
UserStatusType,
} from "aws-sdk/clients/cognitoidentityserviceprovider";
import { InvalidParameterError } from "../errors";
import { AppClient, newId } from "./appClient";
import { Clock } from "./clock";
import { Context } from "./context";
Expand Down Expand Up @@ -351,3 +353,67 @@ export class UserPoolServiceFactoryImpl implements UserPoolServiceFactory {
);
}
}

export const validatePermittedAttributeChanges = (
requestAttributes: AttributeListType,
schemaAttributes: SchemaAttributesListType
): AttributeListType => {
for (const attr of requestAttributes) {
const attrSchema = schemaAttributes.find((x) => x.Name === attr.Name);
if (!attrSchema) {
throw new InvalidParameterError(
`user.${attr.Name}: Attribute does not exist in the schema.`
);
}
if (!attrSchema.Mutable) {
throw new InvalidParameterError(
`user.${attr.Name}: Attribute cannot be updated. (changing an immutable attribute)`
);
}
}

if (
attributesInclude("email_verified", requestAttributes) &&
!attributesInclude("email", requestAttributes)
) {
throw new InvalidParameterError(
"Email is required to verify/un-verify an email"
);
}

if (
attributesInclude("phone_number_verified", requestAttributes) &&
!attributesInclude("phone_number", requestAttributes)
) {
throw new InvalidParameterError(
"Phone Number is required to verify/un-verify a phone number"
);
}

return requestAttributes;
};

export const defaultVerifiedAttributesIfModified = (
attributes: AttributeListType
): AttributeListType => {
const attributesToSet = [...attributes];
if (
attributesInclude("email", attributes) &&
!attributesInclude("email_verified", attributes)
) {
attributesToSet.push(attribute("email_verified", "false"));
}
if (
attributesInclude("phone_number", attributes) &&
!attributesInclude("phone_number_verified", attributes)
) {
attributesToSet.push(attribute("phone_number_verified", "false"));
}
return attributesToSet;
};

export const hasUnverifiedContactAttributes = (
userAttributesToSet: AttributeListType
): boolean =>
attributeValue("email_verified", userAttributesToSet) === "false" ||
attributeValue("phone_number_verified", userAttributesToSet) === "false";
74 changes: 4 additions & 70 deletions src/targets/adminUpdateUserAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,20 @@
import {
AdminUpdateUserAttributesRequest,
AdminUpdateUserAttributesResponse,
AttributeListType,
SchemaAttributesListType,
} from "aws-sdk/clients/cognitoidentityserviceprovider";
import { Messages, Services, UserPoolService } from "../services";
import { InvalidParameterError, NotAuthorizedError } from "../errors";
import { Messages, Services, UserPoolService } from "../services";
import { USER_POOL_AWS_DEFAULTS } from "../services/cognitoService";
import { selectAppropriateDeliveryMethod } from "../services/messageDelivery/deliveryMethod";
import {
attribute,
attributesAppend,
attributesInclude,
attributeValue,
defaultVerifiedAttributesIfModified,
hasUnverifiedContactAttributes,
User,
validatePermittedAttributeChanges,
} from "../services/userPoolService";
import { Context, Target } from "./router";

const validatePermittedAttributeChanges = (
requestAttributes: AttributeListType,
schemaAttributes: SchemaAttributesListType
): AttributeListType => {
for (const attr of requestAttributes) {
const attrSchema = schemaAttributes.find((x) => x.Name === attr.Name);
if (!attrSchema) {
throw new InvalidParameterError(
`user.${attr.Name}: Attribute does not exist in the schema.`
);
}
if (!attrSchema.Mutable) {
throw new InvalidParameterError(
`user.${attr.Name}: Attribute cannot be updated. (changing an immutable attribute)`
);
}
}

if (
attributesInclude("email_verified", requestAttributes) &&
!attributesInclude("email", requestAttributes)
) {
throw new InvalidParameterError(
"Email is required to verify/un-verify an email"
);
}

if (
attributesInclude("phone_number_verified", requestAttributes) &&
!attributesInclude("phone_number", requestAttributes)
) {
throw new InvalidParameterError(
"Phone Number is required to verify/un-verify a phone number"
);
}

return requestAttributes;
};

const defaultVerifiedAttributesIfModified = (
attributes: AttributeListType
): AttributeListType => {
const attributesToSet = [...attributes];
if (
attributesInclude("email", attributes) &&
!attributesInclude("email_verified", attributes)
) {
attributesToSet.push(attribute("email_verified", "false"));
}
if (
attributesInclude("phone_number", attributes) &&
!attributesInclude("phone_number_verified", attributes)
) {
attributesToSet.push(attribute("phone_number_verified", "false"));
}
return attributesToSet;
};

const hasUnverifiedContactAttributes = (
userAttributesToSet: AttributeListType
): boolean =>
attributeValue("email_verified", userAttributesToSet) === "false" ||
attributeValue("phone_number_verified", userAttributesToSet) === "false";

const sendAttributeVerificationCode = async (
ctx: Context,
userPool: UserPoolService,
Expand Down
6 changes: 4 additions & 2 deletions src/targets/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ import { AdminConfirmSignUp } from "./adminConfirmSignUp";
import { AdminUpdateUserAttributes } from "./adminUpdateUserAttributes";
import { AdminInitiateAuth } from "./adminInitiateAuth";
import { RevokeToken } from "./revokeToken";
import { UpdateUserAttributes } from "./updateUserAttributes";
import { VerifyUserAttribute } from "./verifyUserAttribute";

export const Targets = {
AdminConfirmSignUp,
AdminCreateUser,
AdminDeleteUser,
AdminInitiateAuth,
AdminGetUser,
AdminInitiateAuth,
AdminSetUserPassword,
AdminUpdateUserAttributes,
ChangePassword,
Expand All @@ -49,11 +50,12 @@ export const Targets = {
GetUserAttributeVerificationCode,
InitiateAuth,
ListGroups,
ListUsers,
ListUserPools,
ListUsers,
RespondToAuthChallenge,
RevokeToken,
SignUp,
UpdateUserAttributes,
VerifyUserAttribute,
} as const;

Expand Down
Loading

0 comments on commit 308c9c2

Please sign in to comment.