AWSマネジメントコンソールを非IAMユーザー所持者に開放する。（AD非使用、ユーザー管理不要）

#はじめに

皆さん、業務でAWSは使われているでしょうか。
AWS、インフラの構築から運用までいろいろサポートしてくれていて、とても便利ですよね。
それこそインフラ担当だけじゃなくて、CloudWatchやセッションマネージャーのような環境に接続するすべての関係者にとってメリットがある機能なんかもよくリリースされています。

#ところで （問題提起）
これらの素晴らしいAWSの機能って、インフラ担当ばっかりメリットを享受していて、コンサルタントや製品開発者にはあまり届いてこないことが多いです。
何故かというと、「マネジメントコンソール」を使える人間が限られているからです。
AWSはどんどんいい機能を出してくれるのに、それを使える人間が限られているのはとてももったいないですよね。
かといって、それらをいろんな人が使えるようにインターフェースを実装するのは大変ですし、AWSが機能拡張するたびに対応する必要があるのも格好が悪いです。

#だったら （解決案）
AWSのあらゆる機能にアクセスできる最強のインターフェース、「マネジメントコンソール」を「AWSユーザーが割り当てられていない人」にも使えるようにしたらいいんじゃないか。
ただし、ADやAWS SSOなどでアカウント管理するのは面倒なのでしたくないとか、マネージドサービスをつかうよりいろいろ改造して自社で機能追加したいというケースを想定しています。
（ADとか既にあったりして、AWS SSOでいいならAWS SSO使えばいいと思います）
これが、本記事の本題になります。

#今回使うもの

###1.Amazon Cognito
https://dev.classmethod.jp/cloud/aws/what-is-the-cognito/
Cognito は、2014年7月に発表された ユーザーのアイデンティティ をテーマにしたサービスです。「こぐにーと」と読みます。
簡単に言うと、AWS内のリソースをユーザーごとに分けて提供できる機能で、例えばAさん専用のファイルアップロード領域を作ったりできます。

###2.カスタム ID ブローカーに対する AWS コンソールへのアクセスの許可
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html#STSConsoleLink_programPython
組織のネットワークにサインインするユーザーに対して AWS マネジメントコンソール への安全なアクセスを許可するには、そのための URL を生成するコードを記述して実行できます。この URL は、AWS から取得したサインイントークンを含み、それを使って AWS に対してユーザーを認証します。

###3.Python(boto3)
boto3というモジュールを使って、pythonでAWSのAPIを操作します。
https://aws.amazon.com/jp/sdk-for-python/

#どう使うのか (概要)

「Amazon Cognito」のIDPoolを使用することで、アクセス権限を制限したポリシーを持つAWSユーザーを払い出すことができます。
これらはアクセスキーも持っているので、それを使って「カスタム ID ブローカーに対する AWS コンソールへのアクセスの許可」で提示されているログインURLを生成することができます。
これらを行うためにはAWSアカウント上にいくつかのリソースの準備が必要であるため、それらも説明していきます。

#AWSリソースの準備

###1.AWSリソース操作に使用するユーザーに、アクセス権限を追加

AmazonCognitoPowerUserポリシー、AmazonCognitoDeveloperAuthenticatedIdentitiesポリシーをアタッチしてください。

###2.IDPoolの作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540428/b8b7ff92-b7da-6000-4b97-39fefbd50bd2.png)

以下のURLから、ユーザーの情報を保管しておくIDPoolという領域を作成してください。
https://ap-northeast-1.console.aws.amazon.com/cognito/create/?region=ap-northeast-1#
1.Identity Pool Name = 好きな名前を付けましょう。

2.Unauthenticated Identitiesで匿名IDを許可するために「Enable Access to Unauthenticated Identities」にチェックを入れてください。今回、認証ロジックについては解説しないため、未認証でアクセスキーを発行します。

3.未認証ユーザーを使用する場合、基本 (クラシック) フローを使用しないと管理コンソールの使用認可を行えないので、「基本 (クラシック) フローを許可する」にチェックを入れてください。

4.プールの作成を行ってください。

