# -*- coding: utf-8 -*-
"""
Provides functionality to create
`GitHub OpenID Connect <https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services>`_
providers and IAM roles that can be assumed by GitHub Actions workflows.
"""
import typing as T
import dataclasses
import aws_cdk as cdk
import aws_cdk.aws_iam as iam
from constructs import Construct
from func_args.api import REQ, OPT, remove_optional
from ...base import BaseConstruct
from ...params import ConstructParams
from .utils import role_name_to_inline_policy_name
[docs]
def create_github_oidc_provider(
scope: Construct,
id: str,
url: str = "https://token.actions.githubusercontent.com",
client_id_list: T.Optional[list[str]] = None,
thumbprint_list: T.Optional[list[str]] = None,
) -> iam.CfnOIDCProvider:
"""
Create a GitHub OIDC Provider in AWS IAM.
This function creates an OIDC provider configuration that allows GitHub Actions
to authenticate with AWS using short-lived tokens instead of long-term credentials.
The provider is configured with standard GitHub token URL and thumbprint.
Ref: https://github.com/aws-actions/configure-aws-credentials
"""
if client_id_list is None:
client_id_list = ["sts.amazonaws.com"]
if thumbprint_list is None:
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
return cdk.aws_iam.CfnOIDCProvider(
scope=scope,
id=id,
url=url,
client_id_list=client_id_list,
thumbprint_list=thumbprint_list,
)
GITHUB_OIDC_PROVIDER_ARN = (
f"arn:aws:iam::{cdk.Aws.ACCOUNT_ID}:oidc-provider/"
"token.actions.githubusercontent.com"
)
"""
GitHub OIDC Provider ARN in AWS is always in this format.
"""
[docs]
def create_github_repo_main_iam_role_assumed_by(
repo_patterns: T.Union[str, T.List[str]],
federated: str = GITHUB_OIDC_PROVIDER_ARN,
) -> iam.FederatedPrincipal:
"""
Create a FederatedPrincipal for GitHub OIDC authentication.
Creates an IAM FederatedPrincipal that allows GitHub Actions to assume
the role via OIDC authentication.
Usage Example::
iam.Role(
scope=...,
id=...,
role_name=...,
assumed_by=create_github_repo_main_iam_role_assumed_by(
repo_patterns=...,
federated=...,
),
inline_policies=inline_policies,
)
:param repo_patterns: GitHub repository pattern(s) allowed to assume the role.
Can be a single pattern string or a list of patterns.
Example: "repo:organization/repo-name:*" or
["repo:org/repo1:*", "repo:org/repo2:*"].
:param federated: ARN of the OIDC provider. Defaults to GitHub's OIDC provider.
"""
return iam.FederatedPrincipal(
federated=federated,
assume_role_action="sts:AssumeRoleWithWebIdentity",
conditions={
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
},
"StringLike": {
"token.actions.githubusercontent.com:sub": repo_patterns,
},
},
)
[docs]
@dataclasses.dataclass
class GitHubOidcProviderParams(ConstructParams):
"""
Parameters for creating a GitHub OIDC provider.
See :class:`GitHubOidcProvider`
"""
# fmt: off
id: str = dataclasses.field(default="GitHubOidcProviderConstruct")
github_oidc_provider_res_id: str = dataclasses.field(default="GitHubOidcProviderResource")
url: str = dataclasses.field(default="https://token.actions.githubusercontent.com")
client_id_list: list[str] = dataclasses.field(default=OPT)
thumbprint_list: list[str] = dataclasses.field(default=OPT)
# fmt: on
[docs]
class GitHubOidcProvider(BaseConstruct):
"""
Construct for creating a GitHub OIDC provider in AWS IAM.
:param params: :class:`GitHubOidcProviderParams`
"""
def __init__(
self,
scope: Construct,
params: GitHubOidcProviderParams,
):
super().__init__(scope=scope, params=params)
self.params = params
self.create_github_oidc_provider()
def create_github_oidc_provider(self):
self.github_oidc_provider = create_github_oidc_provider(
scope=self,
**remove_optional(
id=self.params.github_oidc_provider_res_id,
url=self.params.url,
client_id_list=self.params.client_id_list,
thumbprint_list=self.params.thumbprint_list,
),
)
[docs]
@dataclasses.dataclass
class SingleRoleWithInlinePolicyConstructParams(ConstructParams):
role_name: str = dataclasses.field(default=REQ)
@property
def inline_policy_name(self) -> str:
return role_name_to_inline_policy_name(self.role_name)
[docs]
@dataclasses.dataclass
class GitHubOidcSingleAccountParams(SingleRoleWithInlinePolicyConstructParams):
"""
Parameters for creating a GitHub OIDC role in a single AWS account setup.
See :class:`GitHubOidcSingleAccount`
"""
# fmt: off
id: str = dataclasses.field(default="GitHubOidcSingleAccountConstruct")
github_repo_main_iam_role_res_id: str = dataclasses.field(default="GitHubRepoMainIamRole")
repo_patterns: T.Union[str, T.List[str]] = dataclasses.field(default=REQ)
federated: str = dataclasses.field(default=GITHUB_OIDC_PROVIDER_ARN)
# fmt: on
[docs]
class GitHubOidcSingleAccount(BaseConstruct):
"""
Construct for creating an IAM role assumable by GitHub Actions.
The role can be assumed directly by GitHub Actions and has the permission
to perform deployment related AWS actions directly.
:param params: :class:`GitHubOidcSingleAccountParams`
"""
def __init__(
self,
scope: Construct,
params: GitHubOidcSingleAccountParams,
):
super().__init__(scope=scope, params=params)
self.params = params
self.create_github_repo_main_iam_role()
[docs]
def create_github_repo_main_iam_role_inline_policy_document(
self,
) -> iam.PolicyDocument:
"""
Implement this method to return the inline policy document for the IAM role.
Example:
.. code-block:: python
def ...(...) -> ...:
return iam.PolicyDocument(
statements=[
iam.PolicyStatement(
actions=...,
resources=...,
),
],
)
"""
raise NotImplementedError(
"You need to implement the "
f"`{self.create_github_repo_main_iam_role_inline_policy_document}` method!"
f"This method should return an instance of `iam.PolicyDocument` for ..."
)
# Example:
# return iam.PolicyDocument(
# statements=[
# iam.PolicyStatement(
# actions=...,
# resources=...,
# ),
# ],
# )
[docs]
def create_github_repo_main_iam_role(self):
"""
Create the main IAM role that will be assumed by GitHub Actions.
.. note::
User can override this method to customize the IAM role creation.
"""
self.github_repo_main_iam_role = iam.Role(
scope=self,
id=self.params.github_repo_main_iam_role_res_id,
description="GitHub OIDC DevOps main IAM role that will be assumed by GitHub Actions",
role_name=self.params.role_name,
assumed_by=create_github_repo_main_iam_role_assumed_by(
repo_patterns=self.params.repo_patterns,
federated=self.params.federated,
),
inline_policies={
self.params.inline_policy_name: self.create_github_repo_main_iam_role_inline_policy_document(),
},
)
[docs]
@dataclasses.dataclass
class GitHubOidcMultiAccountDevopsParams(SingleRoleWithInlinePolicyConstructParams):
"""
Parameters for creating a GitHub OIDC devops role in a multi AWS account setup.
See :class:`GitHubOidcMultiAccountDevops`
"""
# fmt: off
id: str = dataclasses.field(default="GitHubOidcMultiAccountDevopsConstruct")
github_repo_main_iam_role_res_id: str = dataclasses.field(default="GitHubRepoMainIamRole")
repo_patterns: T.Union[str, T.List[str]] = dataclasses.field(default=REQ)
workload_iam_role_arn_list: T.List[str] = dataclasses.field(default=REQ)
federated: str = dataclasses.field(default=GITHUB_OIDC_PROVIDER_ARN)
# fmt: on
[docs]
class GitHubOidcMultiAccountDevops(BaseConstruct):
"""
Construct for creating a GitHub OIDC devops role in a multi AWS account setup.
This role can be assumed by GitHub Actions and has the permission to assume
other roles in different AWS accounts.
:param params: :class:`GitHubOidcMultiAccountDevopsParams`
"""
def __init__(
self,
scope: Construct,
params: GitHubOidcMultiAccountDevopsParams,
):
super().__init__(scope=scope, params=params)
self.params = params
self.create_github_repo_main_iam_role()
def create_github_repo_main_iam_role_inline_policy_document(
self,
) -> iam.PolicyDocument:
return iam.PolicyDocument(
statements=[
iam.PolicyStatement(
actions=["sts:AssumeRole"],
resources=self.params.workload_iam_role_arn_list,
)
]
)
[docs]
def create_github_repo_main_iam_role(self):
"""
Create the main IAM role that will be assumed by GitHub Actions.
.. note::
User can override this method to customize the IAM role creation.
"""
self.github_repo_main_iam_role = iam.Role(
scope=self,
id=self.params.github_repo_main_iam_role_res_id,
description="GitHub OIDC DevOps main IAM role that will be assumed by GitHub Actions",
role_name=self.params.role_name,
assumed_by=create_github_repo_main_iam_role_assumed_by(
repo_patterns=self.params.repo_patterns,
federated=self.params.federated,
),
inline_policies={
self.params.inline_policy_name: self.create_github_repo_main_iam_role_inline_policy_document(),
},
)
[docs]
@dataclasses.dataclass
class GitHubOidcMultiAccountWorkloadParams(SingleRoleWithInlinePolicyConstructParams):
"""
Parameters for creating a GitHub OIDC workload role.
See :class:`GitHubOidcMultiAccountWorkload`
"""
# fmt: off
id: str = dataclasses.field(default="GitHubOidcMultiAccountWorkloadConstruct")
github_repo_workload_iam_role_res_id: str = dataclasses.field(default="GitHubRepoWorkloadIamRole")
devops_iam_role_arn: str = dataclasses.field(default=REQ)
# fmt: on
[docs]
class GitHubOidcMultiAccountWorkload(BaseConstruct):
"""
Construct for creating a workload IAM role in a multi-account setup.
This role can be assumed by a devops IAM role, and it has the permission to
perform deployment related AWS actions.
:param params: :class:`GitHubOidcMultiAccountWorkloadParams`
"""
def __init__(
self,
scope: Construct,
params: GitHubOidcMultiAccountWorkloadParams,
):
super().__init__(scope=scope, params=params)
self.params = params
self.create_github_repo_workload_iam_role()
[docs]
def create_github_repo_workload_iam_role_inline_policy_document(
self,
) -> iam.PolicyDocument:
"""
Implement this method to return the inline policy document for the IAM role.
Example:
.. code-block:: python
def ...(...) -> ...:
return iam.PolicyDocument(
statements=[
iam.PolicyStatement(
actions=...,
resources=...,
),
],
)
"""
raise NotImplementedError(
"You need to implement the "
f"`{self.create_github_repo_workload_iam_role_inline_policy_document}` method!"
f"This method should return an instance of `iam.PolicyDocument` for ..."
)
# Example:
# return iam.PolicyDocument(
# statements=[
# iam.PolicyStatement(
# actions=...,
# resources=...,
# ),
# ],
# )
[docs]
def create_github_repo_workload_iam_role(self):
"""
Create the main IAM role that will be assumed by GitHub Actions.
.. note::
User can override this method to customize the IAM role creation.
"""
self.github_repo_workload_iam_role = iam.Role(
scope=self,
id=self.params.github_repo_workload_iam_role_res_id,
description="GitHub OIDC workload IAM role that will be assumed by the DevOps role",
role_name=self.params.role_name,
assumed_by=iam.ArnPrincipal(
arn=self.params.devops_iam_role_arn,
),
inline_policies={
self.params.inline_policy_name: self.create_github_repo_workload_iam_role_inline_policy_document(),
},
)