Skip to content

feat(FGAPv2): introduce RESET_PASSWORD scope and evaluation#41904

Closed
Bagautdino wants to merge 0 commit intokeycloak:mainfrom
Bagautdino:main
Closed

feat(FGAPv2): introduce RESET_PASSWORD scope and evaluation#41904
Bagautdino wants to merge 0 commit intokeycloak:mainfrom
Bagautdino:main

Conversation

@Bagautdino
Copy link
Contributor

Closes #41901

Summary

This PR adds a dedicated RESET_PASSWORD scope to the USERS resource in FGAP v2 (Fine-Grained Admin Permissions) and updates evaluation so password reset rights are explicitly governed by policy rather than implicitly inherited from MANAGE_USERS.

Motivation

Today, password resets can be implicitly allowed via MANAGE_USERS, which limits governance and auditing. With FGAP v2 enabled, administrators should be able to allow or deny password resets per user/group with explicit policies and deny-overrides semantics.

What’s included

  • Add RESET_PASSWORD to AdminPermissionsSchema.USERS.

  • Expose canResetPassword() and requireResetPassword() in user permissions API.

  • Require RESET_PASSWORD in UserResource.resetPassword() (instead of MANAGE_USERS).

  • Implement full FGAP v2 evaluation in UserPermissionsV2:

    • Deny-overrides decision model.
    • Secure-by-default when policies exist.
    • Optional fallback to legacy MANAGE_USERS via
      fgap.v2.resetPassword.fallbackToManageUsers (default false).
  • Include resetPassword flag in getAccess(user) for Admin Console UI (hides “Reset Password” button when false).

  • Preserve self-service password change (caller == target user) without FGAP checks.

  • Utility to detect policies referencing RESET_PASSWORD scope.

  • Enhanced auditing/logging for allow/deny decisions.

  • Unit and integration tests covering all decision paths.

Behavior changes

  • With FGAP v2 enabled and at least one policy referencing RESET_PASSWORD:

    • MANAGE_USERS alone no longer grants password reset.
    • Explicit DENY always overrides ALLOW.
  • When no RESET_PASSWORD policies are present:

    • fallback=false (default): deny by default.
    • fallback=true: allow if MANAGE_USERS is granted.
  • Self-service flows are unaffected.

Configuration

  • fgap.v2.resetPassword.fallbackToManageUsers (boolean, default false).

Admin Console / API

  • getAccess(user).resetPassword added; UI should hide the “Reset Password” action when false.

}

// Check if there is at least one policy referencing RESET_PASSWORD scope
if (hasResetPasswordPolicies()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

For some, you might want to enforce access managed on the manage scope. I think we should try first the check with reset-password scope and if it fails, we then check manage.

There is a performance penalty with this approach, but perhaps it covers more use cases and avoid forcing people to always set reset-password if the manage scope is enough.

@vramik Can you also give your thoughts on this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is my first time working with this part of Keycloak, so I initially approached the implementation with the goal of making password reset permissions more granular and policy-driven. I wanted RESET_PASSWORD to be a distinct capability, so that realm admins with MANAGE_USERS wouldn't automatically get this right unless allowed explicitly via a policy.
That said, I totally understand that falling back to MANAGE_USERS when no RESET_PASSWORD policies are defined makes the behavior more compatible by default.
If this approach is preferred, I’d really appreciate if someone from the team could help adjust the fallback logic in the PR. But I’m also happy to update it myself if needed.

Copy link
Contributor

@pedroigor pedroigor Aug 18, 2025

Choose a reason for hiding this comment

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

I will take a look and contribute the fallback. Let's see first what @vramik has to say about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @pedroigor and @Bagautdino, thanks for the discussion on this.

I've been thinking about the implicit fallback from RESET_PASSWORD to MANAGE_USERS. While I understand the goal of backward compatibility, I'm a bit concerned that an implicit fallback could be error-prone and harder to maintain in the long run. It couples two distinct permissions, which might lead to confusion for administrators who expect RESET_PASSWORD to be the single source of truth for this capability.

I believe we should follow opt-in approach when dealing with fallbackToManageUsers. As far as I am aware this is also preferred way how to introduce behavioral changes. What are your thoughts on it?

This leads me to a broader question for @pedroigor: what are the team's general policies around changing default settings? With an explicit option in place, perhaps we could plan to switch the default to a more secure setting (i.e., fallback disabled) in a future major release?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @vramik, thanks a lot for the detailed feedback 🙏

I’ve updated the implementation to follow the opt-in fallback approach:
If there are any RESET_PASSWORD policies, they are the single source of truth (deny-overrides, secure-by-default).
Only when no policies exist, and the flag fgap.v2.resetPassword.fallbackToManageUsers=true is set, we fallback to MANAGE_USERS.
With no policies and the flag disabled (default), reset password is denied.

Copy link
Contributor

@vramik vramik Aug 28, 2025

Choose a reason for hiding this comment

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

Thanks a lot, I probably didn't express myself good enough. Sorry about that. What I mean by opt-in approach is something like this

    @Override
    public boolean canResetPassword(UserModel user) {
		// admin roles has the precedence over permissions
		if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
            return true;
        }

		// by default use legacy behavior - with the aim to change the default in next major release
		boolean fallbackToManage = Config.scope("fgap", "v2", "resetPassword").getBoolean("fallbackToManageUsers", true);

		if (fallbackToManage) {
			return eval.hasPermission(new UserModelRecord(user), null, AdminPermissionsSchema.MANAGE);
		}

		return eval.hasPermission(new UserModelRecord(user), null, AdminPermissionsSchema.RESET_PASSWORD);
	}

