diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb index 12fc971a351..2d82ea9eb15 100644 --- a/app/controllers/api/v2/users_controller.rb +++ b/app/controllers/api/v2/users_controller.rb @@ -12,8 +12,9 @@ class UsersController < V2::BaseController ['compute_attributes'] before_action :find_optional_nested_object + before_action :setup_search_options, :only => [:invalidate_jwt] skip_before_action :authorize, :only => [:extlogin] - before_action :authenticate, :only => [:extlogin] + before_action :authenticate, :only => [:extlogin, :invalidate_jwt] api :GET, "/users/", N_("List all users") api :GET, "/auth_source_ldaps/:auth_source_ldap_id/users", N_("List all users for LDAP authentication source") @@ -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. DOC param :id, String, :required => true param_group :user_update @@ -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.") + description <<-DOC + The users you specify will no longer be able to register hosts by using their JWTs. + DOC + + def invalidate_jwt + process_resource_error if params[:search].blank? + @users = resource_scope_for_index(:permission => :edit_users) + if @users.empty?() + render :json => { :error => _("No record found for '%s'") % params[:search]} + else + JwtSecret.where(user_id: @users).destroy_all + process_success _('Successfully invalidated JWTs for %s.' % @users.pluck(:login).to_sentence) + end + end + api :DELETE, "/users/:id/", N_("Delete a user") param :id, String, :required => true diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d95cdd90b8c..2337c395d7d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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 + process_success( + :success_msg => _('Successfully invalidated JWTs for %s.') % @user.login + ) + end + def stop_impersonation if session[:impersonated_by].present? user = User.unscoped.find_by_id(session[:impersonated_by]) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index a782a77ce1c..078b2e37e6a 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -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 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 }) + end + delete_btn = display_delete_if_authorized( hash_for_user_path(:id => user).merge(:auth_object => user, :authorizer => authorizer), :data => { :confirm => _("Delete %s?") % user.name }) diff --git a/config/initializers/f_foreman_permissions.rb b/config/initializers/f_foreman_permissions.rb index 4f9146c7b23..9a3955a7ac8 100644 --- a/config/initializers/f_foreman_permissions.rb +++ b/config/initializers/f_foreman_permissions.rb @@ -562,8 +562,8 @@ :users => [:new, :create], :"api/v2/users" => [:create] map.permission :edit_users, - :users => [:edit, :update], - :"api/v2/users" => [:update] + :users => [:edit, :update, :invalidate_jwt], + :"api/v2/users" => [:update, :invalidate_jwt] map.permission :destroy_users, :users => [:destroy], :"api/v2/users" => [:destroy] diff --git a/config/routes.rb b/config/routes.rb index dc9ebb97c6c..e2723eee05b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -256,6 +256,7 @@ end member do post 'impersonate' + patch 'invalidate_jwt' end resources :ssh_keys, only: [:new, :create, :destroy] end diff --git a/config/routes/api/v2.rb b/config/routes/api/v2.rb index 3eca161f873..47f1ef1c60f 100644 --- a/config/routes/api/v2.rb +++ b/config/routes/api/v2.rb @@ -218,6 +218,7 @@ resources :mail_notifications, :only => [:create, :destroy, :update] get 'mail_notifications', :to => 'mail_notifications#user_mail_notifications', :on => :member get 'extlogin', :to => 'users#extlogin', :on => :collection + patch 'invalidate_jwt', :to => 'users#invalidate_jwt', :on => :collection end end diff --git a/test/controllers/api/v2/users_controller_test.rb b/test/controllers/api/v2/users_controller_test.rb index 8a9a3c765dc..06883b0541f 100644 --- a/test/controllers/api/v2/users_controller_test.rb +++ b/test/controllers/api/v2/users_controller_test.rb @@ -141,6 +141,33 @@ def setup assert mod_user.matching_password?("changeme") end + test 'user with edit permission should be able to invalidate jwt for another user' do + setup_user 'edit', 'users' + user = users(:two) + FactoryBot.build(:jwt_secret, token: 'test_jwt_secret', user: user) + patch :invalidate_jwt, params: { :search => "id ^ (#{user.id})"} + user.reload + assert_response :success + end + + test 'user without edit permission should not be able to invalidate jwt for another user' do + User.current = users(:one) + user = users(:two) + FactoryBot.build(:jwt_secret, token: 'test_jwt_secret', user: user) + patch :invalidate_jwt, params: { :search => "id ^ #{user.id}" } + user.reload + assert_response :forbidden + end + + test 'invalidating jwt should fail without search params' do + setup_user 'edit', 'users' + user = users(:two) + FactoryBot.build(:jwt_secret, token: 'test_jwt_secret', user: user) + patch :invalidate_jwt + user.reload + assert_response :error + end + test "should delete different user" do user = users(:one) diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 49d1929eb1b..d4d84bf2481 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -158,6 +158,33 @@ class UsersControllerTest < ActionController::TestCase assert !User.exists?(user.id) end + test "Admin should be able to invalidate jwt for any user" do + User.current = users(:admin) + user = users(:two) + FactoryBot.build(:jwt_secret, token: 'test_jwt_secret', user: user) + patch :invalidate_jwt, params: { :id => user.id }, session: set_session_user + user.reload + assert_response :found + end + + test 'user with edit users permission should be able to invalidate jwt for another user' do + 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 + user.reload + assert_response :found + end + + test 'user without edit users permission should not be able to invalidate jwt for another user' do + User.current = users(:one) + user = users(:two) + FactoryBot.build(:jwt_secret, token: 'test_jwt_secret', user: user) + patch :invalidate_jwt, params: { :id => user.id } + user.reload + assert_response :forbidden + end + test "should modify session when locale is updated" do as_admin do put :update, params: { :id => users(:admin).id, :user => { :locale => "cs" } }, session: set_session_user