packages/aws-cdk-lib/aws-iam/lib/principals.ts (471 lines of code) (raw):

import { IDependable } from 'constructs'; import { IOpenIdConnectProvider } from './oidc-provider'; import { PolicyDocument } from './policy-document'; import { Condition, Conditions, PolicyStatement } from './policy-statement'; import { defaultAddPrincipalToAssumeRole } from './private/assume-role-policy'; import { LITERAL_STRING_KEY, mergePrincipal } from './private/util'; import { ISamlProvider } from './saml-provider'; import * as cdk from '../../core'; import { RegionInfo } from '../../region-info'; /** * Any object that has an associated principal that a permission can be granted to */ export interface IGrantable { /** * The principal to grant permissions to */ readonly grantPrincipal: IPrincipal; } /** * Represents a logical IAM principal. * * An IPrincipal describes a logical entity that can perform AWS API calls * against sets of resources, optionally under certain conditions. * * Examples of simple principals are IAM objects that you create, such * as Users or Roles. * * An example of a more complex principals is a `ServicePrincipal` (such as * `new ServicePrincipal("sns.amazonaws.com")`, which represents the Simple * Notifications Service). * * A single logical Principal may also map to a set of physical principals. * For example, `new OrganizationPrincipal('o-12345abcde')` represents all * identities that are part of the given AWS Organization. */ export interface IPrincipal extends IGrantable { /** * When this Principal is used in an AssumeRole policy, the action to use. */ readonly assumeRoleAction: string; /** * Return the policy fragment that identifies this principal in a Policy. */ readonly policyFragment: PrincipalPolicyFragment; /** * The AWS account ID of this principal. * Can be undefined when the account is not known * (for example, for service principals). * Can be a Token - in that case, * it's assumed to be AWS::AccountId. */ readonly principalAccount?: string | undefined; /** * Add to the policy of this principal. * * @returns true if the statement was added, false if the principal in * question does not have a policy document to add the statement to. * * @deprecated Use `addToPrincipalPolicy` instead. */ addToPolicy(statement: PolicyStatement): boolean; /** * Add to the policy of this principal. */ addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult; } /** * Interface for principals that can be compared. * * This only needs to be implemented for principals that could potentially be value-equal. * Identity-equal principals will be handled correctly by default. */ export interface IComparablePrincipal extends IPrincipal { /** * Return a string format of this principal which should be identical if the two * principals are the same. */ dedupeString(): string | undefined; } /** * Helper class for working with `IComparablePrincipal`s */ export class ComparablePrincipal { /** * Whether or not the given principal is a comparable principal */ public static isComparablePrincipal(this: void, x: IPrincipal): x is IComparablePrincipal { return 'dedupeString' in x; } /** * Return the dedupeString of the given principal, if available */ public static dedupeStringFor(this: void, x: IPrincipal): string | undefined { return ComparablePrincipal.isComparablePrincipal(x) ? x.dedupeString() : undefined; } } /** * A type of principal that has more control over its own representation in AssumeRolePolicyDocuments * * More complex types of identity providers need more control over Role's policy documents * than simply `{ Effect: 'Allow', Action: 'AssumeRole', Principal: <Whatever> }`. * * If that control is necessary, they can implement `IAssumeRolePrincipal` to get full * access to a Role's AssumeRolePolicyDocument. */ export interface IAssumeRolePrincipal extends IPrincipal { /** * Add the principal to the AssumeRolePolicyDocument * * Add the statements to the AssumeRolePolicyDocument necessary to give this principal * permissions to assume the given role. */ addToAssumeRolePolicy(document: PolicyDocument): void; } /** * Result of calling `addToPrincipalPolicy` */ export interface AddToPrincipalPolicyResult { /** * Whether the statement was added to the identity's policies. * */ readonly statementAdded: boolean; /** * Dependable which allows depending on the policy change being applied * * @default - Required if `statementAdded` is true. */ readonly policyDependable?: IDependable; } /** * Base class for policy principals */ export abstract class PrincipalBase implements IAssumeRolePrincipal, IComparablePrincipal { public readonly grantPrincipal: IPrincipal = this; public readonly principalAccount: string | undefined = undefined; /** * Return the policy fragment that identifies this principal in a Policy. */ public abstract readonly policyFragment: PrincipalPolicyFragment; /** * When this Principal is used in an AssumeRole policy, the action to use. */ public readonly assumeRoleAction: string = 'sts:AssumeRole'; public addToPolicy(statement: PolicyStatement): boolean { return this.addToPrincipalPolicy(statement).statementAdded; } public addToPrincipalPolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { // This base class is used for non-identity principals. None of them // have a PolicyDocument to add to. return { statementAdded: false }; } public addToAssumeRolePolicy(document: PolicyDocument): void { // Default implementation of this protocol, compatible with the legacy behavior document.addStatements(new PolicyStatement({ actions: [this.assumeRoleAction], principals: [this], })); } public toString() { // This is a first pass to make the object readable. Descendant principals // should return something nicer. return JSON.stringify(this.policyFragment.principalJson); } /** * JSON-ify the principal * * Used when JSON.stringify() is called */ public toJSON() { // Have to implement toJSON() because the default will lead to infinite recursion. return this.policyFragment.principalJson; } /** * Returns a new PrincipalWithConditions using this principal as the base, with the * passed conditions added. * * When there is a value for the same operator and key in both the principal and the * conditions parameter, the value from the conditions parameter will be used. * * @returns a new PrincipalWithConditions object. */ public withConditions(conditions: Conditions): PrincipalBase { return new PrincipalWithConditions(this, conditions); } /** * Returns a new principal using this principal as the base, with session tags enabled. * * @returns a new SessionTagsPrincipal object. */ public withSessionTags(): PrincipalBase { return new SessionTagsPrincipal(this); } /** * Return whether or not this principal is equal to the given principal */ public abstract dedupeString(): string | undefined; } /** * Base class for Principals that wrap other principals */ abstract class PrincipalAdapter extends PrincipalBase { public readonly assumeRoleAction = this.wrapped.assumeRoleAction; public readonly principalAccount = this.wrapped.principalAccount; constructor(protected readonly wrapped: IPrincipal) { super(); } public get policyFragment(): PrincipalPolicyFragment { return this.wrapped.policyFragment; } public addToPolicy(statement: PolicyStatement): boolean { return this.wrapped.addToPolicy(statement); } public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { return this.wrapped.addToPrincipalPolicy(statement); } /** * Append the given string to the wrapped principal's dedupe string (if available) */ protected appendDedupe(append: string): string | undefined { const inner = ComparablePrincipal.dedupeStringFor(this.wrapped); return inner !== undefined ? `${this.constructor.name}:${inner}:${append}` : undefined; } } /** * An IAM principal with additional conditions specifying when the policy is in effect. * * For more information about conditions, see: * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html */ export class PrincipalWithConditions extends PrincipalAdapter { private additionalConditions: Conditions; constructor(principal: IPrincipal, conditions: Conditions) { super(principal); this.additionalConditions = conditions; } public addToAssumeRolePolicy(doc: PolicyDocument) { // Lazy import to avoid circular import dependencies during startup // eslint-disable-next-line @typescript-eslint/no-require-imports const adapter: typeof import('./private/policydoc-adapter') = require('./private/policydoc-adapter'); defaultAddPrincipalToAssumeRole(this.wrapped, new adapter.MutatingPolicyDocumentAdapter(doc, (statement) => { // Avoid override of existing actions (see https://github.com/aws/aws-cdk/issues/28426) statement.addActions(this.assumeRoleAction); statement.addConditions(this.conditions); return statement; })); } /** * Add a condition to the principal */ public addCondition(key: string, value: Condition) { validateConditionObject(value); const existingValue = this.additionalConditions[key]; if (!existingValue) { this.additionalConditions[key] = value; return; } validateConditionObject(existingValue); this.additionalConditions[key] = { ...existingValue, ...value }; } /** * Adds multiple conditions to the principal * * Values from the conditions parameter will overwrite existing values with the same operator * and key. */ public addConditions(conditions: Conditions) { Object.entries(conditions).forEach(([key, value]) => { this.addCondition(key, value); }); } /** * The conditions under which the policy is in effect. * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). */ public get conditions() { return this.mergeConditions(this.wrapped.policyFragment.conditions, this.additionalConditions); } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment(this.wrapped.policyFragment.principalJson, this.conditions); } public toString() { return this.wrapped.toString(); } /** * JSON-ify the principal * * Used when JSON.stringify() is called */ public toJSON() { // Have to implement toJSON() because the default will lead to infinite recursion. return this.policyFragment.principalJson; } public dedupeString(): string | undefined { return this.appendDedupe(JSON.stringify(this.conditions)); } private mergeConditions(principalConditions: Conditions, additionalConditions: Conditions): Conditions { const mergedConditions: Conditions = {}; Object.entries(principalConditions).forEach(([operator, condition]) => { mergedConditions[operator] = condition; }); Object.entries(additionalConditions).forEach(([operator, condition]) => { // merge the conditions if one of the additional conditions uses an // operator that's already used by the principal's conditions merge the // inner structure. const existing = mergedConditions[operator]; if (!existing) { mergedConditions[operator] = condition; return; // continue } // if either the existing condition or the new one contain unresolved // tokens, fail the merge. this is as far as we go at this point. if (cdk.Token.isUnresolved(condition) || cdk.Token.isUnresolved(existing)) { throw new Error(`multiple "${operator}" conditions cannot be merged if one of them contains an unresolved token`); } validateConditionObject(existing); validateConditionObject(condition); mergedConditions[operator] = { ...existing, ...condition }; }); return mergedConditions; } } /** * Enables session tags on role assumptions from a principal * * For more information on session tags, see: * https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html */ export class SessionTagsPrincipal extends PrincipalAdapter { constructor(principal: IPrincipal) { super(principal); } public addToAssumeRolePolicy(doc: PolicyDocument) { // Lazy import to avoid circular import dependencies during startup // eslint-disable-next-line @typescript-eslint/no-require-imports const adapter: typeof import('./private/policydoc-adapter') = require('./private/policydoc-adapter'); defaultAddPrincipalToAssumeRole(this.wrapped, new adapter.MutatingPolicyDocumentAdapter(doc, (statement) => { statement.addActions('sts:TagSession'); return statement; })); } public dedupeString(): string | undefined { return this.appendDedupe(''); } } /** * A collection of the fields in a PolicyStatement that can be used to identify a principal. * * This consists of the JSON used in the "Principal" field, and optionally a * set of "Condition"s that need to be applied to the policy. * * Generally, a principal looks like: * * { '<TYPE>': ['ID', 'ID', ...] } * * And this is also the type of the field `principalJson`. However, there is a * special type of principal that is just the string '*', which is treated * differently by some services. To represent that principal, `principalJson` * should contain `{ 'LiteralString': ['*'] }`. */ export class PrincipalPolicyFragment { /** * * @param principalJson JSON of the "Principal" section in a policy statement * @param conditions conditions that need to be applied to this policy */ constructor( public readonly principalJson: { [key: string]: string[] }, /** * The conditions under which the policy is in effect. * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). */ public readonly conditions: Conditions = {}) { } } /** * Specify a principal by the Amazon Resource Name (ARN). * You can specify AWS accounts, IAM users, Federated SAML users, IAM roles, and specific assumed-role sessions. * You cannot specify IAM groups or instance profiles as principals * * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html */ export class ArnPrincipal extends PrincipalBase { /** * * @param arn Amazon Resource Name (ARN) of the principal entity (i.e. arn:aws:iam::123456789012:user/user-name) */ constructor(public readonly arn: string) { super(); } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment({ AWS: [this.arn] }); } public toString() { return `ArnPrincipal(${this.arn})`; } /** * A convenience method for adding a condition that the principal is part of the specified * AWS Organization. */ public inOrganization(organizationId: string) { return this.withConditions({ StringEquals: { 'aws:PrincipalOrgID': organizationId, }, }); } public dedupeString(): string | undefined { return `ArnPrincipal:${this.arn}`; } } /** * Specify AWS account ID as the principal entity in a policy to delegate authority to the account. */ export class AccountPrincipal extends ArnPrincipal { public readonly principalAccount: string | undefined; /** * * @param accountId AWS account ID (i.e. '123456789012') */ constructor(public readonly accountId: any) { super(new StackDependentToken(stack => `arn:${stack.partition}:iam::${accountId}:root`).toString()); if (!cdk.Token.isUnresolved(accountId) && typeof accountId !== 'string') { throw new Error('accountId should be of type string'); } this.principalAccount = accountId; } public toString() { return `AccountPrincipal(${this.accountId})`; } } /** * Options for a service principal. */ export interface ServicePrincipalOpts { /** * The region in which you want to reference the service * * This is only necessary for *cross-region* references to *opt-in* regions. In those * cases, the region name needs to be included to reference the correct service principal. * In all other cases, the global service principal name is sufficient. * * This field behaves differently depending on whether the `@aws-cdk/aws-iam:standardizedServicePrincipals` * flag is set or not: * * - If the flag is set, the input service principal is assumed to be of the form `SERVICE.amazonaws.com`. * That value will always be returned, unless the given region is an opt-in region and the service * principal is rendered in a stack in a different region, in which case `SERVICE.REGION.amazonaws.com` * will be rendered. Under this regime, there is no downside to always specifying the region property: * it will be rendered only if necessary. * - If the flag is not set, the service principal will resolve to a single principal * whose name comes from the `@aws-cdk/region-info` package, using the region to override * the stack region. If there is no entry for this service principal in the database,, the input * service name is returned literally. This is legacy behavior and is not recommended. * * @default - the resolving Stack's region. */ readonly region?: string; /** * Additional conditions to add to the Service Principal * * @default - No conditions */ readonly conditions?: { [key: string]: any }; } /** * An IAM principal that represents an AWS service (i.e. `sqs.amazonaws.com`). */ export class ServicePrincipal extends PrincipalBase { /** * Return the service principal name based on the region it's used in. * * Some service principal names used to be different for different partitions, * and some were not. This method would return the appropriate region-specific * service principal name, getting that information from the `region-info` * module. * * These days all service principal names are standardized, and they are all * of the form `<servicename>.amazonaws.com`. * * To avoid breaking changes, handling is provided for services added with the formats below, * however, no additional handling will be added for new regions or partitions. * - s3 * - s3.amazonaws.com * - s3.amazonaws.com.cn * - s3.c2s.ic.gov * - s3.sc2s.sgov.gov * * @example * const principalName = iam.ServicePrincipal.servicePrincipalName('ec2.amazonaws.com'); */ public static servicePrincipalName(service: string): string { return new ServicePrincipalToken(service, {}).toString(); } /** * Return the service principal using the service principal name as it is passed to the function without * any change regardless of the region used in the stack if it is Opted in or not. * * @example * const principalName = iam.ServicePrincipal.fromStaticServicePrincipleName('elasticmapreduce.amazonaws.com.cn'); */ public static fromStaticServicePrincipleName(servicePrincipalName: string): ServicePrincipal { class StaticServicePrincipal extends ServicePrincipal { constructor(public readonly service: string) { super(service); } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment({ Service: [this.service], }, this.opts.conditions); } } return new StaticServicePrincipal(servicePrincipalName); } /** * Reference an AWS service, optionally in a given region * * @param service AWS service (i.e. sqs.amazonaws.com) */ constructor(public readonly service: string, private readonly opts: ServicePrincipalOpts = {}) { super(); } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment({ Service: [new ServicePrincipalToken(this.service, this.opts).toString()], }, this.opts.conditions); } public toString() { return `ServicePrincipal(${this.service})`; } public dedupeString(): string | undefined { return `ServicePrincipal:${this.service}:${JSON.stringify(this.opts)}`; } } /** * A principal that represents an AWS Organization */ export class OrganizationPrincipal extends PrincipalBase { /** * * @param organizationId The unique identifier (ID) of an organization (i.e. o-12345abcde) * It must match regex pattern ^o-[a-z0-9]{10,32}$ * @see https://docs.aws.amazon.com/organizations/latest/APIReference/API_Organization.html */ constructor(public readonly organizationId: string) { super(); // We can only validate if it's a literal string (not a token) if (!cdk.Token.isUnresolved(organizationId)) { if (!organizationId.match(/^o-[a-z0-9]{10,32}$/)) { throw new Error(`Expected Organization ID must match regex pattern ^o-[a-z0-9]{10,32}$, received ${organizationId}`); } } } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment( { AWS: ['*'] }, { StringEquals: { 'aws:PrincipalOrgID': this.organizationId } }, ); } public toString() { return `OrganizationPrincipal(${this.organizationId})`; } public dedupeString(): string | undefined { return `OrganizationPrincipal:${this.organizationId}`; } } /** * A policy principal for canonicalUserIds - useful for S3 bucket policies that use * Origin Access identities. * * See https://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html * * and * * https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html * * for more details. * */ export class CanonicalUserPrincipal extends PrincipalBase { /** * * @param canonicalUserId unique identifier assigned by AWS for every account. * root user and IAM users for an account all see the same ID. * (i.e. 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be) */ constructor(public readonly canonicalUserId: string) { super(); } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment({ CanonicalUser: [this.canonicalUserId] }); } public toString() { return `CanonicalUserPrincipal(${this.canonicalUserId})`; } public dedupeString(): string | undefined { return `CanonicalUserPrincipal:${this.canonicalUserId}`; } } /** * Principal entity that represents a federated identity provider such as Amazon Cognito, * that can be used to provide temporary security credentials to users who have been authenticated. * Additional condition keys are available when the temporary security credentials are used to make a request. * You can use these keys to write policies that limit the access of federated users. * * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif */ export class FederatedPrincipal extends PrincipalBase { public readonly assumeRoleAction: string; /** * The conditions under which the policy is in effect. * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html */ public readonly conditions: Conditions; /** * * @param federated federated identity provider (i.e. 'cognito-identity.amazonaws.com' for users authenticated through Cognito) * @param sessionTags Whether to enable session tagging (see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html) */ constructor( public readonly federated: string, conditions: Conditions = {}, assumeRoleAction: string = 'sts:AssumeRole') { super(); this.conditions = conditions; this.assumeRoleAction = assumeRoleAction; } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment({ Federated: [this.federated] }, this.conditions); } public toString() { return `FederatedPrincipal(${this.federated})`; } public dedupeString(): string | undefined { return `FederatedPrincipal:${this.federated}:${this.assumeRoleAction}:${JSON.stringify(this.conditions)}`; } } /** * A principal that represents a federated identity provider as Web Identity such as Cognito, Amazon, * Facebook, Google, etc. */ export class WebIdentityPrincipal extends FederatedPrincipal { /** * * @param identityProvider identity provider (i.e. 'cognito-identity.amazonaws.com' for users authenticated through Cognito) * @param conditions The conditions under which the policy is in effect. * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). * @param sessionTags Whether to enable session tagging (see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html) */ constructor(identityProvider: string, conditions: Conditions = {}) { super(identityProvider, conditions ?? {}, 'sts:AssumeRoleWithWebIdentity'); } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment({ Federated: [this.federated] }, this.conditions); } public toString() { return `WebIdentityPrincipal(${this.federated})`; } } /** * A principal that represents a federated identity provider as from a OpenID Connect provider. */ export class OpenIdConnectPrincipal extends WebIdentityPrincipal { /** * * @param openIdConnectProvider OpenID Connect provider * @param conditions The conditions under which the policy is in effect. * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). */ constructor(openIdConnectProvider: IOpenIdConnectProvider, conditions: Conditions = {}) { super(openIdConnectProvider.openIdConnectProviderArn, conditions ?? {}); } public get policyFragment(): PrincipalPolicyFragment { return new PrincipalPolicyFragment({ Federated: [this.federated] }, this.conditions); } public toString() { return `OpenIdConnectPrincipal(${this.federated})`; } } /** * Principal entity that represents a SAML federated identity provider */ export class SamlPrincipal extends FederatedPrincipal { constructor(samlProvider: ISamlProvider, conditions: Conditions) { super(samlProvider.samlProviderArn, conditions, 'sts:AssumeRoleWithSAML'); } public toString() { return `SamlPrincipal(${this.federated})`; } } /** * Principal entity that represents a SAML federated identity provider for * programmatic and AWS Management Console access. */ export class SamlConsolePrincipal extends SamlPrincipal { constructor(samlProvider: ISamlProvider, conditions: Conditions = {}) { super(samlProvider, { ...conditions, StringEquals: { 'SAML:aud': RegionInfo.get(samlProvider.stack.region).samlSignOnUrl ?? 'https://signin.aws.amazon.com/saml', }, }); } public toString() { return `SamlConsolePrincipal(${this.federated})`; } } /** * Use the AWS account into which a stack is deployed as the principal entity in a policy */ export class AccountRootPrincipal extends AccountPrincipal { constructor() { super(new StackDependentToken(stack => stack.account).toString()); } public toString() { return 'AccountRootPrincipal()'; } } /** * A principal representing all AWS identities in all accounts * * Some services behave differently when you specify `Principal: '*'` * or `Principal: { AWS: "*" }` in their resource policy. * * `AnyPrincipal` renders to `Principal: { AWS: "*" }`. This is correct * most of the time, but in cases where you need the other principal, * use `StarPrincipal` instead. */ export class AnyPrincipal extends ArnPrincipal { constructor() { super('*'); } public toString() { return 'AnyPrincipal()'; } } /** * A principal representing all identities in all accounts * @deprecated use `AnyPrincipal` */ export class Anyone extends AnyPrincipal { } /** * A principal that uses a literal '*' in the IAM JSON language * * Some services behave differently when you specify `Principal: "*"` * or `Principal: { AWS: "*" }` in their resource policy. * * `StarPrincipal` renders to `Principal: *`. Most of the time, you * should use `AnyPrincipal` instead. */ export class StarPrincipal extends PrincipalBase { public readonly policyFragment: PrincipalPolicyFragment = { principalJson: { [LITERAL_STRING_KEY]: ['*'] }, conditions: {}, }; public toString() { return 'StarPrincipal()'; } public dedupeString(): string | undefined { return 'StarPrincipal'; } } /** * Represents a principal that has multiple types of principals. A composite principal cannot * have conditions. i.e. multiple ServicePrincipals that form a composite principal */ export class CompositePrincipal extends PrincipalBase { public readonly assumeRoleAction: string; private readonly _principals = new Array<IPrincipal>(); constructor(...principals: IPrincipal[]) { super(); if (principals.length === 0) { throw new Error('CompositePrincipals must be constructed with at least 1 Principal but none were passed.'); } this.assumeRoleAction = principals[0].assumeRoleAction; this.addPrincipals(...principals); } /** * Adds IAM principals to the composite principal. Composite principals cannot have * conditions. * * @param principals IAM principals that will be added to the composite principal */ public addPrincipals(...principals: IPrincipal[]): this { this._principals.push(...principals); return this; } public addToAssumeRolePolicy(doc: PolicyDocument) { for (const p of this._principals) { defaultAddPrincipalToAssumeRole(p, doc); } } public get policyFragment(): PrincipalPolicyFragment { // We only have a problem with conditions if we are trying to render composite // principals into a single statement (which is when `policyFragment` would get called) for (const p of this._principals) { const fragment = p.policyFragment; if (fragment.conditions && Object.keys(fragment.conditions).length > 0) { throw new Error( 'Components of a CompositePrincipal must not have conditions. ' + `Tried to add the following fragment: ${JSON.stringify(fragment)}`); } } const principalJson: { [key: string]: string[] } = {}; for (const p of this._principals) { mergePrincipal(principalJson, p.policyFragment.principalJson); } return new PrincipalPolicyFragment(principalJson); } public toString() { return `CompositePrincipal(${this._principals})`; } public dedupeString(): string | undefined { const inner = this._principals.map(ComparablePrincipal.dedupeStringFor); if (inner.some(x => x === undefined)) { return undefined; } return `CompositePrincipal[${inner.join(',')}]`; } /** * Returns the principals that make up the CompositePrincipal */ public get principals(): IPrincipal[] { return this._principals; } } /** * A lazy token that requires an instance of Stack to evaluate */ class StackDependentToken implements cdk.IResolvable { public readonly creationStack: string[]; constructor(private readonly fn: (stack: cdk.Stack) => any) { this.creationStack = cdk.captureStackTrace(); } public resolve(context: cdk.IResolveContext) { return this.fn(cdk.Stack.of(context.scope)); } public toString() { return cdk.Token.asString(this); } /** * JSON-ify the token * * Used when JSON.stringify() is called */ public toJSON() { return '<unresolved-token>'; } } class ServicePrincipalToken implements cdk.IResolvable { public readonly creationStack: string[]; constructor( private readonly service: string, private readonly opts: ServicePrincipalOpts) { this.creationStack = cdk.captureStackTrace(); } public resolve(ctx: cdk.IResolveContext) { return this.newStandardizedBehavior(ctx); } /** * Return the global (original) service principal, and a second one if region is given and points to an opt-in region */ private newStandardizedBehavior(ctx: cdk.IResolveContext) { const stack = cdk.Stack.of(ctx.scope); // If the user had previously set the feature flag to `false` we would allow them to provide only the service name instead of the // entire service principal. We can't break them so now everyone gets to do it! const match = this.service.match(/^([^.]+)(?:(?:\.amazonaws\.com(?:\.cn)?)|(?:\.c2s\.ic\.gov)|(?:\.sc2s\.sgov\.gov))?$/); const service = match ? `${match[1]}.amazonaws.com` : this.service; if ( this.opts.region && !cdk.Token.isUnresolved(this.opts.region) && stack.region !== this.opts.region && RegionInfo.get(this.opts.region).isOptInRegion ) { return service.replace(/\.amazonaws\.com$/, `.${this.opts.region}.amazonaws.com`); } return service; } public toString() { return cdk.Token.asString(this, { displayHint: this.service, }); } /** * JSON-ify the token * * Used when JSON.stringify() is called */ public toJSON() { return `<${this.service}>`; } } /** * Validate that the given value is a valid Condition object * * The type of `Condition` should have been different, but it's too late for that. * * Also, the IAM library relies on being able to pass in a `CfnJson` instance for * a `Condition`. */ export function validateConditionObject(x: unknown): asserts x is Record<string, unknown> { if (!x || typeof x !== 'object' || Array.isArray(x)) { throw new Error('A Condition should be represented as a map of operator to value'); } }