Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/platform-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ jobs:
continue-on-error: true
run: sh scripts/reconcile-app-auth.sh

# Grant newly registered teams their AWS access: the registry's team
# sync stops at "guardrails pending" until this apply has created
# javabin-team-<team>-guardrails. Async and idempotent; also rolls
# permission-set policy changes out to existing teams.
- name: Sync team AWS access
continue-on-error: true
run: sh scripts/sync-team-access.sh

# --------------------------------------------------------------------------
# Drift Detection — scheduled weekly, plan-only
# --------------------------------------------------------------------------
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ terraform/state/
| `scripts/upload-plan.sh` | CI: upload plan artifact to S3 |
| `scripts/auth-doctor.py` | CI (after app apply) / local: checks an app's Cognito sign-in end to end from auth-status + HTTP probes (tests: `scripts/tests/`) |
| `scripts/reconcile-app-auth.sh` | CI (after platform apply): queue an app-auth-sync reconcile |
| `scripts/sync-team-access.sh` | CI (after platform apply): queue team-provisioner `sync_team_access` (new teams' permission set + account assignment once their guardrails policy exists) |

## Naming

Expand Down Expand Up @@ -242,7 +243,9 @@ Scheduled:

EventBridge (Create/Run) ──► compliance-reporter (report to Slack, no auto-fix)
Hourly ──► override-cleanup (delete stale SSM override tokens)
Registry merge ──► team-provisioner (Google/GitHub/Budget/Cognito/Identity Center sync + member provisioning + access group sync)
Every 3 hours ──► team-provisioner sync_access_members (team members into javabin-developer@; Identity Center group memberships for members Google SCIM has provisioned since)
Registry merge ──► team-provisioner (Google/GitHub/Budget/Cognito/Identity Center sync + member provisioning + access group sync; team and kodesmia members become direct members of javabin-developer@ so Google SCIM provisions them)
Platform apply ──► team-provisioner sync_team_access (team permission sets once guardrails exist; #javabin-infra-alerts on new access or failure)
AWS Budgets (200%) ──► budget-enforcer Lambda ──► ECS scale-to-zero + #javabin-cost-alerts
EventBridge (Create/Run) ──► resource-tagger Lambda ──► Tag created-by + commit
```
Expand Down Expand Up @@ -325,6 +328,7 @@ Team group OWNERs are admin-managed and never removed by the sync; non-@java.no
### Known Issues
- **ECS deploy stabilization**: platform-test-app task registers but service fails health check
- **Cognito**: pools deployed; internal pool's `GoogleSAML` IdP was created outside Terraform. It is replaced by the Terraform-managed Google OIDC IdP at cutover (docs/cognito-google-setup.md), then deleted
- **Google SCIM provisions only direct members** of `drift@`, `styret@` and `javabin-developer@java.no` to Identity Center; it does not expand nested groups. Nobody gets an Identity Center user (and so no AWS access, team role included) unless they are a direct member of one of those. The team provisioner adds team members and (via `flatten: true` in the registry's `groups/access.yaml`) kodesmia members to `javabin-developer@` one by one; SCIM creates the user later, and the 3-hourly `sync_access_members` run adds them to their Identity Center groups. Don't create Identity Center users through the API: they'd have no link to Google (see `docs/lambda-functions.md`).
- **SSO sessions carry no `team` tag**: the Identity Center ABAC attribute (`${path:enterprise.team}`) is never set on users, so `${aws:PrincipalTag/team}` is empty for humans. Per-team permission sets (`javabin-team-{team}`) therefore use `javabin-team-{team}-guardrails` (the developer boundary rendered with the team name) instead. The ABAC-based `javabin-developer` permission set in `terraform/org` cannot reach team resources
- **App databases share one network path** (fixed by `app-network`): service-rds now allows ingress only from the app's own `{team}-{app}-tasks` SG; each app moves off the shared ECS tasks SG on its next pipeline run
- **Team provisioner Lambda**: All sync functions working (Google/GitHub/Budget/Cognito/Identity Center). Password-set flow deployed.
Expand Down
3 changes: 2 additions & 1 deletion docs/ci-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ Monday 08:00 ──► securityhub-summary Lambda ──► #javabin-infra-ale
- Outputs: `has_changes`, `plan_key`, `plan_sha256`, `risk_level`

### apply
- Only on `push` to `main` with changes
- Only on `main` (`push`, `workflow_dispatch`, or the registry's `teams-changed` `repository_dispatch`) with changes
- Checks risk level: blocks on HIGH/FAILED/UNKNOWN
- Downloads plan from S3, verifies SHA256 integrity
- `terraform apply -auto-approve tfplan`
- Then queues (async, non-blocking) an app-auth-sync reconcile (`scripts/reconcile-app-auth.sh`) and a team-provisioner `sync_team_access` run (`scripts/sync-team-access.sh`), which grants a newly registered team its permission set and account assignment once this apply has created its `javabin-team-{team}-guardrails` policy
- Uses `environment: production` (GitHub environment protection)
- OIDC → `javabin-ci-infra`. The role's trust also pins `job_workflow_ref` to `platform-ci.yml@refs/heads/main`, because the environment-based `sub` claim carries no branch. A workflow on another branch that declares `environment: production` cannot assume it.

Expand Down
80 changes: 79 additions & 1 deletion docs/lambda-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Queries Security Hub for active findings at HIGH and CRITICAL severity, aggregat

## team-provisioner

**Trigger:** Registry repo merge events (via `provision-app.yml` workflow dispatch)
**Trigger:** Registry repo merge events (direct invocation from registry CI via `javabin-ci-registry`); `sync_team_access` from platform CI after apply; `sync_access_members` from EventBridge every 3 hours (`javabin-team-provisioner-access-schedule`)
**Purpose:** Syncs team definitions from registry YAML across Google Groups, GitHub teams, AWS Budgets (80% warning + 200% enforcement thresholds), Cognito groups, and Identity Center groups. Also handles hero account provisioning.

| SSM Parameter | Purpose |
Expand Down Expand Up @@ -127,6 +127,84 @@ more; read the current list there first and add to it rather than pasting this o
`admin.directory.user.alias`, `gmail.send`, `gmail.settings.sharing`, `apps.groups.settings`
(all under `https://www.googleapis.com/auth/`).

### Team GitHub team (`sync_github_team`)

Each team gets the GitHub team `{name}`. Members without a `role:` field are team **maintainers**
(`GITHUB_DEFAULT_TEAM_ROLE`), so whoever registered the team can add its repos to it; `role: member`
makes someone a plain member. Registering a team (`javabin register-team`) writes no `role:`. A
member's existing role is changed in place, and logins not in the registry are removed. The Google
group keeps **MEMBER** as its default (only `role: maintainer` makes a Google MANAGER).

### Team AWS access (`sync_identity_center_group`, `sync_team_access`)

The team sync puts members in the Identity Center group `team-{name}` and gives that group the
permission set `javabin-team-{name}` on the account, with the customer-managed policy
`javabin-team-{name}-guardrails`. That policy is made by platform Terraform, which only learns of a
new team when platform CI runs on the registry's `teams-changed` dispatch, after the team sync.
Until it exists the team sync stops before the inline policy and the account assignment and reports
`guardrails: pending` (`:hourglass_flowing_sand:` in Slack): without the guardrails the inline
policy's `Action: *` would be unbounded.

Platform CI finishes the job: after every apply it queues `{"action": "sync_team_access"}`
(`scripts/sync-team-access.sh`, async). For every `team-*` Identity Center group whose guardrails
policy exists and is tagged `managed-by=terraform`, `repo=javaBin/platform`, it attaches the
guardrails, writes the inline policy, makes the account assignment and re-provisions the permission
set; teams without one (e.g. `platform`) are left alone. `{"teams": [...]}` limits the run. It posts
to #javabin-infra-alerts only when a team got access for the first time or something failed
(`CreateAccountAssignment` accepts an assignment that already exists without a conflict, so the
handler checks `ListAccountAssignments` first; if that lookup fails it stays quiet). Since it
runs on every apply it also rolls inline policy changes out to existing teams.

### How a member gets AWS access (`sync_aws_access_group`, `sync_access_members`)

Users reach Identity Center only through Google's SCIM auto-provisioning, and Google provisions only
the **direct** members of the groups the AWS app's *Group membership* setting lists (`drift@`,
`styret@` and `javabin-developer@java.no`; `SCIM_GROUPS` in the handler mirrors it). Google does not
expand nested groups, so `team-{name}@` or `kodesmia@` nested in `javabin-developer@` would give no
one a user. The flow is:

1. **Registry sync adds the person to the AWS access group.** The team sync makes every member's
java.no address (from their `google` field) a direct MEMBER of `AWS_ACCESS_GROUP` (env var, set
from Terraform `var.aws_access_group`, default `javabin-developer@java.no`). For kodesmia, the
registry's `groups/access.yaml` declares the group with `flatten: true`:

```yaml
- name: javabin-developer
google: javabin-developer@java.no
description: Google SCIM provisions direct members to IAM Identity Center
groups: [kodesmia]
flatten: true
```

An access group with `flatten: true` gets the role groups' individual members (from the resolved
members in the `sync_groups_and_heros` payload) instead of the nested group email. Both paths are
additive, like the other access groups: nobody is removed. The result lists `added_members`,
members present but not in kodesmia or any team as `preserved` (they are kept), and personal
(non-java.no) addresses as `skipped_external` (they can't be provisioned).
2. **Google SCIM creates the Identity Center user**, some time later. The same registry run has
already tried the Identity Center groups, so it skips the person and returns them in
`not_in_identity_center` (only members actually in the group count in `member_count`).
3. **The scheduled run adds them to their groups.** Every 3 hours EventBridge invokes
`{"action": "sync_access_members"}`. It reads the registry members the provisioner recorded in the
memberships table (`team-{name}` and every cognito-enabled group, e.g. `kodesmia`), makes the
recorded team members direct members of `AWS_ACCESS_GROUP` (covers teams synced before this
existed or a failed add), and adds recorded members that now have an Identity Center user to the
Identity Center group of the same name. It only adds; removals stay with the registry syncs. It
posts to #javabin-infra-alerts only when someone was added or an add failed.

The Slack summary lists everyone without an Identity Center user with a reason
(`_identity_center_reasons`): **pending SCIM** (just added to `javabin-developer@`, or already a
direct member of a provisioned group; if that persists, check the Google app's provisioning
status), **can't be provisioned** (personal address, suspended in Google, not in Google Workspace)
or **not a direct member of `javabin-developer@`**. The response body carries the same map as
`not_in_identity_center`.

The provisioner does not create these users itself through the Identity Store API, although the
API would accept it: a user made that way has no link to the Google account, so suspending or
deleting the person in Google would not remove their AWS access (SCIM never manages it), and when
Google later tries to provision the same `UserName` its create conflicts with the existing user and
fails. Identity Center users stay SCIM-owned (`CreatedBy: SCIM/...`).

### Atlassian group sync (`sync_atlassian`)

Invoked by the registry's `provision-groups.yml` via `scripts/provision-atlassian.py`, which resolves
Expand Down
17 changes: 17 additions & 0 deletions scripts/sync-team-access.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh
# Queue a team-provisioner sync_team_access run (async). Used by platform-ci
# after apply: a new team's guardrails policy only exists after this apply, so
# the registry's team sync could not grant the team its AWS access yet.
set -eu
CI_CONTEXT="{}"
if [ -n "${GITHUB_ACTOR:-}" ]; then
CI_CONTEXT=$(printf '{"actor":"%s","repo":"%s","sha":"%s","run_id":"%s"}' \
"${GITHUB_ACTOR}" "${GITHUB_REPOSITORY}" "${GITHUB_SHA}" "${GITHUB_RUN_ID}")
fi
aws lambda invoke \
--function-name "${PROJECT:-javabin}-team-provisioner" \
--invocation-type Event \
--cli-binary-format raw-in-base64-out \
--payload "{\"action\":\"sync_team_access\",\"ci_context\":${CI_CONTEXT}}" \
/dev/null
echo "Queued team-provisioner sync_team_access"
Loading
Loading