Skip to content

Read groups from the groups service, and tag apps with community IDs - #303

Open
johnworth wants to merge 11 commits into
mainfrom
groups-service
Open

Read groups from the groups service, and tag apps with community IDs#303
johnworth wants to merge 11 commits into
mainfrom
groups-service

Conversation

@johnworth

@johnworth johnworth commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Part of the Grouper removal: apps reads groups from the new groups service. It depends on that service being deployed, so it merges in the cutover window alongside the PRs listed under Ordering.

apps.clients.iplant-groups becomes apps.clients.groups. Groups are addressed by structured identity (group_type plus name) rather than by colon-delimited Grouper paths, so the name-packing helpers go, along with the cyverse-groups-client dependency.

Scope

22 call sites across 11 files — not the 11 an earlier audit reported. The client is required under three aliases (ipg, groups, groups-client), so any single-alias search misses about two-thirds of them. Keeping every function's name, arity, and response shape meant all but three call sites are untouched.

File Calls Alias
clients/permissions.clj 5 ipg
service/groups.clj 4 ipg
clients/notifications/common_sharing.clj 2 ipg
clients/notifications.clj 2 groups-client
service/apps/job_listings.clj 2 ipg
service/apps/tapis/listings.clj 2 ipg
service/apps/communities.clj 1 groups
service/workspace.clj 1 ipg
user.clj 1 ipg
service/apps/de/jobs/common.clj 1 ipg
service/apps/tapis/sharing.clj 1 ipg

The three that changed

user_groups in the job submission was the one call site that depended on Grouper's colon-path naming, via remove-environment-from-group. It is now the plain group names. The field is inert either way — apps computes it and model carries it, but nothing reads it: FormatUserGroups has no callers outside its own tests.

grouper-user-group-idde-users-group-id, resolving through /groups/lookup rather than building a Grouper path.

The workshop group response drops Grouper's type, extension, display_extension, and id_index in favour of group_type, and its schema moves with it. terrain proxies these admin endpoints; no browser code consumes them.

Community admins now match admin or own

The importer only ever produces admin for a community — ownerGrant returns nil where there is no owner segment. But a community created natively grants own to whoever created it, and that person administers it. Filtering on admin alone would lock a community's own creator out of tagging apps into it.

Verified on the local cluster

Against real imported Grouper data, with the groups and permissions services on their migration branches:

  • de-users-group-id resolves to the preserved Grouper UUID 1e01485adf464393835b808b3bab09af, and /apps used it to mark 2 of 3 apps public — the full chain through the groups service to the permissions service.
  • Workshop group create/read/update works, including lazy creation. The new group's ID is 32-hex undashed, matching the shape of imported IDs.
  • Community authorization: a non-admin gets 403 "User is not an admin of that community"; the holder of admin succeeds.
  • /analyses and /apps listings return normally (these exercise lookup-subject-groups).

Not exercised locally: the notification and Tapis listing paths (Tapis is disabled there and there is no sharing traffic). They use lookup-subject and lookup-subjects, both of which other paths did exercise.

lein check is clean and the suite passes (81 tests, 217 assertions).

Community tags now name a community by ID

This also carries the community-tag change, because the two are inseparable: the tag value is a community name, and this branch is what makes names resolvable to IDs.

The stored value is now the community's ID, and callers send {"community_ids": [...]} rather than composing an AVU. Identifiers are resolved on both the read and write paths, so an ID, a short name, and a legacy colon path all select the same community — which is what lets a browser holding a pre-cutover bundle keep working instead of 404ing. DELETE /apps/:app-id/communities/:community-id joins the body-based form.

The important fix is on the admin route. community-admin-update-avus skipped validate-avu-community-admins entirely when admin? was true, so nothing looked at the value at all and an unresolvable one was written straight through to the metadata service — a tag no listing could ever match. That is the mechanism behind the 17 orphaned tag values in production. Administrators still bypass the community-admin check; they no longer bypass resolution.

Measured against a local cluster before and after:

Route Value posted Before Now
admin iplant:de:de:communities:Vanished 200, orphan written 404
user iplant:de:de:communities:Imaging 404 200, stores the ID
user community_ids: [<id>] 200
user, non-admin any 403 403

Both halves are pinned by tests that fail when the old behaviour is reintroduced — verified by reintroducing it.

The one-off rewrite of existing tags is a command in the groups repo rather than a migration, because the mapping spans two databases; see cyverse-de/deployments#86.

Not addressed here

apps/user.clj:55-56 reads :first-name/:last-name from the subject lookup, but that service returns first_name/last_name — and did before this change too. Those have been nil since at least 2021, so integration-data and private-tool implementor names are built from empty strings. Orthogonal to this migration; worth its own issue.

Ordering

