Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cognito): allow mutable attributes for requiredAttributes #7754

Merged
merged 32 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b45ec85
BREAKING CHANGE: requiredAttributes require an instance of IAttribute
arnulfojr May 2, 2020
b0cffae
Updated README
arnulfojr May 2, 2020
dfc0207
Introduced StandardAttribute
arnulfojr May 2, 2020
e299ec7
simplified StandardAttribute as simple object
arnulfojr May 21, 2020
1ced138
Retouches in comments
arnulfojr May 21, 2020
35e238f
Reinclude RequiredAttributes interface
arnulfojr May 21, 2020
93be392
BREAKING CHANGE: Replaced UserPool property for requiredAttributes wi…
arnulfojr Jun 1, 2020
14dc5e2
Fix small spelling
arnulfojr Jun 1, 2020
aa06632
Removed jest gen file
arnulfojr Jun 1, 2020
175fce9
Removed accidently added Interface
arnulfojr Jun 1, 2020
79bdb65
real-synth
Jun 4, 2020
137a32a
extract "real synth" to a separate file
Jun 4, 2020
8bdf8c5
Merge remote-tracking branch 'origin/master' into benisrae/stage-cons…
rix0rrr Jun 5, 2020
43630e0
Making the synthesis file a little nicer
rix0rrr Jun 5, 2020
853b46a
Merge branch 'master' into benisrae/stage-construct-2
Jun 7, 2020
b811afd
Update allowed-breaking-changes.txt
Jun 7, 2020
431490c
touch ups
Jun 7, 2020
420e6c9
update stack `env` description
Jun 7, 2020
8a8951a
assembly name validation & more tests
Jun 7, 2020
94cc827
rename "embedded" to "nested" and move outdir resolution to app
Jun 7, 2020
711d861
hoist runtimeInfo collection to assembly
Jun 7, 2020
2bb8e62
update cx schema
Jun 7, 2020
7715885
remove "experimental" annotation from Assembly
Jun 7, 2020
603c608
chore(cli): fix "iam diff" integration test (#8421)
Jun 7, 2020
bb15b99
Addressing PR comments
arnulfojr Jun 7, 2020
812375f
Merge branch 'master' of github.com:aws/aws-cdk
arnulfojr Jun 7, 2020
2fb20ac
Fix linting
arnulfojr Jun 7, 2020
3442a35
Merge remote-tracking branch 'origin/master'
Jun 8, 2020
49ec34e
revert core changes
Jun 8, 2020
b305224
remove extrenous file
Jun 8, 2020
daa9318
Apply suggestions from code review
Jun 8, 2020
3eb4fcb
Merge branch 'master' into master
mergify[bot] Jun 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
nija-at marked this conversation as resolved.
Show resolved Hide resolved
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
128 changes: 76 additions & 52 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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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, is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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, is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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, is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @default false
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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, is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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, is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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 is a required attribute.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
* @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