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

1""" 

2GitHub OIDC Provider CDK Stack. 

3 

4Creates an IAM OIDC identity provider for GitHub Actions and an IAM role 

5that GitHub workflows can assume via ``aws-actions/configure-aws-credentials``. 

6 

7This stack is standalone — it does not depend on or import from the main 

8GCO CDK stacks. Deploy it independently in any AWS account: 

9 

10 cd .github/oidc_provider 

11 cdk deploy GCOGitHubOIDCStack 

12 

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. 

15 

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) 

25 

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""" 

30 

31import json 

32import re 

33from pathlib import Path 

34from typing import Any 

35 

36from aws_cdk import CfnOutput, Stack 

37from aws_cdk import aws_iam as iam 

38from constructs import Construct 

39 

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) 

47 

48 

49def _validated_subject_prefix(github_repo: str, configured: str | None) -> str: 

50 """Return a mutable or immutable GitHub repository subject prefix. 

51 

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") 

63 

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 

73 

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 

82 

83 

84class GCOGitHubOIDCStack(Stack): 

85 """Standalone stack that creates a GitHub OIDC provider and CI role. 

86 

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 """ 

97 

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) 

109 

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 ) 

120 

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}} 

131 

132 # Also require the audience claim to match 

133 condition.setdefault("StringEquals", {}) 

134 condition["StringEquals"]["token.actions.githubusercontent.com:aud"] = GITHUB_OIDC_AUDIENCE 

135 

136 principal = iam.OpenIdConnectPrincipal(provider, conditions=condition) 

137 

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 ) 

152 

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()) 

158 

159 role.attach_inline_policy( 

160 iam.Policy( 

161 self, 

162 "CIPolicy", 

163 document=iam.PolicyDocument.from_json(policy_doc), 

164 ) 

165 ) 

166 

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 )