Coverage for .github / oidc_provider / stack.py: 100.00%
53 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
1"""
2GitHub OIDC Provider CDK Stack.
4Creates an IAM OIDC identity provider for GitHub Actions and an IAM role
5that GitHub workflows can assume via ``aws-actions/configure-aws-credentials``.
7This stack is standalone — it does not depend on or import from the main
8GCO CDK stacks. Deploy it independently in any AWS account:
10 cd .github/oidc_provider
11 cdk deploy GCOGitHubOIDCStack
13The IAM policy attached to the role is loaded from ``policy.json`` in this
14directory. Edit that file to grant additional permissions for your CI needs.
16Trust Policy:
17 The role's trust policy restricts assumption to GitHub Actions workflows
18 running in a specific repository (and optionally a specific branch).
19 Legacy repositories use ``repo:<owner>/<repo>``. Repositories created or
20 transferred after July 15, 2026 use the immutable
21 ``repo:<owner>@<owner-id>/<repo>@<repo-id>`` prefix. The remainder is:
22 :ref:refs/heads/<branch> (branch push)
23 :pull_request (PR)
24 :ref:refs/tags/<tag> (tag push)
26 ``github_branch`` defaults to ``"main"`` and uses ``StringEquals`` for
27 that exact branch. ``"*"`` is an explicit opt-in that uses ``StringLike``
28 with the configured repository subject prefix plus ``:*``.
29"""
31import json
32import re
33from pathlib import Path
34from typing import Any
36from aws_cdk import CfnOutput, Stack
37from aws_cdk import aws_iam as iam
38from constructs import Construct
40GITHUB_OIDC_ISSUER = "token.actions.githubusercontent.com"
41GITHUB_OIDC_AUDIENCE = "sts.amazonaws.com"
42GITHUB_OIDC_THUMBPRINT = "6938fd4d98bab03faadb97b34396831e3780aea1"
43GITHUB_OIDC_BACKUP_THUMBPRINT = "1c58a3a8518e8759bf075b76b750d4f2df264fcd"
44_IMMUTABLE_SUBJECT_PREFIX_RE = re.compile(
45 r"^repo:(?P<owner>[^/@:]+)@[1-9]\d*/(?P<repo>[^/@:]+)@[1-9]\d*$"
46)
49def _validated_subject_prefix(github_repo: str, configured: str | None) -> str:
50 """Return a mutable or immutable GitHub repository subject prefix.
52 GitHub repositories created or transferred after July 15, 2026 use
53 ``repo:OWNER@OWNER_ID/REPO@REPO_ID``. The explicit prefix is validated
54 against ``github_repo`` so a copied ID pair cannot silently trust a
55 different named repository.
56 """
57 try:
58 owner, repository = github_repo.split("/", 1)
59 except ValueError as exc:
60 raise ValueError("github_repo must use owner/repo format") from exc
61 if not owner or not repository or "/" in repository:
62 raise ValueError("github_repo must use owner/repo format")
64 mutable_prefix = f"repo:{github_repo}"
65 if configured is None:
66 return mutable_prefix
67 if not isinstance(configured, str):
68 raise ValueError("github_subject_prefix must be a string")
69 if configured != configured.strip() or not configured:
70 raise ValueError("github_subject_prefix must be a non-empty trimmed string")
71 if configured == mutable_prefix:
72 return configured
74 match = _IMMUTABLE_SUBJECT_PREFIX_RE.fullmatch(configured)
75 if match is None:
76 raise ValueError(
77 "github_subject_prefix must be repo:owner/repo or repo:owner@OWNER_ID/repo@REPO_ID"
78 )
79 if (match.group("owner"), match.group("repo")) != (owner, repository):
80 raise ValueError("github_subject_prefix names must match github_repo")
81 return configured
84class GCOGitHubOIDCStack(Stack):
85 """Standalone stack that creates a GitHub OIDC provider and CI role.
87 Parameters:
88 github_repo: GitHub repository in ``owner/repo`` format.
89 Default: ``aws-solutions-library-samples/global-capacity-orchestrator-on-aws``.
90 github_subject_prefix: Exact repository prefix GitHub reports for the
91 OIDC ``sub`` claim. Omit for the legacy ``repo:owner/repo`` format;
92 immutable repositories use ``repo:owner@OWNER_ID/repo@REPO_ID``.
93 github_branch: Exact branch restriction. Defaults to ``"main"``.
94 Set this to the repository's actual default branch when it differs;
95 use ``"*"`` only as an explicit opt-in to any branch or tag.
96 """
98 def __init__(
99 self,
100 scope: Construct,
101 construct_id: str,
102 *,
103 github_repo: str = "aws-solutions-library-samples/global-capacity-orchestrator-on-aws",
104 github_subject_prefix: str | None = None,
105 github_branch: str = "main",
106 **kwargs: Any,
107 ) -> None:
108 super().__init__(scope, construct_id, **kwargs)
110 # ---------------------------------------------------------------------
111 # OIDC Provider
112 # ---------------------------------------------------------------------
113 provider = iam.OpenIdConnectProvider(
114 self,
115 "GitHubOIDCProvider",
116 url=f"https://{GITHUB_OIDC_ISSUER}",
117 client_ids=[GITHUB_OIDC_AUDIENCE],
118 thumbprints=[GITHUB_OIDC_THUMBPRINT, GITHUB_OIDC_BACKUP_THUMBPRINT],
119 )
121 # ---------------------------------------------------------------------
122 # Trust policy — restrict to the specified GitHub repo (and branch)
123 # ---------------------------------------------------------------------
124 subject_prefix = _validated_subject_prefix(github_repo, github_subject_prefix)
125 if github_branch == "*":
126 subject_claim = f"{subject_prefix}:*"
127 condition = {"StringLike": {"token.actions.githubusercontent.com:sub": subject_claim}}
128 else:
129 subject_claim = f"{subject_prefix}:ref:refs/heads/{github_branch}"
130 condition = {"StringEquals": {"token.actions.githubusercontent.com:sub": subject_claim}}
132 # Also require the audience claim to match
133 condition.setdefault("StringEquals", {})
134 condition["StringEquals"]["token.actions.githubusercontent.com:aud"] = GITHUB_OIDC_AUDIENCE
136 principal = iam.OpenIdConnectPrincipal(provider, conditions=condition)
138 # ---------------------------------------------------------------------
139 # IAM Role
140 # ---------------------------------------------------------------------
141 role = iam.Role(
142 self,
143 "GitHubActionsRole",
144 assumed_by=principal,
145 role_name=f"gco-github-actions-{self.region}",
146 description=(
147 f"GitHub Actions OIDC role for {github_repo}. "
148 "Assumed by CI workflows via aws-actions/configure-aws-credentials."
149 ),
150 max_session_duration=None, # default 1 hour
151 )
153 # ---------------------------------------------------------------------
154 # IAM Policy (loaded from policy.json)
155 # ---------------------------------------------------------------------
156 policy_path = Path(__file__).parent / "policy.json"
157 policy_doc = json.loads(policy_path.read_text())
159 role.attach_inline_policy(
160 iam.Policy(
161 self,
162 "CIPolicy",
163 document=iam.PolicyDocument.from_json(policy_doc),
164 )
165 )
167 # ---------------------------------------------------------------------
168 # Outputs
169 # ---------------------------------------------------------------------
170 CfnOutput(
171 self,
172 "RoleArn",
173 value=role.role_arn,
174 description="IAM role ARN for GitHub Actions. Add as GCO_CI_ROLE_ARN secret.",
175 )
176 CfnOutput(
177 self,
178 "OIDCProviderArn",
179 value=provider.open_id_connect_provider_arn,
180 description="OIDC provider ARN.",
181 )