【AWS】Boto3で攻略！ IAMユーザのすべて

### はじめに

Boto3を使ってAWSコンソールのIAMユーザ一覧に表示されている情報取得をやっていく。
IAMユーザの情報がほしい時の参考に。

Boto3とは、PythonでAWSの各種サービスを簡単に扱えるようにするライブラリ。
今回はこちら:point_down:のBoto3 IAMを参照します。

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

### 構成
構成はこちら。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/7a6fe10e-4c27-f2e3-d2e2-57a93ae61228.png)

API Gatewayを使ったのは、Lambdaを実行する際にアクセス制限をしたかったからです。
下図は、API GatewayのリソースポリシーでIPアドレス制限を入れた例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/0cede244-345d-868e-8a2d-36f31b5dbb47.png)

LambdaでBoto3を使います。
Boto3を使うためにLambdaの先頭でimportします。

```python:lambda_function.py
import boto3

iam_client = boto3.client('iam')
```

また、Lambdaのアクセス権限で付与されているロールに「IAMReadOnlyAccess」を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/3b19c521-b74f-fd59-7ae2-f1d053e97c3c.png)


それではやっていきましょう！

### IAMユーザのすべて

AWSコンソールのIAMユーザ一覧です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/18c7029e-d668-091b-7e98-7bdbe3264f4a.png)

IAMユーザ一覧の右上の :gear: マークをクリックすると一覧の表示項目がわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/6025b5bb-1ed8-35ed-8f33-0f7297a26736.png)

それぞれの項目の値を取得していきます。

#### :o:　ユーザ名

* 「ユーザ名」はBoto3のメソッド「list_users」で取得した「Users」の「UserName」

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["ユーザ名"] = user["UserName"]
```

#### :o:　パス

* 「パス」はBoto3のメソッド「list_users」で取得した「Users」の「Path」

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["パス"] = user["Path"]
```

#### :o:　グループ

* 「グループ」はBoto3のメソッド「list_groups_for_user」で取得した「Groups」
* UserNameに「list_users」で取得した「Users」の「UserName」を指定
* IAMユーザ一覧ではIMAユーザが所属するグループの数が表示されている（lenで取得）

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["グループ"] = len(iam_client.list_groups_for_user(UserName=user["UserName"])["Groups"])
```
* 実際に所属するグループ名を取得したければ、グループのリストにappend

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    jsonObj["グループ"] = []
    for user in response["Users"]:
        groups = iam_client.list_groups_for_user(UserName=user["UserName"])["Groups"]
        for group in groups:
            jsonObj["グループ"].append(group["GroupName"])
```

#### :o:　最後のアクティビティ

* 「最後のアクティビティ」は「コンソールを通じたアクセス」と「アクティブなアクセスキーによるプログラムによるアクセス」の日付の若い方

（IAMユーザ一覧の「最後のアクティビティ」の項目をクリックすると以下の表示）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/eb548122-38a4-42af-c99d-d9a115e81e34.png)

* 「コンソールを通じたアクセス」はBoto3のメソッド「list_users」で取得した「PasswordLastUsed」
* IAMユーザ一覧では「x日前」表示のため、現在からの差分で取得
※日付の差分はtimezoneを合わせないとエラーになるので注意。
* あとで「アクティブなアクセスキーによるプログラムによるアクセス」の日付と比較するためリストにappend

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    from datetime import datetime, timezone
    
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["最後のアクティビティ"] = []
        if user.get("PasswordLastUsed"):
            jsonObj["最後のアクティビティ"].append(str((datetime.now(timezone.utc) - user["PasswordLastUsed"]).days) + "日前")
```

* 「アクティブなアクセスキーによるプログラムによるアクセス」はBoto3のメソッド「list_access_keys」で取得した「AccessKeyMetadata」のうち、Activeなキーが最後に使用された日付（LastUsedDate）
* UserNameに「list_users」で取得した「Users」の「UserName」を指定
* アクセスキーは1 IAMユーザにつき最大2つ生成できるためfor文で取得
* IAMユーザ一覧では「x日前」表示のため、現在からの差分で取得
※日付の差分はtimezoneを合わせないとエラーになるので注意。
* あとで「コンソールを通じたアクセス」の日付と比較するためリストにappend

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    from datetime import datetime, timezone
    
    response = iam_client.list_users()
    for user in response["Users"]:
        accesskeymetadatas = iam_client.list_access_keys(UserName=user["UserName"])["AccessKeyMetadata"]
        for accesskeymetadata in accesskeymetadatas:
            if accesskeymetadata["Status"] == "Active":
                if accesskeylastused.get("LastUsedDate"):
                    jsonObj["最後のアクティビティ"].append(str((datetime.now(timezone.utc) - accesskeylastused["LastUsedDate"]).days) + "日前")
```

