The post introduced a solution for automating AWS Identity Center groups to AWS accounts and
permission set assignments for groups created within AWS Identity Center from an external Identity Provider.
This post assumes a familiarity with the AWS IAM service and concepts such as roles and policies.
AWS multi-account environments are commonly set up with central identity and access control instead of doing this on a per-individual account basis. AWS offers the IAM Identity Center (formerly Single Sign On) service for centralized access control, permissions, and access to AWS accounts.
Identity Center supports bringing an organization's existing users and groups into the AWS environment. IAM's documentation refers to this as using an external identity provider (IdP) to federate users and groups with Identity Center.
Within the Identity Center permissions are managed via permission sets, which in turn are a collection of IAM policies. Once a permission set has been assigned for a user or group to an account, Identity Center will automatically create IAM roles in the account. The role's policies are configured from the permission set. In addition, the role has a trust policy configured that allows the role to be only assumed when the user has been authenticated by the federated identity provider.
Managing AWS access control and permissions should always follow the least privilege permissions concept for any task for the environment's users. AWS environments typically are set up for role-based access control where roles have sufficient permissions setup to accomplish the required tasks. Roles in the AWS Identity Center are commonly mapped to federated groups from the IdP. Depending on the requirements for granularity this potentially results in a 1:1 relationship between an IdP's groups and the combination of the AWS account and permission set.
Let's look at a practical example.
The AWS account 'app1-prod' should be accessed by the operators of the account using three different permissions depending on the tasks at hand.
For day-to-day regular operations, most tasks should be read only and fall within the category of visibility, monitoring, and observation. One can use AWS' provided AWS-managed policy ReadOnlyAccess
and the permissions defined by the policies would get the job done.
For emergency changes to the environment and management of the IAM service within the account, the policies from AWS's managed AdministratorAccess
the policy can be used.
For any other tasks requiring interactive changes to the environment permissions defined the policies from AWS's managed PowerUserAccess
the policy can be used.
In the example scenario, the IdP would have to hold three groups, giving the group's members access to the AWS account app1-prod with the three permission sets ReadOnlyAccess
, AdministratorAccess
and PowerUserAccess
.
Creating the groups in the IdP and assignment of the permission sets to the AWS accounts for the group must be accomplished first before users from the groups can log into the AWS account.
Many organizations have automated AWS account creation since new applications and workloads need to be accommodated frequently. It is not uncommon for a workload to be deployed across several AWS accounts, each requiring an IAM Identity Center setup before being handed over to the users of the accounts.
AWS does not provide an out-of-the-box solution for automating the creation of groups and permission sets to AWS account assignments and thus frequently this step is performed manually introducing delays and additional work.
This post describes a solution using AWS serverless resources for AWS Identity Center federated group to account and permission set assignment automation. This solution assumes that the naming of the group follows a regular pattern that contains the target AWS account and permission set as part of the name of the group.
As an example, we will use the group name aws_accountname_a
. The solution implementation is built with the following assumptions that the group name encodes:
aws_
)accountname
- any nonwhite space characters between the _
characters of the whole group name)_p
to refer to for AWSPowerUserAccess
permission set) that the group should be assigned to the accountThe following diagram shows a high-level architecture of the solution.
The basic workflow of the solution is highlighted in the following steps:
The solution is implemented in AWS CloudFormation but should be fairly portable to be implemented in other Infrastructure as Code frameworks such as Terraform, etc.
The main components are the EventBridge rule to listen for the desired event, the Lambda function and its IAM permissions, and the SNS component for sending emails on failure.
The solution ships as a single CloudFormation template that should be fairly easy to deploy to the AWS account that has Identity Center enabled.
Description: AWS SSO Automation Components
Parameters:
ManagedResourcePrefix:
Type: String
InstanceArn:
Type: String
SMTPNotifyAddress:
Type: String
TopicName:
Type: String
Default: "AssigmentTopic"
Resources:
NewSSOGroupEventRule:
Type: AWS::Events::Rule
Properties:
Description: Trigger for when a new SSO Group is propagated from external IdP via SCIM
EventPattern:
source:
- aws.sso-directory
detail-type:
- AWS API Call via CloudTrail
detail:
eventSource:
- sso-directory.amazonaws.com
eventName:
- CreateGroup
Targets:
- Arn: !GetAtt SsoAssignGroupsFunction.Arn
Id: sso-assign-group-function
ExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SSOandOrgPermissions
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sso-directory:Describe*
- sso-directory:Get*
- sso-directory:List*
- sso-directory:Describe*
- sso-directory:Search*
- sso:Describe*
- sso:Get*
- sso:List*
- sso:CreateAccountAssignment
- sso:ProvisionPermissionSet
- identitystore:List*
- identitystore:Describe*
- organizations:ListAccounts
- sns:Publish
Resource: '*'
AssignmentTopic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Endpoint: !Ref SMTPNotifyAddress
Protocol: "email"
TopicName: !Ref TopicName
SsoAssignGroupsFunction:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
""" This function is intended to be a standalone Lambda function for SSO Permission Set
to AWS Account Assignment """
import re
import os
import logging
import json
from time import sleep
import boto3
import traceback
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# This function assumes that groups are named like aws_<account-name>_<a|r|p>
# see the documentation of this function for details.
CONST_PREFIX = "aws"
g_account_pattern = rf"^{CONST_PREFIX}_(\S*)_(\S*)"
PSET_NAME_MAPPING_DICT = {
"r": "AWSReadOnlyAccess",
"a": "AWSAdministratorAccess",
"p": "AWSPowerUserAccess",
}
sso_admin_client = boto3.client("sso-admin")
org_client = boto3.client("organizations")
sns_resource = boto3.resource("sns")
def list_permission_sets(sso_instance_arn) -> dict:
"""Returns a dictionary of permissionssets for a given SSO Instance."""
perm_set_dict = {}
response = sso_admin_client.list_permission_sets(InstanceArn=sso_instance_arn)
results = response["PermissionSets"]
while "NextToken" in response:
response = sso_admin_client.list_permission_sets(
InstanceArn=sso_instance_arn, NextToken=response["NextToken"]
)
results.extend(response["PermissionSets"])
for permission_set in results:
perm_description = sso_admin_client.describe_permission_set(
InstanceArn=sso_instance_arn, PermissionSetArn=permission_set
)
perm_set_dict[perm_description["PermissionSet"]["Name"]] = permission_set
return perm_set_dict
def list_aws_accounts() -> list:
"""Returns a list of account dictionaries containing name id of each account"""
account_list = []
paginator = org_client.get_paginator("list_accounts")
page_iterator = paginator.paginate()
for page in page_iterator:
for acct in page["Accounts"]:
# only add active accounts
if acct["Status"] == "ACTIVE":
data = {"name": acct["Name"], "id": acct["Id"]}
account_list.append(data)
#logger.debug("List of accounts: %s", account_list)
return account_list
def lambda_handler(event, context):
"""Main method for Lambda function, will handle the IAM Identity Center permission set to IAM Identity Center directiry group and AWS account mapping"""
logger.debug("Invoked with event: %s", event)
try:
group_display_name = event["detail"]["responseElements"]["group"]["displayName"]
if group_display_name == "":
logger.debug(
"Recieved SCIM CreateGroup event for roup name '%s'", group_display_name
)
raise Exception("Event did not contain the group display name property")
result = re.search(g_account_pattern, group_display_name)
print(result)
if not result:
logger.error(
"Security group: '%s' does not matching naming convention for account assignment. REGEX retourned matches: %s",
group_display_name, result,
)
raise Exception(
"Security group does not match convention for account assignment automation"
)
account_name = result.group(1)
short_name_pset = result.group(2) # "a" "p" or "r"
if short_name_pset not in PSET_NAME_MAPPING_DICT:
logger.error(
"Short name '%s' for permission set is not known", short_name_pset
)
raise Exception("Short name for permission set is not known")
permission_set_name = PSET_NAME_MAPPING_DICT[short_name_pset]
logger.debug(
"Searching for account '%s' and permission set '%s'",
account_name,
permission_set_name,
)
accounts = list_aws_accounts()
instance_arn = os.getenv("INSTANCE_ARN")
logger.info("IAM Identity Center Instance ARN is configured '%s'", instance_arn)
if instance_arn is None:
raise Exception("No IAM Idenity Center Instance ARN is configured.")
logger.debug("Found IAM Idenity Center instance arn '%s'", instance_arn)
permission_sets = list_permission_sets(instance_arn)
logger.debug("Permission sets found '%s'", permission_sets)
account_id, permission_set_arn, account_name = None, None, None
for account in accounts:
logger.info("Id of desired account is '%s' ", account["id"])
account_id = account.get("id")
account_name = account.get("name")
if account_id is None:
logger.error("Can't find desired account '%s'", account_name)
raise Exception("Clould not find account")
for name, arn in permission_sets.items():
if name == permission_set_name:
logger.info("ARN of desired permission set is '%s'", arn)
permission_set_arn = arn
if permission_set_arn is None:
logger.error("Can't find desired permission set %s", permission_set_arn)
raise Exception("Can't find desired permission set")
principal_id = event["detail"]["responseElements"]["group"]["groupId"]
logger.info("PrincipalId of the group is: %s", principal_id)
if principal_id is None:
logger.error("Could not retrieve the princiapal id of group %s", principal_id)
raise Exception("Could not retrieve the princial id of the group")
request = {
"InstanceArn": instance_arn,
"TargetId": account_id,
"TargetType": "AWS_ACCOUNT",
"PermissionSetArn": permission_set_arn,
"PrincipalType": "GROUP",
"PrincipalId": principal_id, #AWS IAM Identity Center group identifier
}
cracct_response = sso_admin_client.create_account_assignment(**request)
cracct_request_id = cracct_response["AccountAssignmentCreationStatus"][
"RequestId"
]
for tries in range(5):
# The docs explain the following valid status states "IN_PROGRESS"|"FAILED"|"SUCCEEDED"
ps_prov_set_status = (
sso_admin_client.describe_account_assignment_creation_status(
InstanceArn=instance_arn,
AccountAssignmentCreationRequestId=cracct_request_id,
)
)
logger.info("Assignment attempt %s", tries)
status = ps_prov_set_status["AccountAssignmentCreationStatus"]["Status"]
if status == "IN_PROGRESS":
logger.info("Assignment is in progress")
logger.info("Sleeping for 5 seconds")
sleep(5.0)
continue
if status == "FAILED":
logger.error("Account assigned has failed")
raise Exception("Account assignment has failed")
return
logger.info(
"SUCCESS: Security Group: %s assigned to Account: %s with permission set: %s",
group_display_name,
account_name,
permission_set_name,
)
except Exception as err:
message = {"Exception Details": str(err),
"event": event}
return message
sns_topic = os.getenv("SNS_TOPIC")
topic = sns_resource.Topic(sns_topic)
topic.publish(
Message=json.dumps(message),
Subject="Account Association Operation Failed: SSO Automation",
)
logger.info("Error notification sent via SNS")
Handler: 'assign_group_to_account.lambda_handler'
Role: !GetAtt ExecutionRole.Arn
Runtime: 'python3.9'
MemorySize: 128
Timeout: 900
Environment:
Variables:
SNS_TOPIC: !Ref AssignmentTopic
INSTANCE_ARN: !Ref InstanceArn
EventsFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt SsoAssignGroupsFunction.Arn
Principal: events.amazonaws.com
SourceArn: !GetAtt NewSSOGroupEventRule.Arn
`
# References
See https://docs.aws.amazon.com/singlesignon/latest/userguide/provision-automatically.html for details on how to set a external Identity Provider configuration for provisioning.