feat(FGAPv2): introduce RESET_PASSWORD scope and evaluation#41904
feat(FGAPv2): introduce RESET_PASSWORD scope and evaluation#41904Bagautdino wants to merge 0 commit intokeycloak:mainfrom Bagautdino:main
Conversation
| } | ||
|
|
||
| // Check if there is at least one policy referencing RESET_PASSWORD scope | ||
| if (hasResetPasswordPolicies()) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I will take a look and contribute the fallback. Let's see first what @vramik has to say about it.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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?
vramik
left a comment
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
The variable is not used, can we remove it?
| // FGAP v2 disabled or no admin-permissions client initialized -> legacy behavior | ||
| if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(root.realm) || root.realmResourceServer() == null) { |
There was a problem hiding this comment.
| // 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?
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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?
|
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👋 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. |
|
@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. |
|
@vramik @Bagautdino I did some updates to your branch:
Sorry for the mess, but if you can, please re-open your PR or send another one. I appreciate it. |
|
@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? |
|
@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 ... |
|
Okay! I'll do! just give me some time |
|
Follow-up continues in #42307, which restores and replaces this PR |
Closes #41901
Summary
This PR adds a dedicated
RESET_PASSWORDscope to theUSERSresource in FGAP v2 (Fine-Grained Admin Permissions) and updates evaluation so password reset rights are explicitly governed by policy rather than implicitly inherited fromMANAGE_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_PASSWORDtoAdminPermissionsSchema.USERS.Expose
canResetPassword()andrequireResetPassword()in user permissions API.Require
RESET_PASSWORDinUserResource.resetPassword()(instead ofMANAGE_USERS).Implement full FGAP v2 evaluation in
UserPermissionsV2:MANAGE_USERSviafgap.v2.resetPassword.fallbackToManageUsers(defaultfalse).Include
resetPasswordflag ingetAccess(user)for Admin Console UI (hides “Reset Password” button whenfalse).Preserve self-service password change (caller == target user) without FGAP checks.
Utility to detect policies referencing
RESET_PASSWORDscope.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_USERSalone no longer grants password reset.DENYalways overridesALLOW.When no
RESET_PASSWORDpolicies are present:fallback=false(default): deny by default.fallback=true: allow ifMANAGE_USERSis granted.Self-service flows are unaffected.
Configuration
fgap.v2.resetPassword.fallbackToManageUsers(boolean, defaultfalse).Admin Console / API
getAccess(user).resetPasswordadded; UI should hide the “Reset Password” action whenfalse.