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
3 changes: 2 additions & 1 deletion docs/lambda-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion scripts/provision-atlassian.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
Usage: provision-atlassian.py <atlassian.yaml> <members.yaml>

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.
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions scripts/tests/test_provision_atlassian.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
19 changes: 17 additions & 2 deletions terraform/lambda-src/team_provisioner/atlassian.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)}
Expand All @@ -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"]],
Expand Down
8 changes: 8 additions & 0 deletions terraform/lambda-src/tests/test_atlassian_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand Down
Loading