Skip to content

Commit

Permalink
feat(cognito): allow mutable attributes for requiredAttributes (aws#7754
Browse files Browse the repository at this point in the history
)

I've taken the liberty to implement a preview, refer to aws#7752

Any feedback is welcome!

BREAKING CHANGE: `requiredAttributes` on `UserPool` construct is now replaced with `standardAttributes` with a slightly modified signature.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
arnulfojr authored Jun 8, 2020
1 parent 5c3a739 commit 1fabd98
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 138 deletions.
16 changes: 11 additions & 5 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,21 @@ attributes. Besides these, additional attributes can be further defined, and are
Learn more on [attributes in Cognito's
documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html).

The following code sample configures a user pool with two standard attributes (name and address) as required, and adds
four optional attributes.
The following code configures a user pool with two standard attributes (name and address) as required and mutable, and adds
four custom attributes.

```ts
new UserPool(this, 'myuserpool', {
// ...
requiredAttributes: {
fullname: true,
address: true,
standardAttributes: {
fullname: {
required: true,
mutable: false,
},
address: {
required: false,
mutable: true,
},
},
customAttributes: {
'myappid': new StringAttribute({ minLen: 5, maxLen: 15, mutable: false }),
Expand Down
130 changes: 77 additions & 53 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts
Original file line number Diff line number Diff line change
@@ -1,112 +1,136 @@
import { Token } from '@aws-cdk/core';

/**
* The set of standard attributes that can be marked as required.
* The set of standard attributes that can be marked as required or mutable.
*
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#cognito-user-pools-standard-attributes
*/
export interface RequiredAttributes {
export interface StandardAttributes {
/**
* Whether the user's postal address is a required attribute.
* @default false
* The user's postal address.
* @default - see the defaults under `StandardAttribute`
*/
readonly address?: boolean;
readonly address?: StandardAttribute;

/**
* Whether the user's birthday, represented as an ISO 8601:2004 format, is a required attribute.
* @default false
* The user's birthday, represented as an ISO 8601:2004 format.
* @default - see the defaults under `StandardAttribute`
*/
readonly birthdate?: boolean;
readonly birthdate?: StandardAttribute;

/**
* Whether the user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec, is a required attribute.
* @default false
* The user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec.
* @default - see the defaults under `StandardAttribute`
*/
readonly email?: boolean;
readonly email?: StandardAttribute;

/**
* Whether the surname or last name of the user is a required attribute.
* @default false
* The surname or last name of the user.
* @default - see the defaults under `StandardAttribute`
*/
readonly familyName?: boolean;
readonly familyName?: StandardAttribute;

/**
* Whether the user's gender is a required attribute.
* @default false
* The user's gender.
* @default - see the defaults under `StandardAttribute`
*/
readonly gender?: boolean;
readonly gender?: StandardAttribute;

/**
* Whether the user's first name or give name is a required attribute.
* @default false
* The user's first name or give name.
* @default - see the defaults under `StandardAttribute`
*/
readonly givenName?: boolean;
readonly givenName?: StandardAttribute;

/**
* Whether the user's locale, represented as a BCP47 [RFC5646] language tag, is a required attribute.
* @default false
* The user's locale, represented as a BCP47 [RFC5646] language tag.
* @default - see the defaults under `StandardAttribute`
*/
readonly locale?: boolean;
readonly locale?: StandardAttribute;

/**
* Whether the user's middle name is a required attribute.
* @default false
* The user's middle name.
* @default - see the defaults under `StandardAttribute`
*/
readonly middleName?: boolean;
readonly middleName?: StandardAttribute;

/**
* Whether user's full name in displayable form, including all name parts, titles and suffixes, is a required attibute.
* @default false
* The user's full name in displayable form, including all name parts, titles and suffixes.
* @default - see the defaults under `StandardAttribute`
*/
readonly fullname?: boolean;
readonly fullname?: StandardAttribute;

/**
* Whether the user's nickname or casual name is a required attribute.
* @default false
* The user's nickname or casual name.
* @default - see the defaults under `StandardAttribute`
*/
readonly nickname?: boolean;
readonly nickname?: StandardAttribute;

/**
* Whether the user's telephone number is a required attribute.
* @default false
* The user's telephone number.
* @default - see the defaults under `StandardAttribute`
*/
readonly phoneNumber?: boolean;
readonly phoneNumber?: StandardAttribute;

/**
* Whether the URL to the user's profile picture is a required attribute.
* @default false
* The URL to the user's profile picture.
* @default - see the defaults under `StandardAttribute`
*/
readonly profilePicture?: boolean;
readonly profilePicture?: StandardAttribute;

/**
* Whether the user's preffered username, different from the immutable user name, is a required attribute.
* @default false
* The user's preffered username, different from the immutable user name.
* @default - see the defaults under `StandardAttribute`
*/
readonly preferredUsername?: boolean;
readonly preferredUsername?: StandardAttribute;

/**
* Whether the URL to the user's profile page is a required attribute.
* @default false
* The URL to the user's profile page.
* @default - see the defaults under `StandardAttribute`
*/
readonly profilePage?: boolean;
readonly profilePage?: StandardAttribute;

/**
* Whether the user's time zone is a required attribute.
* @default false
* The user's time zone.
* @default - see the defaults under `StandardAttribute`
*/
readonly timezone?: boolean;
readonly timezone?: StandardAttribute;

/**
* Whether the time, the user's information was last updated, is a required attribute.
* @default false
* The time, the user's information was last updated.
* @default - see the defaults under `StandardAttribute`
*/
readonly lastUpdateTime?: boolean;
readonly lastUpdateTime?: StandardAttribute;

/**
* Whether the URL to the user's web page or blog is a required attribute.
* The URL to the user's web page or blog.
* @default - see the defaults under `StandardAttribute`
*/
readonly website?: StandardAttribute;
}

/**
* Standard attribute that can be marked as required or mutable.
*
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#cognito-user-pools-standard-attributes
*/
export interface StandardAttribute {
/**
* Specifies whether the value of the attribute can be changed.
* For any user pool attribute that's mapped to an identity provider attribute, this must be set to `true`.
* Amazon Cognito updates mapped attributes when users sign in to your application through an identity provider.
* If an attribute is immutable, Amazon Cognito throws an error when it attempts to update the attribute.
*
* @default true
*/
readonly mutable?: boolean;
/**
* Specifies whether the attribute is required upon user registration.
* If the attribute is required and the user does not provide a value, registration or sign-in will fail.
*
* @default false
*/
readonly website?: boolean;
readonly required?: boolean;
}

/**
Expand Down Expand Up @@ -152,7 +176,7 @@ export interface CustomAttributeConfig {
*
* @default false
*/
readonly mutable?: boolean
readonly mutable?: boolean;
}

/**
Expand Down
104 changes: 47 additions & 57 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, Duration, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core';
import { CfnUserPool } from './cognito.generated';
import { ICustomAttribute, RequiredAttributes } from './user-pool-attr';
import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr';
import { UserPoolClient, UserPoolClientOptions } from './user-pool-client';
import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain';
import { IUserPoolIdentityProvider } from './user-pool-idp';
Expand Down Expand Up @@ -457,9 +457,9 @@ export interface UserPoolProps {
* The set of attributes that are required for every user in the user pool.
* Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html
*
* @default - No attributes are required.
* @default - All standard attributes are optional and mutable.
*/
readonly requiredAttributes?: RequiredAttributes;
readonly standardAttributes?: StandardAttributes;

/**
* Define a set of custom attributes that can be configured for each user in the user pool.
Expand Down Expand Up @@ -762,24 +762,24 @@ export class UserPool extends UserPoolBase {

if (signIn.username) {
aliasAttrs = [];
if (signIn.email) { aliasAttrs.push(StandardAttribute.EMAIL); }
if (signIn.phone) { aliasAttrs.push(StandardAttribute.PHONE_NUMBER); }
if (signIn.preferredUsername) { aliasAttrs.push(StandardAttribute.PREFERRED_USERNAME); }
if (signIn.email) { aliasAttrs.push(StandardAttributeNames.email); }
if (signIn.phone) { aliasAttrs.push(StandardAttributeNames.phoneNumber); }
if (signIn.preferredUsername) { aliasAttrs.push(StandardAttributeNames.preferredUsername); }
if (aliasAttrs.length === 0) { aliasAttrs = undefined; }
} else {
usernameAttrs = [];
if (signIn.email) { usernameAttrs.push(StandardAttribute.EMAIL); }
if (signIn.phone) { usernameAttrs.push(StandardAttribute.PHONE_NUMBER); }
if (signIn.email) { usernameAttrs.push(StandardAttributeNames.email); }
if (signIn.phone) { usernameAttrs.push(StandardAttributeNames.phoneNumber); }
}

if (props.autoVerify) {
autoVerifyAttrs = [];
if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttribute.EMAIL); }
if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttribute.PHONE_NUMBER); }
if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttributeNames.email); }
if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); }
} else if (signIn.email || signIn.phone) {
autoVerifyAttrs = [];
if (signIn.email) { autoVerifyAttrs.push(StandardAttribute.EMAIL); }
if (signIn.phone) { autoVerifyAttrs.push(StandardAttribute.PHONE_NUMBER); }
if (signIn.email) { autoVerifyAttrs.push(StandardAttributeNames.email); }
if (signIn.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); }
}

return { usernameAttrs, aliasAttrs, autoVerifyAttrs };
Expand Down Expand Up @@ -863,30 +863,16 @@ export class UserPool extends UserPoolBase {
private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined {
const schema: CfnUserPool.SchemaAttributeProperty[] = [];

if (props.requiredAttributes) {
const stdAttributes: StandardAttribute[] = [];

if (props.requiredAttributes.address) { stdAttributes.push(StandardAttribute.ADDRESS); }
if (props.requiredAttributes.birthdate) { stdAttributes.push(StandardAttribute.BIRTHDATE); }
if (props.requiredAttributes.email) { stdAttributes.push(StandardAttribute.EMAIL); }
if (props.requiredAttributes.familyName) { stdAttributes.push(StandardAttribute.FAMILY_NAME); }
if (props.requiredAttributes.fullname) { stdAttributes.push(StandardAttribute.NAME); }
if (props.requiredAttributes.gender) { stdAttributes.push(StandardAttribute.GENDER); }
if (props.requiredAttributes.givenName) { stdAttributes.push(StandardAttribute.GIVEN_NAME); }
if (props.requiredAttributes.lastUpdateTime) { stdAttributes.push(StandardAttribute.LAST_UPDATE_TIME); }
if (props.requiredAttributes.locale) { stdAttributes.push(StandardAttribute.LOCALE); }
if (props.requiredAttributes.middleName) { stdAttributes.push(StandardAttribute.MIDDLE_NAME); }
if (props.requiredAttributes.nickname) { stdAttributes.push(StandardAttribute.NICKNAME); }
if (props.requiredAttributes.phoneNumber) { stdAttributes.push(StandardAttribute.PHONE_NUMBER); }
if (props.requiredAttributes.preferredUsername) { stdAttributes.push(StandardAttribute.PREFERRED_USERNAME); }
if (props.requiredAttributes.profilePage) { stdAttributes.push(StandardAttribute.PROFILE_URL); }
if (props.requiredAttributes.profilePicture) { stdAttributes.push(StandardAttribute.PICTURE_URL); }
if (props.requiredAttributes.timezone) { stdAttributes.push(StandardAttribute.TIMEZONE); }
if (props.requiredAttributes.website) { stdAttributes.push(StandardAttribute.WEBSITE); }

schema.push(...stdAttributes.map((attr) => {
return { name: attr, required: true };
}));
if (props.standardAttributes) {
const stdAttributes = (Object.entries(props.standardAttributes) as Array<[keyof StandardAttributes, StandardAttribute]>)
.filter(([, attr]) => !!attr)
.map(([attrName, attr]) => ({
name: StandardAttributeNames[attrName],
mutable: attr.mutable ?? true,
required: attr.required ?? false,
}));

schema.push(...stdAttributes);
}

if (props.customAttributes) {
Expand All @@ -904,8 +890,12 @@ export class UserPool extends UserPoolBase {
return {
name: attrName,
attributeDataType: attrConfig.dataType,
numberAttributeConstraints: (attrConfig.numberConstraints) ? numberConstraints : undefined,
stringAttributeConstraints: (attrConfig.stringConstraints) ? stringConstraints : undefined,
numberAttributeConstraints: attrConfig.numberConstraints
? numberConstraints
: undefined,
stringAttributeConstraints: attrConfig.stringConstraints
? stringConstraints
: undefined,
mutable: attrConfig.mutable,
};
});
Expand All @@ -919,25 +909,25 @@ export class UserPool extends UserPoolBase {
}
}

const enum StandardAttribute {
ADDRESS = 'address',
BIRTHDATE = 'birthdate',
EMAIL = 'email',
FAMILY_NAME = 'family_name',
GENDER = 'gender',
GIVEN_NAME = 'given_name',
LOCALE = 'locale',
MIDDLE_NAME = 'middle_name',
NAME = 'name',
NICKNAME = 'nickname',
PHONE_NUMBER = 'phone_number',
PICTURE_URL = 'picture',
PREFERRED_USERNAME = 'preferred_username',
PROFILE_URL = 'profile',
TIMEZONE = 'zoneinfo',
LAST_UPDATE_TIME = 'updated_at',
WEBSITE = 'website',
}
const StandardAttributeNames: Record<keyof StandardAttributes, string> = {
address: 'address',
birthdate: 'birthdate',
email: 'email',
familyName: 'family_name',
gender: 'gender',
givenName: 'given_name',
locale: 'locale',
middleName: 'middle_name',
fullname: 'name',
nickname: 'nickname',
phoneNumber: 'phone_number',
profilePicture: 'picture',
preferredUsername: 'preferred_username',
profilePage: 'profile',
timezone: 'zoneinfo',
lastUpdateTime: 'updated_at',
website: 'website',
};

function undefinedIfNoKeys(struct: object): object | undefined {
const allUndefined = Object.values(struct).reduce((acc, v) => acc && (v === undefined), true);
Expand Down
Loading

0 comments on commit 1fabd98

Please sign in to comment.