From acc76d4a21a1dbb24c0da9ca3b84cf3f51ce2ca0 Mon Sep 17 00:00:00 2001 From: tom139 Date: Sat, 25 Apr 2020 18:00:03 +0200 Subject: [PATCH 01/17] feat(cognito): allow set read write attributes in client When creating a userpool client, the user can now specify which attributes (custom or standard) the app client will be able to read or write. Closes #7407 --- .../aws-cognito/lib/user-pool-client.ts | 68 +++++++++++- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 103 +++++++++++++++++- .../aws-cognito/test/user-pool-client.test.ts | 100 ++++++++++++++++- 3 files changed, 268 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index cde6bf72191ca..8c071532433eb 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -1,6 +1,6 @@ import { Construct, IResource, Resource } from '@aws-cdk/core'; import { CfnUserPoolClient } from './cognito.generated'; -import { IUserPool } from './user-pool'; +import { IUserPool, StandardAttribute } from './user-pool'; /** * Types of authentication flow @@ -173,6 +173,50 @@ export interface UserPoolClientOptions { * @default - see defaults in `OAuthSettings` */ readonly oAuth?: OAuthSettings; + + /** + * The attributes this client will be able to read. + * + * This should be used to restrict the attributes the client will be + * able to read. + * + * @default undefined all attributes are readable + */ + readonly readAttributes?: UserPoolClientAttributes; + + /** + * The attributes this client will be able to write. + * + * This should be used to restrict the attributes the client will be able to write, + * e.g. the client used by a webapp should not be able to set the `customerPlan` attribute, + * while the client used by the billing backend should. + * + * @default undefined all attributes are writable + */ + readonly writeAttributes?: UserPoolClientAttributes; +} + +/** + * Represent a list of attributes (standard and custom) + */ +export interface UserPoolClientAttributes { + /** + * A list of standard attributes + * + * @default [] + */ + readonly standard?: StandardAttribute[] + + /** + * A list of custom attributes. + * + * @note You don't need to prepend them with `custom:` + * + * @example customerPlan + * + * @default [] + */ + readonly custom?: string[] } /** @@ -234,6 +278,8 @@ export class UserPoolClient extends Resource implements IUserPoolClient { allowedOAuthScopes: this.configureOAuthScopes(props.oAuth), callbackUrLs: (props.oAuth?.callbackUrls && props.oAuth?.callbackUrls.length > 0) ? props.oAuth?.callbackUrls : undefined, allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined, + readAttributes: this.configureAttributes(props.readAttributes), + writeAttributes: this.configureAttributes(props.writeAttributes), }); this.userPoolClientId = resource.ref; @@ -251,6 +297,26 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return this._userPoolClientName; } + private configureAttributes(conf: UserPoolClientAttributes | undefined): string[] | undefined { + let aux: string[] | undefined; + // if the configuration in missing (undefined) + // the default behavior is to allow all attributes to be read/written + // so undefined is returned. + if (conf === undefined) { + aux = undefined; + } else { + // get the standard attributes, if any is specified + const standard: string[] = conf.standard ?? []; + // get all custom attributes and prepend the `custom:` prefix + const custom: string[] = conf.custom?.map(attr => { + // if the attribute does not start with `custom:` it should be added + return attr.startsWith('custom:') ? attr : `custom:${attr}`; + }) ?? []; + aux = standard.concat(custom); + } + return aux; + } + private configureAuthFlows(props: UserPoolClientProps): string[] | undefined { const authFlows: string[] = []; if (props.authFlows?.userPassword) { authFlows.push('ALLOW_USER_PASSWORD_AUTH'); } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index b8ba8176be0a9..a066ececdf392 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -897,23 +897,124 @@ export class UserPool extends Resource implements IUserPool { } } -const enum StandardAttribute { +/** + * All standard attributes available in Cognito by default. + * + * Documentation of attributes are given as per OpenID Connect + * [specification](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) + * + * @see [docs](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html) + */ +export enum StandardAttribute { + /** + * End-User's preferred postal address. The value of the address member is a JSON [RFC4627] structure. + */ ADDRESS = 'address', + /** + * End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format. The year MAY be 0000, indicating that it is omitted. + * To represent only the year, YYYY format is allowed. + * + * Note that depending on the underlying platform's date related function, providing just year can result in varying month and day, + * so the implementers need to take this factor into account to correctly process the dates. + */ BIRTHDATE = 'birthdate', + /** + * End-User's preferred e-mail address. Its value MUST conform to the RFC 5322 addr-spec syntax. + * The RP MUST NOT rely upon this value being unique + */ EMAIL = 'email', + /** + * True if the End-User's e-mail address has been verified; otherwise false. + * + * When this Claim Value is true, this means that the OP took affirmative steps to ensure that this e-mail address was controlled by the End-User + * at the time the verification was performed. + * The means by which an e-mail address is verified is context-specific, and dependent upon the trust framework or + * contractual agreements within which the parties are operating. + */ + EMAIL_VERIFIED = 'email_verified', + /** + * Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have multiple family names or no family name; + * all can be present, with the names being separated by space characters. + */ FAMILY_NAME = 'family_name', + /** + * End-User's gender. Values defined by this specification are female and male. + * Other values MAY be used when neither of the defined values are applicable. + */ GENDER = 'gender', + /** + * Given name(s) or first name(s) of the End-User. Note that in some cultures, + * people can have multiple given names; all can be present, with the names being separated by space characters. + */ GIVEN_NAME = 'given_name', + /** + * End-User's locale, represented as a BCP47 [RFC5646] language tag. This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase + * and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. For example, en-US or fr-CA. + * + * As a compatibility note, some implementations have used an underscore as the separator rather than a dash, for example, en_US; + * Relying Parties MAY choose to accept this locale syntax as well. + */ LOCALE = 'locale', + /** + * Middle name(s) of the End-User. Note that in some cultures, people can have multiple middle names; all can be present, + * with the names being separated by space characters. Also note that in some cultures, middle names are not used. + */ MIDDLE_NAME = 'middle_name', + /** + * End-User's full name in displayable form including all name parts, possibly including titles and suffixes, + * ordered according to the End-User's locale and preferences. + */ NAME = 'name', + /** + * Casual name of the End-User that may or may not be the same as the given_name. + * For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. + */ NICKNAME = 'nickname', + /** + * End-User's preferred telephone number. E.164 [E.164] is RECOMMENDED as the format of this Claim, for example, + * +1 (425) 555-1212 or +56 (2) 687 2400. + * If the phone number contains an extension, it is RECOMMENDED that the extension be represented using the RFC 3966 [RFC3966] extension syntax, + * for example, +1 (604) 555-1234;ext=5678. + */ PHONE_NUMBER = 'phone_number', + /** + * True if the End-User's phone number has been verified; otherwise false. + * When this Claim Value is true, this means that the OP took affirmative steps to ensure that this phone number was controlled by the End-User + * at the time the verification was performed. The means by which a phone number is verified is context-specific, + * and dependent upon the trust framework or contractual agreements within which the parties are operating. + * When true, the phone_number Claim MUST be in E.164 format and any extensions MUST be represented in RFC 3966 format. + */ + PHONE_NUMBER_VERIFIED = 'phone_number_verified', + /** + * URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), + * rather than to a Web page containing an image. + * + * Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, + * rather than an arbitrary photo taken by the End-User. + */ PICTURE_URL = 'picture', + /** + * Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. + * This value MAY be any valid JSON string including special characters such as @, /, or whitespace. + */ PREFERRED_USERNAME = 'preferred_username', + /** + * URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User. + */ PROFILE_URL = 'profile', + /** + * String from zoneinfo [zoneinfo] time zone database representing the End-User's time zone. For example, Europe/Paris or America/Los_Angeles. + */ TIMEZONE = 'zoneinfo', + /** + * Time the End-User's information was last updated. + * Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. + */ LAST_UPDATE_TIME = 'updated_at', + /** + * URL of the End-User's Web page or blog. + * This Web page SHOULD contain information published by the End-User or an organization that the End-User is affiliated with. + */ WEBSITE = 'website', } diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index d309f379611a2..f2a6763ecefab 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -1,7 +1,7 @@ import { ABSENT } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import { OAuthScope, UserPool, UserPoolClient } from '../lib'; +import { OAuthScope, StandardAttribute, UserPool, UserPoolClient } from '../lib'; describe('User Pool Client', () => { test('default setup', () => { @@ -283,4 +283,102 @@ describe('User Pool Client', () => { AllowedOAuthScopes: [ 'aws.cognito.signin.user.admin' ], }); }); + + test('read/write attributes defaults are set as expected', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client1', { + userPoolClientName: 'Client1', + }); + pool.addClient('Client2', { + userPoolClientName: 'Client2', + readAttributes: undefined, + }); + pool.addClient('Client3', { + userPoolClientName: 'Client3', + writeAttributes: undefined, + }); + pool.addClient('Client4', { + userPoolClientName: 'Client4', + readAttributes: undefined, + writeAttributes: undefined, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client1', + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client2', + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client3', + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client4', + }); + }); + + test('read/write attributes are set as expected', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('None', { + userPoolClientName: 'None', + }); + pool.addClient('CustomOnly', { + userPoolClientName: 'CustomOnly', + readAttributes: { + custom: ['read1', 'custom:read2'], + }, + writeAttributes: { + custom: ['write1'], + }, + }); + pool.addClient('StandardOnly', { + userPoolClientName: 'StandardOnly', + readAttributes: { + standard: [StandardAttribute.ADDRESS, StandardAttribute.EMAIL], + }, + writeAttributes: { + standard: [StandardAttribute.EMAIL], + }, + }); + pool.addClient('AllAttributes', { + userPoolClientName: 'AllAttributes', + readAttributes: { + custom: ['read1'], + standard: [StandardAttribute.EMAIL], + }, + writeAttributes: { + custom: ['write1', 'custom:write2'], + standard: [StandardAttribute.ADDRESS, StandardAttribute.EMAIL], + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'None', + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'CustomOnly', + WriteAttributes: ['custom:write1'], + ReadAttributes: ['custom:read1', 'custom:read2'], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'StandardOnly', + WriteAttributes: ['email'], + ReadAttributes: ['address', 'email'], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'AllAttributes', + WriteAttributes: ['address', 'email', 'custom:write1', 'custom:write2'], + ReadAttributes: ['email', 'custom:read1'], + }); + }); }); \ No newline at end of file From 7ca180c6a1484dbaed12ca0983c35f467b93ff2e Mon Sep 17 00:00:00 2001 From: tom139 Date: Sat, 25 Apr 2020 18:16:04 +0200 Subject: [PATCH 02/17] docs(cognito): add section for read/write attributes in clients Add section to explain hot to make specific attributes available for read and write to specific clients. Fixes #7407 --- packages/@aws-cdk/aws-cognito/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 4c9a5c60ea77f..c596621f00a3d 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -400,6 +400,22 @@ pool.addClient('app-client', { }); ``` +For each client you should specify wich user attributes it will be able to read or write. +The default behavior is to allow read and write of all attribute, but it is often not the best option. +For example, in a webapp there might be a `customerPlan` custom attribute to store the user's plan (`free`, `silver` +or `gold`) but only some clients (i.e. those used by the invoice system) should be allowed to change its value. + +```ts +const pool = new UserPool(this, 'Pool'); +pool.addClient('webapp-client', { + readAttributes: undefined, // by default all attributes are readable/writable + writeAttributes: { + standard: [StandardAttribute.NICKNAME], + custom: ['bio'], + }, // only the nickname and the bio are writable +}); +``` + ### Domains After setting up an [app client](#app-clients), the address for the user pool's sign-up and sign-in webpages can be From 562cc4d5c6e39d84959ab3a6dc03ef601cb46776 Mon Sep 17 00:00:00 2001 From: tom139 Date: Sun, 3 May 2020 12:31:26 +0200 Subject: [PATCH 03/17] docs: improve read-write attributes section in README Co-authored-by: Niranjan Jayakar --- packages/@aws-cdk/aws-cognito/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index c596621f00a3d..a0a4c626f698f 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -400,10 +400,11 @@ pool.addClient('app-client', { }); ``` -For each client you should specify wich user attributes it will be able to read or write. -The default behavior is to allow read and write of all attribute, but it is often not the best option. -For example, in a webapp there might be a `customerPlan` custom attribute to store the user's plan (`free`, `silver` -or `gold`) but only some clients (i.e. those used by the invoice system) should be allowed to change its value. +Clients can be configured so that only specific user attributes can be read or modified by it. +The default behavior is to allow read and write all available attributes, but it is often not the best option. +For example, consider a webapp that contains a custom attribute `customerPlan` to store the user's pricing plan +where only some clients, such as the administrators, should be allowed to change this value. Read more about [Attribute +Permissions and Scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes). ```ts const pool = new UserPool(this, 'Pool'); @@ -445,4 +446,4 @@ pool.addDomain('CustomDomain', { Read more about [Using the Amazon Cognito Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain-prefix.html) and [Using Your Own -Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). \ No newline at end of file +Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). From f57d268215310d37d0f46ff1577baf717954fe2b Mon Sep 17 00:00:00 2001 From: tom139 Date: Sun, 3 May 2020 12:36:49 +0200 Subject: [PATCH 04/17] docs: improve read-write attribute section in user pool client --- packages/@aws-cdk/aws-cognito/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index a0a4c626f698f..049a33f8d283c 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -409,11 +409,13 @@ Permissions and Scopes](https://docs.aws.amazon.com/cognito/latest/developerguid ```ts const pool = new UserPool(this, 'Pool'); pool.addClient('webapp-client', { - readAttributes: undefined, // by default all attributes are readable/writable + readAttributes: { + standard: [StandardAttribute.NICKNAME, StandardAttribute.EMAIL, StandardAttribute.NAME], + custom: ['customerPlan'], + }, writeAttributes: { standard: [StandardAttribute.NICKNAME], - custom: ['bio'], - }, // only the nickname and the bio are writable + }, // only the nickname will be writable }); ``` From 8fe041b85871729fa972dcd1348486c04e28620d Mon Sep 17 00:00:00 2001 From: tom139 Date: Sun, 3 May 2020 12:48:04 +0200 Subject: [PATCH 05/17] docs(cognito): add clarification about user pool client specifying how read/write attributes behave when new attributes get added to a user pool. --- packages/@aws-cdk/aws-cognito/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 049a33f8d283c..7671cf1921fa5 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -419,6 +419,11 @@ pool.addClient('webapp-client', { }); ``` +The default behavior of allowing read/write of all attributes is enforced at Client creation, and only those +attributes available at that time will be included. +If any attribute is added to the User Pool after the creation of the Client, that attribute will not be +available unless explicitly specified. + ### Domains After setting up an [app client](#app-clients), the address for the user pool's sign-up and sign-in webpages can be From 9315f41371df324cab08af8804b740edb08746b8 Mon Sep 17 00:00:00 2001 From: tom139 Date: Sun, 3 May 2020 13:24:56 +0200 Subject: [PATCH 06/17] docs(cognito): improve docstring in userpool Co-authored-by: Niranjan Jayakar --- .../@aws-cdk/aws-cognito/lib/user-pool-client.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index 8c071532433eb..087af3fffdf66 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -176,22 +176,20 @@ export interface UserPoolClientOptions { /** * The attributes this client will be able to read. - * * This should be used to restrict the attributes the client will be * able to read. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes * - * @default undefined all attributes are readable + * @default - all attributes are readable */ readonly readAttributes?: UserPoolClientAttributes; /** * The attributes this client will be able to write. - * * This should be used to restrict the attributes the client will be able to write, - * e.g. the client used by a webapp should not be able to set the `customerPlan` attribute, - * while the client used by the billing backend should. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes * - * @default undefined all attributes are writable + * @default - all attributes are writable */ readonly writeAttributes?: UserPoolClientAttributes; } @@ -212,8 +210,6 @@ export interface UserPoolClientAttributes { * * @note You don't need to prepend them with `custom:` * - * @example customerPlan - * * @default [] */ readonly custom?: string[] From 117eb53ecb6db91fd90453704b646347ef8d46d9 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Mon, 14 Dec 2020 21:43:00 +0000 Subject: [PATCH 07/17] feat(cognito): add AttributeSet and StandardAttributeMask Initial work to add the notion of set of attributes (mixed standard and custom) fixes #7407 --- .../aws-cognito/lib/user-pool-attr.ts | 246 ++++++++++++++++++ .../aws-cognito/test/user-pool-attr.test.ts | 54 +++- 2 files changed, 299 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index e9881bd8b3a14..8b2e6e175f49d 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -341,3 +341,249 @@ export class DateTimeAttribute implements ICustomAttribute { }; } } + +/** + * This interface contains all standard attributes recognized by Cognito + * from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html + * including `preferred_email` and `preferred_phone_number` + */ +export interface StandardAttributesMask { + /** + * The user's postal address. + * @default false + */ + readonly address?: boolean; + + /** + * The user's birthday, represented as an ISO 8601:2004 format. + * @default false + */ + readonly birthdate?: boolean; + + /** + * The user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec. + * @default false + */ + readonly email?: boolean; + + /** + * The surname or last name of the user. + * @default false + */ + readonly familyName?: boolean; + + /** + * The user's gender. + * @default false + */ + readonly gender?: boolean; + + /** + * The user's first name or give name. + * @default false + */ + readonly givenName?: boolean; + + /** + * The user's locale, represented as a BCP47 [RFC5646] language tag. + * @default false + */ + readonly locale?: boolean; + + /** + * The user's middle name. + * @default false + */ + readonly middleName?: boolean; + + /** + * The user's full name in displayable form, including all name parts, titles and suffixes. + * @default false + */ + readonly fullname?: boolean; + + /** + * The user's nickname or casual name. + * @default false + */ + readonly nickname?: boolean; + + /** + * The user's telephone number. + * @default false + */ + readonly phoneNumber?: boolean; + + /** + * The URL to the user's profile picture. + * @default false + */ + readonly profilePicture?: boolean; + + /** + * The user's preffered username, different from the immutable user name. + * @default false + */ + readonly preferredUsername?: boolean; + + /** + * The URL to the user's profile page. + * @default false + */ + readonly profilePage?: boolean; + + /** + * The user's time zone. + * @default false + */ + readonly timezone?: boolean; + + /** + * The time, the user's information was last updated. + * @default false + */ + readonly lastUpdateTime?: boolean; + + /** + * Whether the email address has been verified. + * @default false + */ + readonly emailVerified?: boolean; + + /** + * Whether the phone number has been verified. + * @default false + */ + readonly phoneNumberVerified?: boolean; + + /** + * The URL to the user's web page or blog. + * @default false + */ + readonly website?: boolean; +} + + +/** + * A set of attributes, useful to set Read and Write attributes + */ +export class AttributeSet { + + /** + * Creates a custom AttributeSet with the specified attributes + * @param standard a mask with the standard attributes to include in the set + * @param custom a list of custom attributes to add to the set + */ + public static from(standard: StandardAttributesMask, custom?: string[]): AttributeSet { + const aux = new Set(custom); + if (standard.address === true) { aux.add('address'); } + if (standard.birthdate === true) { aux.add('birthdate'); } + if (standard.email === true) { aux.add('email'); } + if (standard.familyName === true) { aux.add('family_name'); } + if (standard.fullname === true) { aux.add('fullname'); } + if (standard.gender === true) { aux.add('gender'); } + if (standard.givenName === true) { aux.add('given_name'); } + if (standard.lastUpdateTime === true) { aux.add('updated_at'); } + if (standard.locale === true) { aux.add('locale'); } + if (standard.middleName === true) { aux.add('middle_name'); } + if (standard.nickname === true) { aux.add('nickname'); } + if (standard.phoneNumber === true) { aux.add('phone_number'); } + if (standard.preferredUsername === true) { aux.add('preferred_username'); } + if (standard.profilePage === true) { aux.add('profile_page'); } + if (standard.profilePicture === true) { aux.add('profile_picture'); } + if (standard.timezone === true) { aux.add('zoneinfo'); } + if (standard.emailVerified === true) { aux.add('email_verified'); } + if (standard.phoneNumberVerified === true) { aux.add('phone_number_verified'); } + if (standard.website === true) { aux.add('website'); } + return new AttributeSet(aux); + } + + /** + * Creates an empty AttributeSet + */ + public static empty(): AttributeSet { + return new AttributeSet(new Set()); + } + + /** + * Creates an attributes set with all default Cognito Attributes + * from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html + * + * @note there are some attributes (i.e. `verified_email` and `verified_phone_number`) + * that should be kept readonly by most clients. @see `AttributeSet.profileWritable` + * @param custom a list of custom attributes to add to the set + */ + public static allStandard(custom?: string[]): AttributeSet { + let standardAttributes: StandardAttributesMask = { + address: true, + birthdate: true, + email: true, + familyName: true, + fullname: true, + gender: true, + givenName: true, + lastUpdateTime: true, + locale: true, + middleName: true, + nickname: true, + phoneNumber: true, + preferredUsername: true, + profilePage: true, + profilePicture: true, + timezone: true, + emailVerified: true, + phoneNumberVerified: true, + website: true, + }; + return AttributeSet.from(standardAttributes, custom); + } + + /** + * Creates an attributes set with all Cognito Attributes except verified_email and verified_phone_number + * from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html + * + * @note there are some attributes (i.e. `verified_email` and `verified_phone_number`) + * that should be kept readonly by most clients. + * @param custom a list of custom attributes to add to the set + */ + public static profileWritable(custom?: string[]): AttributeSet { + let standardAttributes: StandardAttributesMask = { + address: true, + birthdate: true, + email: true, + familyName: true, + fullname: true, + gender: true, + givenName: true, + lastUpdateTime: true, + locale: true, + middleName: true, + nickname: true, + phoneNumber: true, + preferredUsername: true, + profilePage: true, + profilePicture: true, + timezone: true, + emailVerified: false, + phoneNumberVerified: false, + website: true, + }; + return AttributeSet.from(standardAttributes, custom); + } + + /** + * The set of attributes + */ + private readonly attributeSet: Set = new Set(); + + private constructor(attributeSet: Set) { + this.attributeSet = attributeSet; + } + + /** + * The list of attributes represented by this AttributeSet + */ + public attributes(): string[] { + return Array.from(this.attributeSet); + } +} diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index 43ef1a48d5dd1..f1e490e5f6edf 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { CfnParameter, Stack } from '@aws-cdk/core'; -import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute } from '../lib'; +import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute, AttributeSet } from '../lib'; describe('User Pool Attributes', () => { @@ -178,4 +178,56 @@ describe('User Pool Attributes', () => { expect(bound.numberConstraints).toBeUndefined(); }); }); + + describe('AttributeSet', () => { + test('create empty AttributeSet', () => { + // WHEN + const attributeSet = AttributeSet.empty(); + + // THEN + expect(attributeSet.attributes()).toStrictEqual([]); + }); + + test('create AttributeSet with all standard attributes', () => { + // GIVEN + const customAttributes = ['custom:my_attribute']; + + // WHEN + const attributeSet = AttributeSet.allStandard(customAttributes); + const attributes = attributeSet.attributes(); + + // THEN + expect(attributes.length).toEqual(20); + expect(attributes).toContain('preferred_username'); + expect(attributes).toContain('email_verified'); + expect(attributes).toContain('phone_number_verified'); + expect(attributes).toContain('custom:my_attribute'); + }); + + test('create AttributeSet with profileWritable attributes', () => { + // GIVEN + const attributeSet = AttributeSet.profileWritable(); + const attributes = attributeSet.attributes(); + + // THEN + expect(attributes.length).toEqual(17); + expect(attributes).toContain('preferred_username'); + expect(attributes).not.toContain('email_verified'); + expect(attributes).not.toContain('phone_number_verified'); + }); + + test('create AttributeSet with custom attributes only', () => { + // GIVEN + const customAttributes = ['custom:my_first', 'custom:my_second']; + + // WHEN + const attributeSet = AttributeSet.from({}, customAttributes); + const attributes = attributeSet.attributes(); + + // EXPECT + expect(attributes.length).toEqual(2); + expect(attributes).toContain('custom:my_first'); + expect(attributes).toContain('custom:my_second'); + }); + }); }); From c95781bcfbae8aaca4d7e3e999005fb378e82789 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Mon, 14 Dec 2020 22:11:55 +0000 Subject: [PATCH 08/17] fix: wrong attribute name for profile and picture --- packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index 8b2e6e175f49d..efae71fe9e065 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -480,7 +480,7 @@ export class AttributeSet { if (standard.birthdate === true) { aux.add('birthdate'); } if (standard.email === true) { aux.add('email'); } if (standard.familyName === true) { aux.add('family_name'); } - if (standard.fullname === true) { aux.add('fullname'); } + if (standard.fullname === true) { aux.add('name'); } if (standard.gender === true) { aux.add('gender'); } if (standard.givenName === true) { aux.add('given_name'); } if (standard.lastUpdateTime === true) { aux.add('updated_at'); } @@ -489,8 +489,8 @@ export class AttributeSet { if (standard.nickname === true) { aux.add('nickname'); } if (standard.phoneNumber === true) { aux.add('phone_number'); } if (standard.preferredUsername === true) { aux.add('preferred_username'); } - if (standard.profilePage === true) { aux.add('profile_page'); } - if (standard.profilePicture === true) { aux.add('profile_picture'); } + if (standard.profilePage === true) { aux.add('profile'); } + if (standard.profilePicture === true) { aux.add('picture'); } if (standard.timezone === true) { aux.add('zoneinfo'); } if (standard.emailVerified === true) { aux.add('email_verified'); } if (standard.phoneNumberVerified === true) { aux.add('phone_number_verified'); } From a9bbdeb3231fbb6e79dfc99273dea8dbdbb18cb0 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Mon, 14 Dec 2020 22:21:07 +0000 Subject: [PATCH 09/17] feat(cognito): add ability to set read and write attributes in client fixes #7407 --- .../aws-cognito/lib/user-pool-attr.ts | 3 +- .../aws-cognito/lib/user-pool-client.ts | 17 ++++++++ ...r-pool-client-explicit-props.expected.json | 5 +++ .../integ.user-pool-client-explicit-props.ts | 3 +- .../aws-cognito/test/user-pool-client.test.ts | 41 ++++++++++++++++++- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index efae71fe9e065..e9a08604f120f 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -584,6 +584,7 @@ export class AttributeSet { * The list of attributes represented by this AttributeSet */ public attributes(): string[] { - return Array.from(this.attributeSet); + // sorting is unnecessary but it simplify testing + return Array.from(this.attributeSet).sort(); } } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index d8c8ae2d946bf..4b38d73d34b87 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -2,6 +2,7 @@ import { IResource, Resource, Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnUserPoolClient } from './cognito.generated'; import { IUserPool } from './user-pool'; +import { AttributeSet } from './user-pool-attr'; import { IUserPoolResourceServer, ResourceServerScope } from './user-pool-resource-server'; /** @@ -272,6 +273,20 @@ export interface UserPoolClientOptions { * @default Duration.minutes(60) */ readonly accessTokenValidity?: Duration; + + /** + * The set of attributes this client will be able to read. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes + * @default undefined - all attributes will be readable + */ + readonly readAttributes?: AttributeSet; + + /** + * The set of attributes this client will be able to write. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes + * @default undefined - all attributes will be writable + */ + readonly writeAttributes?: AttributeSet; } /** @@ -358,6 +373,8 @@ export class UserPoolClient extends Resource implements IUserPoolClient { allowedOAuthFlowsUserPoolClient: !props.disableOAuth, preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors), supportedIdentityProviders: this.configureIdentityProviders(props), + readAttributes: props.readAttributes?.attributes(), + writeAttributes: props.writeAttributes?.attributes(), }); this.configureTokenValidity(resource, props); diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json index be2e268a29eac..6d118b0cf046c 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json @@ -62,6 +62,11 @@ "PreventUserExistenceErrors": "ENABLED", "SupportedIdentityProviders": [ "COGNITO" + ], + "WriteAttributes": [ + "address", "birthdate", "custom:attribute_one", "custom:attribute_two", "email", + "family_name", "gender", "given_name", "locale", "middle_name", "name", "nickname", "phone_number", + "picture", "preferred_username", "profile", "updated_at", "website", "zoneinfo" ] } } diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index dbad2591fc1bc..0ef1194144066 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -1,5 +1,5 @@ import { App, Stack } from '@aws-cdk/core'; -import { OAuthScope, UserPool } from '../lib'; +import { OAuthScope, UserPool, AttributeSet } from '../lib'; const app = new App(); const stack = new Stack(app, 'integ-user-pool-client-explicit-props'); @@ -30,4 +30,5 @@ userpool.addClient('myuserpoolclient', { callbackUrls: ['https://redirect-here.myapp.com'], }, preventUserExistenceErrors: true, + writeAttributes: AttributeSet.profileWritable(['custom:attribute_one', 'custom:attribute_two']), }); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 1f961616b095b..06451def954f3 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -1,7 +1,7 @@ -import { ABSENT } from '@aws-cdk/assert'; +import { ABSENT, arrayWith } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack, Duration } from '@aws-cdk/core'; -import { OAuthScope, ResourceServerScope, UserPool, UserPoolClient, UserPoolClientIdentityProvider, UserPoolIdentityProvider } from '../lib'; +import { OAuthScope, ResourceServerScope, UserPool, UserPoolClient, UserPoolClientIdentityProvider, UserPoolIdentityProvider, AttributeSet } from '../lib'; describe('User Pool Client', () => { test('default setup', () => { @@ -827,4 +827,41 @@ describe('User Pool Client', () => { }); }); }); + + describe('read and write attributes', () => { + test('undefined by default', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client', {}); + + // EXPECT + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ReadAttributes: ABSENT, + WriteAttributes: ABSENT, + }); + }); + + test('set attributes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const writeAttributes = AttributeSet.profileWritable(['custom:my_first']); + const readAttributes = AttributeSet.allStandard(); + + // WHEN + pool.addClient('Client', { + readAttributes, + writeAttributes, + }); + + // EXPECT + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ReadAttributes: arrayWith('name', 'given_name', 'family_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'email', 'email_verified', 'gender', 'birthdate', 'zoneinfo', 'locale', 'phone_number', 'phone_number_verified', 'address', 'updated_at'), + WriteAttributes: arrayWith('name', 'given_name', 'family_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'email', 'gender', 'birthdate', 'zoneinfo', 'locale', 'phone_number', 'address', 'updated_at', 'custom:my_first'), + }); + }); + }); }); \ No newline at end of file From 36aa2166f36f8905d733ab7d8272d72ece9e6d02 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Mon, 14 Dec 2020 23:42:59 +0100 Subject: [PATCH 10/17] docs(cognito): for read and write attributes in userpool client Allow to set read and write attributes in Cognito UserPoolClient fixes #7407 --- packages/@aws-cdk/aws-cognito/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index f139f9e7b55bc..fa6d8723f13cf 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -572,6 +572,21 @@ pool.addClient('app-client', { }); ``` +Clients can (and should) be allowed to read and write relevant user attributes only. Usually every client can be allowed to read the `given_name` +attribute but not every client should be allowed to set the `email_verified` attribute. +The same criteria applies for both standard and custom attributes, more info is available at +[Attribute Permissions and Scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes). +The default behaviour is to allow read and write permissions on all attributes. + +```ts +const pool = new cognito.UserPool(this, 'Pool'); +pool.addClient('app-client', { + // ... + readAttributes: AttributeSet.allStandard(['custom:my_attribute_1', 'custom:my_attribute_2']), + writeAttributes: AttributeSet.from({name: true, locale: true}), +}); +``` + ### Resource Servers A resource server is a server for access-protected resources. It handles authenticated requests from an app that has an From 41a22207b10291360b57fdfdbd86982dad5e3c4a Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Tue, 12 Jan 2021 20:43:15 +0000 Subject: [PATCH 11/17] refactor: rename AttributeSet to ClientAttributes --- packages/@aws-cdk/aws-cognito/README.md | 4 +-- .../aws-cognito/lib/user-pool-attr.ts | 34 +++++++++---------- .../aws-cognito/lib/user-pool-client.ts | 6 ++-- .../integ.user-pool-client-explicit-props.ts | 4 +-- .../aws-cognito/test/user-pool-attr.test.ts | 28 +++++++-------- .../aws-cognito/test/user-pool-client.test.ts | 6 ++-- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index fa6d8723f13cf..1ff165eb2e97b 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -582,8 +582,8 @@ The default behaviour is to allow read and write permissions on all attributes. const pool = new cognito.UserPool(this, 'Pool'); pool.addClient('app-client', { // ... - readAttributes: AttributeSet.allStandard(['custom:my_attribute_1', 'custom:my_attribute_2']), - writeAttributes: AttributeSet.from({name: true, locale: true}), + readAttributes: ClientAttributes.allStandard(['custom:my_attribute_1', 'custom:my_attribute_2']), + writeAttributes: ClientAttributes.from({name: true, locale: true}), }); ``` diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index e9a08604f120f..3b1adaa32077b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -467,14 +467,14 @@ export interface StandardAttributesMask { /** * A set of attributes, useful to set Read and Write attributes */ -export class AttributeSet { +export class ClientAttributes { /** - * Creates a custom AttributeSet with the specified attributes + * Creates a custom ClientAttributes with the specified attributes * @param standard a mask with the standard attributes to include in the set * @param custom a list of custom attributes to add to the set */ - public static from(standard: StandardAttributesMask, custom?: string[]): AttributeSet { + public static from(standard: StandardAttributesMask, custom?: string[]): ClientAttributes { const aux = new Set(custom); if (standard.address === true) { aux.add('address'); } if (standard.birthdate === true) { aux.add('birthdate'); } @@ -495,14 +495,14 @@ export class AttributeSet { if (standard.emailVerified === true) { aux.add('email_verified'); } if (standard.phoneNumberVerified === true) { aux.add('phone_number_verified'); } if (standard.website === true) { aux.add('website'); } - return new AttributeSet(aux); + return new ClientAttributes(aux); } /** - * Creates an empty AttributeSet + * Creates an empty ClientAttributes */ - public static empty(): AttributeSet { - return new AttributeSet(new Set()); + public static empty(): ClientAttributes { + return new ClientAttributes(new Set()); } /** @@ -510,10 +510,10 @@ export class AttributeSet { * from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html * * @note there are some attributes (i.e. `verified_email` and `verified_phone_number`) - * that should be kept readonly by most clients. @see `AttributeSet.profileWritable` + * that should be kept readonly by most clients. @see `ClientAttributes.profileWritable` * @param custom a list of custom attributes to add to the set */ - public static allStandard(custom?: string[]): AttributeSet { + public static allStandard(custom?: string[]): ClientAttributes { let standardAttributes: StandardAttributesMask = { address: true, birthdate: true, @@ -535,7 +535,7 @@ export class AttributeSet { phoneNumberVerified: true, website: true, }; - return AttributeSet.from(standardAttributes, custom); + return ClientAttributes.from(standardAttributes, custom); } /** @@ -546,7 +546,7 @@ export class AttributeSet { * that should be kept readonly by most clients. * @param custom a list of custom attributes to add to the set */ - public static profileWritable(custom?: string[]): AttributeSet { + public static profileWritable(custom?: string[]): ClientAttributes { let standardAttributes: StandardAttributesMask = { address: true, birthdate: true, @@ -568,23 +568,23 @@ export class AttributeSet { phoneNumberVerified: false, website: true, }; - return AttributeSet.from(standardAttributes, custom); + return ClientAttributes.from(standardAttributes, custom); } /** * The set of attributes */ - private readonly attributeSet: Set = new Set(); + private readonly attributesSet: Set = new Set(); - private constructor(attributeSet: Set) { - this.attributeSet = attributeSet; + private constructor(attributesSet: Set) { + this.attributesSet = attributesSet; } /** - * The list of attributes represented by this AttributeSet + * The list of attributes represented by this ClientAttributes */ public attributes(): string[] { // sorting is unnecessary but it simplify testing - return Array.from(this.attributeSet).sort(); + return Array.from(this.attributesSet).sort(); } } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index 4b38d73d34b87..311660502e3ee 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -2,7 +2,7 @@ import { IResource, Resource, Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnUserPoolClient } from './cognito.generated'; import { IUserPool } from './user-pool'; -import { AttributeSet } from './user-pool-attr'; +import { ClientAttributes } from './user-pool-attr'; import { IUserPoolResourceServer, ResourceServerScope } from './user-pool-resource-server'; /** @@ -279,14 +279,14 @@ export interface UserPoolClientOptions { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes * @default undefined - all attributes will be readable */ - readonly readAttributes?: AttributeSet; + readonly readAttributes?: ClientAttributes; /** * The set of attributes this client will be able to write. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes * @default undefined - all attributes will be writable */ - readonly writeAttributes?: AttributeSet; + readonly writeAttributes?: ClientAttributes; } /** diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index 0ef1194144066..d339658e62e6e 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -1,5 +1,5 @@ import { App, Stack } from '@aws-cdk/core'; -import { OAuthScope, UserPool, AttributeSet } from '../lib'; +import { OAuthScope, UserPool, ClientAttributes } from '../lib'; const app = new App(); const stack = new Stack(app, 'integ-user-pool-client-explicit-props'); @@ -30,5 +30,5 @@ userpool.addClient('myuserpoolclient', { callbackUrls: ['https://redirect-here.myapp.com'], }, preventUserExistenceErrors: true, - writeAttributes: AttributeSet.profileWritable(['custom:attribute_one', 'custom:attribute_two']), + writeAttributes: ClientAttributes.profileWritable(['custom:attribute_one', 'custom:attribute_two']), }); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index f1e490e5f6edf..978e1640b1cd2 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { CfnParameter, Stack } from '@aws-cdk/core'; -import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute, AttributeSet } from '../lib'; +import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute, ClientAttributes } from '../lib'; describe('User Pool Attributes', () => { @@ -179,22 +179,22 @@ describe('User Pool Attributes', () => { }); }); - describe('AttributeSet', () => { - test('create empty AttributeSet', () => { + describe('ClientAttributes', () => { + test('create empty ClientAttributes', () => { // WHEN - const attributeSet = AttributeSet.empty(); + const clientAttributes = ClientAttributes.empty(); // THEN - expect(attributeSet.attributes()).toStrictEqual([]); + expect(clientAttributes.attributes()).toStrictEqual([]); }); - test('create AttributeSet with all standard attributes', () => { + test('create ClientAttributes with all standard attributes', () => { // GIVEN const customAttributes = ['custom:my_attribute']; // WHEN - const attributeSet = AttributeSet.allStandard(customAttributes); - const attributes = attributeSet.attributes(); + const clientAttributes = ClientAttributes.allStandard(customAttributes); + const attributes = clientAttributes.attributes(); // THEN expect(attributes.length).toEqual(20); @@ -204,10 +204,10 @@ describe('User Pool Attributes', () => { expect(attributes).toContain('custom:my_attribute'); }); - test('create AttributeSet with profileWritable attributes', () => { + test('create ClientAttributes with profileWritable attributes', () => { // GIVEN - const attributeSet = AttributeSet.profileWritable(); - const attributes = attributeSet.attributes(); + const clientAttributes = ClientAttributes.profileWritable(); + const attributes = clientAttributes.attributes(); // THEN expect(attributes.length).toEqual(17); @@ -216,13 +216,13 @@ describe('User Pool Attributes', () => { expect(attributes).not.toContain('phone_number_verified'); }); - test('create AttributeSet with custom attributes only', () => { + test('create ClientAttributes with custom attributes only', () => { // GIVEN const customAttributes = ['custom:my_first', 'custom:my_second']; // WHEN - const attributeSet = AttributeSet.from({}, customAttributes); - const attributes = attributeSet.attributes(); + const clientAttributes = ClientAttributes.from({}, customAttributes); + const attributes = clientAttributes.attributes(); // EXPECT expect(attributes.length).toEqual(2); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 06451def954f3..60e312eaff7de 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -1,7 +1,7 @@ import { ABSENT, arrayWith } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack, Duration } from '@aws-cdk/core'; -import { OAuthScope, ResourceServerScope, UserPool, UserPoolClient, UserPoolClientIdentityProvider, UserPoolIdentityProvider, AttributeSet } from '../lib'; +import { OAuthScope, ResourceServerScope, UserPool, UserPoolClient, UserPoolClientIdentityProvider, UserPoolIdentityProvider, ClientAttributes } from '../lib'; describe('User Pool Client', () => { test('default setup', () => { @@ -848,8 +848,8 @@ describe('User Pool Client', () => { // GIVEN const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); - const writeAttributes = AttributeSet.profileWritable(['custom:my_first']); - const readAttributes = AttributeSet.allStandard(); + const writeAttributes = ClientAttributes.profileWritable(['custom:my_first']); + const readAttributes = ClientAttributes.allStandard(); // WHEN pool.addClient('Client', { From 9264458eee029bb9ba81f2afece69f22d8688984 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Tue, 12 Jan 2021 23:04:24 +0000 Subject: [PATCH 12/17] refactor: ClientAttributes to more expressive API --- packages/@aws-cdk/aws-cognito/README.md | 12 +- .../aws-cognito/lib/user-pool-attr.ts | 141 ++++++------------ .../integ.user-pool-client-explicit-props.ts | 21 ++- .../aws-cognito/test/user-pool-attr.test.ts | 56 +++++-- .../aws-cognito/test/user-pool-client.test.ts | 26 +++- 5 files changed, 144 insertions(+), 112 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 1ff165eb2e97b..cc07f75c8b5fb 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -580,10 +580,18 @@ The default behaviour is to allow read and write permissions on all attributes. ```ts const pool = new cognito.UserPool(this, 'Pool'); +// create a set of attributes that the client will be allowed to set +const clientWriteAttributes = ClientAttributes.empty() + .withStandardAttributes({name: true, email: true}) + .withCustomAttributes(['favouritePizza']); +// read attributes are usually more than the writable ones +const clientReadAttributes = clientWriteAttributes + .withStandardAttributes({emailVerified: true}) + .withCustomAttributes(['pointsEarned']); pool.addClient('app-client', { // ... - readAttributes: ClientAttributes.allStandard(['custom:my_attribute_1', 'custom:my_attribute_2']), - writeAttributes: ClientAttributes.from({name: true, locale: true}), + readAttributes: clientReadAttributes, + writeAttributes: clientWriteAttributes, }); ``` diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index 3b1adaa32077b..1dd8f227b60f0 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -469,115 +469,64 @@ export interface StandardAttributesMask { */ export class ClientAttributes { - /** - * Creates a custom ClientAttributes with the specified attributes - * @param standard a mask with the standard attributes to include in the set - * @param custom a list of custom attributes to add to the set - */ - public static from(standard: StandardAttributesMask, custom?: string[]): ClientAttributes { - const aux = new Set(custom); - if (standard.address === true) { aux.add('address'); } - if (standard.birthdate === true) { aux.add('birthdate'); } - if (standard.email === true) { aux.add('email'); } - if (standard.familyName === true) { aux.add('family_name'); } - if (standard.fullname === true) { aux.add('name'); } - if (standard.gender === true) { aux.add('gender'); } - if (standard.givenName === true) { aux.add('given_name'); } - if (standard.lastUpdateTime === true) { aux.add('updated_at'); } - if (standard.locale === true) { aux.add('locale'); } - if (standard.middleName === true) { aux.add('middle_name'); } - if (standard.nickname === true) { aux.add('nickname'); } - if (standard.phoneNumber === true) { aux.add('phone_number'); } - if (standard.preferredUsername === true) { aux.add('preferred_username'); } - if (standard.profilePage === true) { aux.add('profile'); } - if (standard.profilePicture === true) { aux.add('picture'); } - if (standard.timezone === true) { aux.add('zoneinfo'); } - if (standard.emailVerified === true) { aux.add('email_verified'); } - if (standard.phoneNumberVerified === true) { aux.add('phone_number_verified'); } - if (standard.website === true) { aux.add('website'); } - return new ClientAttributes(aux); - } - /** * Creates an empty ClientAttributes */ public static empty(): ClientAttributes { - return new ClientAttributes(new Set()); + return new ClientAttributes(); } /** - * Creates an attributes set with all default Cognito Attributes - * from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html - * - * @note there are some attributes (i.e. `verified_email` and `verified_phone_number`) - * that should be kept readonly by most clients. @see `ClientAttributes.profileWritable` - * @param custom a list of custom attributes to add to the set - */ - public static allStandard(custom?: string[]): ClientAttributes { - let standardAttributes: StandardAttributesMask = { - address: true, - birthdate: true, - email: true, - familyName: true, - fullname: true, - gender: true, - givenName: true, - lastUpdateTime: true, - locale: true, - middleName: true, - nickname: true, - phoneNumber: true, - preferredUsername: true, - profilePage: true, - profilePicture: true, - timezone: true, - emailVerified: true, - phoneNumberVerified: true, - website: true, - }; - return ClientAttributes.from(standardAttributes, custom); + * The set of attributes + */ + private readonly attributesSet: Set; + + private constructor(attributesSet?: Set) { + this.attributesSet = attributesSet ?? new Set(); } /** - * Creates an attributes set with all Cognito Attributes except verified_email and verified_phone_number - * from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html - * - * @note there are some attributes (i.e. `verified_email` and `verified_phone_number`) - * that should be kept readonly by most clients. - * @param custom a list of custom attributes to add to the set - */ - public static profileWritable(custom?: string[]): ClientAttributes { - let standardAttributes: StandardAttributesMask = { - address: true, - birthdate: true, - email: true, - familyName: true, - fullname: true, - gender: true, - givenName: true, - lastUpdateTime: true, - locale: true, - middleName: true, - nickname: true, - phoneNumber: true, - preferredUsername: true, - profilePage: true, - profilePicture: true, - timezone: true, - emailVerified: false, - phoneNumberVerified: false, - website: true, - }; - return ClientAttributes.from(standardAttributes, custom); + * Creates a custom ClientAttributes with the specified attributes + * @param attributes a list of standard attributes to add to the set + */ + public withStandardAttributes(attributes: StandardAttributesMask): ClientAttributes { + let attributesSet = new Set(this.attributesSet); + if (attributes.address === true) { attributesSet.add('address'); } + if (attributes.birthdate === true) { attributesSet.add('birthdate'); } + if (attributes.email === true) { attributesSet.add('email'); } + if (attributes.familyName === true) { attributesSet.add('family_name'); } + if (attributes.fullname === true) { attributesSet.add('name'); } + if (attributes.gender === true) { attributesSet.add('gender'); } + if (attributes.givenName === true) { attributesSet.add('given_name'); } + if (attributes.lastUpdateTime === true) { attributesSet.add('updated_at'); } + if (attributes.locale === true) { attributesSet.add('locale'); } + if (attributes.middleName === true) { attributesSet.add('middle_name'); } + if (attributes.nickname === true) { attributesSet.add('nickname'); } + if (attributes.phoneNumber === true) { attributesSet.add('phone_number'); } + if (attributes.preferredUsername === true) { attributesSet.add('preferred_username'); } + if (attributes.profilePage === true) { attributesSet.add('profile'); } + if (attributes.profilePicture === true) { attributesSet.add('picture'); } + if (attributes.timezone === true) { attributesSet.add('zoneinfo'); } + if (attributes.emailVerified === true) { attributesSet.add('email_verified'); } + if (attributes.phoneNumberVerified === true) { attributesSet.add('phone_number_verified'); } + if (attributes.website === true) { attributesSet.add('website'); } + return new ClientAttributes(attributesSet); } /** - * The set of attributes - */ - private readonly attributesSet: Set = new Set(); - - private constructor(attributesSet: Set) { - this.attributesSet = attributesSet; + * Creates a custom ClientAttributes with the specified attributes + * @param attributes a list of custom attributes to add to the set + */ + public withCustomAttributes(...attributes: string[]): ClientAttributes { + let attributesSet: Set = new Set(this.attributesSet); + for (let attribute of attributes) { + // custom attributes MUST begin with `custom:`, so add the string if not present + if (!attribute.startsWith('custom:')) { + attribute = 'custom:' + attribute; + } + attributesSet.add(attribute); + } + return new ClientAttributes(attributesSet); } /** diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index d339658e62e6e..f5e81c1e5e6f5 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -30,5 +30,24 @@ userpool.addClient('myuserpoolclient', { callbackUrls: ['https://redirect-here.myapp.com'], }, preventUserExistenceErrors: true, - writeAttributes: ClientAttributes.profileWritable(['custom:attribute_one', 'custom:attribute_two']), + writeAttributes: ClientAttributes.empty().withStandardAttributes( + { + address: true, + birthdate: true, + email: true, + familyName: true, + fullname: true, + gender: true, + givenName: true, + lastUpdateTime: true, + locale: true, + middleName: true, + nickname: true, + phoneNumber: true, + preferredUsername: true, + profilePage: true, + profilePicture: true, + timezone: true, + website: true, + }).withCustomAttributes('attribute_one', 'attribute_two'), }); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index 978e1640b1cd2..1eb4cf34db28d 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -193,7 +193,27 @@ describe('User Pool Attributes', () => { const customAttributes = ['custom:my_attribute']; // WHEN - const clientAttributes = ClientAttributes.allStandard(customAttributes); + const clientAttributes = ClientAttributes.empty().withStandardAttributes({ + address: true, + birthdate: true, + email: true, + emailVerified: true, + familyName: true, + fullname: true, + gender: true, + givenName: true, + lastUpdateTime: true, + locale: true, + middleName: true, + nickname: true, + phoneNumber: true, + phoneNumberVerified: true, + preferredUsername: true, + profilePage: true, + profilePicture: true, + timezone: true, + website: true, + }).withCustomAttributes(...customAttributes); const attributes = clientAttributes.attributes(); // THEN @@ -204,24 +224,40 @@ describe('User Pool Attributes', () => { expect(attributes).toContain('custom:my_attribute'); }); - test('create ClientAttributes with profileWritable attributes', () => { + test('create ClientAttributes copying another one', () => { // GIVEN - const clientAttributes = ClientAttributes.profileWritable(); - const attributes = clientAttributes.attributes(); + const original = ClientAttributes.empty() + .withStandardAttributes({ email: true }) + .withCustomAttributes('custom1'); + const copied = original + .withStandardAttributes({ emailVerified: true }) + .withCustomAttributes('custom2'); + + // WHEN + const originalAttributes = original.attributes(); + const copiedAttributes = copied.attributes(); // THEN - expect(attributes.length).toEqual(17); - expect(attributes).toContain('preferred_username'); - expect(attributes).not.toContain('email_verified'); - expect(attributes).not.toContain('phone_number_verified'); + expect(originalAttributes.length).toEqual(2); + expect(copiedAttributes.length).toEqual(4); + // originals MUST NOT contain the added ones + expect(originalAttributes).toContain('email'); + expect(originalAttributes).toContain('custom:custom1'); + expect(originalAttributes).not.toContain('email_verified'); + expect(originalAttributes).not.toContain('custom:custom2'); + // copied MUST contain all attributes + expect(copiedAttributes).toContain('email'); + expect(copiedAttributes).toContain('custom:custom1'); + expect(copiedAttributes).toContain('email_verified'); + expect(copiedAttributes).toContain('custom:custom2'); }); test('create ClientAttributes with custom attributes only', () => { // GIVEN - const customAttributes = ['custom:my_first', 'custom:my_second']; + const customAttributes = ['custom:my_first', 'my_second']; // WHEN - const clientAttributes = ClientAttributes.from({}, customAttributes); + const clientAttributes = ClientAttributes.empty().withCustomAttributes(...customAttributes); const attributes = clientAttributes.attributes(); // EXPECT diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 60e312eaff7de..4d7805892fee2 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -848,8 +848,28 @@ describe('User Pool Client', () => { // GIVEN const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); - const writeAttributes = ClientAttributes.profileWritable(['custom:my_first']); - const readAttributes = ClientAttributes.allStandard(); + const writeAttributes = ClientAttributes.empty().withCustomAttributes('my_first').withStandardAttributes({ givenName: true, familyName: true }); + const readAttributes = ClientAttributes.empty().withStandardAttributes({ + address: true, + birthdate: true, + email: true, + emailVerified: true, + familyName: true, + fullname: true, + gender: true, + givenName: true, + lastUpdateTime: true, + locale: true, + middleName: true, + nickname: true, + phoneNumber: true, + phoneNumberVerified: true, + preferredUsername: true, + profilePage: true, + profilePicture: true, + timezone: true, + website: true, + }); // WHEN pool.addClient('Client', { @@ -860,7 +880,7 @@ describe('User Pool Client', () => { // EXPECT expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { ReadAttributes: arrayWith('name', 'given_name', 'family_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'email', 'email_verified', 'gender', 'birthdate', 'zoneinfo', 'locale', 'phone_number', 'phone_number_verified', 'address', 'updated_at'), - WriteAttributes: arrayWith('name', 'given_name', 'family_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'email', 'gender', 'birthdate', 'zoneinfo', 'locale', 'phone_number', 'address', 'updated_at', 'custom:my_first'), + WriteAttributes: arrayWith('given_name', 'family_name', 'custom:my_first'), }); }); }); From 76be44d8408800f0f42838c80025a518fce3adc3 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Tue, 12 Jan 2021 23:18:17 +0000 Subject: [PATCH 13/17] docs: fix comment on email_verified and phone_number_verified --- packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index 1dd8f227b60f0..c38613c2bda7c 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -345,7 +345,7 @@ export class DateTimeAttribute implements ICustomAttribute { /** * This interface contains all standard attributes recognized by Cognito * from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html - * including `preferred_email` and `preferred_phone_number` + * including `email_verified` and `phone_number_verified` */ export interface StandardAttributesMask { /** From 9c986b36245527c6ba435280edf0f8dc8f08ce65 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo <8103786+tom139@users.noreply.github.com> Date: Wed, 13 Jan 2021 00:43:20 +0100 Subject: [PATCH 14/17] docs: update default behaviour description for ClientAttributes Co-authored-by: Niranjan Jayakar --- packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index 311660502e3ee..ea5693f45d1c4 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -277,14 +277,14 @@ export interface UserPoolClientOptions { /** * The set of attributes this client will be able to read. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes - * @default undefined - all attributes will be readable + * @default - all standard and custom attributes */ readonly readAttributes?: ClientAttributes; /** * The set of attributes this client will be able to write. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes - * @default undefined - all attributes will be writable + * @default - all standard and custom attributes */ readonly writeAttributes?: ClientAttributes; } From 6b6630caa6dfa88a47960e8bcd0d211f46b6d521 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Thu, 14 Jan 2021 12:28:09 +0000 Subject: [PATCH 15/17] fix: add emailVerified and phoneNumberVerified to StandardAttributes --- .../aws-cognito/lib/private/attr-names.ts | 2 + .../aws-cognito/lib/user-pool-attr.ts | 52 ++++++++++--------- .../aws-cognito/test/user-pool-attr.test.ts | 22 ++++++++ 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts b/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts index 1f0891cec1704..c3b07ddbb7742 100644 --- a/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts +++ b/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts @@ -16,4 +16,6 @@ export const StandardAttributeNames = { timezone: 'zoneinfo', lastUpdateTime: 'updated_at', website: 'website', + emailVerified: 'email_verified', + phoneNumberVerified: 'phone_number_verified', }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index c38613c2bda7c..03ea1eb73aab0 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -1,4 +1,5 @@ import { Token } from '@aws-cdk/core'; +import { StandardAttributeNames } from './private/attr-names'; /** * The set of standard attributes that can be marked as required or mutable. @@ -107,6 +108,18 @@ export interface StandardAttributes { * @default - see the defaults under `StandardAttribute` */ readonly website?: StandardAttribute; + + /** + * Whether the email address has been verified. + * @default - see the defaults under `StandardAttribute` + */ + readonly emailVerified?: StandardAttribute; + + /** + * Whether the phone number has been verified. + * @default - see the defaults under `StandardAttribute` + */ + readonly phoneNumberVerified?: StandardAttribute; } /** @@ -445,22 +458,22 @@ export interface StandardAttributesMask { readonly lastUpdateTime?: boolean; /** - * Whether the email address has been verified. + * The URL to the user's web page or blog. * @default false */ - readonly emailVerified?: boolean; + readonly website?: boolean; /** - * Whether the phone number has been verified. + * Whether the email address has been verified. * @default false */ - readonly phoneNumberVerified?: boolean; + readonly emailVerified?: boolean; /** - * The URL to the user's web page or blog. + * Whether the phone number has been verified. * @default false */ - readonly website?: boolean; + readonly phoneNumberVerified?: boolean; } @@ -491,25 +504,14 @@ export class ClientAttributes { */ public withStandardAttributes(attributes: StandardAttributesMask): ClientAttributes { let attributesSet = new Set(this.attributesSet); - if (attributes.address === true) { attributesSet.add('address'); } - if (attributes.birthdate === true) { attributesSet.add('birthdate'); } - if (attributes.email === true) { attributesSet.add('email'); } - if (attributes.familyName === true) { attributesSet.add('family_name'); } - if (attributes.fullname === true) { attributesSet.add('name'); } - if (attributes.gender === true) { attributesSet.add('gender'); } - if (attributes.givenName === true) { attributesSet.add('given_name'); } - if (attributes.lastUpdateTime === true) { attributesSet.add('updated_at'); } - if (attributes.locale === true) { attributesSet.add('locale'); } - if (attributes.middleName === true) { attributesSet.add('middle_name'); } - if (attributes.nickname === true) { attributesSet.add('nickname'); } - if (attributes.phoneNumber === true) { attributesSet.add('phone_number'); } - if (attributes.preferredUsername === true) { attributesSet.add('preferred_username'); } - if (attributes.profilePage === true) { attributesSet.add('profile'); } - if (attributes.profilePicture === true) { attributesSet.add('picture'); } - if (attributes.timezone === true) { attributesSet.add('zoneinfo'); } - if (attributes.emailVerified === true) { attributesSet.add('email_verified'); } - if (attributes.phoneNumberVerified === true) { attributesSet.add('phone_number_verified'); } - if (attributes.website === true) { attributesSet.add('website'); } + // iterate through key-values in the `StandardAttributeNames` constant + // to get the value for all attributes + for (const attributeKey in StandardAttributeNames) { + if ((attributes as any)[attributeKey] === true) { + const attributeName = (StandardAttributeNames as any)[attributeKey]; + attributesSet.add(attributeName); + } + } return new ClientAttributes(attributesSet); } diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index 1eb4cf34db28d..dd9e64b2a66b4 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; import { CfnParameter, Stack } from '@aws-cdk/core'; import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute, ClientAttributes } from '../lib'; +import { StandardAttributeNames } from '../lib/private/attr-names'; describe('User Pool Attributes', () => { @@ -265,5 +266,26 @@ describe('User Pool Attributes', () => { expect(attributes).toContain('custom:my_first'); expect(attributes).toContain('custom:my_second'); }); + + test('create ClientAttributes with all StandardAttributeNames', () => { + // this test is intended to check if changes in the `StandardAttributeNames` constant + // does not reflect as changes in the `StandardAttributesMask` + // GIVEN + let allStandardAttributes = {} as any; + let standardAttributeNamesCount = 0; // the count of StandardAttributeNames + // iterate through the standard attribute names + for (const attributeKey in StandardAttributeNames) { + standardAttributeNamesCount += 1; + expect(StandardAttributeNames).toHaveProperty(attributeKey); + // add the standard attribute + allStandardAttributes[attributeKey] = true; + } + + // WHEN + const attributes = ClientAttributes.empty().withStandardAttributes(allStandardAttributes).attributes(); + + // EXPECT + expect(attributes.length).toEqual(standardAttributeNamesCount); + }); }); }); From 2c2009dde51e07f070be663247f41ad98c0906b1 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Mon, 18 Jan 2021 21:52:11 +0000 Subject: [PATCH 16/17] refactor: remove ClientAttributes.empty() method and replace it with (new ClientAttribute()) --- packages/@aws-cdk/aws-cognito/README.md | 2 +- .../aws-cognito/lib/user-pool-attr.ts | 24 ++++++++++--------- .../integ.user-pool-client-explicit-props.ts | 2 +- .../aws-cognito/test/user-pool-attr.test.ts | 10 ++++---- .../aws-cognito/test/user-pool-client.test.ts | 4 ++-- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index cc07f75c8b5fb..36d4977a861c5 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -581,7 +581,7 @@ The default behaviour is to allow read and write permissions on all attributes. ```ts const pool = new cognito.UserPool(this, 'Pool'); // create a set of attributes that the client will be allowed to set -const clientWriteAttributes = ClientAttributes.empty() +const clientWriteAttributes = (new ClientAttributes()) .withStandardAttributes({name: true, email: true}) .withCustomAttributes(['favouritePizza']); // read attributes are usually more than the writable ones diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index 03ea1eb73aab0..0687a0cb123ba 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -483,19 +483,17 @@ export interface StandardAttributesMask { export class ClientAttributes { /** - * Creates an empty ClientAttributes + * The set of attributes */ - public static empty(): ClientAttributes { - return new ClientAttributes(); - } + private attributesSet: Set; /** - * The set of attributes + * Creates a ClientAttributes with the specified attributes + * + * @default - a ClientAttributes object without any attributes */ - private readonly attributesSet: Set; - - private constructor(attributesSet?: Set) { - this.attributesSet = attributesSet ?? new Set(); + constructor() { + this.attributesSet = new Set(); } /** @@ -512,7 +510,9 @@ export class ClientAttributes { attributesSet.add(attributeName); } } - return new ClientAttributes(attributesSet); + let aux = new ClientAttributes(); + aux.attributesSet = attributesSet; + return aux; } /** @@ -528,7 +528,9 @@ export class ClientAttributes { } attributesSet.add(attribute); } - return new ClientAttributes(attributesSet); + let aux = new ClientAttributes(); + aux.attributesSet = attributesSet; + return aux; } /** diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index f5e81c1e5e6f5..2cd4557cdb48a 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -30,7 +30,7 @@ userpool.addClient('myuserpoolclient', { callbackUrls: ['https://redirect-here.myapp.com'], }, preventUserExistenceErrors: true, - writeAttributes: ClientAttributes.empty().withStandardAttributes( + writeAttributes: (new ClientAttributes()).withStandardAttributes( { address: true, birthdate: true, diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index dd9e64b2a66b4..1d5b5a7bd5d5f 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -183,7 +183,7 @@ describe('User Pool Attributes', () => { describe('ClientAttributes', () => { test('create empty ClientAttributes', () => { // WHEN - const clientAttributes = ClientAttributes.empty(); + const clientAttributes = (new ClientAttributes()); // THEN expect(clientAttributes.attributes()).toStrictEqual([]); @@ -194,7 +194,7 @@ describe('User Pool Attributes', () => { const customAttributes = ['custom:my_attribute']; // WHEN - const clientAttributes = ClientAttributes.empty().withStandardAttributes({ + const clientAttributes = (new ClientAttributes()).withStandardAttributes({ address: true, birthdate: true, email: true, @@ -227,7 +227,7 @@ describe('User Pool Attributes', () => { test('create ClientAttributes copying another one', () => { // GIVEN - const original = ClientAttributes.empty() + const original = (new ClientAttributes()) .withStandardAttributes({ email: true }) .withCustomAttributes('custom1'); const copied = original @@ -258,7 +258,7 @@ describe('User Pool Attributes', () => { const customAttributes = ['custom:my_first', 'my_second']; // WHEN - const clientAttributes = ClientAttributes.empty().withCustomAttributes(...customAttributes); + const clientAttributes = (new ClientAttributes()).withCustomAttributes(...customAttributes); const attributes = clientAttributes.attributes(); // EXPECT @@ -282,7 +282,7 @@ describe('User Pool Attributes', () => { } // WHEN - const attributes = ClientAttributes.empty().withStandardAttributes(allStandardAttributes).attributes(); + const attributes = (new ClientAttributes()).withStandardAttributes(allStandardAttributes).attributes(); // EXPECT expect(attributes.length).toEqual(standardAttributeNamesCount); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 4d7805892fee2..3a056cd02dda7 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -848,8 +848,8 @@ describe('User Pool Client', () => { // GIVEN const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); - const writeAttributes = ClientAttributes.empty().withCustomAttributes('my_first').withStandardAttributes({ givenName: true, familyName: true }); - const readAttributes = ClientAttributes.empty().withStandardAttributes({ + const writeAttributes = (new ClientAttributes()).withCustomAttributes('my_first').withStandardAttributes({ givenName: true, familyName: true }); + const readAttributes = (new ClientAttributes()).withStandardAttributes({ address: true, birthdate: true, email: true, From b9e0a2c35e90cfd8a99b320667810feacb8ea9a8 Mon Sep 17 00:00:00 2001 From: Tommaso Panozzo Date: Mon, 18 Jan 2021 22:06:03 +0000 Subject: [PATCH 17/17] docs: improve README on ClientAttributes as suggested by @nija-at closes #7607 --- packages/@aws-cdk/aws-cognito/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 36d4977a861c5..5067ae8f44b63 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -576,18 +576,19 @@ Clients can (and should) be allowed to read and write relevant user attributes o attribute but not every client should be allowed to set the `email_verified` attribute. The same criteria applies for both standard and custom attributes, more info is available at [Attribute Permissions and Scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes). -The default behaviour is to allow read and write permissions on all attributes. +The default behaviour is to allow read and write permissions on all attributes. The following code shows how this can be configured for a client. ```ts const pool = new cognito.UserPool(this, 'Pool'); -// create a set of attributes that the client will be allowed to set + const clientWriteAttributes = (new ClientAttributes()) .withStandardAttributes({name: true, email: true}) .withCustomAttributes(['favouritePizza']); -// read attributes are usually more than the writable ones + const clientReadAttributes = clientWriteAttributes .withStandardAttributes({emailVerified: true}) .withCustomAttributes(['pointsEarned']); + pool.addClient('app-client', { // ... readAttributes: clientReadAttributes,