Skip to content

feat(registry): per-user access lists (verdaccio-style groups)#12048

Merged
zkochan merged 2 commits into
mainfrom
feat/registry-named-groups
May 29, 2026
Merged

feat(registry): per-user access lists (verdaccio-style groups)#12048
zkochan merged 2 commits into
mainfrom
feat/registry-named-groups

Conversation

@zkochan

@zkochan zkochan commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Models each package's access / publish permission as a list of tokens (verdaccio's space-separated groups) matched against the caller's identity, instead of a single rule. This adds per-user access (list usernames directly) and lays the matching groundwork for named groups.

packages:
  '@team/*':
    access:  alice bob        # only these users (+ they can read)
    publish: alice            # only alice can publish
  '@public/*':
    access:  $all
    publish: $authenticated

How verdaccio does it (and what we mirror)

verdaccio's access/publish/unpublish are space-separated lists; a request is allowed when the caller's group set intersects the list. A logged-in user's groups are the built-ins plus their username, plus whatever the auth plugin returns — and the default htpasswd plugin contributes only the username. Named groups beyond that come from other auth backends (LDAP/GitLab/…).

So there is intentionally no new groups: config key: that's not how verdaccio works with htpasswd. Usernames give per-user access today; group names will resolve here for free once a group-providing auth backend lands (the separate "pluggable auth" item in #11973).

What's in

  • AccessRule (single enum) → AccessList (Vec<AccessToken>) + Identity. A permission is satisfied if the identity matches any token (union).
  • Tokens: $all / @all / all, $authenticated / @authenticated, $anonymous / @anonymous, or a name. A name matches an authenticated caller whose username equals it.
  • Each permission accepts both a space-separated string (access: alice bob) and a YAML sequence (access: [alice, bob]), like verdaccio.
  • enforce_access: a denied anonymous caller gets 401 (can authenticate); a denied authenticated caller gets 403.

Behavior change to note

This reverses the InvalidAccessRule stopgap from #12043, which rejected bare names because groups weren't modeled yet. Bare names are now valid tokens (usernames/groups) — verdaccio's actual behavior. The error variant and its two "unknown token is a config error" tests are removed, replaced by per-user / list tests.

Scope

  • Completes the mechanism for "Named groups, per-user group membership": usernames work fully today; group names beyond usernames await a group-providing auth backend (verdaccio is identical with htpasswd).
  • unpublish is still parsed for config compatibility but folds into publish at enforcement (unchanged; separate item).

Test plan

  • Unit tests: token parsing (built-ins + names), $all/$authenticated/$anonymous/username/empty-list semantics, mixed-list union, YAML string-vs-sequence equivalence, defaults, first-match-wins, and bundled-config protections preserved.
  • Integration (tests/policy.rs): $authenticated gating, $anonymous admit/forbid, and a new username_in_access_list_grants_only_that_user (anon → 401, wrong user → 403, listed user → allowed).
  • cargo test, cargo clippy --deny warnings, cargo dylint (perfectionist), cargo fmt --check, cargo doc -D warnings, typos all clean.

Refs #11973 (Access control → "Named groups, per-user group membership").


Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • New Features

    • Multi-token permission lists for package access, accepting both string and sequence YAML formats.
    • Username-specific access rules supported and enforced.
  • Improvements

    • Clearer handling of unauthenticated vs forbidden authorization responses.
  • Tests

    • Added integration tests covering username-based rules and token-list parsing.

Review Change Stack

