-
Notifications
You must be signed in to change notification settings - Fork 4k
/
util.ts
143 lines (120 loc) · 4.34 KB
/
util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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);
}