@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 provider = new iam.OpenIdConnectProvider(this, "OidcProvider", {
12
- url: issuerUrl,
13
- clientIds: ["sts.amazonaws.com"],
14
- // Placeholder thumbprint. CDK auto-resolves at deploy time for known IdPs;
15
- // STS trust is enforced via the OIDC issuer URL + `:sub` / `:aud` match.
16
- thumbprints: ["0000000000000000000000000000000000000000"]
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(provider.openIdConnectProviderArn, {
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: provider.openIdConnectProviderArn,
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.19.6",
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.19.6",
67
- "@fjall/util": "^2.19.6",
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": "f41aae2245b7e82af7e8e7f5cf098b83b22cc198"
81
+ "gitHead": "ec556d44a8a32fbdc1a2e1ca3c1f22224b0d27c2"
82
82
  }