Skip to content
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

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

girijaasoni
Copy link
Contributor

@girijaasoni girijaasoni commented Oct 21, 2024

No description provided.

Copy link
Member

@ares ares left a 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 !@user.can?(:edit_users)
deny_access N_("User %s does not have permissions to invalidate" % @user.login)
else @user.can?(:edit_users) && @user.invalidate_jwt.blank?
Copy link
Member

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.

Copy link
Contributor Author

@girijaasoni girijaasoni Oct 22, 2024

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

@@ -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"),
Copy link
Member

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.

Copy link
Contributor Author

@girijaasoni girijaasoni Oct 24, 2024

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?

@@ -111,6 +111,23 @@ def update
end
end

api :PATCH, "/users/:id/invalidate_jwt", N_("Get vm attributes of host")
Copy link
Member

Choose a reason for hiding this comment

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

Copy-paste leftovers?

Copy link
Contributor Author

@girijaasoni girijaasoni Oct 29, 2024

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
Copy link
Member

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.

Copy link
Contributor Author

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])
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@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]
Copy link
Member

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.

@@ -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]
Copy link
Member

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],

@MariaAga
Copy link
Member

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.

@girijaasoni
Copy link
Contributor Author

girijaasoni commented Oct 29, 2024

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

@MariaAga
Copy link
Member

@girijaasoni I think it looks a bit weird in the UI that other users have the invalidate button, but not the current (admin) user:
admin:
Screenshot from 2024-10-29 11-54-26
other user:
Screenshot from 2024-10-29 11-53-41

@girijaasoni
Copy link
Contributor Author

girijaasoni commented Oct 29, 2024

@girijaasoni I think it looks a bit weird in the UI that other users have the invalidate button, but not the current (admin)
Some users don't have edit_users permissions and they shall still be able to invalidate tokens for themselves which is why it was suggested that we have the invalidate option in the user details page if the user wants to do it for self.

@girijaasoni
Copy link
Contributor Author

Test failures seem unrelated

app/controllers/api/v2/users_controller.rb Outdated Show resolved Hide resolved
@@ -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
Copy link
Contributor

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 Show resolved Hide resolved
def invalidate_jwt
@user = find_resource(:edit_users)
User.find_by(id: @user.id).jwt_secret&.destroy
if @user.jwt_secret.blank?
Copy link
Contributor

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?

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 mean just like a safety check

Copy link
Contributor

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.

Comment on lines 121 to 123
if params[:search].blank?
process_resource_error
end
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
if params[:search].blank?
process_resource_error
end
process_resource_error if params[:search].blank?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ack

render :json => { :error => _("No record found for '%s'") % params[:search]}
else
response = JwtSecret.where(user_id: @users).destroy_all
if response.blank?
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the if response.blank? needed? In which cases is it not blank?

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 mean it's optional, just like for safety. I'm open to remove it on vote? what do you think @ShimShtein

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, I would like to see the reasoning behind the check.
I want to know which scenario the response is not blank in.

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 don't think there would be a scenario as such.
I can remove the if condition

@stejskalleos
Copy link
Contributor

Failing tests are fixed in #10377 and fog/fog-libvirt#163

Copy link

@Lennonka Lennonka left a 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")

Choose a reason for hiding this comment

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

Suggested change
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?

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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.

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:

Suggested change
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")

Copy link
Contributor

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
Copy link

@Lennonka Lennonka Nov 20, 2024

Choose a reason for hiding this comment

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

Suggested change
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.

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

Choose a reason for hiding this comment

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

Suggested change
: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?

@@ -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"),

Choose a reason for hiding this comment

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

Suggested change
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)

Choose a reason for hiding this comment

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

Suggested change
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)
Copy link
Member

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

@@ -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
Copy link
Member

Choose a reason for hiding this comment

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

I still think that

Suggested change
User.find_by(id: @user.id).jwt_secret&.destroy
find_resource(:edit_users).jwt_secret&.destroy

will do the trick

additional_actions << display_link_if_authorized(_("Invalidate JWTs"),
hash_for_invalidate_jwt_user_path(:id => user.id).merge(:auth_object => user, :permission => "edit_users"),
:method => :patch, :id => user.id,
:data => { :confirm => _("Invalidate tokens for %s?") % user.name })
Copy link

@Lennonka Lennonka Nov 26, 2024

Choose a reason for hiding this comment

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

Suggested change
:data => { :confirm => _("Invalidate tokens for %s?") % user.name })
:data => { :confirm => _("Invalidate all JSON Web Tokens for %s?") % user.name })

I strongly suggest using "JWTs" here or spelling them out because "tokens" might get confused with Personal Access Tokens.

Copy link
Member

@ekohl ekohl left a 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.")
Copy link
Member

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.

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 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

Please keep correct indenting.

Suggested change
Only another admin can change the admin account attribute.
Only another admin can change the admin account attribute.

Comment on lines 565 to 566
:users => [:edit, :update, :invalidate_jwt],
:"api/v2/users" => [:update, :invalidate_jwt]
Copy link
Member

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

Copy link
Contributor Author

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?

@girijaasoni girijaasoni changed the title Fixes #37936 - As a user, I want to invalidate jwt for specific user Fixes #37936 - As a user, I want to invalidate jwt for specific user( UI ) Nov 28, 2024
@girijaasoni
Copy link
Contributor Author

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
CC: @ShimShtein

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

Successfully merging this pull request may close these issues.

7 participants