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
| Repo | Owns | IaC tool | Deploys to |
|---|---|---|---|
AdAction/aws-organization-management | Org/OU structure, accounts, the IdC instance itself, Groups, Users | CDK (TS) + Projen | Management account 172122050326, us-east-2 |
AdAction/aws-sso | Permission Sets, Group → PermissionSet → Account assignments | CDK (TS) | Management account 172122050326, us-east-2 |
AdAction/aws-cdk-identity-center-users | Reusable "adopt-or-create" User construct (a dependency, not edited for day-to-day changes) | CDK construct library (npm package) | N/A (library) |
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-managementandAdAction/aws-sso, and a GitHub PAT withread:packagesscope configured in~/.npmrc(see.npmrc.exampleinaws-organization-management) — a plainnpm install404s 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 runcdk difflocally — 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, docs — not 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.
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()],
);
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.
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,
);
});
}
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
| Stage | aws-sso | aws-organization-management |
|---|---|---|
| PR title lint | Conventional Commits (no chore) | Conventional Commits (no chore) |
| Merge gate | ≥1 approval (branch protection) | ≥1 approval + Mergify automerge-on-approval (squash, delete branch) |
| On merge | semantic-release → GitHub Release + CHANGELOG | semantic-release via AdAction/shared-workflows → GitHub Release |
| On release | deploy.yml: assumes GithubOIDC_aws-sso role, cdk deploy --all --require-approval never, emits Datadog DORA event | deploy-iac.yml → AdAction/shared-workflows/deploy-iac.yml: parallel deploy_org_iac (--all) and deploy_cognito_iac jobs, via GithubOidcRole |
cdk deploy by handManual 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-managementhas no CODEOWNERS file, andaws-sso's only scopes.github/workflows/andpackage*.json. Worth deciding whether Group/User/Permission Set changes should require a Platform reviewer specifically, since nothing enforces that beyond convention today.