Skip to content

Add resource-oriented v2 APIs for Basic Auth users and RBAC roles/permissions - #4916

Draft
epugh wants to merge 5 commits into
apache:mainfrom
epugh:security-v2-resource-apis
Draft

epugh wants to merge 5 commits into
apache:mainfrom
epugh:security-v2-resource-apis

Conversation

@epugh

@epugh epugh commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Solr's existing v2 security endpoints (/api/cluster/security/authentication, /api/cluster/security/authorization) are command-batch-over-POST — the same set-user/set-permission/etc. commands as v1, just reachable at a v2 URL, not genuine resource-oriented REST. This PR adds a real resource-oriented v2 surface alongside the existing one, following the same JAX-RS/Jersey pattern already used by ClusterPropertyApis/AliasPropertyApis:

  • GET/PUT/DELETE /api/cluster/security/authentication/{scheme}/users/{username}
  • GET/PUT/DELETE /api/cluster/security/authorization/{scheme}/roles/{username}
  • GET/POST/PUT/DELETE /api/cluster/security/authorization/permissions/{index}

The {scheme} path segment lets these APIs work correctly under MultiAuthPlugin/MultiAuthRuleBasedAuthorizationPlugin (routing to the right sub-plugin's config), and is simply ignored for a plain BasicAuthPlugin/RuleBasedAuthorizationPlugin setup. Permissions have no such segment — MultiAuthRuleBasedAuthorizationPlugin.edit() already treats every *-permission command as shared across all schemes rather than per-scheme, so no wrapping is needed there.

All mutations still funnel through SecurityConfHandler's existing CommandOperation/ConfigEditablePlugin machinery (extracted here into a new public editSecurityConfig() shared with the legacy v1/v2 command endpoints) — this is additive REST surface, not a rewrite of the security model or security.json semantics.

Also included:

  • Wires the Admin UI's Security screen (security.js/services.js) to the new endpoints for user/role create/update/delete, following the existing AliasesV2-style generated-client pattern.
  • Documents the new endpoints in the ref guide (basic-authentication-plugin.adoc, rule-based-authorization-plugin.adoc).
  • Fixes a latent doc gap found along the way: rule-based-authorization-plugin.adoc was missing v2 URL examples for the existing command-batch authorization commands.

Test plan

  • SecurityV2ApiStandaloneTest — standalone-mode HTTP tests for all three resources via the generated SolrJ client (create/list/update/delete, including 401/404/409 error cases)
  • SecurityV2ApiCloudTest — SolrCloud-mode coverage; also catches and regression-tests a ClassCastException found via manual testing (Utils.getDeepCopy(..., mutable=false) wraps nested lists in Collections.unmodifiableCollection, not unmodifiableList, on the ZK-cached read path)
  • MultiAuthUsersAndRolesApiCloudTest — a genuine 2-scheme MultiAuthPlugin/MultiAuthRuleBasedAuthorizationPlugin cluster proving users/roles written to one scheme never leak into another
  • Manually verified end-to-end in a browser against a live Basic-Auth-enabled Solr instance (Admin UI Security screen: add/edit/delete user, set/clear roles, delete permission)
  • Existing security test suites (SecurityConfHandlerTest, V2SecurityAPIMappingTest, BasicAuthStandaloneTest, MultiAuthPluginTest, BasicAuthOnSingleNodeTest) pass unchanged
  • ./gradlew spotlessCheck forbiddenApisMain clean
  • Ref guide builds clean (./gradlew :solr:solr-ref-guide:checkSite)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kapco1bnqFBMkFHFPF6MFL

…missions

Solr's existing v2 security endpoints are command-batch-over-POST (the same
"set-user"/"set-permission"/etc. commands as v1, just reachable at a v2 URL)
rather than true resource-oriented REST. This adds a genuine v2 surface
alongside it, following the same JAX-RS/Jersey pattern already used by
ClusterPropertyApis/AliasPropertyApis:

