Skip to content

feat: introduce access groups for sharing-tag grants#26

Merged
AshDevFr merged 8 commits into
mainfrom
access-groups
May 25, 2026
Merged

feat: introduce access groups for sharing-tag grants#26
AshDevFr merged 8 commits into
mainfrom
access-groups

Conversation

@AshDevFr
Copy link
Copy Markdown
Owner

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

  • Access groups model. Admins can create named access groups, attach allow/deny grants for any set of sharing tags, and assign users to one or more groups. A user's effective rules are the union of their group rules plus their existing per-user grants, with deny-wins preserved. Existing per-user grants continue to work unchanged and act as overrides.
  • Admin API. New endpoints to list, create, rename, and delete access groups; manage members; add and remove tag grants on a group; and attach optional OIDC group-name mappings. Gated behind a new admin-only permission for access-group management.
  • Effective-grants debug endpoint. New admin endpoint that returns the full set of effective tag grants for a given user, including which group (or per-user rule) each grant came from, to make "why can/can't this user see X" easy to diagnose.
  • OIDC auto-assignment. When an OIDC user signs in, their IdP group claims are reconciled against the configured group-name mappings: matching memberships are added, no-longer-matching ones are removed, and manually-assigned memberships are left alone. Reconciliation failures are logged and do not block login.
  • Admin UI. New settings area for managing access groups (list, create/edit, members, grants, OIDC mappings) and a per-user "effective grants" panel that surfaces each grant's source and highlights per-user overrides.
  • Operators / migrations. Purely additive schema: four new tables for groups, memberships, group grants, and OIDC mappings. No backfill, and users with no group memberships behave exactly as before.

Notes

  • Whitelist-mode behavior is inherited unchanged: any allow grant from any source (group or per-user) flips the user into "only-tagged" mode and hides untagged content. Worth flagging to admins who expect a "give this group access to manga" grant to be purely additive.
  • OIDC group-name matching is case-sensitive and exact; per-mapping normalization is a possible follow-up.
  • No feature flag: with no groups configured, the system behaves identically to today, so the rollout is safe to ship in one piece.

AshDevFr added 2 commits May 24, 2026 13:59
…-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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 24, 2026

Deploying codex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1cfc893
Status: ✅  Deploy successful!
Preview URL: https://26e2479e.codex-asm.pages.dev
Branch Preview URL: https://access-groups.codex-asm.pages.dev

View logs

AshDevFr added 6 commits May 24, 2026 15:52
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.
@AshDevFr AshDevFr merged commit f5c8ea6 into main May 25, 2026
19 checks passed
@AshDevFr AshDevFr deleted the access-groups branch May 25, 2026 22:04
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