5.Identify the IAM roles to use with your new identity pool のページが表示されるので詳細の表示をしてください。（ちなみに、上が認証済みID、下がゲストIDの設定）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540428/91ef525b-46c0-3b8e-db40-bde40469b28d.png)

6.新しいロールの作成をします。認証済み、未認証それぞれで権限を変えることができるので、Unauth ・Authedそれぞれを作成しましょう。ちなみに、今回Authedは使う予定がないので中身は適当でも問題ありません。

7.Unauth にReadOnlyAccessと以下をアタッチしましょう。今回はセッションマネージャーを使用可能なゲスト用ロールにしてみます。

```json


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Cognito",
            "Effect": "Allow",
            "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "sts:GetFederationToken"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "Ssm",
            "Effect": "Allow",
            "Action": [
                "ssmmessages:CreateDataChannel",
                "s3:GetEncryptionConfiguration",
                "ssm:UpdateInstanceInformation",
                "ssmmessages:OpenDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:CreateControlChannel",
                "ssm:StartSession"
            ],
            "Resource": "*"
        }
    ]
}
		
```

8.サンプルコードのところに書いてあるIDプールのIDをメモしておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540428/9ed8f553-875c-ebdf-469a-9cc1915946e8.png)

#Pythonの実装

####1.以下のpythonのコードからcredentialの設定部分をcognitoに置き換えたlogin_test.pyを実装します。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html#STSConsoleLink_programPython

```python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#以下のように実行します。
#python3 login_test.py user_name

import urllib, json, sys
import requests
import boto3
import datetime

_args = sys.argv
_USER                       = _args[1]
_AWS_ACCOUNT                = "123456789012"
_CONV_AWS_ACCESS_KEY_ID     = "ASIA*******:"
_CONV_AWS_SECRET_ACCESS_KEY = "**********"
_CONV_AWS_SESSION_TOKEN     = "*********"
_REGION_NAME                = "ap-northeast-1"
_ID_POOL_ID                 = "手順8でメモしておいたものを使用します。"
_ARN                        = "arn:aws:cognito-identity:ap-northeast-1:123456789012:identitypool/ap-northeast-1:*******"
_ROLE_ARN                   = "arn:aws:iam::123456789012:role/Unauth"

class TestClass():
    def cognito_auth(self):
        try:
            #cognitを操作するためのclient作成
            cognito_client = boto3.client('cognito-identity',
                region_name           = _REGION_NAME,
                aws_access_key_id     = _CONV_AWS_ACCESS_KEY_ID,
                aws_secret_access_key = _CONV_AWS_SECRET_ACCESS_KEY,
                aws_session_token     = _CONV_AWS_SESSION_TOKEN #MFAを使用していないなら不要
            )
            #ID取得
            user_id = cognito_client.get_id(
                AccountId      = _AWS_ACCOUNT,
                IdentityPoolId = _ID_POOL_ID
            )["IdentityId"]
            print("Cognito ID:"+user_id)
        except:
            raise Exception("get id faild.")

        try:
            #Role設定確認
            roles = cognito_client.get_identity_pool_roles(IdentityPoolId=_ID_POOL_ID)["Roles"]["unauthenticated"]
            print("Use role:"+roles)

            # GetOpenIdToken + AssumeRoleWithWebIdenity
            token = cognito_client.get_open_id_token(
                IdentityId=user_id
            )

            sts_client = boto3.client('sts',
                region_name           = _REGION_NAME,
                aws_access_key_id     = _CONV_AWS_ACCESS_KEY_ID,
                aws_secret_access_key = _CONV_AWS_SECRET_ACCESS_KEY,
                aws_session_token     = _CONV_AWS_SESSION_TOKEN #MFAを使用していないなら不要
            )

            d_today = str(datetime.date.today())
            credentials_for_identity = sts_client.assume_role_with_web_identity(
                RoleArn = _ROLE_ARN,
                RoleSessionName = _USER + "-" + d_today,#ログの頭に付く文字列　誰のセッションか識別できる情報を入れよう。
                WebIdentityToken = token["Token"]
            )

            AccessKeyId = credentials_for_identity["Credentials"]["AccessKeyId"]
            SecretKey = credentials_for_identity["Credentials"]["SecretAccessKey"]
            SessionToken = credentials_for_identity["Credentials"]["SessionToken"]
        except:
            #id削除
            #失敗したときは無駄に作成したidは消しておこう。
            del_response = cognito_client.delete_identities(
                IdentityIdsToDelete=[
                user_id
                ]
            )
            raise Exception("cognito_auth faild.","delete id :"+str(del_response["ResponseMetadata"]["RequestId"]))

        url_credentials = {}
        url_credentials['sessionId'] = AccessKeyId
        url_credentials['sessionKey'] = SecretKey
        url_credentials['sessionToken'] = SessionToken
        json_string_with_temp_credentials = json.dumps(url_credentials)

        request_parameters = "?Action=getSigninToken"
        request_parameters += "&SessionDuration=43200"
        if sys.version_info[0] < 3:
            def quote_plus_function(s):
                return urllib.quote_plus(s)
        else:
            def quote_plus_function(s):
                return urllib.parse.quote_plus(s)
        request_parameters += "&Session=" + quote_plus_function(json_string_with_temp_credentials)
        request_url = "https://signin.aws.amazon.com/federation" + request_parameters
        r = requests.get(request_url)
        # Returns a JSON document with a single element named SigninToken.
        signin_token = json.loads(r.text)

        # Step 5: Create URL where users can use the sign-in token to sign in to
        # the console. This URL must be used within 15 minutes after the
        # sign-in token was issued.
        request_parameters = "?Action=login"
        request_parameters += "&Issuer=Example.org"
        request_parameters += "&Destination=" + quote_plus_function("https://console.aws.amazon.com/")
        request_parameters += "&SigninToken=" + signin_token["SigninToken"]
        request_url = "https://signin.aws.amazon.com/federation" + request_parameters

        # Send final URL to stdout
        print (request_url)

    def main(self):
        print("success login! welcome "+_USER)
        self.cognito_auth()

if __name__ == "__main__":
    TC = TestClass()
    TC.main()

```

