Uh oh!
There was an error while loading. Please reload this page.
Fix admin permissions for external plugin LiveViews - #521
Merged
ddon merged 2 commits intoMay 8, 2026
Conversation
Custom roles granted plugin permission keys (entities, billing, ai, …) were denied access to those plugins' admin pages because infer_permission_key_from_module/1 only resolved the core PhoenixKit.Modules.* namespace. Add a registry-backed fallback so top-level plugin namespaces (PhoenixKitEntities, PhoenixKitBilling, …) resolve to their declared module_key, while preserving the fail-closed default for genuinely unmapped views. - Add ModuleRegistry.get_module_key_for_namespace/1 next to get_by_key/1 - Wire it into Auth.infer_permission_key_from_module/1 fallback - Expose Auth.permission_key_for_admin_view/1 as @doc false def - Add unit tests for both helpers (4 + 2 cases)
Reviewer caught that get_module_key_for_namespace/1 used
[^top_namespace | _], which matches any registered module whose
Module.split path *starts with* the segment, not just modules
whose top-level *is* the segment. Live repro:
get_module_key_for_namespace("PhoenixKit") returned "db" because
PhoenixKit.Modules.DB.Module.split begins with "PhoenixKit".
Today this is masked by infer_permission_from_custom_tabs for every
existing PhoenixKit.<X>.Web.* admin view, so no user-visible
regression exists. But it's a footgun for any future view in the
PhoenixKit.* namespace that doesn't register a tab.
Tighten the match to [^top_namespace] (exact, single segment) and
narrow safe_call's return via is_binary/1 so the @SPEC is truthful
even if a misbehaving plugin returns a non-string key. Add a
regression test pinning get_module_key_for_namespace("PhoenixKit")
to nil.ddon added a commit
that referenced
this pull request
May 8, 2026
Eight post-merge reviews covering V111 PDF tables, the DB-module extraction, MediaBrowser modal + LV login return_to, external-plugin admin permissions, dashboard sidebar gettext, live Hex.pm known-packages fetch, the publishing routing-strategy shim, and the LanguageSwitcher per_translation_urls attr (plus the bundled DnD work). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon added a commit
that referenced
this pull request
May 8, 2026
…525 Code/doc fixes addressing one finding per PR (or several where trivial). Each closes a NITPICK or IMPROVEMENT-LOW from the matching CLAUDE_REVIEW.md; design-level / breaking / risky items deferred per the FOLLOW_UP.md "Skipped" sections. - #516: Drop dead `String.to_atom` fallback in OAuth interpolate_url - #518: Delete stray 0-byte pages_html.ex - #519: Fix stale `viewer={true}` template comment + login_path trailing-slash self-loop guard - #521: Resolution-order doc on permission_key_for_admin_view/1 - #522: Hot-reload safety pitfall in per-module-i18n.md - #523: KnownPackages — max-pages cap, ensure_table race comment, Logger-levels operational signals in moduledoc - #524: __mix_recompile__?/0 note next to apply/3 explanation - #525: LanguageSwitcher attr doc atom/string keys + DRY resolve_url per-language + JS sortable:flash defensive status check Plus FOLLOW_UP.md per PR enumerating closed vs deferred items. PR #525's FOLLOW_UP also captures the bundled DnD audit trail (table_default drag-handle scoping, sortable:flash, TR cell-width preservation) absent from the original PR body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon added a commit
that referenced
this pull request
May 8, 2026
Three-day window of accumulated work since 1.7.105 (2026-05-05): PRs #516, #518, #519, #521, #522, #523, #524, #525, plus the review-doc suite and post-merge triage closing nitpicks across all eight. Headline changes — V111 PDF library tables, DB module extracted to phoenix_kit_db, MediaBrowser modal viewer, sidebar gettext API, live Hex.pm catalog, publishing routing-strategy shim closing the /:locale/<literal>/... host-route shadowing bug, LanguageSwitcher :per_translation_urls, and bundled DnD improvements (drag-handle scoping, sortable:flash, TR cell-width preservation). All changes are strictly additive / non-breaking; one transitional extraction (DB → phoenix_kit_db) requires the paired Hex package once it ships. Co-Authored-By: Claude Opus 4.7 (1M context) <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 freeto 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
Custom roles granted plugin permission keys (e.g.
entities,billing,ai) were denied access to those plugins' admin pages — even when the role explicitly had the permission and the module was enabled — becausePhoenixKitWeb.Users.Auth.infer_permission_key_from_module/1only resolved the corePhoenixKit.Modules.*namespace.For an external plugin LiveView like
PhoenixKitEntities.Web.Entities, all three resolution fallbacks inpermission_key_for_admin_view/1(static map, custom-tabs map, namespace inference) returnednil. The fail-closed branch inenforce_admin_view_permission/2then redirected every non-system role with "You do not have permission to access this section.", regardless of the role's actual permissions.The fix adds a registry-backed fourth option: when the LiveView's top-level namespace doesn't match
PhoenixKit.Modules, askPhoenixKit.ModuleRegistrywhether any registered module starts with that namespace, and return its declaredmodule_key/0. Owner/Admin behaviour and the fail-closed default for genuinely unknown views are preserved.Changes
lib/phoenix_kit/module_registry.exget_module_key_for_namespace/1— symmetric with the existingget_by_key/1. Iteratesall_modules/0, matches onModule.split(mod)head, returns the module'smodule_key/0via the existingsafe_call/3. No new state, no caching layer.lib/phoenix_kit_web/users/auth.exinfer_permission_key_from_module/1gets a new[top | _rest]clause that callsModuleRegistry.get_module_key_for_namespace(top)._ -> nilfallback is removed:Module.split/1always returns a non-empty list of binaries, so it was unreachable (Dialyzer flags this on strict).permission_key_for_admin_view/1is exposed as@doc false def(wasdefp) so unit tests can reach it without LiveView mounting machinery.alias PhoenixKit.ModuleRegistryadded.Tests
test/phoenix_kit_web/users/auth_test.exs(4 cases): static-map hit,PhoenixKit.Modules.*namespace, registered-plugin namespace, unknown view → nil.test/phoenix_kit/module_registry_test.exsforget_module_key_for_namespace/1(3 cases). UsesModule.create/3with explicit top-level fixture names to avoiddefmodule Xgetting auto-nested under the test module's namespace.Tightening (second commit, post-internal-review)
Initial implementation used
[^top_namespace | _]which matched any registered module whoseModule.splitpath starts with the segment, not just modules whose top-level is the segment. Live repro on a parent app:get_module_key_for_namespace("PhoenixKit") => "db"becausePhoenixKit.Modules.DBhappens to be the first registered module beginning with"PhoenixKit".Today this is masked by
infer_permission_from_custom_tabs/1for every existingPhoenixKit.<X>.Web.*admin view, so no user-visible regression exists. But it's a footgun for any futurePhoenixKit.*view that doesn't register a tab.Fix: tightened to
[^top_namespace](exact, single segment) and narrowedsafe_call's return viais_binary/1so@specis truthful even if a misbehaving plugin returns a non-stringmodule_key/0. Added regression test pinningget_module_key_for_namespace("PhoenixKit") == nil.Affected scope
Every plugin shipping its own admin UI under a top-level
PhoenixKit<Name>namespace was unreachable for custom roles. Verified plugins in our deps tree:PhoenixKitEntities,PhoenixKitCRM,PhoenixKitStaff,PhoenixKitProjects,PhoenixKitBilling,PhoenixKitCatalogue,PhoenixKitDocumentCreator,PhoenixKitLocations,PhoenixKitEcommerce. After the fix, any registered module is auto-resolved by namespace.Test Plan
mix formatmix credo --strict— 0 issues on the changed filesmix dialyzer— 0 new warningsmix test test/phoenix_kit_web/users/auth_test.exs test/phoenix_kit/module_registry_test.exs— 45 tests, 0 failures (DB optional; integration tests excluded automatically)PhoenixKitEntities.Web.Entities → "entities",PhoenixKitAI.Web.Index → "ai", etc.); static map (Dashboard → "dashboard",Users → "users") andPhoenixKit.Modules.*namespace (Languages → "languages") preserved; genuinely unknown views still returnnil(fail-closed)