- GET/PUT/DELETE /api/cluster/security/authentication/{scheme}/users/{username}
- GET/PUT/DELETE /api/cluster/security/authorization/{scheme}/roles/{username}
- GET/POST/PUT/DELETE /api/cluster/security/authorization/permissions/{index}

The {scheme} segment lets these APIs work under MultiAuthPlugin/
MultiAuthRuleBasedAuthorizationPlugin (routing to the right sub-plugin), and
is simply ignored for a plain BasicAuthPlugin/RuleBasedAuthorizationPlugin
setup. Permissions have no such segment - MultiAuthRuleBasedAuthorizationPlugin
already treats *-permission commands as shared across every scheme.

All mutations still funnel through SecurityConfHandler's existing
CommandOperation/ConfigEditablePlugin machinery (extracted here into a new
public editSecurityConfig() shared with the legacy v1/v2 command endpoints),
so this is additive REST surface, not a rewrite of the security model.

Also wires the Admin UI's Security screen to the new endpoints (falling back
to nothing now needed - the scheme parameter means the same call path works
for both plain and multi-auth setups), and documents the new endpoints in
the ref guide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kapco1bnqFBMkFHFPF6MFL
@github-actions github-actions Bot added documentation Improvements or additions to documentation admin-ui tests cat:api labels Sep 16, 2026
epugh and others added 4 commits September 16, 2026 08:48
Assigning a role to its users and granting it permissions previously went
through the legacy set-user-role/set-permission/update-permission
command-batch calls. Migrate both to the resource-oriented v2 endpoints:
one AuthorizationV2.setUserRoles PUT per selected user (fanned out, since
the new API is per-user rather than the old bulk multi-user command), and
AuthorizationV2.createPermission/updatePermission for granting the role to
each selected permission.

Verified live against a running Basic-Auth-enabled Solr instance: creating a
role, assigning it to two users, and granting it a predefined permission all
hit the new /authorization/basic/roles/{username} and
/authorization/permissions/{index} endpoints with zero console errors, and
the resulting state matches what the legacy path used to produce.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kapco1bnqFBMkFHFPF6MFL
…of a new one

The previous version introduced a second, bespoke polling primitive
(pollUntil/userHasRole/permissionHasRole) run once per user and once per
permission, on top of the pre-existing whenReflected helper the rest of
this file already uses to wait out ZK-cache propagation after a write.
That's needless duplication for a real concern (the need to wait at all
predates this change entirely - it's inherent to SecurityConfHandler's
cached ZK reads, not something the v2 API introduced).

Collapse back to a single whenReflected("authorization", ...) check after
all the per-user/per-permission v2 writes have returned, exactly like the
legacy command-batch code did - just checking the users/permissions this
dialog actually touched. Also fixes a real bug the first version would have
had: only wait on permissions actually written (existing or newly-created
predefined ones), not ones silently skipped as custom-and-nonexistent,
which would otherwise never converge.

Verified live: assigning a role to a user while both updating an existing
permission and creating a brand-new predefined one now produces exactly
one PUT/POST per write plus two whenReflected polls against the legacy
blob GET (down from a poll per resource), with correct resulting state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kapco1bnqFBMkFHFPF6MFL
Adds a list-every-user's-roles endpoint alongside the existing per-user
getUserRoles/setUserRoles/deleteUserRoles, returning {"userRoles": {user:
[role, ...]}}. This is the missing piece for building a UI table of all
role assignments without one request per user - the per-user GET has no
bulk equivalent today.

Same scheme-isolation guarantees as the rest of the Roles API: under
MultiAuthRuleBasedAuthorizationPlugin each scheme's mappings stay separate,
verified in MultiAuthUsersAndRolesApiCloudTest; under a plain
RuleBasedAuthorizationPlugin the scheme segment is ignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kapco1bnqFBMkFHFPF6MFL
…polling

These three classes' internal reads were all calling
SecurityConfHandler#getSecurityConfig(false) - the cached ZK snapshot path,
refreshed asynchronously by a watcher after any write - purely because
that's what the legacy command-batch GET does, not because it was actually
necessary. Under SolrCloud, a GET immediately following a PUT/POST/DELETE
could observe the pre-write state until that watcher fires.

