新しいブランチが作成されたら、既存のIAMポリシーを自動更新するLambdaを書いてみた

# はじめに
所定のレポジトリ内に、ブランチが作成されるたび、特定のポリシーの権限を自動更新するLambdaを書いてみました。
結局この内容は使用されなかったのですが、せっかく書いたので、記事にします。

やりたいこととしては
    ①**以下の記事を参考にして、所定のブランチに対してアクセス制限をかけるIAMポリシーを作成する**

https://dev.classmethod.jp/articles/tsnote-codecommit-what-actions-can-i-use-to-control-access-on-a-branch-by-branch-basis-in-codecommit/

➁**新しいブランチが作成されるたびに、そのブランチ名を上記で作成したポリシーに追加する**

です。




# 目次
<!-- タイトルとアンカー名を編集 -->
1. フローの説明
1. コードの解説
1. 終わりに

<!-- 各チャプター -->
<a id="#Chapter1"></a>
# フローの説明
今回の流れは以下となります。
※うち➁～⑥がLambdaの処理
①新しいブランチの作成がトリガーとなり、Lambdaにeventが渡される
➁渡されたeventから必要情報を抽出
③更新対象のポリシーの必要情報を抽出
④新しいVersionのポリシーを、既存ポリシーをコピーして作成
⑤新しく作成されたブランチ名を、更新対象のポリシーに反映
⑥最も古いVersionのポリシーを削除

<a id="#Chapter2"></a>
# コードの解説
以下が実際のコードになります。
この後詳細を説明していきます。

```python
import boto3
import json
import copy

def get_new_policy_document(old_document, branch_name):
    new_document = copy.deepcopy(old_document)
    new_document["Statement"][0]["Condition"]["StringEquals"]["codecommit:References"].append(f'refs/heads/{branch_name}')
    return(new_document)

def lambda_handler(event, context):
    # CodeCommitからのイベント情報を取得
    record = event['Records'][0]
    repository_name = record['eventSourceARN'].split(':')[5]
    ref = event['Records'][0]['codecommit']['references'][0]['ref']
    branch_name = ref.split('/')[-1]
    print(f"New branch {branch_name} was created in {repository_name} repository.")
    
    # IAMポリシーの更新

    iam = boto3.client('iam')
    policy_arn = 'arn:aws:iam::133285731447:policy/MusakaTestForDevelopmentMember'
    response = iam.list_policy_versions(PolicyArn=policy_arn)
    latest_version = response['Versions'][0]['VersionId']
    policy = iam.get_policy_version(PolicyArn=policy_arn, VersionId=latest_version)
    
    policy_version = iam.create_policy_version(
        PolicyArn=policy_arn,
        PolicyDocument=json.dumps(get_new_policy_document(policy["PolicyVersion"]["Document"], branch_name)),
        SetAsDefault=True
    )
    iam.delete_policy_version(
        PolicyArn=policy_arn, 
        VersionId=response['Versions'][-1]['VersionId']
        )

    print(f"IAM policy {policy_arn} was updated with new policy version {policy_version['PolicyVersion']['VersionId']}.")
```
ではフローの順番に説明します。
#### 新しいブランチの作成がトリガーとなり、Lambdaにeventが渡される

コンソール上からLambdaにアクセスし、「トリガーを追加」を選択します。
その後、CodeCommitを選択し、必要な項目を埋めていきます。
今回は「ブランチまたはタグを作成する」がリッスンするイベントになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2666121/69fb5bed-0de8-1642-8326-e87b752b9998.png)

#### 渡されたeventから必要情報を抽出
ここまでで、ブランチが作成されるたびにeventがLambdaに渡されるフローが完成しました。
ここから、Lambdaの処理について説明していきます。
ゴールとしては、**渡されたeventからレポジトリ名とブランチ名を取得して、以下をプリントすること**です。
    
```python
"New branch {branch_name} was created in {repository_name} repository.
```


```python
import boto3
import json
import copy

def get_new_policy_document(old_document, branch_name):
    new_document = copy.deepcopy(old_document)
    new_document["Statement"][0]["Condition"]["StringEquals"]["codecommit:References"].append(f'refs/heads/{branch_name}')
    return(new_document)

def lambda_handler(event, context):
    # CodeCommitからのイベント情報を取得
    record = event['Records'][0]
    repository_name = record['eventSourceARN'].split(':')[5]
    ref = event['Records'][0]['codecommit']['references'][0]['ref']
    branch_name = ref.split('/')[-1]
    print(f"New branch {branch_name} was created in {repository_name} repository.")
```

