0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jit作成

0
Last updated at Posted at 2026-02-11

簡単な承認フローのシステムを実装してみました。(未完成なので今後機能を追加する予定)

StepFunctionsのコード

{
  "Comment": "承認ワークフロー",
  "StartAt": "ResolveEmailAndLog",
  "States": {
    "ResolveEmailAndLog": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "<※LambdaのARNを指定>",
        "Payload": {
          "Email.$": "$.Email",
          "Reason.$": "$.Reason",
          "TargetAccountId.$": "$.TargetAccountId",
          "PermissionSetArn.$": "$.PermissionSetArn",
          "ProjectName.$": "$.ProjectName",
          "DurationSeconds.$": "$.DurationSeconds",
          "ExecutionId.$": "$$.Execution.Id",
          "Timestamp.$": "$$.State.EnteredTime"
        }
      },
      "ResultSelector": {
        "Email.$": "$.Payload.Email",
        "Reason.$": "$.Payload.Reason",
        "PrincipalId.$": "$.Payload.PrincipalId",
        "TargetAccountId.$": "$.Payload.TargetAccountId",
        "PermissionSetArn.$": "$.Payload.PermissionSetArn",
        "ProjectName.$": "$.Payload.ProjectName",
        "DurationSeconds.$": "$.Payload.DurationSeconds"
      },
      "ResultPath": "$",
      "Next": "SendApprovalRequest"
    },
    "SendApprovalRequest": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
      "Parameters": {
        "FunctionName": "<LambdaのARNを指定>",
        "Payload": {
          "taskToken.$": "$$.Task.Token",
          "Reason.$": "$.Reason",
          "Email.$": "$.Email",
          "PrincipalId.$": "$.PrincipalId",
          "TargetAccountId.$": "$.TargetAccountId",
          "PermissionSetArn.$": "$.PermissionSetArn",
          "ProjectName.$": "$.ProjectName",
          "DurationSeconds.$": "$.DurationSeconds"
        }
      },
      "Next": "GrantAndNotify",
      "Catch": [
        {
          "ErrorEquals": [
            "States.TaskFailed",
            "Rejected"
          ],
          "Next": "FinalizeRejection",
          "ResultPath": "$.error"
        }
      ],
      "ResultPath": "$.approvalResult"
    },
    "GrantAndNotify": {
      "Type": "Parallel",
      "Branches": [
        {
          "StartAt": "CreateAccountAssignment",
          "States": {
            "CreateAccountAssignment": {
              "Type": "Task",
              "Parameters": {
                "InstanceArn": "<※インスタンスARNを入れる>",
                "TargetId.$": "$.TargetAccountId",
                "TargetType": "AWS_ACCOUNT",
                "PermissionSetArn.$": "$.PermissionSetArn",
                "PrincipalId.$": "$.PrincipalId",
                "PrincipalType": "USER"
              },
              "Resource": "arn:aws:states:::aws-sdk:ssoadmin:createAccountAssignment",
              "End": true
            }
          }
        },
        {
          "StartAt": "NotifyApproval",
          "States": {
            "NotifyApproval": {
              "Type": "Task",
              "Resource": "arn:aws:states:::sns:publish",
              "Parameters": {
                  "TopicArn": "<※作成した申請者用TOPICのARNを入れる>",
                "Subject": "SSO アクセス承認通知",
                "Message": "申請が承認されました。SSOアクセスが付与されます。"
              },
              "End": true
            }
          }
        }
      ],
      "Next": "Wait",
      "ResultPath": "$.grantResult"
    },
    "Wait": {
      "Type": "Wait",
      "Next": "DeleteAccountAssignment",
      "SecondsPath": "$.DurationSeconds"
    },
    "DeleteAccountAssignment": {
      "Type": "Task",
      "Parameters": {
        "InstanceArn": "<※インスタンスARNを入れる>",
        "TargetId.$": "$.TargetAccountId",
        "TargetType": "AWS_ACCOUNT",
        "PermissionSetArn.$": "$.PermissionSetArn",
        "PrincipalId.$": "$.PrincipalId",
        "PrincipalType": "USER"
      },
      "Resource": "arn:aws:states:::aws-sdk:ssoadmin:deleteAccountAssignment",
      "Next": "LogRevocation",
      "ResultPath": "$.ssoDeleteResult"
    },
    "LogRevocation": {
      "Type": "Task",
      "Resource": "arn:aws:states:::dynamodb:updateItem",
      "Parameters": {
        "TableName": "Dynamoのテーブル名",
        "Key": {
          "ExecutionId": {
            "S.$": "$$.Execution.Id"
          }
        },
        "UpdateExpression": "SET #s = :status, #a = :approver",
        "ExpressionAttributeNames": {
          "#s": "Status",
          "#a": "Approver"
        },
        "ExpressionAttributeValues": {
          ":status": {
            "S": "REVOKED"
          },
          ":approver": {
            "S.$": "$.approvalResult.approver"
          }
        }
      },
      "End": true,
      "ResultPath": null
    },
    "FinalizeRejection": {
      "Type": "Parallel",
      "Branches": [
        {
          "StartAt": "LogRejection",
          "States": {
            "LogRejection": {
              "Type": "Task",
              "Resource": "arn:aws:states:::dynamodb:updateItem",
              "Parameters": {
                "TableName": "Dynamoのテーブル名",
                "Key": {
                  "ExecutionId": {
                    "S.$": "$$.Execution.Id"
                  }
                },
                "UpdateExpression": "SET #s = :status",
                "ExpressionAttributeNames": {
                  "#s": "Status"
                },
                "ExpressionAttributeValues": {
                  ":status": {
                    "S": "REJECTED"
                  }
                }
              },
              "End": true,
              "ResultPath": null
            }
          }
        },
        {
          "StartAt": "NotifyRejection",
          "States": {
            "NotifyRejection": {
              "Type": "Task",
              "Resource": "arn:aws:states:::sns:publish",
              "Parameters": {
                "TopicArn": "<※申請者用TOPICのARNを入れる>",
                "Subject": "SSO アクセス拒否通知",
                "Message": "申請が拒否されました。"
              },
              "End": true
            }
          }
        }
      ],
      "End": true
    }
  }
}

