Authenticating...
Skip to main content

AWS Identity Center — Groups, Users & Permission Sets

Overview

This runbook covers adding new AWS IAM Identity Center (IdC) Groups, Users, and Permission Sets, plus assigning Permission Sets to Groups and Accounts, using our IaC repos. All changes are CDK (TypeScript) and deploy exclusively via GitHub Actions on release — manual cdk deploy is forbidden in both repos.

Repo map

RepoOwnsIaC toolDeploys to
AdAction/aws-organization-managementOrg/OU structure, accounts, the IdC instance itself, Groups, UsersCDK (TS) + ProjenManagement account 172122050326, us-east-2
AdAction/aws-ssoPermission Sets, Group → PermissionSet → Account assignmentsCDK (TS)Management account 172122050326, us-east-2
AdAction/aws-cdk-identity-center-usersReusable "adopt-or-create" User construct (a dependency, not edited for day-to-day changes)CDK construct library (npm package)N/A (library)
Deploy order matters

aws-organization-management exports Account/Group/User IDs and the IdC instance ARN via CloudFormation; aws-sso imports those with Fn.importValue. If you're adding a new account or group, aws-organization-management must merge, release, and deploy before aws-sso can reference it — the export has to exist first.

Prerequisites

  • Push access to AdAction/aws-organization-management and AdAction/aws-sso, and a GitHub PAT with read:packages scope configured in ~/.npmrc (see .npmrc.example in aws-organization-management) — a plain npm install 404s on the @adaction/* scoped packages without it.
  • Node/npm matching each repo's .nvmrc / engines.
  • An AWS CLI profile for the management account (172122050326) if you want to run cdk diff locally — actual deploys happen only through CI.
  • At least one approving PR reviewer. Neither repo enforces a stricter CODEOWNERS gate for stack code today, just the generic ≥1-approval branch protection rule.

Add a new Group

1. Add the group

In aws-organization-management, edit src/data/org-inventory.ts and append (never reorder existing entries) a new entry to GROUPS:

{ displayName: '<new-group-name>' },

Omit groupId for a brand-new group. TeamsStack.createGroups() builds a CfnGroup for every entry in GROUPS and passes only displayName, so CDK creates the group either way. The groupId field exists purely for cross-referencing from IMPORTED_MEMBERSHIPS and elsewhere, and is present for groups that already existed when CDK adopted them via cdk import.

2. Backfill the group ID after deploy

Once the stack has deployed, look up the real group ID from the CloudFormation export named adaction-<display-name>-GroupId (underscores and spaces in the display name are replaced with hyphens) and fill it into the GROUPS entry in a follow-up PR. Keeping GROUPS as the single source of truth for group identities is what lets downstream call sites reference the group without hardcoding an ID.

3. Add initial memberships

Still in org-inventory.ts, append rows to IMPORTED_MEMBERSHIPS referencing the group's groupId / groupDisplayName and each member's userName. A membership that references a user with no corresponding construct throws at synth time with an explicit error telling you to add the user to IMPORTED_USERS or buildNewUsers — so a typo here is a build failure, not a silent no-op.

4. Verify, PR, deploy

Run npm run build && npm test && npx cdk diff against the management profile — confirm the diff shows a Create (not a failed import) for the new CfnGroup. Open a PR with a Conventional Commits title (feat, fix, docsnot chore). On merge, semantic-release cuts a release, which triggers deploy-iac.yml and deploys to the management account.

5. Wire it into aws-sso if it needs any Permission Sets

A group with no assignment throws at synth time in aws-sso — see Assign a Permission Set to a Group or Account. This is mandatory, not optional, once the group exists.

Add a new User

1. Add the user

In aws-organization-management/src/stacks/engineers-stack.ts, append an entry to the newUsers array inside buildNewUsers():

{
userName: 'newperson@adaction.com',
displayName: 'New Person',
givenName: 'New',
familyName: 'Person',
email: 'newperson@adaction.com',
emailType: 'work',
emailPrimary: true,
},

Users that already existed before CDK adopted them live in IMPORTED_USERS in src/data/org-inventory.ts instead — don't add a new person there.

2. Add group memberships

Append rows to IMPORTED_MEMBERSHIPS in src/data/org-inventory.ts, referencing the group's existing groupId / groupDisplayName and the new user's userName. This is the only place group membership gets set — permission sets are never assigned per-user, only per-group.

3. Verify, PR, deploy

Same as the Group flow: npm run build && npm test && npx cdk diff, open a PR, merge, let semantic-release and CI deploy.

Access comes from group membership

Permission Sets attach to groups, never to anything set directly on the user. A new user with no group memberships can sign in to Identity Center but won't have permissions to anything.

Add a new Permission Set

1. Define the policy

In aws-sso/src/updated-permission-sets-stack.ts, add a build*PermissionSets() private method following the existing pattern (e.g. buildWebApplicationAccessPermissionSets). Use AWS managed policy ARNs for the common case, and reach for a builder class in src/policies/ for anything needing an inline policy.

this.permissionSets['NewPermissionSetName'] = buildPermissionSet(
this, this.instanceArn, 'NewPermissionSetName', 'NewPermissionSetName',
['arn:aws:iam::aws:policy/SomeManagedPolicy'],
[someBuilder.buildSomePolicy()],
);
Repo guardrail: 10 managed policies per permission set

The 10-policy limit is our own guardrail, enforced by a Jest test in test/updated-permission-sets.test.ts (MAX_MANAGED_POLICIES_PER_PERMISSION_SET), not an AWS hard cap. The AWS picture:

  • IAM Identity Center allows up to 25 AWS managed + customer managed policies per permission set, but the effective ceiling is IAM's Managed policies attached to an IAM role quota in each target account — a permission set is provisioned as an IAM role there. Raising it above the account's default requires a Service Quotas increase in every account the permission set deploys to (max 25).
  • Inline policies are a separate quota and don't count against the managed-policy limit: one inline policy document per permission set, max 32,768 bytes, of which at most 10,240 bytes may be non-whitespace.

Keep the guardrail at 10 unless the quota has actually been raised everywhere the permission set lands — otherwise provisioning fails per-account, after merge.

2. Register it

Call your new method from the stack constructor, alongside the existing build*PermissionSets() calls. Exporting is automatic via exportPermissionSets() — no manual export needed. Then add a matching importPermissionSet({ name: '...' }) entry to importPermissionSets() in src/import-utils.ts so the assignment stack can see it.

3. Verify, PR, deploy

npm run build && npm test && npx cdk diff, PR (Conventional Commit title, ≥1 approval), merge, release, CI deploy via deploy.yml.

What a new Permission Set grants depends on the group. Non-platform groups need an explicit assignment in updated-assignment-stack.ts before anyone gets it — covered next. The platform group gets it automatically: assignPlatformPermissions() loops over every imported permission set and attaches it to the platform group in every imported account, so the importPermissionSets() entry from step 2 is all it takes.

Review the policy and the target accounts before merging

Because of that platform auto-grant, merging a new Permission Set immediately grants its policies to the entire platform group across all imported accounts, production included. Review the policy document and the account list in importAccounts() at PR time — there is no separate approval step between merge and deploy.

Assign a Permission Set to a Group or Account

1. New account? Import it first

If the target account is new, add this.importAccount({ name: '<account-name>' }) to importAccounts() in aws-sso/src/import-utils.ts. The account must already have a CloudFormation export from aws-organization-management — the deploy-order caveat above applies. Accounts named sandbox-* auto-route to SandboxAssignmentStack; anything else needing sandbox treatment needs an explicit entry in isSandboxAccount().

2. Wire the assignment

In src/updated-assignment-stack.ts, either extend an existing assign<TeamName>Permissions() method to include the new account/permission set, or add a new one following the pattern:

private assignPlatformPermissions(platformGroup: Group) {
const platformGroupAssignment = [{ name: platformGroup.name, arn: `${platformGroup.id}` }];
Object.values(this.importedPermissionSets).forEach((permissionSet) => {
common.attachPermissionSetToGroupsAndAccountsByArn(
this, permissionSet.arn, permissionSet.name,
this.accounts, platformGroupAssignment, this.instanceArn, this.defaultRemovalPolicy,
);
});
}
Every group needs an explicit assignment case

The platform group gets every permission set on every account automatically — no action needed there. Every other group must be wired explicitly: an unmapped imported group throws at synth time (a mandatory switch statement in assignPermissionSets() enforces this), so a group with no assignment case is a build failure, not a silent no-op.

3. Watch the per-account ceiling

Every assignment provisions a separate IAM role (AWSReservedSSO_<PermissionSet>_<id>) in the target account, so the managed-policy limit applies per role and does not accumulate as you fan a permission set out to more accounts. What fan-out does consume is per-account resource counts: provisioned permission sets per account (AWS default 500, adjustable) and IAM roles per account. Sandbox accounts accumulate the most roles, since platform fans every permission set out to all of them.

4. Verify, PR, deploy

Same flow: npm run build && npm test && npx cdk diff, PR, merge, release, CI deploy.

CI/CD & deploy pipeline

Stageaws-ssoaws-organization-management
PR title lintConventional Commits (no chore)Conventional Commits (no chore)
Merge gate≥1 approval (branch protection)≥1 approval + Mergify automerge-on-approval (squash, delete branch)
On mergesemantic-release → GitHub Release + CHANGELOGsemantic-release via AdAction/shared-workflows → GitHub Release
On releasedeploy.yml: assumes GithubOIDC_aws-sso role, cdk deploy --all --require-approval never, emits Datadog DORA eventdeploy-iac.ymlAdAction/shared-workflows/deploy-iac.yml: parallel deploy_org_iac (--all) and deploy_cognito_iac jobs, via GithubOidcRole
danger
Never run cdk deploy by hand

Manual cdk deploy is explicitly forbidden in both repos' CLAUDE.md files — let the pipeline deploy. Use cdk diff locally for sanity-checking only.

Open questions

  • Neither repo requires a Platform-team reviewer for identity changes. aws-organization-management has no CODEOWNERS file, and aws-sso's only scopes .github/workflows/ and package*.json. Worth deciding whether Group/User/Permission Set changes should require a Platform reviewer specifically, since nothing enforces that beyond convention today.