・必要なモジュールのインポート
今回は以下のモジュールが必要なのでインポートします。

```python
import boto3
import json
import copy
```
※コードの順番的に、def get_new_policy_document(old_document, branch_name):関数がありますが、こちらの説明は後回しにします。

・渡されたeventから必要な情報の抽出
では**渡されたeventから、レポジトリ名とブランチ名を取得**していきます。
そもそもeventがどんな形で渡されるかというと、こんな感じのJSONで渡されます。
※以下はあくまで一例で、実際の値ではありません。こちらはLambdaのテストで以下を選択することで確認可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2666121/e57c12aa-9673-4c49-6617-56aafab0242c.png)



```json
{
  "Records": [
    {
      "awsRegion": "us-east-1",
      "codecommit": {
        "references": [
          {
            "commit": "5c4ef1049f1d27deadbeeff313e0730018be182b",
            "ref": "refs/heads/master"
          }
        ]
      },
      "customData": "this is custom data",
      "eventId": "5a824061-17ca-46a9-bbf9-114edeadbeef",
      "eventName": "TriggerEventTest",
      "eventPartNumber": 1,
      "eventSource": "aws:codecommit",
      "eventSourceARN": "arn:aws:codecommit:us-east-1:123456789012:my-repo",
      "eventTime": "2016-01-01T23:59:59.000+0000",
      "eventTotalParts": 1,
      "eventTriggerConfigId": "5a824061-17ca-46a9-bbf9-114edeadbeef",
      "eventTriggerName": "my-trigger",
      "eventVersion": "1.0",
      "userIdentityARN": "arn:aws:iam::123456789012:root"
    }
  ]
}
```
そのためには、以下のコードが必要になります。


```python
    record = event['Records'][0]
    repository_name = record['eventSourceARN'].split(':')[5]
    ref = event['Records'][0]['codecommit']['references'][0]['ref']
    branch_name = ref.split('/')[-1]
```
JSONで必要な値にアクセスするには順番に下っていけばよいので
①「Event」の中の「Records」配列の0番目を変数recordに格納
➁「Records配列」0番目の中の「eventSourceARN」を:で割った5番目を変数repository_nameに格納
③「Records配列」0番目の中の「codecommit」の中の「reference配列」の0番目の「ref」を変数refに格納
④refに格納されたrefs/heads/masterの中の後ろから一つ目

という順番で地道に必要な情報にアクセスしています。
はい、ここまで問題なければ以下がプリントされることになります。

```python
"New branch {branch_name} was created in {repository_name} repository.
```

#### 更新対象のポリシーの必要情報を抽出

次は以下のコードを用いて、更新対象のポリシーの情報を取得していきます。