Model each packages[*] access/publish permission as a list of tokens
(verdaccio's space-separated groups) instead of a single rule, and
match it against the caller's identity. Tokens are the built-in
pseudo-groups ($all/$authenticated/$anonymous and their @/bare
aliases) or a name; a name matches an authenticated caller whose
username equals it — verdaccio's per-user access, where the htpasswd
backend contributes the username and richer groups would come from a
group-providing auth backend.

- AccessRule (single enum) -> AccessList (Vec<AccessToken>) + Identity.
- Accept both a space-separated string and a YAML sequence for each
  permission, like verdaccio.
- Bare names are now valid tokens (drops the InvalidAccessRule
  stopgap from #12043 that rejected them).
- enforce_access: anonymous denial -> 401, authenticated denial -> 403.

Refs #11973.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1046e3f4-0739-41a6-8f91-16a09e610ffc

📥 Commits

Reviewing files that changed from the base of the PR and between 11a1d2f and 6c6adf4.

📒 Files selected for processing (1)
  • registry/crates/pnpm-registry/src/policy.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • registry/crates/pnpm-registry/src/policy.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Doc
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Dylint
  • GitHub Check: Code Coverage
  • GitHub Check: Compile & Lint

📝 Walkthrough

Walkthrough

This PR refactors the pnpm-registry access control system from a single-token enum model to a multi-token permission list model. AccessToken, AccessList, and Identity types replace AccessRule. Config parsing introduces AccessSpec to normalize string and sequence token forms. Policy compilation builds AccessList-based rules with defaults. Server enforcement uses Identity-driven allows checks. Tests are updated to validate permissions for anonymous and named users.

Changes

Access Control Model Refactor

Layer / File(s) Summary
Core access control model types
registry/crates/pnpm-registry/src/policy.rs
Introduces AccessToken (built-ins and named tokens), AccessList (permission lists with allows), and Identity (anonymous vs authenticated). Removes AccessRule; PackagePolicy/PackagePolicies now use AccessList and include default_access/default_publish with Effective<'a'> borrowing.
Config schema with AccessSpec abstraction
registry/crates/pnpm-registry/src/config.rs
PackageAccess fields (access, publish, unpublish) change from raw String to Option<AccessSpec> (untagged serde supporting single-string or sequence), and imports updated to use AccessList/PackagePolicy.
Policy compilation from config rules
registry/crates/pnpm-registry/src/config.rs
build_policies now compiles AccessSpecAccessList, applies $all/$authenticated defaults when omitted, and test assertions updated to use allows(...) for Identity::Anonymous and named users across matching, defaults, and string/sequence variants.
Server-side access control enforcement
registry/crates/pnpm-registry/src/server.rs
enforce_access derives an Identity from authentication state, selects the effective policy for package/action, and authorizes via list.allows(&identity); denies map to 401 for anonymous and 403 for authenticated-but-not-allowed.
Error handling and public exports
registry/crates/pnpm-registry/src/error.rs, registry/crates/pnpm-registry/src/lib.rs
Removes InvalidAccessRule error variant and its 400 mapping; updates pub use to export AccessList, AccessToken, and Identity instead of AccessRule.
Integration test for username rules
registry/crates/pnpm-registry/tests/policy.rs
Adds a test that configures YAML access for @team/* restricted to alice, asserting 401 for anonymous, 403 for non-matching bob, and allowed access for alice.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server
  participant Identity as identify()
  participant Policy as PackagePolicies
  participant AccessList
  Client->>Server: request (authenticated/anonymous)
  Server->>Identity: derive from auth state
  Identity-->>Server: User{username} or Anonymous
  Server->>Policy: for_package(name)
  Policy-->>Server: Effective { access, publish }
  Server->>AccessList: select by Action
  Server->>AccessList: allows(&identity)
  AccessList-->>Server: true/false
  alt allowed
    Server-->>Client: permit request
  else denied
    alt anonymous
      Server-->>Client: 401 Unauthenticated
    else authenticated
      Server-->>Client: 403 Forbidden
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11970: Modifies PackageAccess parsing in config.rs, related to this PR's change to AccessSpec/AccessList.
  • pnpm/pnpm#11977: Changes server-side auth wiring used by enforcement; relevant to the Identity integration here.
  • pnpm/pnpm#12043: Also updates YAML policy compilation and token parsing paths that overlap with these changes.

Suggested reviewers

  • KSXGitHub

Poem

🐰 I chewed the token one by one,

Now lists convene beneath the sun,
Anonymous hops, users stand in line,
Identity checks who's rightly signed,
A registry dance — permissions align.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main change: introducing per-user access lists with verdaccio-style groups support, matching the core refactor from AccessRule to AccessList with Identity-based authorization.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/registry-named-groups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zkochan zkochan marked this pull request as ready for review May 28, 2026 23:51
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Support multi-token access lists with per-user access control

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Model access/publish permissions as token lists instead of single rules
• Support per-user access by allowing usernames as valid tokens
• Accept both space-separated strings and YAML sequences for permissions
• Replace InvalidAccessRule error with flexible token parsing
• Introduce Identity type to represent caller authentication state
Diagram
flowchart LR
  Config["Config YAML<br/>access/publish fields"] -->|AccessSpec| Parse["Parse string or<br/>sequence form"]
  Parse -->|AccessList| Tokens["List of<br/>AccessTokens"]
  Tokens -->|evaluate| Identity["Caller Identity<br/>Anonymous/User"]
  Identity -->|satisfies| Allow["Permission<br/>allowed/denied"]

Loading

Grey Divider

File Changes

1. registry/crates/pnpm-registry/src/config.rs ✨ Enhancement +103/-62

Refactor config parsing for access lists

• Replace AccessRule import with AccessList
• Add AccessSpec enum to handle both string and sequence YAML forms
• Implement AccessSpec::to_access_list() to normalize both formats
• Update build_policies() to use AccessList::parse() instead of parse_access_rule()
• Remove parse_access_rule() function and InvalidAccessRule validation
• Update all tests to use Identity and allows() method instead of enum equality

registry/crates/pnpm-registry/src/config.rs


2. registry/crates/pnpm-registry/src/error.rs 🐞 Bug fix +0/-10

Remove InvalidAccessRule error variant

• Remove InvalidAccessRule error variant entirely
• Remove corresponding error display message
• Remove InvalidAccessRule from HTTP status code mapping

registry/crates/pnpm-registry/src/error.rs


3. registry/crates/pnpm-registry/src/lib.rs ✨ Enhancement +1/-1

Update public API exports

• Update public exports from AccessRule to AccessList, AccessToken, and Identity
• Expose new types for external use

registry/crates/pnpm-registry/src/lib.rs


View more (3)
4. registry/crates/pnpm-registry/src/policy.rs ✨ Enhancement +199/-88

Redesign access control with token lists and identity

• Replace AccessRule enum with AccessToken enum supporting built-ins and named tokens
• Add AccessList struct wrapping Vec with parse() and allows() methods
• Introduce Identity enum representing Anonymous or User { username }
• Implement Identity::satisfies() to match against tokens
• Update PackagePolicy to use AccessList instead of AccessRule
• Modify PackagePolicies to store default access/publish lists
• Change Effective struct to hold borrowed AccessList references
• Add comprehensive unit tests for token parsing, list evaluation, and identity matching

registry/crates/pnpm-registry/src/policy.rs


5. registry/crates/pnpm-registry/src/server.rs ✨ Enhancement +17/-16

Update access enforcement with identity-based checks

• Replace AccessRule import with Identity
• Refactor enforce_access() to construct Identity from auth headers
• Use list.allows(&identity) instead of pattern matching on rules
• Return 401 for denied anonymous callers, 403 for denied authenticated users

registry/crates/pnpm-registry/src/server.rs


6. registry/crates/pnpm-registry/tests/policy.rs 🧪 Tests +19/-0

Add per-user access integration test

• Add new integration test username_in_access_list_grants_only_that_user()
• Test per-user access with anonymous (401), wrong user (403), and correct user (allowed)
• Verify verdaccio-style username-based access control

registry/crates/pnpm-registry/tests/policy.rs


Grey Divider

Qodo Logo

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.68508% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.93%. Comparing base (a593082) to head (6c6adf4).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
registry/crates/pnpm-registry/src/policy.rs 94.23% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12048      +/-   ##
==========================================
+ Coverage   88.81%   88.93%   +0.12%     
==========================================
  Files         234      235       +1     
  Lines       30594    30871     +277     
==========================================
+ Hits        27172    27456     +284     
+ Misses       3422     3415       -7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@registry/crates/pnpm-registry/src/policy.rs`:
- Around line 25-45: The documentation for the AccessToken enum's Authenticated
and Anonymous variants is incomplete: the From<&str> implementation also accepts
the bare strings "authenticated" and "anonymous" (in addition to the $/@ forms),
so update the doc comments on Authenticated and Anonymous to mention the plain
aliases ("authenticated" and "anonymous") as accepted pseudo-group tokens;
locate the enum AccessToken and its variants Authenticated and Anonymous and
expand their doc comments to list all accepted forms to match the From<&str>
mapping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d544c330-5f02-4b49-a30c-34664c4fc5bf

📥 Commits

Reviewing files that changed from the base of the PR and between a593082 and 11a1d2f.

📒 Files selected for processing (6)
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/error.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/policy.rs
  • registry/crates/pnpm-registry/src/server.rs
  • registry/crates/pnpm-registry/tests/policy.rs
💤 Files with no reviewable changes (1)
  • registry/crates/pnpm-registry/src/error.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
🧰 Additional context used
📓 Path-based instructions (1)
registry/crates/**/*.rs

📄 CodeRabbit inference engine (registry/AGENTS.md)

Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, error handling, and test layout

Files:

  • registry/crates/pnpm-registry/tests/policy.rs
  • registry/crates/pnpm-registry/src/server.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/policy.rs
🔇 Additional comments (10)
registry/crates/pnpm-registry/src/policy.rs (4)

47-92: LGTM!


94-102: $anonymous is stricter here than in verdaccio — confirm the divergence is intended.

satisfies makes Anonymous match only logged-out callers. In verdaccio the anonymous group is granted to everyone: all users receive those groups independently of whether they are anonymous, so an authenticated user's group list still includes $anonymous/anonymous. So a migrated config using access: $anonymous to mean "everyone may read" would, under this implementation, deny authenticated readers. The doc on Lines 28-29 states this stricter behavior is deliberate — flagging only so the verdaccio-compatibility gap is a conscious choice.


105-204: LGTM!


206-307: LGTM!

registry/crates/pnpm-registry/src/config.rs (3)

10-10: LGTM!

Also applies to: 232-268


553-579: LGTM!


635-645: LGTM!

Also applies to: 1335-1452

registry/crates/pnpm-registry/src/server.rs (1)

20-20: LGTM!

registry/crates/pnpm-registry/src/lib.rs (1)

28-28: LGTM!

registry/crates/pnpm-registry/tests/policy.rs (1)

99-116: LGTM!

Comment thread registry/crates/pnpm-registry/src/policy.rs Outdated
AccessToken::from also accepts the unprefixed `authenticated` /
`anonymous` forms; document them on the variants so they aren't
misread as usernames.
@zkochan zkochan merged commit efcc336 into main May 29, 2026
22 checks passed
@zkochan zkochan deleted the feat/registry-named-groups branch May 29, 2026 00:17
zkochan added a commit that referenced this pull request May 29, 2026
* feat(registry): support multi-token access lists with usernames

Model each packages[*] access/publish permission as a list of tokens
(verdaccio's space-separated groups) instead of a single rule, and
match it against the caller's identity. Tokens are the built-in
pseudo-groups ($all/$authenticated/$anonymous and their @/bare
aliases) or a name; a name matches an authenticated caller whose
username equals it — verdaccio's per-user access, where the htpasswd
backend contributes the username and richer groups would come from a
group-providing auth backend.

- AccessRule (single enum) -> AccessList (Vec<AccessToken>) + Identity.
- Accept both a space-separated string and a YAML sequence for each
  permission, like verdaccio.
- Bare names are now valid tokens (drops the InvalidAccessRule
  stopgap from #12043 that rejected them).
- enforce_access: anonymous denial -> 401, authenticated denial -> 403.

Refs #11973.

* docs(registry): note the bare authenticated/anonymous token aliases

AccessToken::from also accepts the unprefixed `authenticated` /
`anonymous` forms; document them on the variants so they aren't
misread as usernames.
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.

2 participants