Skip to content

Commit

Permalink
feat(cognito): throw ValidationError instead of untyped errors (#33170
Browse files Browse the repository at this point in the history
)

### Issue # (if applicable)
`aws-cognito` for #32569

### Description of changes
ValidationErrors everywhere





### Describe any new or updated permissions being added
n/a




### Description of how you validated changes
Existing tests. Exemptions granted as this is basically a refactor of existing code.




### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
mazyu36 authored Jan 26, 2025
1 parent 988043e commit ecbe1bf
Show file tree
Hide file tree
Showing 11 changed files with 56 additions and 48 deletions.
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ baseConfig.rules['import/no-extraneous-dependencies'] = [
// no-throw-default-error
const enableNoThrowDefaultErrorIn = [
'aws-amplify',
'aws-amplifyuibuilder',
'aws-amplifyuibuilder',
'aws-apigatewayv2-authorizers',
'aws-apigatewayv2-integrations',
'aws-cognito',
'aws-elasticloadbalancing',
'aws-elasticloadbalancingv2',
'aws-elasticloadbalancingv2-actions',
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-cognito/lib/user-pool-attr.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { StandardAttributeNames } from './private/attr-names';
import { Token } from '../../core';
import { UnscopedValidationError } from '../../core/lib/errors';

/**
* The set of standard attributes that can be marked as required or mutable.
Expand Down Expand Up @@ -242,10 +243,10 @@ export class StringAttribute implements ICustomAttribute {

constructor(props: StringAttributeProps = {}) {
if (props.minLen && !Token.isUnresolved(props.minLen) && props.minLen < 0) {
throw new Error(`minLen cannot be less than 0 (value: ${props.minLen}).`);
throw new UnscopedValidationError(`minLen cannot be less than 0 (value: ${props.minLen}).`);
}
if (props.maxLen && !Token.isUnresolved(props.maxLen) && props.maxLen > 2048) {
throw new Error(`maxLen cannot be greater than 2048 (value: ${props.maxLen}).`);
throw new UnscopedValidationError(`maxLen cannot be greater than 2048 (value: ${props.maxLen}).`);
}
this.minLen = props?.minLen;
this.maxLen = props?.maxLen;
Expand Down
23 changes: 11 additions & 12 deletions packages/aws-cdk-lib/aws-cognito/lib/user-pool-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IUserPool } from './user-pool';
import { ClientAttributes } from './user-pool-attr';
import { IUserPoolResourceServer, ResourceServerScope } from './user-pool-resource-server';
import { IResource, Resource, Duration, Stack, SecretValue, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '../../custom-resources';

/**
Expand Down Expand Up @@ -383,7 +384,7 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
class Import extends Resource implements IUserPoolClient {
public readonly userPoolClientId = userPoolClientId;
get userPoolClientSecret(): SecretValue {
throw new Error('UserPool Client Secret is not available for imported Clients');
throw new ValidationError('UserPool Client Secret is not available for imported Clients', this);
}
}

Expand Down Expand Up @@ -414,7 +415,7 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
super(scope, id);

if (props.disableOAuth && props.oAuth) {
throw new Error('OAuth settings cannot be specified when disableOAuth is set.');
throw new ValidationError('OAuth settings cannot be specified when disableOAuth is set.', this);
}

this.oAuthFlows = props.oAuth?.flows ?? {
Expand All @@ -427,23 +428,23 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
if (callbackUrls === undefined) {
callbackUrls = ['https://example.com'];
} else if (callbackUrls.length === 0) {
throw new Error('callbackUrl must not be empty when codeGrant or implicitGrant OAuth flows are enabled.');
throw new ValidationError('callbackUrl must not be empty when codeGrant or implicitGrant OAuth flows are enabled.', this);
}
}

if (props.oAuth?.defaultRedirectUri && !Token.isUnresolved(props.oAuth.defaultRedirectUri)) {
if (callbackUrls && !callbackUrls.includes(props.oAuth.defaultRedirectUri)) {
throw new Error('defaultRedirectUri must be included in callbackUrls.');
throw new ValidationError('defaultRedirectUri must be included in callbackUrls.', this);
}

const defaultRedirectUriPattern = /^(?=.{1,1024}$)[\p{L}\p{M}\p{S}\p{N}\p{P}]+$/u;
if (!defaultRedirectUriPattern.test(props.oAuth.defaultRedirectUri)) {
throw new Error(`defaultRedirectUri must match the \`^(?=.{1,1024}$)[\p{L}\p{M}\p{S}\p{N}\p{P}]+$\` pattern, got ${props.oAuth.defaultRedirectUri}`);
throw new ValidationError(`defaultRedirectUri must match the \`^(?=.{1,1024}$)[\p{L}\p{M}\p{S}\p{N}\p{P}]+$\` pattern, got ${props.oAuth.defaultRedirectUri}`, this);
}
}

if (!props.generateSecret && props.enablePropagateAdditionalUserContextData) {
throw new Error('Cannot activate enablePropagateAdditionalUserContextData in an app client without a client secret.');
throw new ValidationError('Cannot activate enablePropagateAdditionalUserContextData in an app client without a client secret.', this);
}

this._generateSecret = props.generateSecret;
Expand Down Expand Up @@ -480,16 +481,14 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
*/
public get userPoolClientName(): string {
if (this._userPoolClientName === undefined) {
throw new Error('userPoolClientName is available only if specified on the UserPoolClient during initialization');
throw new ValidationError('userPoolClientName is available only if specified on the UserPoolClient during initialization', this);
}
return this._userPoolClientName;
}

public get userPoolClientSecret(): SecretValue {
if (!this._generateSecret) {
throw new Error(
'userPoolClientSecret is available only if generateSecret is set to true.',
);
throw new ValidationError('userPoolClientSecret is available only if generateSecret is set to true.', this);
}

// Create the Custom Resource that assists in resolving the User Pool Client secret
Expand Down Expand Up @@ -540,7 +539,7 @@ export class UserPoolClient extends Resource implements IUserPoolClient {

private configureOAuthFlows(): string[] | undefined {
if ((this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) && this.oAuthFlows.clientCredentials) {
throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.');
throw new ValidationError('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.', this);
}
const oAuthFlows: string[] = [];
if (this.oAuthFlows.clientCredentials) { oAuthFlows.push('client_credentials'); }
Expand Down Expand Up @@ -614,7 +613,7 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
private validateDuration(name: string, min: Duration, max: Duration, value?: Duration) {
if (value === undefined) { return; }
if (value.toMilliseconds() < min.toMilliseconds() || value.toMilliseconds() > max.toMilliseconds()) {
throw new Error(`${name}: Must be a duration between ${min.toHumanString()} and ${max.toHumanString()} (inclusive); received ${value.toHumanString()}.`);
throw new ValidationError(`${name}: Must be a duration between ${min.toHumanString()} and ${max.toHumanString()} (inclusive); received ${value.toHumanString()}.`, this);
}
}
}
7 changes: 4 additions & 3 deletions packages/aws-cdk-lib/aws-cognito/lib/user-pool-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IUserPool } from './user-pool';
import { UserPoolClient } from './user-pool-client';
import { ICertificate } from '../../aws-certificatemanager';
import { IResource, Resource, Stack, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '../../custom-resources';

/**
Expand Down Expand Up @@ -126,14 +127,14 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain {
super(scope, id);

if (!!props.customDomain === !!props.cognitoDomain) {
throw new Error('One of, and only one of, cognitoDomain or customDomain must be specified');
throw new ValidationError('One of, and only one of, cognitoDomain or customDomain must be specified', this);
}

if (props.cognitoDomain?.domainPrefix &&
!Token.isUnresolved(props.cognitoDomain?.domainPrefix) &&
!/^[a-z0-9-]+$/.test(props.cognitoDomain.domainPrefix)) {

throw new Error('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens');
throw new ValidationError('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens', this);
}

this.isCognitoDomain = !!props.cognitoDomain;
Expand Down Expand Up @@ -214,7 +215,7 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain {
} else if (client.oAuthFlows.implicitCodeGrant) {
responseType = 'token';
} else {
throw new Error('signInUrl is not supported for clients without authorizationCodeGrant or implicitCodeGrant flow enabled');
throw new ValidationError('signInUrl is not supported for clients without authorizationCodeGrant or implicitCodeGrant flow enabled', this);
}
const path = options.signInPath ?? '/login';
return `${this.baseUrl(options)}${path}?client_id=${client.userPoolClientId}&response_type=${responseType}&redirect_uri=${options.redirectUri}`;
Expand Down
7 changes: 4 additions & 3 deletions packages/aws-cdk-lib/aws-cognito/lib/user-pool-email.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Construct } from 'constructs';
import { toASCII as punycodeEncode } from 'punycode/';
import { Stack, Token } from '../../core';
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';

/**
* Configuration for Cognito sending emails via Amazon SES
Expand Down Expand Up @@ -159,7 +160,7 @@ class SESEmail extends UserPoolEmail {
const region = Stack.of(scope).region;

if (Token.isUnresolved(region) && !this.options.sesRegion) {
throw new Error('Your stack region cannot be determined so "sesRegion" is required in SESOptions');
throw new ValidationError('Your stack region cannot be determined so "sesRegion" is required in SESOptions', scope);
}

let from = encodeAndTest(this.options.fromEmail);
Expand All @@ -171,7 +172,7 @@ class SESEmail extends UserPoolEmail {
if (this.options.sesVerifiedDomain) {
const domainFromEmail = this.options.fromEmail.split('@').pop();
if (domainFromEmail !== this.options.sesVerifiedDomain) {
throw new Error('"fromEmail" contains a different domain than the "sesVerifiedDomain"');
throw new ValidationError('"fromEmail" contains a different domain than the "sesVerifiedDomain"', scope);
}
}

Expand All @@ -194,7 +195,7 @@ function encodeAndTest(input: string | undefined): string | undefined {
if (input) {
const local = input.split('@')[0];
if (!/[\p{ASCII}]+/u.test(local)) {
throw new Error('the local part of the email address must use ASCII characters only');
throw new UnscopedValidationError('the local part of the email address must use ASCII characters only');
}
return punycodeEncode(input);
} else {
Expand Down
7 changes: 4 additions & 3 deletions packages/aws-cdk-lib/aws-cognito/lib/user-pool-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CfnUserPoolGroup } from './cognito.generated';
import { IUserPool } from './user-pool';
import { IRole } from '../../aws-iam';
import { IResource, Resource, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';

/**
* Represents a user pool group.
Expand Down Expand Up @@ -90,21 +91,21 @@ export class UserPoolGroup extends Resource implements IUserPoolGroup {
if (props.description !== undefined &&
!Token.isUnresolved(props.description) &&
(props.description.length > 2048)) {
throw new Error(`\`description\` must be between 0 and 2048 characters. Received: ${props.description.length} characters`);
throw new ValidationError(`\`description\` must be between 0 and 2048 characters. Received: ${props.description.length} characters`, this);
}

if (props.precedence !== undefined &&
!Token.isUnresolved(props.precedence) &&
(props.precedence < 0 || props.precedence > 2 ** 31 - 1)) {
throw new Error(`\`precedence\` must be between 0 and 2^31-1. Received: ${props.precedence}`);
throw new ValidationError(`\`precedence\` must be between 0 and 2^31-1. Received: ${props.precedence}`, this);
}

if (
props.groupName !== undefined &&
!Token.isUnresolved(props.groupName) &&
!/^[\p{L}\p{M}\p{S}\p{N}\p{P}]{1,128}$/u.test(props.groupName)
) {
throw new Error('\`groupName\` must be between 1 and 128 characters and can include letters, numbers, and symbols.');
throw new ValidationError('\`groupName\` must be between 1 and 128 characters and can include letters, numbers, and symbols.', this);
}

const resource = new CfnUserPoolGroup(this, 'Resource', {
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-cognito/lib/user-pool-idps/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { UserPoolIdentityProviderProps } from './base';
import { CfnUserPoolIdentityProvider } from '../cognito.generated';
import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base';
import { SecretValue } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';

/**
* Properties to initialize UserPoolAppleIdentityProvider
Expand Down Expand Up @@ -56,7 +57,7 @@ export class UserPoolIdentityProviderApple extends UserPoolIdentityProviderBase
// Exactly one of the properties must be configured
if ((!props.privateKey && !props.privateKeyValue) ||
(props.privateKey && props.privateKeyValue)) {
throw new Error('Exactly one of "privateKey" or "privateKeyValue" must be configured.');
throw new ValidationError('Exactly one of "privateKey" or "privateKeyValue" must be configured.', this);
}

const resource = new CfnUserPoolIdentityProvider(this, 'Resource', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Construct } from 'constructs';
import { UserPoolIdentityProviderProps } from './base';
import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base';
import { SecretValue } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { CfnUserPoolIdentityProvider } from '../cognito.generated';

/**
Expand Down Expand Up @@ -49,7 +50,7 @@ export class UserPoolIdentityProviderGoogle extends UserPoolIdentityProviderBase
// at least one of the properties must be configured
if ((!props.clientSecret && !props.clientSecretValue) ||
(props.clientSecret && props.clientSecretValue)) {
throw new Error('Exactly one of "clientSecret" or "clientSecretValue" must be configured.');
throw new ValidationError('Exactly one of "clientSecret" or "clientSecretValue" must be configured.', this);
}

const resource = new CfnUserPoolIdentityProvider(this, 'Resource', {
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-cognito/lib/user-pool-idps/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Construct } from 'constructs';
import { UserPoolIdentityProviderProps } from './base';
import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base';
import { Names, Token } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { CfnUserPoolIdentityProvider } from '../cognito.generated';

/**
Expand Down Expand Up @@ -134,12 +135,12 @@ export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase {
private getProviderName(name?: string): string {
if (name) {
if (!Token.isUnresolved(name) && (name.length < 3 || name.length > 32)) {
throw new Error(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`);
throw new ValidationError(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`, this);
}
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpoolidentityprovider.html#cfn-cognito-userpoolidentityprovider-providername
// u is for unicode
if (!name.match(/^[^_\p{Z}][\p{L}\p{M}\p{S}\p{N}\p{P}][^_\p{Z}]+$/u)) {
throw new Error(`Expected provider name must match [^_\p{Z}][\p{L}\p{M}\p{S}\p{N}\p{P}][^_\p{Z}]+, received ${name}`);
throw new ValidationError(`Expected provider name must match [^_\p{Z}][\p{L}\p{M}\p{S}\p{N}\p{P}][^_\p{Z}]+, received ${name}`, this);
}
return name;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-cognito/lib/user-pool-idps/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Construct } from 'constructs';
import { UserPoolIdentityProviderProps } from './base';
import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base';
import { Names, Token } from '../../../core';
import { ValidationError } from '../../../core/lib/errors';
import { CfnUserPoolIdentityProvider } from '../cognito.generated';

/**
Expand Down Expand Up @@ -163,7 +164,7 @@ export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase {

private validateName(name?: string) {
if (name && !Token.isUnresolved(name) && (name.length < 3 || name.length > 32)) {
throw new Error(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`);
throw new ValidationError(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`, this);
}
}
}
Loading

0 comments on commit ecbe1bf

Please sign in to comment.