Skip to content

feat: user directory protocol; modules stop importing users.models; strict dependency discovery (#501) - #566

Merged
antosubash merged 9 commits into
mainfrom
feat/user-directory-501
Oct 21, 2026
Merged

antosubash merged 9 commits into
mainfrom
feat/user-directory-501

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Summary

Closes #501.

On a Keycloak install, modules that imported users.models directly were quietly querying empty users_* tables. They now go through a framework user directory, which the installed auth provider supplies.

  • Core (simple_module_core.user_directory):
    • UserDirectory protocol: get_many (batched; keyed by the caller's id strings; invalid or unknown ids are left out), search, match_ids (a SELECT, so the audit actor filter stays an unbounded subquery) and stats.
    • DirectoryUser, whose label is the one naming rule: full name, else email.
    • NullUserDirectory when no provider is installed.
  • Activity feed (simple_module_core.activity_feed): ActivityFeed breaks the users ↔ audit_log import cycle. audit_log registers it; the users "recent activity" card reads it, with the permission gating unchanged (GH Audit log entity labels expose account names and emails to audit_log.view holders #300).
  • Implementations: users/directory.py, and keycloak/directory.py over its user cache table. Under Keycloak, counts mean "users who have signed in at least once", "created this month" is unknown, and roles are unknown.
  • Ported:
    • audit_log: actor names and actor filter;
    • dashboard: user tiles (a tile with an unknown value is hidden);
    • file_storage: its copy of the label resolver is deleted;
    • api_tokens: owner checks and personal-token resolution;
    • permissions: user lookup and search;
    • auth: a structural AccountLike type replaces the users.User hint;
    • background_tasks: unused dependency dropped.
  • Strict discovery (behaviour change):
    • Outside development, a module whose depends_on target is missing refuses the boot with one MissingDependencyError listing every missing pair.
    • Development still warns. make doctor reports it as SM030.
  • New ModuleMeta.optional_depends_on: sorts after a module when it is present and never requires it. It's used for modules that listed Settings only for load order, so the app still boots with settings disabled (Design: the settings plugin module has become de facto framework; make the dependency explicit #500), and for ApiTokens → Users.

Decisions

  • Permissions under Keycloak: Permissions still requires Users, because its role pages need the Role table. A strict Keycloak boot with Permissions enabled therefore fails with a clear error instead of running against empty tables. Keying roles by name for Keycloak is a follow-up.
  • API tokens under Keycloak: ApiTokens works there now. Service tokens resolve; personal tokens are refused because Keycloak doesn't expose roles to the app. Review caught that the first cut would have refused ApiTokens entirely.
  • No migrations.

Verification

  • Local CI:
    • make lint passes.
    • make test-py: 5069 passed, 22 skipped.
    • make test-js: 668 passed.
    • make ci-doctor: 0 errors.
  • /ship:
    • Optimize made one cleanup commit.
    • Code review was clean after 2 passes, and an independent Codex review found no regressions.
    • Browser QA passed 10 out of 10: dashboard tiles, audit actor names and filter, the activity card and its empty state, permissions, API token scope, a disabled owner's token being refused, and file storage uploader names.
  • Reports (start private; share them from claude.ai/code/artifacts if reviewers need access):

Test plan

  • CI is green, including make doctor
  • A production boot with a missing dependency fails, naming every missing pair

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

Core protocols so modules read users and recent activity without
importing a provider module: DirectoryUser/DirectoryStats, the
UserDirectory protocol with a NullUserDirectory fallback, the shared
naming rule (DirectoryUser.label), id parsing/chunking helpers, and the
ActivityFeed protocol, each registered on app.state.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
UsersDirectory over users_user (roles via users_user_role, disabled
accounts inactive); users.audit's label resolver delegates to it.
KeycloakDirectory over keycloak_user_cache: users who have signed in,
roles unknown, no creation date; match_ids renders hyphenated ids on
SQLite where Uuid is bare hex. Each provider registers its directory in
register_settings. users.contracts.roles re-exports Role for the
permissions role pages.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…ty feed breaks the cycle (#501)

audit_log no longer imports users: actors are named and searched through
the app's user directory (actor_filter stays an unbounded SQL subquery via
match_ids, and falls back to an exact id match without one), actor links
key off the directory's entity_type, and depends_on=[Users] is gone.

audit_log registers an ActivityFeed; the users edit page's Recent
activity card reads it instead of lazily importing audit_log, keeping
'no feed' (no card) distinct from 'no activity' (empty card).

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
… onto the user directory (#501)

- dashboard: user tiles from UserDirectory.stats(); a None figure hides
  its tile; Users dependency (meta and package) dropped.
- file_storage: uploader names via the directory; the copied users query
  is gone.
- api_tokens: owner checks and emails via get_many; PAT resolution via
  get_many(with_roles=True), refusing owners whose roles are unknown.
  Keeps depends_on Users.
- permissions: user side via get_many/search; role pages keep Users
  through users.contracts.roles; admin sync uses core ADMIN_ROLE.
- auth: a structural AccountLike protocol replaces the users.User hint.
- background_tasks: drop the unused Users dependency.
- smpy catalog: requires now mirrors depends_on (users needs settings,
  background_tasks needs settings, dashboard needs nothing), pinned by a
  test.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
New docs/framework/user-directory.md (sidebar entry), module docs for
audit_log, dashboard, background_tasks, api_tokens, permissions, users
and keycloak, a CLAUDE.md architecture note, the conventions skill rule,
and CHANGELOG entries.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
topological_sort(modules, strict=...) collects every (module, dependency)
pair whose dependency is not loaded; strict raises one
MissingDependencyError (an InvalidModuleError) naming them all. create_app
passes strict=not is_development; development keeps logging per pair and
the new SM030 diagnostic reports it (warning at a dev boot, error in
make doctor). The outbox worker stays lenient.

ModuleMeta.optional_depends_on orders after a module when it is loaded and
is never missing. The ordering-only Settings entries (users, tenants,
file_storage, background_tasks, site_lock, api_tokens, keycloak) move
there, keeping the boot order unchanged and the settings plugin optional
(#500). The smpy catalog no longer makes users require settings.

Keycloak test apps leave out modules that need Users (permissions,
api_tokens) instead of booting leniently; a test pins that the full set
fails a non-development boot naming both.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: cb480e6
Status: ✅  Deploy successful!
Preview URL: https://3bf1e935.simple-module-python.pages.dev
Branch Preview URL: https://feat-user-directory-501.simple-module-python.pages.dev

View logs

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-21T13:26:40.427633Z cb480e6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@antosubash
antosubash merged commit a552e02 into main Oct 21, 2026
16 checks passed
@antosubash
antosubash deleted the feat/user-directory-501 branch October 21, 2026 14:01
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.

Design: a user-directory protocol so modules stop importing users.models (Keycloak installs break)

1 participant