So that user who would like to leverage the RESET_PASSWORD permission, would need to pass the fallbackToManageUsers=false option. In next version we might be able to switch the default to false, it should provide users some time to leverage new behavior. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, thanks for clarifying 🙏 I’ve updated the implementation to follow this opt-in fallback approach with fallbackToManageUsers=true as the current default. Let me know if you’d like any further adjustments 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the late reply. I'm not sure about introducing the fallbackToManageUsers. For me, we can just check if the reset password scope is set, and if not we check the manager scope.

Or is there any backward compatibility issue with this approach?

Copy link
Contributor

@vramik vramik left a comment

Choose a reason for hiding this comment

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

Thank you @Bagautdino for preparing the PR. Overall it looks great. I have only few nitpicks. and the reply to the discussion about fallback mechanism.

private static final String IMPERSONATE_SCOPE="impersonate";
private static final String USER_IMPERSONATED_SCOPE="user-impersonated";
private static final String MANAGE_GROUP_MEMBERSHIP_SCOPE="manage-group-membership";
private static final String RESET_PASSWORD_SCOPE="reset-password";
Copy link
Contributor

Choose a reason for hiding this comment

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

The variable is not used, can we remove it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Comment on lines 156 to 157
// FGAP v2 disabled or no admin-permissions client initialized -> legacy behavior
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(root.realm) || root.realmResourceServer() == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// FGAP v2 disabled or no admin-permissions client initialized -> legacy behavior
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(root.realm) || root.realmResourceServer() == null) {
// FGAP v2 disabled for a realm -> legacy behavior
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(root.realm)) {

isAdminPermissionsEnabled(root.realm) also tests for the realm being null and returns false in that case, it seems that the second part of the if is redundant. wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ve removed the redundant root.realmResourceServer() == null check and simplified the condition as suggested. Thanks!

}

// Check if there is at least one policy referencing RESET_PASSWORD scope
if (hasResetPasswordPolicies()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @pedroigor and @Bagautdino, thanks for the discussion on this.

I've been thinking about the implicit fallback from RESET_PASSWORD to MANAGE_USERS. While I understand the goal of backward compatibility, I'm a bit concerned that an implicit fallback could be error-prone and harder to maintain in the long run. It couples two distinct permissions, which might lead to confusion for administrators who expect RESET_PASSWORD to be the single source of truth for this capability.

I believe we should follow opt-in approach when dealing with fallbackToManageUsers. As far as I am aware this is also preferred way how to introduce behavioral changes. What are your thoughts on it?

This leads me to a broader question for @pedroigor: what are the team's general policies around changing default settings? With an explicit option in place, perhaps we could plan to switch the default to a more secure setting (i.e., fallback disabled) in a future major release?

@Bagautdino
Copy link
Contributor Author

I’ve updated the PR. If you’d like me to change anything else, just let me know 👍

@vramik
Copy link
Contributor

vramik commented Aug 28, 2025

I’ve updated the PR. If you’d like me to change anything else, just let me know 👍

Thanks a lot, great job. There are few points from top of my head we need to consider before merging it.

@Bagautdino
Copy link
Contributor Author

I’ve updated the PR. If you’d like me to change anything else, just let me know 👍

Thanks a lot, great job. There are few points from top of my head we need to consider before merging it.

Hi @vramik again👋
I’ve updated the documentation (Server Admin Guide and Upgrading Guide) according to your comments.
Could you please check if everything looks correct?

Regarding group permissions, I think it’s better to address that in a separate follow-up PR. This way, the current PR stays focused and easier to review, and I’ll handle support for RESET_PASSWORD group policies (with tests) in the next PR.

Thanks a lot! 🙏

@vramik
Copy link
Contributor

vramik commented Sep 2, 2025

Hi @vramik again👋 I’ve updated the documentation (Server Admin Guide and Upgrading Guide) according to your comments. Could you please check if everything looks correct?

Regarding group permissions, I think it’s better to address that in a separate follow-up PR. This way, the current PR stays focused and easier to review, and I’ll handle support for RESET_PASSWORD group policies (with tests) in the next PR.

Thanks a lot! 🙏

Awesome job @Bagautdino, I agree that possible addition to allow setup reset-password for group resource should be handled in follow-up PR.

The PR looks almost ready to merge. I just realized it's missing test coverage (my apologies for not catching that sooner). Do you have the capacity to add tests?

Alternatively I can contribute the tests, probably as a PR to you branch.

@pedroigor pedroigor closed this Sep 2, 2025
@pedroigor
Copy link
Contributor

@vramik @Bagautdino Sorry, I'm working on some changes to your branch, and GH closed the PR. Will let you know when I'm done.

@pedroigor
Copy link
Contributor

@vramik @Bagautdino I did some updates to your branch:

  • Added the fallback, where the fallback is only executed if there are no permissions associated with the target scope (reset-password).
  • Added a test

Sorry for the mess, but if you can, please re-open your PR or send another one. I appreciate it.

@pedroigor
Copy link
Contributor

@Bagautdino I really messed up your branch. As you are working on main, I accidentally pushed the wrong changeset to your branch.

Hopefully you can force-push the commits in your local branch?

@pedroigor
Copy link
Contributor

@Bagautdino Sent #42298. Can you please check it out? Please, feel free to rebase your branch with that one if you prefer to send/re-open a PR from your side.

Sorry ...

@Bagautdino
Copy link
Contributor Author

Okay! I'll do! just give me some time

@Bagautdino
Copy link
Contributor Author

Follow-up continues in #42307, which restores and replaces this PR

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.

FGAP v2: RESET_PASSWORD capability for USERS

3 participants