* ここで「コンソールを通じたアクセス」と「アクティブなアクセスキーによるプログラムによるアクセス」の日付の若い方を取得（minで取得）
* そもそも「最後のアクティビティ」がない場合は「-」を表示

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    if len(jsonObj["最後のアクティビティ"]) != 0:
        jsonObj["最後のアクティビティ"] = min(jsonObj["最後のアクティビティ"])
    else:
        jsonObj["最後のアクティビティ"] = "-"
```

#### :o:　MFA

* 「MFA」はBoto3のメソッド「list_mfa_devices」で取得した「MFADevices」
* UserNameに「list_users」で取得した「Users」の「UserName」を指定
* MFAデバイスも複数設定可能のためfor文で取得
* IAMユーザ一覧では「仮想（x）」表示であるが、詳細を取得したければリストにappend

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["MFA"] = []
        mfadevices = iam_client.list_mfa_devices(UserName=user["UserName"])["MFADevices"]
        for mfadevice in mfadevices:
            jsonObj["MFA"].append(mfadevice["SerialNumber"])
```

#### :o:　パスワードが作成されてから経過した期間

* 「パスワードが作成されてから経過した期間」はBoto3のメソッド「get_login_profile」で取得した「LoginProfile」の「CreateDate」　もし値がなければ「Users」で取得した「CreateDate」
つまり、LoginProfileが生成された日か、そうでなければIAMユーザが作成された日
* LoginProfileはAWSコンソールログインを有効にした場合に生成
* UserNameに「list_users」で取得した「Users」の「UserName」を指定
* IAMユーザ一覧では「x日前」表示のため、現在からの差分で取得
※日付の差分はtimezoneを合わせないとエラーになるので注意。
* **実データと比較すると一致しないケースがあるため別ロジックかもしれません。
正確にはIAMユーザの認証情報レポートのpasswrd_last_changedから算出？**

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    from datetime import datetime, timezone
        
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["パスワードが作成されてから経過した期間"] = "-"
        loginprofile = iam_client.get_login_profile(UserName=user["UserName"])['LoginProfile']
        if loginprofile.get("CreateDate"):
            jsonObj["パスワードが作成されてから経過した期間"] = str((datetime.now(timezone.utc) - loginprofile.get("CreateDate")).days) + "日前"
        else:
            jsonObj["パスワードが作成されてから経過した期間"] = str((datetime.now(timezone.utc) - user.get("CreateDate")).days) + "日前"
```

#### :o:　コンソールの最終サインイン

* 「コンソールの最終サインイン」はBoto3のメソッド「list_users」で取得した「Users」の「PasswordLastUsed」
* 日本時間で表示するためtimedeltaで+9時間

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    from datetime import timedelta

    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["コンソールの最終サインイン"] = "-"
        if user.get("PasswordLastUsed"):
            jsonObj["コンソールの最終サインイン"] = getTimeFromUser(user, "PasswordLastUsed")

    def getTimeFromUser(user, key):
      try:
          return str((user[key] + timedelta(hours=9)).strftime('%Y/%m/%d %H:%M:%S'))
      except (KeyError):
          return "None"
```

#### :o:　アクセスキーID、アクティブなキーが作成されてから経過した期間、最後に使用したアクセスキー

* 「アクセスキーID」はBoto3のメソッド「list_access_keys」で取得した「AccessKeyMetadata」の「AccessKeyId」
* 「アクティブなキーが作成されてから経過した期間」はアクセスキーIDのStatusがActiveなもので「CreateDate」の日付
* 「最後に使用したアクセスキー」はBoto3メソッド「get_access_key_last_used」で取得した「AccessKeyLastUsed」の「LastUsedDate」
* UserNameに「list_users」で取得した「Users」の「UserName」を指定
* IAMユーザ一覧では「x日前」表示のため、現在からの差分で取得
※日付の差分はtimezoneを合わせないとエラーになるので注意。
* アクセスキーは1 IAMユーザにつき最大2つ生成できるためfor文で取得

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    from datetime import datetime, timezone

    response = iam_client.list_users()
    for user in response["Users"]:
        accesskeymetadatas = iam_client.list_access_keys(UserName=user["UserName"])["AccessKeyMetadata"]
        for accesskeymetadata in accesskeymetadatas:
            jsonObj["アクセスキーID"].append(accesskeymetadata["Status"] + " - " + accesskeymetadata["AccessKeyId"])
            if accesskeymetadata["Status"] == "Active":
                jsonObj["アクティブなキーが作成されてから経過した期間"].append(str((datetime.now(timezone.utc) - accesskeymetadata["CreateDate"]).days) + "日前")
                accesskeylastused = iam_client.get_access_key_last_used(AccessKeyId=accesskeymetadata["AccessKeyId"])["AccessKeyLastUsed"]
                if accesskeylastused.get("LastUsedDate"):
                    jsonObj["最後に使用したアクセスキー"].append(str((datetime.now(timezone.utc) - accesskeylastused["LastUsedDate"]).days) + "日前")