The image builds in-container against the released common-swagger-api 3.4.22 (cyverse-de/common-swagger-api#101, released to Clojars and pinned in project.clj) and is deployed and verified on a local cluster.

Replaces apps.clients.iplant-groups with apps.clients.groups, keeping every
function's name, arity, and response shape so the 22 call sites across 11
files are unchanged apart from the three noted below. Groups are addressed by
structured identity (group_type plus name) rather than by colon-delimited
Grouper paths, so the name-packing helpers are gone along with the
cyverse-groups-client dependency.

Three call sites do change:

  - grouper-user-group-id becomes de-users-group-id, which resolves the group
    through /groups/lookup rather than by building its Grouper path.

  - The job submission's user_groups is now the plain group names.
    remove-environment-from-group existed to strip the environment prefix from
    a Grouper path, and the new names carry no prefix. The field is inert
    either way: apps computes it and model carries it, but nothing reads it --
    FormatUserGroups has no callers outside its own tests.

  - The workshop group response drops Grouper's type, extension,
    display_extension, and id_index in favour of group_type, and its schema
    moves with it. terrain proxies these admin endpoints but no browser code
    consumes them.

Community admins are now the subjects holding admin or own rather than
Grouper's admins privilege. Own matters going forward: the importer only ever
produces admin for a community, but a community created natively grants own
to whoever created it, and that person administers it too. Filtering on admin
alone would lock a community's own creator out of tagging apps into it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
John Wregglesworth and others added 2 commits August 4, 2026 14:57
The community tag on an app was the community's name -- in Grouper, its full
colon-delimited path -- composed by the browser and stored verbatim. A name is
not a stable identifier: renaming a community dropped every app tagged with the
old name out of its own listing, silently. Production holds 17 such orphaned
tag values across 48 apps.

The stored value is now the community's ID, and callers name a community by ID
instead of composing the value themselves. Identifiers are resolved on both
sides, so an ID, a plain name, and a legacy colon path all select the same
community; that is what lets a browser holding a pre-cutover bundle keep
working. The read path resolves too, so a listing finds the same apps however
the community was named in the URL.

The important fix is that administrators now resolve communities as well.
community-admin-update-avus skipped validation entirely when admin? was true,
so an administrator's request stored whatever value it carried without anything
looking at it -- writing a tag no listing could ever match. Administrators
still bypass the community-admin check; they no longer bypass resolution.

DELETE /apps/:app-id/communities/:community-id is added alongside the
body-based form, which remains for the transition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the two properties the listing depends on: that a community ID, a short
name, and a legacy Grouper path all filter on the same community ID, so a
browser holding a pre-cutover bundle sees the same apps; and that an identifier
naming no community selects nothing without asking the metadata service, since
filtering on an unresolvable value would match orphaned tag rows literally and
keep listing apps under a community that no longer exists.

Both fail if the resolution is reverted to matching the identifier verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnworth johnworth changed the title Read groups from the groups service instead of iplant-groups Read groups from the groups service, and tag apps with community IDs Aug 4, 2026
@johnworth
johnworth marked this pull request as ready for review August 4, 2026 22:56
John Wregglesworth and others added 8 commits August 5, 2026 14:33
The groups service answers a membership replacement with per-subject
results, and the previous code passed the raw member listing through
after re-fetching it. The listing's subjects can omit source_id, which
GroupMembersUpdateResponse requires, so response coercion failed the
route with ERR_SCHEMA_VALIDATION. Build the response directly from the
results instead: successful entries become the members (the update is a
full replacement, so they are the new membership), failed entries'
subject IDs become the failures. This also drops an extra round trip.

The integration test still asserted the pre-migration Grouper shape
(:type "role"); the group is now group_type "system".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
community-short-name took the last colon segment of any path, so a
legacy tag like iplant:de:prod:teams:Imaging resolved to the Imaging
community even though it names a team, and any group name reused across
folders could capture the wrong community. Only treat an identifier as
a legacy path when its second-to-last segment is `communities`;
anything else is looked up as a plain name and simply fails to resolve
if it contains colons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The legacy AVU request shape and the legacy Grouper-path identifiers
are accepted silently for compatibility, so there was no way to tell
from the logs whether stale browser bundles are still out there. Log a
warning naming the probable cause on each fallback so operators can
tell when the compatibility paths stop being exercised and can be
removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The thrown? Exception assertions passed for any failure, including
wiring mistakes in the tests themselves, so they proved nothing about
which refusal fired. Catch the slingshot map and assert its :type
(not-found vs forbidden) instead, and pin both directions of the admin
guard: a caller outside the community's admin set is refused while an
administrator resolves the same request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The community-admin notification passed the raw identifiers from the
publish request into community_list, so admins got 32-hex group IDs in
their emails when a client posted IDs. get-community-admin-set already
resolved the community internally but only surfaced the admin IDs;
surface the resolved name alongside them and notify with that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A replacement's results list only the changes performed: a member that
was already present and stays appears in neither list, and a removal
reports success, so mapping successful results to members both dropped
kept members and resurfaced removed ones. Verified live: replacing the
membership with an empty list returned the removed member as a member.
The failures still come from the results; the membership comes from the
same listing call the GET route serves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The branch built against 3.4.22-SNAPSHOT while the release was pending;
the real release is now on Clojars, so nothing mutable is left in the
dependency tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant