1 | ;
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.handler = void 0;
|
4 | const diff_1 = require("./diff");
|
5 | const external_1 = require("./external");
|
6 | async function handler(event) {
|
7 | if (event.RequestType === 'Create') {
|
8 | return onCreate(event);
|
9 | }
|
10 | if (event.RequestType === 'Update') {
|
11 | return onUpdate(event);
|
12 | }
|
13 | if (event.RequestType === 'Delete') {
|
14 | return onDelete(event);
|
15 | }
|
16 | throw new Error('invalid request type');
|
17 | }
|
18 | exports.handler = handler;
|
19 | async function onCreate(event) {
|
20 | const issuerUrl = event.ResourceProperties.Url;
|
21 | const thumbprints = (event.ResourceProperties.ThumbprintList ?? []).sort(); // keep sorted for UPDATE
|
22 | const clients = (event.ResourceProperties.ClientIDList ?? []).sort();
|
23 | if (thumbprints.length === 0) {
|
24 | thumbprints.push(await external_1.external.downloadThumbprint(issuerUrl));
|
25 | }
|
26 | const resp = await external_1.external.createOpenIDConnectProvider({
|
27 | Url: issuerUrl,
|
28 | ClientIDList: clients,
|
29 | ThumbprintList: thumbprints,
|
30 | });
|
31 | return {
|
32 | PhysicalResourceId: resp.OpenIDConnectProviderArn,
|
33 | };
|
34 | }
|
35 | async function onUpdate(event) {
|
36 | const issuerUrl = event.ResourceProperties.Url;
|
37 | const thumbprints = (event.ResourceProperties.ThumbprintList ?? []).sort(); // keep sorted for UPDATE
|
38 | const clients = (event.ResourceProperties.ClientIDList ?? []).sort();
|
39 | // determine which update we are talking about.
|
40 | const oldIssuerUrl = event.OldResourceProperties.Url;
|
41 | // if this is a URL update, then we basically create a new resource and cfn will delete the old one
|
42 | // since the physical resource ID will change.
|
43 | if (oldIssuerUrl !== issuerUrl) {
|
44 | return onCreate({ ...event, RequestType: 'Create' });
|
45 | }
|
46 | const providerArn = event.PhysicalResourceId;
|
47 | // if thumbprints changed, we can update in-place, but bear in mind that if the new thumbprint list
|
48 | // is empty, we will grab it from the server like we do in CREATE
|
49 | const oldThumbprints = (event.OldResourceProperties.ThumbprintList || []).sort();
|
50 | if (JSON.stringify(oldThumbprints) !== JSON.stringify(thumbprints)) {
|
51 | const thumbprintList = thumbprints.length > 0 ? thumbprints : [await external_1.external.downloadThumbprint(issuerUrl)];
|
52 | external_1.external.log('updating thumbprint list from', oldThumbprints, 'to', thumbprints);
|
53 | await external_1.external.updateOpenIDConnectProviderThumbprint({
|
54 | OpenIDConnectProviderArn: providerArn,
|
55 | ThumbprintList: thumbprintList,
|
56 | });
|
57 | // don't return, we might have more updates...
|
58 | }
|
59 | // if client ID list has changed, determine "diff" because the API is add/remove
|
60 | const oldClients = (event.OldResourceProperties.ClientIDList || []).sort();
|
61 | const diff = diff_1.arrayDiff(oldClients, clients);
|
62 | external_1.external.log(`client ID diff: ${JSON.stringify(diff)}`);
|
63 | for (const addClient of diff.adds) {
|
64 | external_1.external.log(`adding client id "${addClient}" to provider ${providerArn}`);
|
65 | await external_1.external.addClientIDToOpenIDConnectProvider({
|
66 | OpenIDConnectProviderArn: providerArn,
|
67 | ClientID: addClient,
|
68 | });
|
69 | }
|
70 | for (const deleteClient of diff.deletes) {
|
71 | external_1.external.log(`removing client id "${deleteClient}" from provider ${providerArn}`);
|
72 | await external_1.external.removeClientIDFromOpenIDConnectProvider({
|
73 | OpenIDConnectProviderArn: providerArn,
|
74 | ClientID: deleteClient,
|
75 | });
|
76 | }
|
77 | return;
|
78 | }
|
79 | async function onDelete(deleteEvent) {
|
80 | await external_1.external.deleteOpenIDConnectProvider({
|
81 | OpenIDConnectProviderArn: deleteEvent.PhysicalResourceId,
|
82 | });
|
83 | }
|
84 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxpQ0FBbUM7QUFDbkMseUNBQXNDO0FBRS9CLEtBQUssVUFBVSxPQUFPLENBQUMsS0FBa0Q7SUFDOUUsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsRUFBRTtRQUFFLE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDO0tBQUU7SUFDL0QsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsRUFBRTtRQUFFLE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDO0tBQUU7SUFDL0QsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsRUFBRTtRQUFFLE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDO0tBQUU7SUFDL0QsTUFBTSxJQUFJLEtBQUssQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDO0FBQzFDLENBQUM7QUFMRCwwQkFLQztBQUVELEtBQUssVUFBVSxRQUFRLENBQUMsS0FBd0Q7SUFDOUUsTUFBTSxTQUFTLEdBQUcsS0FBSyxDQUFDLGtCQUFrQixDQUFDLEdBQUcsQ0FBQztJQUMvQyxNQUFNLFdBQVcsR0FBYSxDQUFDLEtBQUssQ0FBQyxrQkFBa0IsQ0FBQyxjQUFjLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyx5QkFBeUI7SUFDL0csTUFBTSxPQUFPLEdBQWEsQ0FBQyxLQUFLLENBQUMsa0JBQWtCLENBQUMsWUFBWSxJQUFJLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0lBRS9FLElBQUksV0FBVyxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUU7UUFDNUIsV0FBVyxDQUFDLElBQUksQ0FBQyxNQUFNLG1CQUFRLENBQUMsa0JBQWtCLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztLQUNoRTtJQUVELE1BQU0sSUFBSSxHQUFHLE1BQU0sbUJBQVEsQ0FBQywyQkFBMkIsQ0FB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RUFBRSxZQUFZO1NBQ3ZCLENBQUMsQ0FBQztLQUNKO0lBRUQsT0FBTztBQUNULENBQUM7QUFFRCxLQUFLLFVBQVUsUUFBUSxDQUFDLFdBQThEO0lBQ3BGLE1BQU0sbUJBQVEsQ0FBQywyQkFBMkIsQ0FBQztRQUN6Qyx3QkFBd0IsRUFBRSxXQUFXLENBQUMsa0JBQWtCO0tBQ3pELENBQUMsQ0FBQztBQUNMLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBhcnJheURpZmYgfSBmcm9tICcuL2RpZmYnO1xuaW1wb3J0IHsgZXh0ZXJuYWwgfSBmcm9tICcuL2V4dGVybmFsJztcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQpIHtcbiAgaWYgKGV2ZW50LlJlcXVlc3RUeXBlID09PSAnQ3JlYXRlJykgeyByZXR1cm4gb25DcmVhdGUoZXZlbnQpOyB9XG4gIGlmIChldmVudC5SZXF1ZXN0VHlwZSA9PT0gJ1VwZGF0ZScpIHsgcmV0dXJuIG9uVXBkYXRlKGV2ZW50KTsgfVxuICBpZiAoZXZlbnQuUmVxdWVzdFR5cGUgPT09ICdEZWxldGUnKSB7IHJldHVybiBvbkRlbGV0ZShldmVudCk7IH1cbiAgdGhyb3cgbmV3IEVycm9yKCdpbnZhbGlkIHJlcXVlc3QgdHlwZScpO1xufVxuXG5hc3luYyBmdW5jdGlvbiBvbkNyZWF0ZShldmVudDogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VDcmVhdGVFdmVudCkge1xuICBjb25zdCBpc3N1ZXJVcmwgPSBldmVudC5SZXNvdXJjZVByb3BlcnRpZXMuVXJsO1xuICBjb25zdCB0aHVtYnByaW50czogc3RyaW5nW10gPSAoZXZlbnQuUmVzb3VyY2VQcm9wZXJ0aWVzLlRodW1icHJpbnRMaXN0ID8/IFtdKS5zb3J0KCk7IC8vIGtlZXAgc29ydGVkIGZvciBVUERBVEVcbiAgY29uc3QgY2xpZW50czogc3RyaW5nW10gPSAoZXZlbnQuUmVzb3VyY2VQcm9wZXJ0aWVzLkNsaWVudElETGlzdCA/PyBbXSkuc29ydCgpO1xuXG4gIGlmICh0aHVtYnByaW50cy5sZW5ndGggPT09IDApIHtcbiAgICB0aHVtYnByaW50cy5wdXNoKGF3YWl0IGV4dGVybmFsLmRvd25sb2FkVGh1bWJwcmludChpc3N1ZXJVcmwpKTtcbiAgfVxuXG4gIGNvbnN0IHJlc3AgPSBhd2FpdCBleHRlcm5hbC5jcmVhdGVPcGVuSURDb25uZWN0UHJvdmlkZXIoe1xuICAgIFVybDogaXNzdWVyVXJsLFxuICAgIENsaWVudElETGlzdDogY2xpZW50cyxcbiAgICBUaHVtYnByaW50TGlzdDogdGh1bWJwcmludHMsXG4gIH0pO1xuXG4gIHJldHVybiB7XG4gICAgUGh5c2ljYWxSZXNvdXJjZUlkOiByZXNwLk9wZW5JRENvbm5lY3RQcm92aWRlckFybixcbiAgfTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gb25VcGRhdGUoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlVXBkYXRlRXZlbnQpIHtcbiAgY29uc3QgaXNzdWVyVXJsID0gZXZlbnQuUmVzb3VyY2VQcm9wZXJ0aWVzLlVybDtcbiAgY29uc3QgdGh1bWJwcmludHM6IHN0cmluZ1tdID0gKGV2ZW50LlJlc291cmNlUHJvcGVydGllcy5UaHVtYnByaW50TGlzdCA/PyBbXSkuc29ydCgpOyAvLyBrZWVwIHNvcnRlZCBmb3IgVVBEQVRFXG4gIGNvbnN0IGNsaWVudHM6IHN0cmluZ1tdID0gKGV2ZW50LlJlc291cmNlUHJvcGVydGllcy5DbGllbnRJRExpc3QgPz8gW10pLnNvcnQoKTtcblxuICAvLyBkZXRlcm1pbmUgd2hpY2ggdXBkYXRlIHdlIGFyZSB0YWxraW5nIGFib3V0LlxuICBjb25zdCBvbGRJc3N1ZXJVcmwgPSBldmVudC5PbGRSZXNvdXJjZVByb3BlcnRpZXMuVXJsO1xuXG4gIC8vIGlmIHRoaXMgaXMgYSBVUkwgdXBkYXRlLCB0aGVuIHdlIGJhc2ljYWxseSBjcmVhdGUgYSBuZXcgcmVzb3VyY2UgYW5kIGNmbiB3aWxsIGRlbGV0ZSB0aGUgb2xkIG9uZVxuICAvLyBzaW5jZSB0aGUgcGh5c2ljYWwgcmVzb3VyY2UgSUQgd2lsbCBjaGFuZ2UuXG4gIGlmIChvbGRJc3N1ZXJVcmwgIT09IGlzc3VlclVybCkge1xuICAgIHJldHVybiBvbkNyZWF0ZSh7IC4uLmV2ZW50LCBSZXF1ZXN0VHlwZTogJ0NyZWF0ZScgfSk7XG4gIH1cblxuICBjb25zdCBwcm92aWRlckFybiA9IGV2ZW50LlBoeXNpY2FsUmVzb3VyY2VJZDtcblxuICAvLyBpZiB0aHVtYnByaW50cyBjaGFuZ2VkLCB3ZSBjYW4gdXBkYXRlIGluLXBsYWNlLCBidXQgYmVhciBpbiBtaW5kIHRoYXQgaWYgdGhlIG5ldyB0aHVtYnByaW50IGxpc3RcbiAgLy8gaXMgZW1wdHksIHdlIHdpbGwgZ3JhYiBpdCBmcm9tIHRoZSBzZXJ2ZXIgbGlrZSB3ZSBkbyBpbiBDUkVBVEVcbiAgY29uc3Qgb2xkVGh1bWJwcmludHMgPSAoZXZlbnQuT2xkUmVzb3VyY2VQcm9wZXJ0aWVzLlRodW1icHJpbnRMaXN0IHx8IFtdKS5zb3J0KCk7XG4gIGlmIChKU09OLnN0cmluZ2lmeShvbGRUaHVtYnByaW50cykgIT09IEpTT04uc3RyaW5naWZ5KHRodW1icHJpbnRzKSkge1xuICAgIGNvbnN0IHRodW1icHJpbnRMaXN0ID0gdGh1bWJwcmludHMubGVuZ3RoID4gMCA/IHRodW1icHJpbnRzIDogW2F3YWl0IGV4dGVybmFsLmRvd25sb2FkVGh1bWJwcmludChpc3N1ZXJVcmwpXTtcbiAgICBleHRlcm5hbC5sb2coJ3VwZGF0aW5nIHRodW1icHJpbnQgbGlzdCBmcm9tJywgb2xkVGh1bWJwcmludHMsICd0bycsIHRodW1icHJpbnRzKTtcbiAgICBhd2FpdCBleHRlcm5hbC51cGRhdGVPcGVuSURDb25uZWN0UHJvdmlkZXJUaHVtYnByaW50KHtcbiAgICAgIE9wZW5JRENvbm5lY3RQcm92aWRlckFybjogcHJvdmlkZXJBcm4sXG4gICAgICBUaHVtYnByaW50TGlzdDogdGh1bWJwcmludExpc3QsXG4gICAgfSk7XG5cbiAgICAvLyBkb24ndCByZXR1cm4sIHdlIG1pZ2h0IGhhdmUgbW9yZSB1cGRhdGVzLi4uXG4gIH1cblxuICAvLyBpZiBjbGllbnQgSUQgbGlzdCBoYXMgY2hhbmdlZCwgZGV0ZXJtaW5lIFwiZGlmZlwiIGJlY2F1c2UgdGhlIEFQSSBpcyBhZGQvcmVtb3ZlXG4gIGNvbnN0IG9sZENsaWVudHM6IHN0cmluZ1tdID0gKGV2ZW50Lk9sZFJlc291cmNlUHJvcGVydGllcy5DbGllbnRJRExpc3QgfHwgW10pLnNvcnQoKTtcbiAgY29uc3QgZGlmZiA9IGFycmF5RGlmZihvbGRDbGllbnRzLCBjbGllbnRzKTtcbiAgZXh0ZXJuYWwubG9nKGBjbGllbnQgSUQgZGlmZjogJHtKU09OLnN0cmluZ2lmeShkaWZmKX1gKTtcblxuICBmb3IgKGNvbnN0IGFkZENsaWVudCBvZiBkaWZmLmFkZHMpIHtcbiAgICBleHRlcm5hbC5sb2coYGFkZGluZyBjbGllbnQgaWQgXCIke2FkZENsaWVudH1cIiB0byBwcm92aWRlciAke3Byb3ZpZGVyQXJufWApO1xuICAgIGF3YWl0IGV4dGVybmFsLmFkZENsaWVudElEVG9PcGVuSURDb25uZWN0UHJvdmlkZXIoe1xuICAgICAgT3BlbklEQ29ubmVjdFByb3ZpZGVyQXJuOiBwcm92aWRlckFybixcbiAgICAgIENsaWVudElEOiBhZGRDbGllbnQsXG4gICAgfSk7XG4gIH1cblxuICBmb3IgKGNvbnN0IGRlbGV0ZUNsaWVudCBvZiBkaWZmLmRlbGV0ZXMpIHtcbiAgICBleHRlcm5hbC5sb2coYHJlbW92aW5nIGNsaWVudCBpZCBcIiR7ZGVsZXRlQ2xpZW50fVwiIGZyb20gcHJvdmlkZXIgJHtwcm92aWRlckFybn1gKTtcbiAgICBhd2FpdCBleHRlcm5hbC5yZW1vdmVDbGllbnRJREZyb21PcGVuSURDb25uZWN0UHJvdmlkZXIoe1xuICAgICAgT3BlbklEQ29ubmVjdFByb3ZpZGVyQXJuOiBwcm92aWRlckFybixcbiAgICAgIENsaWVudElEOiBkZWxldGVDbGllbnQsXG4gICAgfSk7XG4gIH1cblxuICByZXR1cm47XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG9uRGVsZXRlKGRlbGV0ZUV2ZW50OiBBV1NMYW1iZGEuQ2xvdWRGb3JtYXRpb25DdXN0b21SZXNvdXJjZURlbGV0ZUV2ZW50KSB7XG4gIGF3YWl0IGV4dGVybmFsLmRlbGV0ZU9wZW5JRENvbm5lY3RQcm92aWRlcih7XG4gICAgT3BlbklEQ29ubmVjdFByb3ZpZGVyQXJuOiBkZWxldGVFdmVudC5QaHlzaWNhbFJlc291cmNlSWQsXG4gIH0pO1xufVxuIl19 |
\ | No newline at end of file |