```

#### :o:　ARN

* 「ARN」はBoto3のメソッド「list_users」で取得した「Users」の「Arn」

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["Arn"] = user["Arn"]
```

#### :o:　作成時刻

* 「作成時刻」はBoto3のメソッド「list_users」で取得した「Users」の「CreateDate」
* IAMユーザ一覧では「x日前」表示のため、現在からの差分で取得
※日付の差分はtimezoneを合わせないとエラーになるので注意。

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    from datetime import datetime, timezone

    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["作成時刻"] = str((datetime.now(timezone.utc) - user["CreateDate"]).days) + "日前"
```

#### :o:　コンソールを通じたアクセス

* 「コンソールを通じたアクセス」はBoto3のメソッド「get_login_profile」で取得した「LoginProfile」の「CreateDate」の有無
* IAMユーザ一覧では「有効」の表示のため、「CreateDate」があれば「有効」、それ以外は「無効」
* UserNameに「list_users」で取得した「Users」の「UserName」を指定

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    for user in response["Users"]:
        jsonObj["コンソールを通じたアクセス"] = "-"
        loginprofile = iam_client.get_login_profile(UserName=user["UserName"])['LoginProfile']
        if loginprofile.get("CreateDate"):
            jsonObj["コンソールを通じたアクセス"] = "有効"
        else:
            jsonObj["コンソールを通じたアクセス"] = "無効"
```

#### :o:　署名証明書

* 「署名証明書」はBoto3のメソッド「list_signing_certificates」で取得した「Certificates」
* UserNameに「list_users」で取得した「Users」の「UserName」を指定
* 複数の証明書が考えられるためリストにappend

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    response = iam_client.list_users()
    for user in response["Users"]:
        jjsonObj["署名証明書"] = []
        listsigningcertificates = iam_client.list_signing_certificates(UserName=user["UserName"])["Certificates"]
        for listsigningcertificate in listsigningcertificates:
            jsonObj["署名証明書"].append(listsigningcertificate["Status"])
```

仕上げとして
Lambdaで取得したIAMユーザの情報をjsonからhtmlに変換する。
htmlに変換するためpandasを利用。
pandasを利用するためのLambdaの設定方法はこちら:point_down:が参考になります。

https://qiita.com/polarbear08/items/202752d5ffcb65595bd9

jsonデータをhtmlに変換して返す。

```python:lambda_function.py
    # プログラムは必要な個所のみ記載
    df = pd.DataFrame(jsonObj)
    html_table = df.to_html(index=False, justify='center')
    with open('/tmp/table.html', 'w', encoding='utf-8') as file:
        file.write(html_table)
    print(html_table)

    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'text/html; charset=UTF-8'
        },
        'body': html_table
    }
```

ブラウザで確認するとこんな感じ。（マスクしてるためほとんどわからない:sweat_smile:）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/458ac695-7c8b-1cb4-731f-c5fc29c5f65c.png)

<details><summary>　LambdaのPythonコード全文です。</summary>

```python:lambda_function.py
import json
import boto3
from datetime import datetime, timedelta, timezone
import pandas as pd

iam_client = boto3.client('iam')

