feat: introduce access groups for sharing-tag grants#26
Merged
Conversation
…-tag access groups
Introduce reusable access groups that bundle a set of sharing-tag
allow/deny rules and can be assigned to many users. Per-user grants in
user_sharing_tags continue to act as overrides on top of group rules;
no data migration is required and behavior is unchanged for users with
no group memberships.
Schema (four new additive tables, cascading FKs):
- access_groups: the group itself
- user_access_groups: M:N membership with a source column
('manual' | 'oidc') so OIDC-driven reconciliation can leave
admin-managed assignments alone
- access_group_sharing_tags: per-group tag grants, mirroring the shape
of user_sharing_tags
- access_group_oidc_mappings: optional IdP-group-name -> access-group
mapping for auto-assignment at login
Entities expose AccessMode (reused from user_sharing_tags) and a new
MembershipSource enum, plus relations that enable joined queries
through access_groups (groups -> users, groups -> sharing_tags).
AccessGroupRepository covers full CRUD over groups plus member, grant,
and OIDC-mapping management. add_member, add_oidc_mapping, and
set_grant are upserts/idempotent; update uses the Option<Option<T>>
pattern already used by SharingTagRepository to distinguish "leave
alone" from "set to NULL".
SharingTagRepository gains get_effective_grants(user_id), which UNIONs
the user's own grants with grants from every group the user belongs
to and returns deduped (allows, denies) vecs. The query intentionally
surfaces a tag in both vecs when sources disagree; the deny-wins rule
is left to the caller (ContentFilter). Dual SQL variants ($N for
Postgres, ? for SQLite) mirror the pattern already used by
task_metrics.
Tests cover every repository method and all effective-grants
scenarios (user-only, group-only, merged, multi-source dedupe, empty,
and conflicting allow+deny on the same tag). Workspace clippy is
clean and the full codex-db suite passes on SQLite.
ContentFilter::for_user now sources effective sharing-tag grants from both per-user rows and every access group the user belongs to via SharingTagRepository::get_effective_grants. The deny-wins rule, whitelist mode trigger, and default-open behavior are unchanged; users with no group memberships continue to produce identical boolean outputs. Excluded series are now derived from the unioned deny tag list using the existing get_series_ids_with_any_tags helper, keeping the query count flat regardless of grant count. The pre-existing in-memory unit tests pass unmodified. New DB-backed tests cover the canonical group-allow + user-deny scenario, deny-wins-across-sources between two groups, union of allows across groups, legacy zero-group compatibility, default-open, and a many-groups build-time smoke check.
Deploying codex with
|
| Latest commit: |
1cfc893
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://26e2479e.codex-asm.pages.dev |
| Branch Preview URL: | https://access-groups.codex-asm.pages.dev |
Expose 13 REST endpoints under /api/v1/access-groups and
/api/v1/users/{id}/ for managing access groups end to end:
- Group CRUD: list, get (with full detail), create, update, delete
- Members: add users to a group, remove a user from a group
- Grants: add/remove sharing-tag allow/deny grants per group
- OIDC mappings: add/remove IdP group-name mappings per group
- User queries: list a user's groups, effective-grants debug view
The effective-grants endpoint returns each (tag, mode) with source
attribution (user override vs group name), enabling admins to diagnose
"why can/can't this user see content X" without inspecting raw tables.
All endpoints are gated behind SystemAdmin permission, consistent with
sharing tag and settings endpoints. OpenAPI spec and TypeScript types
regenerated via make openapi-all. Integration tests cover all endpoints
including permission-denied and cascade-delete scenarios.
Add AccessGroupRepository::reconcile_oidc_group_memberships() which diffs a user's current OIDC-sourced memberships against the access groups mapped to their IdP group claims, then adds/removes memberships accordingly. Manual memberships are never touched. Wire the reconciliation into the OIDC callback handler at both code paths (returning user and first-time login). Errors are logged and do not break the login flow, so a misconfigured mapping cannot lock users out. Unit tests cover join, leave, no-change, no-mappings, manual-preserved, and idempotent back-to-back scenarios.
The test was rolling back `Some(1)` migration, which previously targeted migration 089 (file_path rename). With migration 090 (access_groups) added, rolling back 1 now rolls back 090 instead. Change to `Some(2)` so both 090 and 089 are rolled back before asserting `file_path` exists.
Add settings pages for managing access groups through the web UI: - AccessGroupsSettings: list all groups with member/grant counts, create, edit (rename/re-describe), and delete with confirmation - AccessGroupDetail: view and manage a group's members, sharing-tag grants (allow/deny), and OIDC group-name mappings inline - UserEffectiveGrants: combined view of all sharing-tag grants from user overrides and group memberships with source attribution badges, shown in the user edit modal alongside existing per-user grants Also adds the API client module, sidebar navigation link under the Access section, and route entries for both the list and detail pages.
Add docs/docs/users/access-groups.md covering the full access group feature: grant merging rules, API examples for groups/members/grants/ OIDC mappings, the effective-grants debug endpoint as a troubleshooting runbook, whitelist-mode caveat, and a worked example combining groups with per-user overrides.
… list endpoints
Sharing-tag access control was only enforced on a subset of read endpoints.
The /api/v1/series listing filtered by ContentFilter, but /api/v1/books,
POST /api/v1/books/list, /books/recently-added, /books/on-deck, /books/in-progress,
/books/recently-read, /books/{id}/adjacent, the library-scoped variants of all
of the above, plus the OPDS, OPDS 2.0, and Komga book paths, all returned
books in series the caller had been denied access to.
The series handlers that did filter were doing it in memory after the repo
returned every row, so paginated totals reflected the unfiltered set even
though the page itself dropped denied rows. Pagination links became wrong
once any deny grant was active.
This change moves visibility enforcement into the SQL layer:
- New SeriesVisibility helper in codex-db: a deny set plus an optional
allow set (whitelist mode). visibility_predicate() builds the SeaORM
SimpleExpr; apply_book_visibility / apply_series_visibility add it to
a Select<books::Entity> / Select<series::Entity>. Empty whitelists
short-circuit to an empty result without touching the DB.
- ContentFilter::to_visibility() turns the resolved per-user grants into
a SeriesVisibility, returning None when the user has no restrictions
so the natural query is unchanged for the common case.
- Every BookRepository and SeriesRepository method used by user-facing
list / search endpoints now takes Option<&SeriesVisibility>: list_all,
list_recently_added, list_by_ids, list_by_ids_sorted, hydrate_by_ids,
list_by_library_sorted, list_with_progress, list_recently_read,
list_on_deck, search_by_title, search_by_name, get_adjacent_in_series
for books; list_by_library, list_all, list_recently_added,
list_recently_updated, list_by_library_sorted, list_by_ids_sorted,
search_by_title, search_by_name, list_in_progress for series.
- All affected v1, OPDS, OPDS 2.0, and Komga handlers load the
ContentFilter once and thread visibility through. The in-memory
filter passes in series handlers are gone, so totals reflect the
user's view.
- Internal callers that should not be visibility-scoped (scanner,
thumbnail generation, library reprocess, plugin auto-match) pass
None explicitly.
Tests cover the SeriesVisibility helper, every affected repository
method (deny mode, whitelist mode, empty-whitelist short-circuit, and
None pass-through), and the user-facing endpoints end-to-end through
sharing-tag deny and whitelist grants.
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.
Summary
Adds reusable access groups that bundle sharing-tag allow/deny rules and can be assigned to many users at once. Group rules merge with existing per-user grants under the same deny-wins semantics, so today's behavior is preserved for users in zero groups. OIDC group claims at login are now used to reconcile group membership automatically, and admins get full CRUD plus a debug view of effective per-user grants.
Motivation
Sharing-tag grants are stored exclusively per-user, so giving 10 users the same access profile across 5 tags requires 50 rows and editing 10 places to change the profile. There's no way to express "kids" or "staff" content access beyond duplicating grants by hand, and OIDC group claims pulled from the IdP at login were collected but never used. Access groups make grant management scale with the organization while keeping per-user grants as a simple override layer.
Changes
Notes