```python
iam = boto3.client('iam')
    policy_arn = 'arn:aws:iam::133285731447:policy/MusakaTestForDevelopmentMember'
    response = iam.list_policy_versions(PolicyArn=policy_arn)
    latest_version = response['Versions'][0]['VersionId']
    policy = iam.get_policy_version(PolicyArn=policy_arn, VersionId=latest_version)
```
まずiamという変数にboto3.client('iam'）で取得した値を格納しています。
そもそもboto3ってなに？という説明ですと、要するにPythonを使用してAWSリソースを使用することができるSDKの一種となります。

例えばS3サービスのアクションを実行したい場合は、以下のように利用することが出来ます。

```python
import boto3

s3 = boto3.client('s3')

# S3クライアント上の 'list_buckets' メソッドを呼び出す
response = s3.list_buckets()

# バケットの一覧を表示
print(response['Buckets'])
```
今回必要なアクションはIAMですので、boto3.client('IAM')をIAMという変数に代入することを最初に実施しているわけです。
以下公式ドキュメントです。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html

このコードでは
①更新ポリシーのARNを指定
➁list_policy_versionsで対象ポリシーのversionをリストアップ
③取得されたポリシーのversionのうち最も新しいものを変数latest_versionに格納
④get_policy_versionで更新ポリシー最新versionの情報を取得
という流れの処理が書かれています。

➁番以降を説明します。

以下を見ると、必要な引数を確認することができます。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/list_policy_versions.html

以下がレスポンスとして返ってきますので、その中から必要情報を変数latest_versionに格納します。
そもそもlatest_versionが必要だった理由ですが、次のget_policy_versionの引数としてVersionIdが必要だったからです。

```JS
{
    'Versions': [
        {
            'Document': 'string',
            'VersionId': 'string',
            'IsDefaultVersion': True|False,
            'CreateDate': datetime(2015, 1, 1)
        },
    ],
    'IsTruncated': True|False,
    'Marker': 'string'
}
```
この後、get_policy_versionでARNで指定したポリシーの最新Versionの詳細を取得します。
以下は取得できる例です。

この後中にある「Document」に対して更新をかけるので、ここで更新対象のポリシーの情報をまとめて取得しています。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/get_policy_version.html

```Json
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "RestrictBranches",
                    "Effect": "Deny",
                    "Action": [
                        "codecommit:GitPush",
                        "codecommit:DeleteBranch",
                        "codecommit:PutFile",
                        "codecommit:DeleteFile",
                        "codecommit:MergeBranchesByFastForward",
                        "codecommit:MergeBranchesBySquash",
                        "codecommit:MergeBranchesByThreeWay",
                        "codecommit:MergePullRequestByFastForward",
                        "codecommit:MergePullRequestBySquash",
                        "codecommit:MergePullRequestByThreeWay"
                    ],
                    "Resource": "arn:aws:codecommit:*:*:test",
                    "Condition": {
                        "StringEqualsIfExists": {
                            "codecommit:References": [
                                "refs/heads/develop"
                            ]
                        },
                        "Null": {
                            "codecommit:References": False
                        }
                    }
                }
            ]
        },
        "VersionId": "v5",
        "IsDefaultVersion": true,
        "CreateDate": "2023-03-10T06:41:18.000000+00:00"
    },
    "ResponseMetadata": {
        "RequestId": "d097edca-8b6e-4c66-9fff-6883d15d9a47",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "d097edca-8b6e-4c66-9fff-6883d15d9a47",
            "content-type": "text/xml",
            "content-length": "2673",
            "date": "Sun, 12 Mar 2023 06:16:26 GMT"
        },
        "RetryAttempts": 0
    }
}
```

#### 新しいVersionのポリシーを、既存ポリシーをコピーして作成

ここでは、一つ上のセクションで取得したポリシーをコピーして、さらにその中の「Document」に変更を加えていきます。

```python

def get_new_policy_document(old_document, branch_name):
    new_document = copy.deepcopy(old_document)
    new_document["Statement"][0]["Condition"]["StringEquals"]["codecommit:References"].append(f'refs/heads/{branch_name}')
    return(new_document)

 policy_version = iam.create_policy_version(
        PolicyArn=policy_arn,
        PolicyDocument=json.dumps(get_new_policy_document(policy["PolicyVersion"]["Document"], branch_name)),
        SetAsDefault=True
    )
```
まずは関数の解説から
引数としては、old_documentとbranch_nameを受け取ります。

①受け取ったold_documentをコピーし、new_documentという変数に格納
➁new_document内のdocumentに引数として渡されたbranch_nameを追加
③new_documentを返す

以上で関数の解説は終了です。

#### 新しく作成されたブランチ名を、更新対象のポリシーに反映

上記で解説した関数の処理を、create_policy_versionの中で実施します。
create_policy_versionの詳細は以下より確認できます。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/create_policy_version.html

必要な引数としては以下になります。

```javascript
response = client.create_policy_version(
    PolicyArn='string',
    PolicyDocument='string',
    SetAsDefault=True|False
)
```

そのためPolicyDocumentの指定で、 get_new_policy_document(old_document, branch_name)関数で取得したnew_documentをjson形式に変換しています。
補足すると、この関数の中で引数として渡しているのは、
①policy = version = response['Versions'][0]['VersionId']
    policy = iam.get_policy_version(PolicyArn=policy_arn, VersionId=latest_version)

➁branch_name = ref.split('/')[-1]

になります。
はい、これで**新しいブランチが作成されるたび、そのブランチ名をポリシーに自動追加する**
が実装できました。

ただしこれではIAMポリシーのversionが無限に増え続けてしまうので、以下の記述で最も古いIAMポリシーを削除します。

```python
    iam.delete_policy_version(
        PolicyArn=policy_arn, 
        VersionId=response['Versions'][-1]['VersionId']
        )
```

以上コードの解説を終了します。

<a id="#reference"></a>
# 終わりに
頑張ってコードを書いたのですが、所定のブランチに対してアクセス権限を管理したいのであれば
①PowerUserポリシーをアタッチ
➁アクセスを制限したい箇所に対して、denyをかける

で実装できたので、なんというか空回りしてしまった感じがあります。
とはいえ、Boto3のよい勉強になったので結果的にはよかったかと思います。