-
Notifications
You must be signed in to change notification settings - Fork 993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #37936 - As a user, I want to invalidate jwt for specific user( UI ) #10357
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving the comment for the the proper review, this can be easily missed but I believe is important to get it right.
render :json => { :error => _("User %s does not exist.") % @user.login} | ||
elsif [email protected]?(:edit_users) | ||
deny_access N_("User %s does not have permissions to invalidate" % @user.login) | ||
else @user.can?(:edit_users) && @user.invalidate_jwt.blank? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is not enough. edit_users
permission may have specific conditions set. This would allow any user that can edit one specific user's profile to invalidate token for all users, introducing a privilege escalation.
If you look at can?
definition you can see it also accepts subject
which should the permission be verified on. We need to be checking that user can edit_users
on the specific @user
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack, I'll look into updating the subject
app/helpers/users_helper.rb
Outdated
@@ -33,14 +33,22 @@ def user_action_buttons(user, additional_actions = []) | |||
:method => :post, | |||
:data => { :no_turbolink => true }) | |||
end | |||
|
|||
if user != User.current | |||
additional_actions << display_link_if_authorized(_("Invalidate JWT"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the exact same problem like with can?
above, the auth object needs to be specified. If this is used on some users listing page, to avoid N+1 issue, an authorized object needs to be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am already adding :auth_object
here, is this what you're talking about?
37cd91b
to
eeb34c3
Compare
@@ -111,6 +111,23 @@ def update | |||
end | |||
end | |||
|
|||
api :PATCH, "/users/:id/invalidate_jwt", N_("Get vm attributes of host") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy-paste leftovers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, my bad
@@ -111,6 +111,23 @@ def update | |||
end | |||
end | |||
|
|||
api :PATCH, "/users/:id/invalidate_jwt", N_("Get vm attributes of host") | |||
param :id, String, :required => true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose you would want to have an array parameter here. To be able to invalidate tokens for multiple users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently in the scope, (atleast UI wise) we can invalidate tokens for a specific user or all users because otherwise I would have to add a select action in the UI. I was thinking, I can add another endpoint to invalidate all at once. Allowing multiple (but not all) users token invalidation can take some time to integrate
DOC | ||
|
||
def invalidate_jwt | ||
@user = User.find(params[:id]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@user = User.find(params[:id]) | |
@user = User.where(id: params[:id]).authorized(:edit_user) |
This will filter only the users that edit_user
permission applies to them, including custom filters.
Now you will have a set of users that you can safely delete the tokens for:
JwtSecret.where(user_id: @users).destroy_all
@@ -12,8 +12,8 @@ class UsersController < V2::BaseController | |||
['compute_attributes'] | |||
|
|||
before_action :find_optional_nested_object | |||
skip_before_action :authorize, :only => [:extlogin] | |||
before_action :authenticate, :only => [:extlogin] | |||
skip_before_action :authorize, :only => [:extlogin, :invalidate_jwt] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need to skip authorization for invalidate_jwt
action?
You can specify the action in the security block:
:"api/v2/users" => [:update] |
This will make sure the current user has the
edit_users
permission before accessing the endpoint.
app/controllers/users_controller.rb
Outdated
@@ -8,7 +8,7 @@ class UsersController < ApplicationController | |||
rescue_from ActionController::InvalidAuthenticityToken, with: :login_token_reload | |||
skip_before_action :require_mail, :only => [:edit, :update, :logout, :stop_impersonation] | |||
skip_before_action :require_login, :check_user_enabled, :authorize, :session_expiry, :update_activity_time, :set_taxonomy, :set_gettext_locale_db, :only => [:login, :logout, :extlogout] | |||
skip_before_action :authorize, :only => [:extlogin, :impersonate, :stop_impersonation] | |||
skip_before_action :authorize, :only => [:extlogin, :impersonate, :stop_impersonation, :invalidate_jwt] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same as for the API endpoint. You can specify the new action in the security block:
:users => [:edit, :update], |
Tested and does as needed, one small thing that acts weird to me is that a user cant invalidate their own jwt, so for example admin doesnt have the invalidate button, but a user with edit_users permission can invalidate admins tokens. |
It's because there will be a different button in the user details page for this, from UI Perspective, for a user to invalidate tokens for themselves, they will have it in the profile page |
@girijaasoni I think it looks a bit weird in the UI that other users have the invalidate button, but not the current (admin) user: |
|
bf39e67
to
7e62bf0
Compare
Test failures seem unrelated |
@@ -111,6 +112,25 @@ def update | |||
end | |||
end | |||
|
|||
api :PATCH, "/users/:id/invalidate_jwt", N_("Invalidate JWT for a specific userS") | |||
description <<-DOC | |||
Invalidate JWT for a specific users |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the description is one-liner, then I would go with description 'Invalidate JWT for a specific users'
app/controllers/users_controller.rb
Outdated
def invalidate_jwt | ||
@user = find_resource(:edit_users) | ||
User.find_by(id: @user.id).jwt_secret&.destroy | ||
if @user.jwt_secret.blank? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to have this if statement?
How can it happen that the jwt_secret
IS NOT blank?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean just like a safety check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean just like a safety check
Yes, but is it really needed?
How the User.find_by(id: @user.id).jwt_secret&.destroy
can result in @user.jwt_secret
not being blank, that's what I'm asking.
Failing tests are fixed in #10377 and fog/fog-libvirt#163 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestions for the microcopy. It would be great to get the singulars and plurals right.
@@ -111,6 +112,26 @@ def update | |||
end | |||
end | |||
|
|||
api :PATCH, "/users/:id/invalidate_jwt", N_("Invalidate JWT for a specific users") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api :PATCH, "/users/:id/invalidate_jwt", N_("Invalidate JWT for a specific users") | |
api :PATCH, "/users/:id/invalidate_jwt", N_("Invalidate all JWTs for the user") |
We're invalidating all JWTs for a single user, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for API, we are doing it for multiple users
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the user is identified by an ID that must be unique for each user. So how is it for multiple users?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for API, we are using the scoped search approach, so the search params in the curl command would be something like {"seach" : "id ^ (1,2,3)"}
translating to search for id in 1, 2 ,3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. In that case I suggest:
api :PATCH, "/users/:id/invalidate_jwt", N_("Invalidate JWT for a specific users") | |
api :PATCH, "/users/:id/invalidate_jwt", N_("Invalidate JWT for specific users") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you use the search param to get users, then there should not be any :id
in the URL.
api :PATCH, "/users/invalidate_jwt"
@@ -111,6 +112,26 @@ def update | |||
end | |||
end | |||
|
|||
api :PATCH, "/users/:id/invalidate_jwt", N_("Invalidate JWT for a specific users") | |||
description <<-DOC | |||
Invalidate JWT for a specific users |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invalidate JWT for a specific users | |
Invalidate all JSON Web Tokens (JWTs) for a user or users. | |
The users you specify will no longer be able to register hosts by using their JWTs. |
app/controllers/users_controller.rb
Outdated
User.find_by(id: @user.id).jwt_secret&.destroy | ||
if @user.jwt_secret.blank? | ||
process_success( | ||
:success_msg => _('Successfully invalidated JWT for %s.') % @user.login |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:success_msg => _('Successfully invalidated JWT for %s.') % @user.login | |
:success_msg => _('Successfully invalidated JWTs for %s.') % @user.login |
We're invalidating all of user's JWTs, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each user can have only one JWT. In my opinion, we should keep it singular.
app/helpers/users_helper.rb
Outdated
@@ -34,6 +34,13 @@ def user_action_buttons(user, additional_actions = []) | |||
:data => { :no_turbolink => true }) | |||
end | |||
|
|||
if user != User.current | |||
additional_actions << display_link_if_authorized(_("Invalidate JWT"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
additional_actions << display_link_if_authorized(_("Invalidate JWT"), | |
additional_actions << display_link_if_authorized(_("Invalidate JWTs"), |
else | ||
response = JwtSecret.where(user_id: @users).destroy_all | ||
if response.blank? | ||
process_success _('Successfully invalidated JWT for %s.' % @users.pluck(:login).to_sentence) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
process_success _('Successfully invalidated JWT for %s.' % @users.pluck(:login).to_sentence) | |
process_success _('Successfully invalidated JWTs for %s.' % @users.pluck(:login).to_sentence) |
|
||
def invalidate_jwt | ||
process_resource_error if params[:search].blank? | ||
@users = resource_scope_for_index(:permission => :edit_users) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to add except_hidden
scope to here too
app/controllers/users_controller.rb
Outdated
@@ -93,6 +93,14 @@ def impersonate | |||
end | |||
end | |||
|
|||
def invalidate_jwt | |||
@user = find_resource(:edit_users) | |||
User.find_by(id: @user.id).jwt_secret&.destroy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think that
User.find_by(id: @user.id).jwt_secret&.destroy | |
find_resource(:edit_users).jwt_secret&.destroy |
will do the trick
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add these to PersonalAccessTokensController
, not the users controller. That's already where we have all the resources to manage them.
https://restful-api-design.readthedocs.io/en/latest/ is an old document, but it's where I learned RESTful API design. Please read https://restful-api-design.readthedocs.io/en/latest/urls.html#url-structure to understand how to think about collections and resources. https://restful-api-design.readthedocs.io/en/latest/methods.html is another on the methods to use. It doesn't explicitly mention DELETE
on a (sub)collection, but that doesn't mean it can't be done. I'd argue it's well defined.
@@ -111,6 +112,22 @@ def update | |||
end | |||
end | |||
|
|||
api :PATCH, "/users/invalidate_jwt", N_("Invalidate all JSON Web Tokens (JWTs) for a user or users.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now looks like it invalidates it for all users and there's no way to specify it for users. REST API wise I think this is also poor design. If you have a user that's called invalidate_jwt
then it overlaps with other resources.
I'd expect DELETE /users/:id/jwt
to delete all JWTs for a specific user while for all JWTs it should operate on the JWT collection, so something like DELETE /jwt
. We currently don't have such an endpoint so you'd need to create that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a user with invalidate_jwt
would be an edge case
we are using scoped search approach in the API request so the user can use id or name or login anything for reference. From API POV, its valid for one specific user or multiple users as well. Request params would go like "{"search" : "id ^ (1, 2, 3)" } , does it make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @ShimShtein
@@ -96,7 +97,7 @@ def create | |||
api :PUT, "/users/:id/", N_("Update a user") | |||
description <<-DOC | |||
Adds role 'Default role' to the user if it is not already present. | |||
Only another admin can change the admin account attribute. | |||
Only another admin can change the admin account attribute. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep correct indenting.
Only another admin can change the admin account attribute. | |
Only another admin can change the admin account attribute. |
:users => [:edit, :update, :invalidate_jwt], | ||
:"api/v2/users" => [:update, :invalidate_jwt] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does invalidate_jwt
come from? I don't see it defined in PermissionsList
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's an action in userscontrollers, do we need to add it in the permissions list?
As per further discussion, we have decided to split the UI and the API PR. @ekohl suggests we add a separate controller for JWTs. It will be updated in the next PR |
@ekohl This one looks good. Feel free to merge, if it's OK with you |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Microcopy looks good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🍏 CI
🍏 Tests
🍏 Code
One scenario doesn't work for me, and the other is to think about.
1 - Users with the Viewer
role cannot invalidate tokens for themselves.
2 - If users don't have view|edit _users
permissions, how can they invalidate their own tokens? There are no actions at My Account
page.
This PR Is for the story "User to invalidate tokens for specific(other) users" , I will be updating invalidating self in the next PR as its a separate story |
setup_user 'edit', 'users' | ||
user = users(:two) | ||
FactoryBot.build(:jwt_secret, token: 'test_jwt_secret', user: user) | ||
patch :invalidate_jwt, params: { :id => user.id }, session: set_session_user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set_session_user
use :admin
user as default. The test is using admin
user instead of "non-admin"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🍏 LGTM, failing tests are not related.
Let's get this in.
All comments have been addressed.
Thanks @girijaasoni and everyone involved. |
Link to API PR: #10397