※必要なロール
・DynamoDBFullAccess
・SNS
・IDCの下記権限
"sso:CreateAccountAssignment",
"sso:DeleteAccountAssignment",
"sso:DescribeAccountAssignmentCreationStatus",
"sso:DescribeAccountAssignmentDeletionStatus"

#必要なリソース
①DynamoDB
DynamoDBテーブルの作成
証跡を保存する箱を作ります。
設定値:
テーブル名:任意
パーティションキー: ExecutionId (文字列)

②SNS-TOPICの作成
・ApproverTopic(承認者用トピック)
・ApplicantTopic(申請者用トピック)
フィルターポリシーを下記のように設定する
{
"Email": [
"example@gmail.com"
]
}
③API Gatewayの作成
Getメソッドで/requestとする

Lamdaコード(ApprovalLinkHandler)

# 承認リンク処理Lambda
import json
import boto3

sfn_client = boto3.client('stepfunctions')


def lambda_handler(event, context):
    print("Received event:", json.dumps(event))

    try:
        query_params = event.get('queryStringParameters', {})
        if not query_params:
            return {'statusCode': 400, 'body': 'パラメータが見つかりません'}

        raw_token = query_params.get('taskToken', '')
        action = query_params.get('action', '')
        approver = query_params.get('approver', '')

        if not raw_token or not action:
            return {'statusCode': 400, 'body': '必須パラメータが不足しています'}

        # API GatewayがURLデコード時にスペースを+に変換する問題の修正
        task_token = raw_token.replace(' ', '+')

        if action == 'approve':
            sfn_client.send_task_success(
                taskToken=task_token,
                output=json.dumps({'approver': approver})
            )
            return {
                'statusCode': 200,
                'headers': {'Content-Type': 'text/html'},
                'body': '<h1>承認しました</h1><p>SSOアクセスが付与されます。</p>',
            }

        elif action == 'reject':
            sfn_client.send_task_failure(
                taskToken=task_token,
                error='Rejected',
                cause='承認者により拒否されました。',
            )
            return {
                'statusCode': 200,
                'headers': {'Content-Type': 'text/html'},
                'body': '<h1>拒否しました</h1><p>申請は拒否されました。</p>',
            }

        else:
            return {'statusCode': 400, 'body': '無効なアクションです'}

    except Exception as e:
        print(f"Error: {e}")
        return {'statusCode': 500, 'body': '内部エラーが発生しました'}