####2.login_test.pyを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540428/fdf220cc-dcf6-dac8-2607-1e9bad82f2ac.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540428/f67b95ed-824b-ba7b-d8a2-9396ae31720d.png)

login_test.pyは、実行されるまでにユーザーが認証を通したという前提で実装されています。
なので、これ単体でたたくとそれだけでアカウントが発行され、とても長いログイン用のURLが発行されます。
そのURLにアクセスすると、マネジメントコンソールの画面にリダイレクトされ、フェデレーテッドログインに成功します。
権限はReadOnlyに加え、セッションマネージャーも許可しているので、EC2からどのインスタンスが立っているか確認したり、セッションマネージャーを使用して環境にアクセスしたりすることができます。

#追記
類似の手法として、AWS SSOがあります。
こちらはADなどと連携して社員にSSOを提供することができるサービスで、単純に社員にマネジメントコンソールを提供したいだけならAWS SSOを使用するのが楽です。

今回の手法のメリットは、現状AWS単体では対応できていない（AWS SSOではcliによるリソース操作がほとんどサポートされていないため）「アクセス可能アカウントを申請内容から自動で仕分ける」などの高度な制御を挟み込む余地を作ったり、IDプールを使用することでユーザー管理をせずにより広範囲にマネジメントコンソールを提供できることがあげられます。
（実は書いた当時はAWS SSOの存在を知らなかったので頑張ってしまっただけ）

Azuru AD＋AWS SSO
https://blog.cybozu.io/entry/2019/10/18/080000

#おわりに

今回は、AWSマネジメントコンソールに対してAWSユーザーを発行されていないユーザーがログインできるようにしました。
この仕組みを使えば、IAMで個別にアカウントを管理して、退職者が出たりするたびにメンテナンスする必要もなく、お手軽にマネジメントコンソールを提供することができます。
マネジメントコンソールはAWSが勝手に強化してくれるので、我々の開発工数をかけずに業務効率化を継続して行えるようにもなります。
とても便利ですので、ぜひ一度お試しください。

私もAWS初心者（クラウドプラクティショナー程度）であるため、ご指摘・ご質問・ご感想はどのようなものでもありがたくいただく所存です。
何かございましたら、ぜひコメントを頂ければと存じます。

以上、お疲れ様でした。