@fjall/components-infrastructure 2.19.6 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,25 +1,89 @@
|
|
|
1
1
|
import { CfnOutput, Duration } from "aws-cdk-lib";
|
|
2
2
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import { Runtime } from "aws-cdk-lib/aws-lambda";
|
|
3
4
|
import { Construct } from "constructs";
|
|
4
5
|
import { Role } from "../../resources/aws/iam/role.js";
|
|
6
|
+
import { CustomResource } from "../../resources/aws/utilities/customResource.js";
|
|
7
|
+
/**
|
|
8
|
+
* The `https://fjall.io` OIDC provider is an account-global IAM singleton
|
|
9
|
+
* (one per account+URL). Both this construct AND the Quick-Create template can
|
|
10
|
+
* provision it, so it MUST be adopt-if-exists, not raw-create: a bare
|
|
11
|
+
* `new iam.OpenIdConnectProvider(...)` throws `EntityAlreadyExistsException`
|
|
12
|
+
* the moment the other source got there first (the mis-root collision class).
|
|
13
|
+
* The custom resource lists-then-adopts the existing provider (creating it only
|
|
14
|
+
* when absent) and NEVER deletes it on stack teardown — other deploy roles in
|
|
15
|
+
* the account federate the same singleton.
|
|
16
|
+
*/
|
|
17
|
+
const OIDC_PROVIDER_HANDLER = `
|
|
18
|
+
const { IAMClient, ListOpenIDConnectProvidersCommand, GetOpenIDConnectProviderCommand, CreateOpenIDConnectProviderCommand, AddClientIDToOpenIDConnectProviderCommand } = require('@aws-sdk/client-iam');
|
|
19
|
+
|
|
20
|
+
const AUDIENCE = 'sts.amazonaws.com';
|
|
21
|
+
const THUMBPRINT = '0000000000000000000000000000000000000000';
|
|
22
|
+
|
|
23
|
+
exports.handler = async (event) => {
|
|
24
|
+
const physicalId = event.PhysicalResourceId || event.LogicalResourceId;
|
|
25
|
+
// Never delete the account-global singleton — shared with the Quick-Create
|
|
26
|
+
// stack and federated by other deploy roles.
|
|
27
|
+
if (event.RequestType === 'Delete') {
|
|
28
|
+
return { PhysicalResourceId: physicalId };
|
|
29
|
+
}
|
|
30
|
+
const issuerUrl = event.ResourceProperties.IssuerUrl;
|
|
31
|
+
const host = new URL(issuerUrl).host;
|
|
32
|
+
const iam = new IAMClient({});
|
|
33
|
+
const list = await iam.send(new ListOpenIDConnectProvidersCommand({}));
|
|
34
|
+
const suffix = ':oidc-provider/' + host;
|
|
35
|
+
let arn = (list.OpenIDConnectProviderList || [])
|
|
36
|
+
.map((p) => p.Arn)
|
|
37
|
+
.find((a) => typeof a === 'string' && a.endsWith(suffix));
|
|
38
|
+
if (!arn) {
|
|
39
|
+
const created = await iam.send(new CreateOpenIDConnectProviderCommand({
|
|
40
|
+
Url: issuerUrl,
|
|
41
|
+
ClientIDList: [AUDIENCE],
|
|
42
|
+
ThumbprintList: [THUMBPRINT],
|
|
43
|
+
}));
|
|
44
|
+
arn = created.OpenIDConnectProviderArn;
|
|
45
|
+
} else {
|
|
46
|
+
const got = await iam.send(new GetOpenIDConnectProviderCommand({ OpenIDConnectProviderArn: arn }));
|
|
47
|
+
if (!(got.ClientIDList || []).includes(AUDIENCE)) {
|
|
48
|
+
await iam.send(new AddClientIDToOpenIDConnectProviderCommand({ OpenIDConnectProviderArn: arn, ClientID: AUDIENCE }));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { PhysicalResourceId: arn, Data: { ProviderArn: arn } };
|
|
52
|
+
};`;
|
|
5
53
|
export class OidcConnector extends Construct {
|
|
6
54
|
deployRoleArn;
|
|
7
55
|
constructor(scope, id, props) {
|
|
8
56
|
super(scope, id);
|
|
9
57
|
const issuerDomain = "fjall.io";
|
|
10
58
|
const issuerUrl = `https://${issuerDomain}`;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
59
|
+
const providerResource = new CustomResource(this, "OidcProvider", {
|
|
60
|
+
runtime: Runtime.NODEJS_22_X,
|
|
61
|
+
timeout: Duration.minutes(5),
|
|
62
|
+
lambdaDescription: "Adopt-or-create the account-global Fjall OIDC provider",
|
|
63
|
+
resourceType: "Custom::FjallOidcProvider",
|
|
64
|
+
properties: { IssuerUrl: issuerUrl },
|
|
65
|
+
inlinePolicy: [
|
|
66
|
+
new iam.PolicyStatement({
|
|
67
|
+
effect: iam.Effect.ALLOW,
|
|
68
|
+
actions: [
|
|
69
|
+
"iam:ListOpenIDConnectProviders",
|
|
70
|
+
"iam:GetOpenIDConnectProvider",
|
|
71
|
+
"iam:CreateOpenIDConnectProvider",
|
|
72
|
+
"iam:AddClientIDToOpenIDConnectProvider"
|
|
73
|
+
],
|
|
74
|
+
resources: ["*"]
|
|
75
|
+
})
|
|
76
|
+
],
|
|
77
|
+
inlineCode: OIDC_PROVIDER_HANDLER
|
|
17
78
|
});
|
|
79
|
+
const providerArn = providerResource.resource
|
|
80
|
+
.getAtt("ProviderArn")
|
|
81
|
+
.toString();
|
|
18
82
|
const deployRole = new Role(this, "DeployRole", {
|
|
19
83
|
roleName: `FjallDeploy${props.fjallOrgId}`,
|
|
20
84
|
path: "/fjall/",
|
|
21
85
|
maxSessionDuration: Duration.hours(1),
|
|
22
|
-
assumedBy: new iam.FederatedPrincipal(
|
|
86
|
+
assumedBy: new iam.FederatedPrincipal(providerArn, {
|
|
23
87
|
StringEquals: { [`${issuerDomain}:aud`]: "sts.amazonaws.com" },
|
|
24
88
|
StringLike: {
|
|
25
89
|
[`${issuerDomain}:sub`]: `org:${props.fjallOrgId}:*`
|
|
@@ -38,7 +102,7 @@ export class OidcConnector extends Construct {
|
|
|
38
102
|
});
|
|
39
103
|
new CfnOutput(this, "OidcProviderArn", {
|
|
40
104
|
key: "OidcProviderArn",
|
|
41
|
-
value:
|
|
105
|
+
value: providerArn,
|
|
42
106
|
exportName: "OidcProviderArn",
|
|
43
107
|
description: "ARN of the Fjall OIDC provider"
|
|
44
108
|
});
|
|
@@ -15,6 +15,13 @@ interface CustomResourceProps {
|
|
|
15
15
|
properties?: {
|
|
16
16
|
[key: string]: string;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* CloudFormation resource type for the custom resource (must begin with
|
|
20
|
+
* `Custom::`). Defaults to `AWS::CloudFormation::CustomResource`; set a
|
|
21
|
+
* `Custom::<Name>` value to make the resource self-identifying in the CFN
|
|
22
|
+
* console and addressable by type in synth assertions.
|
|
23
|
+
*/
|
|
24
|
+
resourceType?: string;
|
|
18
25
|
codePath?: string;
|
|
19
26
|
inlineCode?: string;
|
|
20
27
|
assetOptions?: AssetOptions;
|
|
@@ -58,7 +58,10 @@ export class CustomResource extends Construct {
|
|
|
58
58
|
});
|
|
59
59
|
this.resource = new customResource(this, `${id}Resource`, {
|
|
60
60
|
serviceToken: provider.serviceToken,
|
|
61
|
-
properties: props.properties
|
|
61
|
+
properties: props.properties,
|
|
62
|
+
...(props.resourceType !== undefined && {
|
|
63
|
+
resourceType: props.resourceType
|
|
64
|
+
})
|
|
62
65
|
});
|
|
63
66
|
this.response = this.resource.getAtt("Response").toString();
|
|
64
67
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
66
|
-
"@fjall/generator": "^2.
|
|
67
|
-
"@fjall/util": "^2.
|
|
66
|
+
"@fjall/generator": "^2.20.0",
|
|
67
|
+
"@fjall/util": "^2.20.0",
|
|
68
68
|
"constructs": "^10.0.0"
|
|
69
69
|
},
|
|
70
70
|
"overrides": {
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
"engines": {
|
|
79
79
|
"node": ">=18.0.0"
|
|
80
80
|
},
|
|
81
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "ec556d44a8a32fbdc1a2e1ca3c1f22224b0d27c2"
|
|
82
82
|
}
|