packages/aws-cdk-lib/aws-iam/lib/private/util.ts (81 lines of code) (raw):
import { IConstruct } from 'constructs';
import { captureStackTrace, DefaultTokenResolver, IPostProcessor, IResolvable, IResolveContext, Lazy, StringConcat, Token, Tokenization } from '../../../core';
import { IPolicy } from '../policy';
export const MAX_POLICY_NAME_LEN = 128;
export const LITERAL_STRING_KEY = 'LiteralString';
export function undefinedIfEmpty(f: () => string[]): string[] {
return Lazy.list({
produce: () => {
const array = f();
return (array && array.length > 0) ? array : undefined;
},
});
}
/**
* Used to generate a unique policy name based on the policy resource construct.
* The logical ID of the resource is a great candidate as long as it doesn't exceed
* 128 characters, so we take the last 128 characters (in order to make sure the hash
* is there).
*/
export function generatePolicyName(scope: IConstruct, logicalId: string): string {
// as logicalId is itself a Token, resolve it first
const resolvedLogicalId = Tokenization.resolve(logicalId, {
scope,
resolver: new DefaultTokenResolver(new StringConcat()),
});
return lastNCharacters(resolvedLogicalId, MAX_POLICY_NAME_LEN);
}
/**
* Returns a string composed of the last n characters of str.
* If str is shorter than n, returns str.
*
* @param str the string to return the last n characters of
* @param n how many characters to return
*/
function lastNCharacters(str: string, n: number) {
const startIndex = Math.max(str.length - n, 0);
return str.substring(startIndex, str.length);
}
/**
* Helper class that maintains the set of attached policies for a principal.
*/
export class AttachedPolicies {
private policies = new Array<IPolicy>();
/**
* Adds a policy to the list of attached policies.
*
* If this policy is already, attached, returns false.
* If there is another policy attached with the same name, throws an exception.
*/
public attach(policy: IPolicy) {
if (this.policies.find(p => p === policy)) {
return; // already attached
}
if (this.policies.find(p => p.policyName === policy.policyName)) {
throw new Error(`A policy named "${policy.policyName}" is already attached`);
}
this.policies.push(policy);
}
}
/**
* Merge two dictionaries that represent IAM principals
*
* Does an in-place merge.
*/
export function mergePrincipal(target: { [key: string]: string[] }, source: { [key: string]: string[] }) {
// If one represents a literal string, the other one must be empty
const sourceKeys = Object.keys(source);
const targetKeys = Object.keys(target);
if ((LITERAL_STRING_KEY in source && targetKeys.some(k => k !== LITERAL_STRING_KEY)) ||
(LITERAL_STRING_KEY in target && sourceKeys.some(k => k !== LITERAL_STRING_KEY))) {
throw new Error(`Cannot merge principals ${JSON.stringify(target)} and ${JSON.stringify(source)}; if one uses a literal principal string the other one must be empty`);
}
for (const key of sourceKeys) {
target[key] = target[key] ?? [];
let value = source[key];
if (!Array.isArray(value)) {
value = [value];
}
target[key].push(...value);
}
return target;
}
/**
* Lazy string set token that dedupes entries
*
* Needs to operate post-resolve, because the inputs could be
* `[ '${Token[TOKEN.9]}', '${Token[TOKEN.10]}', '${Token[TOKEN.20]}' ]`, which
* still all resolve to the same string value.
*
* Needs to JSON.stringify() results because strings could resolve to literal
* strings but could also resolve to `{ Fn::Join: [...] }`.
*/
export class UniqueStringSet implements IResolvable, IPostProcessor {
public static from(fn: () => string[]) {
return Token.asList(new UniqueStringSet(fn));
}
public readonly creationStack: string[];
private constructor(private readonly fn: () => string[]) {
this.creationStack = captureStackTrace();
}
public resolve(context: IResolveContext) {
context.registerPostProcessor(this);
return this.fn();
}
public postProcess(input: any, _context: IResolveContext) {
if (!Array.isArray(input)) { return input; }
if (input.length === 0) { return undefined; }
const uniq: Record<string, any> = {};
for (const el of input) {
uniq[JSON.stringify(el)] = el;
}
return Object.values(uniq);
}
public toString(): string {
return Token.asString(this);
}
}
export function sum(xs: number[]) {
return xs.reduce((a, b) => a + b, 0);
}