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): add mutable property in cognito user pool custom attribute #7190

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/@aws-cdk/aws-cognito/README.md
Expand Up @@ -172,9 +172,9 @@ new UserPool(this, 'myuserpool', {
address: true,
},
customAttributes: {
'myappid': new StringAttribute({ minLen: 5, maxLen: 15 }),
'myappid': new StringAttribute({ minLen: 5, maxLen: 15, mutable: false }),
'callingcode': new NumberAttribute({ min: 1, max: 3 }),
'isEmployee': new BooleanAttribute(),
'isEmployee': new BooleanAttribute({ developerOnly: true }),
'joinedOn': new DateTimeAttribute(),
},
});
Expand All @@ -183,7 +183,12 @@ new UserPool(this, 'myuserpool', {
As shown in the code snippet, there are data types that are available for custom attributes. The 'String' and 'Number'
data types allow for further constraints on their length and values, respectively.

Custom attributes cannot be marked as required.
nija-at marked this conversation as resolved.
Show resolved Hide resolved
All custom attributes share the common properties `developerOnly` and `mutable`.

- `developerOnly` means that this attribute can only be modified by an administrator. The use of this property is discouraged
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of this property is discouraged

Is this Cognito's recommendation or something you are suggesting?
If it's the former, can you point to where they are saying so?

OTOH, if it's the latter, could you explain why you are suggesting this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is Cognito's recommendation

We recommend that you use WriteAttributes in the user pool client to control how attributes can be mutated for new use cases instead of using DeveloperOnlyAttribute.

from AWS::Cognito::UserPool SchemaAttribute in Cloudformation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In such a case, should the CDK even implement this? Can we instead implement WriteAttributes on the client, so users use the right thing?

For users who really want to use this attribute or if they're migrating an existing user pool to the CDK, they can always access this via CDK's escape hatches.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like your point.

I think the implementation might involve changing the UserPoolClient class to allow set the read/write attributes for that specific client.

Do you think this should be implemented in its own issue/PR or is it fine to do it here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this should be implemented in its own issue/PR or is it fine to do it here?

A separate PR would be preferable and cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will remove the support for the developerOnly attribute altogether and propose a new PR.

in favour of the use of write permissions for attributes in the user pool client (see [AppClients](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html)),

- `mutable` allows the value to be changed after the value is set by the user.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does setting mutable to false mean that even administrators can't modify it?

In general, is there a combination of mutable and developerOnly that is invalid or does not make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question.
Yes, setting mutable to false means that even administrators can't modify it but it still make sense to keep both properties in case the users are allowed to sign up by themselves.

If only admins (==developers) can create new users than mutable && developerOnly has the same meaning of mutable because the attribute must be set on creation and only developers can do it.

If sign up is enabled, then mutable && developerOnly on an attribute means that only users created via the AdminCreateUser api can have that attribute set while users created via the SignUp API will never be able to set it.


### Security

Expand Down
116 changes: 102 additions & 14 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts
Expand Up @@ -141,6 +141,84 @@ export interface CustomAttributeConfig {
* @default - None.
*/
readonly numberConstraints?: NumberAttributeConstraints;

/**
* Specifies whether the attribute type is developer only. This attribute can only be modified by an administrator.
* Users will not be able to modify this attribute using their access token.
*
* @default false
*/
readonly developerOnly?: boolean

/**
* Specifies whether the value of the attribute can be changed.
* For any user pool attribute that's mapped to an identity provider attribute, you must set this parameter 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 false
*/
readonly mutable?: boolean

}

/**
* Constraints that can be applied to a custom attribute of string type.
*/
export interface BaseCustomAttributeProps {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the prefix Base from here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

/**
* Specifies whether the attribute type is developer only. This attribute can only be modified by an administrator.
* Users will not be able to modify this attribute using their access token.
*
* @default false
*/
readonly developerOnly?: boolean

/**
* Specifies whether the value of the attribute can be changed.
* For any user pool attribute that's mapped to an identity provider attribute, you must set this parameter 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 false
*/
readonly mutable?: boolean
}

/**
* The base class for all custom attribute types.
* All common properties are set here and the method `baseAttributeConfig`
* should be used by subclasses to create base CustomAttributeConfig object inside the `bind()` method.
*/
abstract class BaseCustomAttribute implements ICustomAttribute {
Copy link
Contributor

@nija-at nija-at Apr 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Let's drop 'Base'.

Unfortunately, if an exported class inherits this, this will also need to be exported.

Suggested change
abstract class BaseCustomAttribute implements ICustomAttribute {
export abstract class CustomAttribute implements ICustomAttribute {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

protected readonly developerOnly?: boolean;
protected readonly mutable?: boolean;

/**
* Every subclass must define its dataType (i.e. `String`, `Number`, ...)
*/
protected abstract readonly dataType: string;

constructor(props: BaseCustomAttributeProps = {}) {
this.developerOnly = props.developerOnly;
this.mutable = props.mutable;
}

public abstract bind(): CustomAttributeConfig;

/**
* Creates a CustomAttributeConfig to be used as starting point by each
* subclass do provide the right CustomAttributeConfig.
*
* This must contain all common properties between attribute types.
*/
protected baseAttributeConfig(): CustomAttributeConfig {
return {
dataType: this.dataType,
developerOnly: this.developerOnly,
mutable: this.mutable,
};
}
}

/**
Expand All @@ -163,17 +241,19 @@ export interface StringAttributeConstraints {
/**
* Props for constructing a StringAttr
*/
export interface StringAttributeProps extends StringAttributeConstraints {
export interface StringAttributeProps extends BaseCustomAttributeProps, StringAttributeConstraints {
}

/**
* The String custom attribute type.
*/
export class StringAttribute implements ICustomAttribute {
export class StringAttribute extends BaseCustomAttribute {
protected readonly dataType = 'String';
private readonly minLen?: number;
private readonly maxLen?: number;

constructor(props: StringAttributeProps = {}) {
super(props);
if (props.minLen && props.minLen < 0) {
throw new Error(`minLen cannot be less than 0 (value: ${props.minLen}).`);
}
Expand All @@ -193,8 +273,12 @@ export class StringAttribute implements ICustomAttribute {
};
}

const aux = this.baseAttributeConfig();

return {
dataType: 'String',
dataType: aux.dataType,
developerOnly: aux.developerOnly,
mutable: aux.mutable,
stringConstraints,
};
}
Expand All @@ -220,17 +304,19 @@ export interface NumberAttributeConstraints {
/**
* Props for NumberAttr
*/
export interface NumberAttributeProps extends NumberAttributeConstraints {
export interface NumberAttributeProps extends BaseCustomAttributeProps, NumberAttributeConstraints {
}

/**
* The Number custom attribute type.
*/
export class NumberAttribute implements ICustomAttribute {
export class NumberAttribute extends BaseCustomAttribute {
protected readonly dataType = 'Number';
private readonly min?: number;
private readonly max?: number;

constructor(props: NumberAttributeProps = {}) {
super(props);
this.min = props?.min;
this.max = props?.max;
}
Expand All @@ -244,8 +330,12 @@ export class NumberAttribute implements ICustomAttribute {
};
}

const aux = this.baseAttributeConfig();

return {
dataType: 'Number',
dataType: aux.dataType,
developerOnly: aux.developerOnly,
mutable: aux.mutable,
numberConstraints,
};
}
Expand All @@ -254,21 +344,19 @@ export class NumberAttribute implements ICustomAttribute {
/**
* The Boolean custom attribute type.
*/
export class BooleanAttribute implements ICustomAttribute {
export class BooleanAttribute extends BaseCustomAttribute {
protected readonly dataType = 'Boolean';
public bind(): CustomAttributeConfig {
return {
dataType: 'Boolean'
};
return this.baseAttributeConfig();
}
}

/**
* The DateTime custom attribute type.
*/
export class DateTimeAttribute implements ICustomAttribute {
export class DateTimeAttribute extends BaseCustomAttribute {
protected readonly dataType = 'DateTime';
public bind(): CustomAttributeConfig {
return {
dataType: 'DateTime'
};
return this.baseAttributeConfig();
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Expand Up @@ -852,6 +852,8 @@ export class UserPool extends Resource implements IUserPool {
attributeDataType: attrConfig.dataType,
numberAttributeConstraints: (attrConfig.numberConstraints) ? numberConstraints : undefined,
stringAttributeConstraints: (attrConfig.stringConstraints) ? stringConstraints : undefined,
developerOnlyAttribute: attrConfig.developerOnly,
mutable: attrConfig.mutable,
};
});
schema.push(...customAttrs);
Expand Down
74 changes: 73 additions & 1 deletion packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts
@@ -1,7 +1,79 @@
import '@aws-cdk/assert/jest';
import { BooleanAttribute, DateTimeAttribute, NumberAttribute, StringAttribute } from '../lib';
import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute } from '../lib';

describe('User Pool Attributes', () => {

describe('BaseCustomAttributeProperties', () => {
test('default', () => {
// GIVEN
const allAttributes: ICustomAttribute[] = [
new StringAttribute(),
new NumberAttribute(),
new BooleanAttribute(),
new DateTimeAttribute(),
];

// WHEN
const bounds: CustomAttributeConfig[] = allAttributes.map((attr) => attr.bind() );

// THEN
bounds.forEach((bound) => {
expect(bound.developerOnly).toBeUndefined();
expect(bound.mutable).toBeUndefined();
});
});

describe('CustomAttribute base properties are set true as expected', () => {
// GIVEN
const allTrueProps = {
developerOnly: true,
mutable: true,
};
const allAttributeTypes: ICustomAttribute[] = [
new StringAttribute(allTrueProps),
new NumberAttribute(allTrueProps),
new BooleanAttribute(allTrueProps),
new DateTimeAttribute(allTrueProps),
];

// WHEN
const bounds: CustomAttributeConfig[] = allAttributeTypes.map((attr) => attr.bind() );

// THEN
bounds.forEach((bound) => {
test(`in attribute of type ${bound.dataType}:`, () => {
expect(bound.developerOnly).toEqual(true);
expect(bound.mutable).toEqual(true);
});
});
});

describe('CustomAttribute base properties are set false as expected', () => {
// GIVEN
const allFalseProps = {
developerOnly: false,
mutable: false,
};
const allAttributeTypes: ICustomAttribute[] = [
new StringAttribute(allFalseProps),
new NumberAttribute(allFalseProps),
new BooleanAttribute(allFalseProps),
new DateTimeAttribute(allFalseProps),
];

// WHEN
const bounds: CustomAttributeConfig[] = allAttributeTypes.map((attr) => attr.bind() );

// THEN
bounds.forEach((bound) => {
test(`in attribute of type ${bound.dataType}`, () => {
expect(bound.developerOnly).toEqual(false);
expect(bound.mutable).toEqual(false);
});
});
});
});

describe('StringAttribute', () => {
test('default', () => {
// GIVEN
Expand Down