簡単な承認フローのシステムを実装してみました。(未完成なので今後機能を追加する予定)
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'],
}