def lambda_handler(event, context):
    response = iam_client.list_users()
    jsonObj = [""] * len(response["Users"])
    i = 0
    for user in response["Users"]:
        jsonObj[i] = {}

        jsonObj[i]["ユーザ名"] = user["UserName"]

        jsonObj[i]["パス"] = user["Path"]

        # jsonObj[i]["グループ"] = len(iam_client.list_groups_for_user(UserName=user["UserName"])["Groups"])
        jsonObj[i]["Groups"] = []
        groups = iam_client.list_groups_for_user(UserName=user["UserName"])["Groups"]
        for group in groups:
            jsonObj[i]["Groups"].append(group["GroupName"])

        # 最後のアクティビティは「コンソールログイン」と「最後に使用したアクセスキー」の日付の若い方
        jsonObj[i]["最後のアクティビティ"] = []
        if user.get("PasswordLastUsed"):
            jsonObj[i]["最後のアクティビティ"].append(str((datetime.now(timezone.utc) - user["PasswordLastUsed"]).days) + "日前")

        jsonObj[i]["MFA"] = []
        mfadevices = iam_client.list_mfa_devices(UserName=user["UserName"])["MFADevices"]
        for mfadevice in mfadevices:
            jsonObj[i]["MFA"].append(mfadevice["SerialNumber"])

        # 「パスワードが作成されてから経過した期間」は、「get_userのPasswordLastUsed」もし値がなければ、「get_userのCreateDate」
        # 正確にはcredential_reportのpasswrd_last_changedから算出すると思われる。
        loginprofile = iam_client.get_login_profile(UserName=user["UserName"])['LoginProfile']
        jsonObj[i]["パスワードが作成されてから経過した期間"] = "-"
        if loginprofile.get("CreateDate"):
            jsonObj[i]["パスワードが作成されてから経過した期間"] = str((datetime.now(timezone.utc) - loginprofile.get("CreateDate")).days) + "日前"
        else:
            jsonObj[i]["パスワードが作成されてから経過した期間"] = str((datetime.now(timezone.utc) - user.get("CreateDate")).days) + "日前"

        jsonObj[i]["コンソールの最終サインイン"] = "-"
        if user.get("PasswordLastUsed"):
            jsonObj[i]["コンソールの最終サインイン"] = getTimeFromUser(user, "PasswordLastUsed")

        jsonObj[i]["アクセスキーID"] = []
        jsonObj[i]["アクティブなキーが作成されてから経過した期間"] = []
        jsonObj[i]["最後に使用したアクセスキー"] = []
        accesskeymetadatas = iam_client.list_access_keys(UserName=user["UserName"])["AccessKeyMetadata"]
        for accesskeymetadata in accesskeymetadatas:
            jsonObj[i]["アクセスキーID"].append(accesskeymetadata["Status"] + " - " + accesskeymetadata["AccessKeyId"])
            if accesskeymetadata["Status"] == "Active":
                jsonObj[i]["アクティブなキーが作成されてから経過した期間"].append(str((datetime.now(timezone.utc) - accesskeymetadata["CreateDate"]).days) + "日前")
                accesskeylastused = iam_client.get_access_key_last_used(AccessKeyId=accesskeymetadata["AccessKeyId"])["AccessKeyLastUsed"]
                if accesskeylastused.get("LastUsedDate"):
                    jsonObj[i]["最後に使用したアクセスキー"].append(str((datetime.now(timezone.utc) - accesskeylastused["LastUsedDate"]).days) + "日前")
                    jsonObj[i]["最後のアクティビティ"].append(str((datetime.now(timezone.utc) - accesskeylastused["LastUsedDate"]).days) + "日前")

        if len(jsonObj[i]["最後のアクティビティ"]) != 0:
            jsonObj[i]["最後のアクティビティ"] = min(jsonObj[i]["最後のアクティビティ"])
        else:
            jsonObj[i]["最後のアクティビティ"] = "-"

        jsonObj[i]["Arn"] = user["Arn"]

        jsonObj[i]["作成時刻"] = str((datetime.now(timezone.utc) - user["CreateDate"]).days) + "日前"

        # 「コンソールを通じたアクセス」はget_login_profileがcreateされていれば有効
        jsonObj[i]["コンソールを通じたアクセス"] = "-"
        if loginprofile.get("CreateDate"):
            jsonObj[i]["コンソールを通じたアクセス"] = "有効"
        else:
            jsonObj[i]["コンソールを通じたアクセス"] = "無効"
        
        jsonObj[i]["署名証明書"] = []
        listsigningcertificates = iam_client.list_signing_certificates(UserName=user["UserName"])["Certificates"]
        for listsigningcertificate in listsigningcertificates:
            jsonObj[i]["署名証明書"].append(listsigningcertificate["Status"])
        i = i + 1  

    scripts = """
                <style type="text/css">
                .design10 {
                 width: 100%;
                 text-align: center;
                 border-collapse: collapse;
                 border-spacing: 0;
                }
                .design10 th {
                 padding: 10px;
                 border-bottom: solid 4px #778ca3;
                 color: #778ca3
                }
                .design10 td {
                 padding: 10px;
                 border-bottom: solid 1px #778ca3;
                }
                </style>
            """


    df = pd.DataFrame(jsonObj)
    html_table = scripts + df.to_html(index=False, justify='center', classes='design10')
    with open('/tmp/table.html', 'w', encoding='utf-8') as file:
        file.write(html_table)
    print(html_table)

    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'text/html; charset=UTF-8'
        },
        'body': html_table
    }

def getTimeFromUser(user, key):
  try:
      return str((user[key] + timedelta(hours=9)).strftime('%Y/%m/%d %H:%M:%S'))
  except (KeyError):
      return "None"
```
</details>


### さいごに

* 今回はBoto3を使ってIAMの情報取得をしましたが、IAMの追加、更新、削除もできます。使い方はBoto3のマニュアルとにらめっこ。
* 他のサービスについてもやっていく予定です。
* 少しでもみなさんの参考になりますように！