Lamdaコード(SendApprovalRequest)

import json
import os
import boto3
import urllib.parse

sns_client = boto3.client('sns')
BASE_URL = os.environ['BASE_URL']
APPROVER_TOPIC_ARN= os.environ['APPROVER_TOPIC_ARN']

# 各プロジェクトごとの承認者マップ
APPROVER_MAP = {
    "Project-A": ["example@gmail.com", "test@gmail.com"],
    "Project-B": ["example@gmail.com"]
}

def lambda_handler(event, context):
    project_name = event['ProjectName']
    approvers = APPROVER_MAP[project_name]
    task_token = event['taskToken']
    encoded_token = urllib.parse.quote(task_token, safe='')

    for approver_email in approvers:
        # 承認者ごとに個別URL生成
        encoded_approver = urllib.parse.quote(approver_email, safe='')
        approve_url = f'{BASE_URL}?action=approve&taskToken={encoded_token}&approver={encoded_approver}'
        reject_url = f'{BASE_URL}?action=reject&taskToken={encoded_token}&approver={encoded_approver}'
        #下記で送信する内容を決める
        message = (
            '承認依頼が届きました。\n\n'
            f'■申請内容\n'
            f'理由: {event["Reason"]}\n'
            f'対象アカウント: {event["TargetAccountId"]}\n'
            f'申請者メール: {event["Email"]}\n'
            f'権限セット: {event["PermissionSetArn"]}\n'
            f'--------------------\n\n'
            f'▼承認する場合:\n{approve_url}\n\n'
            f'▼拒否する場合:\n{reject_url}'
        )
        sns_client.publish(
            TopicArn=APPROVER_TOPIC_ARN,
            Subject='一時権限昇格承認依頼',
            Message=message,
            MessageAttributes={
                'Email': {
                    'DataType': 'String',
                    'StringValue': approver_email
                }
            }
        )

ResolveEmailAndLog

# Email→PrincipalId解決 + 監査ログ追加
import json
import os
import boto3

identitystore_client = boto3.client('identitystore')
dynamodb_client = boto3.client('dynamodb')

IDENTITY_STORE_ID = os.environ['IDENTITY_STORE_ID']
DYNAMODB_TABLE_NAME = os.environ['DYNAMODB_TABLE_NAME']

def lambda_handler(event, context):
    email = event['Email']
    # Email→PrincipalId解決
    response = identitystore_client.get_user_id(
        IdentityStoreId=IDENTITY_STORE_ID,
        AlternateIdentifier={
            'UniqueAttribute': {
                'AttributePath': 'emails.value',
                'AttributeValue': email,
            }
        },
    )
    principal_id = response['UserId']
    # 監査ログ書き込み(ここで最初にテーブルに書き込む値を決める)
    dynamodb_client.put_item(
        TableName=DYNAMODB_TABLE_NAME,
        Item={
            'ExecutionId': {'S': event['ExecutionId']},
            'Requester': {'S': email},
            'TargetAccountId': {'S': event['TargetAccountId']},
            'PermissionSetArn': {'S': event['PermissionSetArn']},
            'Status': {'S': 'APPLIED'},
            'Timestamp': {'S': event['Timestamp']},
        },
    )
    # 下記を次のステップに渡す
    return {
        'Email': email,
        'Reason': event['Reason'],
        'PrincipalId': principal_id,
        'TargetAccountId': event['TargetAccountId'],
        'ProjectName': event['ProjectName'],
        'PermissionSetArn': event['PermissionSetArn'],
        'DurationSeconds': event['DurationSeconds'],
    }
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?