Switch all three to getSecurityConfig(true) (a no-op for standalone's
SecurityConfHandlerLocal, which always reads the file fresh regardless).
Since editSecurityConfig only returns 200 once the write is durably
persisted, a fresh read afterward is now guaranteed to observe it - no
client needs to poll-until-reflected to know when a write "landed."
Documented the getFresh semantics on SecurityConfHandler#getSecurityConfig
itself, since this wasn't written down anywhere before.

SecurityV2ApiCloudTest's two tests no longer need the poll loop they had for
exactly this reason - confirmed by 5 consecutive clean runs before removing
it, and renamed them to describe what they now actually verify.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kapco1bnqFBMkFHFPF6MFL
@epugh

epugh commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

I am going to have to break this up!

* MultiAuthPlugin} isn't in use - a plain {@code BasicAuthPlugin} setup has only one set of users,
* and any value may be supplied (conventionally "basic").
*/
@Path("/cluster/security/authentication/{scheme}/users")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was thinking that scheme is basic or jwt or certificate... But then below it says "LIst the usernames configured for Basic autnetication... Need to rethink the summary. (originaly all of this was just for basic with no scheme.

import org.apache.solr.client.api.model.SolrJerseyResponse;

/**
* Definitions for v2 JAX-RS APIs managing Basic Authentication users.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Need to rethinkg Basic everyhwere in the comments!

import org.apache.solr.client.api.model.SolrJerseyResponse;

/**
* Definitions for v2 JAX-RS APIs managing Rule-Based Authorization permissions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

will this work for ExternalRoleRuleBasedAuthorizationPlugin and MultiAuthRuleBasedAuthorizationPlugin ??? Or do we need to nest here?

/**
* Definitions for v2 JAX-RS APIs managing Rule-Based Authorization permissions.
*
* <p>Resource-oriented alternative to the {@code set-permission}/{@code update-permission}/{@code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we are ripping out set-permission and update-permission in v2, so don't need that.

@PUT
@Path("/{username}")
@Operation(
summary = "Create a new user, or change an existing user's password.",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

why are se not having POST for the create and PUT for the chagne? We do later on for permissions!

* reads security.json fresh from disk regardless of this flag, so this behavior needs cloud
* coverage specifically to mean anything.
*
* <p>This also incidentally guards against a real bug this suite caught during development: the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

too much commentry on the changes

[source,bash]
----
curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authorization -H 'Content-type:application/json' -d '{
"set-permission": {"collection": null,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

umm, why do we have set-permission here? In v2 we use the HTTP verb and the end point, no commands liek this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

is this maybe left over from the first pass where we asked Claude to fill in documentation gaps and whe had old style V2?

[source,bash]
----
curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authorization -H 'Content-type:application/json' -d '{
"update-permission": {"index": 3,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

old style v2

====
[source,bash]
----
curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authorization -H 'Content-type:application/json' -d '{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

old style v2

var strongPasswordRegex = /^(?=.*[0-9])(?=.*[!@#$%^&*\-_()[\]])[a-zA-Z0-9!@#$%^&*\-_()[\]]{8,30}$/;

// The Users/Roles v2 APIs address the authentication/authorization scheme they operate on via
// this path segment - this panel only ever manages the "basic" scheme (see multiAuthWithBasic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

interesting, I ddin't know that it only handles BASIC

$scope.toggleRoleDialog();
var userTasks = usersForRole.map(u => cb => assignRoleToUser(u, cb));
if (perms.length === 0) {
runTasks(userTasks, () => finishUp([]));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we may not need finishUp after we tweaked how we interact with Zookeeper to not ever use a cached copy of the security.json file!

whenReflected("authorization", roleReflected, $scope.refreshSecurityPanel);
return;
}
// Once every write above has returned, this is the same single whenReflected("authorization",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We may not need this now that our API calls don't ever use a cached security.json file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

admin-ui cat:api documentation Improvements or additions to documentation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant