Read groups from the groups service, and tag apps with community IDs - #303
Open
johnworth wants to merge 11 commits into
Open
Read groups from the groups service, and tag apps with community IDs#303johnworth wants to merge 11 commits into
johnworth wants to merge 11 commits into
Conversation
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>
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>
This was referenced Aug 4, 2026
johnworth
marked this pull request as ready for review
August 4, 2026 22:56
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the Grouper removal:
appsreads groups from the newgroupsservice. It depends on that service being deployed, so it merges in the cutover window alongside the PRs listed under Ordering.apps.clients.iplant-groupsbecomesapps.clients.groups. Groups are addressed by structured identity (group_typeplusname) rather than by colon-delimited Grouper paths, so the name-packing helpers go, along with thecyverse-groups-clientdependency.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.clients/permissions.cljipgservice/groups.cljipgclients/notifications/common_sharing.cljipgclients/notifications.cljgroups-clientservice/apps/job_listings.cljipgservice/apps/tapis/listings.cljipgservice/apps/communities.cljgroupsservice/workspace.cljipguser.cljipgservice/apps/de/jobs/common.cljipgservice/apps/tapis/sharing.cljipgThe three that changed
user_groupsin the job submission was the one call site that depended on Grouper's colon-path naming, viaremove-environment-from-group. It is now the plain group names. The field is inert either way —appscomputes it andmodelcarries it, but nothing reads it:FormatUserGroupshas no callers outside its own tests.grouper-user-group-id→de-users-group-id, resolving through/groups/lookuprather than building a Grouper path.The workshop group response drops Grouper's
type,extension,display_extension, andid_indexin favour ofgroup_type, and its schema moves with it. terrain proxies these admin endpoints; no browser code consumes them.Community admins now match
adminorownThe importer only ever produces
adminfor a community —ownerGrantreturns nil where there is no owner segment. But a community created natively grantsownto whoever created it, and that person administers it. Filtering onadminalone 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-idresolves to the preserved Grouper UUID1e01485adf464393835b808b3bab09af, and/appsused it to mark 2 of 3 apps public — the full chain through the groups service to the permissions service.adminsucceeds./analysesand/appslistings return normally (these exerciselookup-subject-groups).Not exercised locally: the notification and Tapis listing paths (Tapis is disabled there and there is no sharing traffic). They use
lookup-subjectandlookup-subjects, both of which other paths did exercise.lein checkis 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-idjoins the body-based form.The important fix is on the admin route.
community-admin-update-avusskippedvalidate-avu-community-adminsentirely whenadmin?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:
iplant:de:de:communities:Vanishediplant:de:de:communities:Imagingcommunity_ids: [<id>]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-56reads:first-name/:last-namefrom the subject lookup, but that service returnsfirst_name/last_name— and did before this change too. Those have beennilsince 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-api3.4.22(cyverse-de/common-swagger-api#101, released to Clojars and pinned inproject.clj) and is deployed and verified on a local cluster.common-swagger-api— 3.4.22 released and pinned