diff --git a/docs/lambda-functions.md b/docs/lambda-functions.md index 04f51c5..3ee17e9 100644 --- a/docs/lambda-functions.md +++ b/docs/lambda-functions.md @@ -88,7 +88,8 @@ Queries Security Hub for active findings at HIGH and CRITICAL severity, aggregat Invoked by the registry's `provision-groups.yml` via `scripts/provision-atlassian.py`, which resolves `groups/atlassian.yaml` to members' java.no emails. `atlassian.py` makes each declared Atlassian group contain exactly those accounts (creates missing groups, adds members, removes everyone else, including -personal-email accounts). Undeclared groups are never touched; `org-admins`, `site-admins` and +personal-email accounts). A group with `all_java_no_accounts: true` also gets every active managed +`@java.no` account — everyone with a java.no Google account. Undeclared groups are never touched; `org-admins`, `site-admins` and IdP-synced groups are refused. **Report-only by default.** Writes happen only when the `lambdas` module's `atlassian_sync_apply` is diff --git a/scripts/provision-atlassian.py b/scripts/provision-atlassian.py index a8660e6..23075cc 100755 --- a/scripts/provision-atlassian.py +++ b/scripts/provision-atlassian.py @@ -4,7 +4,9 @@ Usage: provision-atlassian.py Each Atlassian group in atlassian.yaml lists the registry groups (`from`) whose -members belong in it. Members are resolved to their javabin_google_email — the +members belong in it, and/or sets `all_java_no_accounts` to include everyone +with a java.no Google account (resolved by the Lambda from the Atlassian +directory, which the Google Workspace user sync keeps in step). Members are resolved to their javabin_google_email — the account the Google Workspace user sync creates in Atlassian — so personal emails never reach Atlassian. The Lambda runs with action=sync_atlassian and reports (or, when enabled in Terraform, applies) the membership changes. @@ -43,6 +45,7 @@ def resolve_atlassian_groups(atlassian_data, members_data): "name": group["name"], "description": group.get("description") or "", "members": emails, + "all_java_no_accounts": bool(group.get("all_java_no_accounts")), }) return resolved diff --git a/scripts/tests/test_provision_atlassian.py b/scripts/tests/test_provision_atlassian.py index 4da5745..f5e0482 100644 --- a/scripts/tests/test_provision_atlassian.py +++ b/scripts/tests/test_provision_atlassian.py @@ -29,6 +29,11 @@ def test_unions_source_groups_into_java_no_emails(self): self.assertEqual(got["confluence-admins-javabin"]["members"], ["anne.a@java.no", "bob.b@java.no"]) self.assertEqual(got["empty"]["members"], []) + def test_passes_all_java_no_accounts_flag(self): + atl = {"groups": [{"name": "confluence-users", "all_java_no_accounts": True}, {"name": "x", "from": ["drift"]}]} + got = pa.resolve_atlassian_groups(atl, MEMBERS) + self.assertEqual([(g["all_java_no_accounts"], g["members"]) for g in got], [(True, []), (False, ["bob.b@java.no"])]) + def test_personal_emails_never_reach_the_payload(self): atl = {"groups": [{"name": "confluence-users", "from": ["helter"]}]} emails = pa.resolve_atlassian_groups(atl, MEMBERS)[0]["members"] diff --git a/terraform/lambda-src/team_provisioner/atlassian.py b/terraform/lambda-src/team_provisioner/atlassian.py index cf140fd..378d37d 100644 --- a/terraform/lambda-src/team_provisioner/atlassian.py +++ b/terraform/lambda-src/team_provisioner/atlassian.py @@ -6,6 +6,8 @@ - members are matched by email against the org directory, so only accounts the Google Workspace user sync created (managed java.no accounts) are added +- a group with all_java_no_accounts also gets every active managed java.no + account, i.e. everyone with a java.no Google account - anyone else in a declared group is removed, including personal-email accounts; groups not declared in the YAML are never touched - report mode (the default) computes the same plan without writing @@ -130,6 +132,15 @@ def plan_group(desired_emails, current_members, accounts_by_email): return {"add": add, "remove": remove, "missing_account": missing} +def _java_no_accounts(accounts_by_email): + """Active accounts the Google Workspace sync owns — one per java.no Google user.""" + return [ + e for e, u in accounts_by_email.items() + if e.endswith("@java.no") and u.get("claimStatus") == "managed" + and u.get("accountStatus", "active") == "active" + ] + + def _describe(user): label = _email(user) or user["accountId"] return f"{label} (personal)" if user.get("claimStatus") == "unmanaged" else label @@ -138,7 +149,8 @@ def _describe(user): def sync(admin, groups, apply=False): """Plan, and if apply is set, enforce each declared group's membership. - groups: [{"name": str, "description": str, "members": [email, ...]}] + groups: [{"name": str, "description": str, "members": [email, ...], + "all_java_no_accounts": bool}] Returns per-group results; one failing group doesn't stop the others. """ accounts_by_email = {_email(u): u for u in admin.users() if _email(u)} @@ -155,7 +167,10 @@ def sync(admin, groups, apply=False): raise ValueError(f"{name} is synced from an identity provider and can't be edited") members = admin.users(current["id"]) if current else [] - plan = plan_group(group.get("members") or [], members, accounts_by_email) + desired = list(group.get("members") or []) + if group.get("all_java_no_accounts"): + desired += _java_no_accounts(accounts_by_email) + plan = plan_group(desired, members, accounts_by_email) result = { "create": current is None, "add": [_describe(u) for u in plan["add"]], diff --git a/terraform/lambda-src/tests/test_atlassian_sync.py b/terraform/lambda-src/tests/test_atlassian_sync.py index da83c35..e0b310c 100644 --- a/terraform/lambda-src/tests/test_atlassian_sync.py +++ b/terraform/lambda-src/tests/test_atlassian_sync.py @@ -88,6 +88,14 @@ def test_apply_creates_missing_groups_and_enforces_membership(self): ("add", "new-kodesmia", "b1"), ]) + def test_all_java_no_accounts_adds_every_managed_java_no_account(self): + admin = self.admin() + admin._users = [*admin._users, user("x1", "cfo@java.no"), user("d1", "drift@java.no", managed=False), + {**user("s1", "gone@java.no"), "accountStatus": "inactive"}] + results = atlassian.sync(admin, [{"name": "styret", "members": [], "all_java_no_accounts": True}]) + self.assertEqual(results["styret"]["add"], ["anne.a@java.no", "bob.b@java.no", "cfo@java.no"]) + self.assertEqual(results["styret"]["remove"], ["bob@gmail.com (personal)"]) + def test_refuses_protected_and_idp_synced_groups(self): admin = self.admin() results = atlassian.sync(admin, [