diff --git a/app/controllers/users/applications_controller.rb b/app/controllers/users/applications_controller.rb new file mode 100644 index 000000000..cdae51656 --- /dev/null +++ b/app/controllers/users/applications_controller.rb @@ -0,0 +1,20 @@ +class Users::ApplicationsController < ApplicationController + layout "admin_layout" + + before_action :authenticate_user! + + def show + user = User.find(params[:user_id]) + authorize user, :edit? + + redirect_to user_applications_path(user) + end + + def index + @user = User.find(params[:user_id]) + authorize @user, :edit? + + @applications_with_signin = Doorkeeper::Application.not_api_only.can_signin(@user) + @applications_without_signin = Doorkeeper::Application.not_api_only.without_signin_permission_for(@user) + end +end diff --git a/app/controllers/users/permissions_controller.rb b/app/controllers/users/permissions_controller.rb new file mode 100644 index 000000000..438351c60 --- /dev/null +++ b/app/controllers/users/permissions_controller.rb @@ -0,0 +1,46 @@ +class Users::PermissionsController < ApplicationController + layout "admin_layout" + + before_action :authenticate_user! + before_action :set_user + before_action :set_application + + def show + authorize @user, :edit? + + @permissions = @application + .sorted_supported_permissions_grantable_from_ui + .sort_by { |permission| @user.has_permission?(permission) ? 0 : 1 } + end + + def edit + authorize UserApplicationPermission.for(@user, @application) + + @permissions = @application.sorted_supported_permissions_grantable_from_ui(include_signin: false) + end + + def update + authorize UserApplicationPermission.for(@user, @application) + + permission_ids_for_other_applications = @user.supported_permissions.excluding_application(@application).pluck(:id) + user_update_params = { supported_permission_ids: permission_ids_for_other_applications + update_params[:supported_permission_ids].map(&:to_i) } + UserUpdate.new(@user, user_update_params, current_user, user_ip_address).call + + flash[:application_id] = @application.id + redirect_to user_applications_path(@user) + end + +private + + def update_params + params.require(:application).permit(supported_permission_ids: []) + end + + def set_user + @user = User.find(params[:user_id]) + end + + def set_application + @application = Doorkeeper::Application.with_signin_permission_for(@user).not_api_only.find(params[:application_id]) + end +end diff --git a/app/controllers/users/signin_permissions_controller.rb b/app/controllers/users/signin_permissions_controller.rb new file mode 100644 index 000000000..ce54a05dd --- /dev/null +++ b/app/controllers/users/signin_permissions_controller.rb @@ -0,0 +1,40 @@ +class Users::SigninPermissionsController < ApplicationController + layout "admin_layout" + + before_action :authenticate_user! + before_action :set_user + before_action :set_application, except: [:create] + + def create + application = Doorkeeper::Application.not_api_only.find(params[:application_id]) + authorize UserApplicationPermission.for(@user, application) + + params = { supported_permission_ids: @user.supported_permissions.map(&:id) + [application.signin_permission.id] } + UserUpdate.new(@user, params, current_user, user_ip_address).call + + redirect_to user_applications_path(@user) + end + + def delete + authorize UserApplicationPermission.for(@user, @application) + end + + def destroy + authorize UserApplicationPermission.for(@user, @application) + + params = { supported_permission_ids: @user.supported_permissions.map(&:id) - [@application.signin_permission.id] } + UserUpdate.new(@user, params, current_user, user_ip_address).call + + redirect_to user_applications_path(@user) + end + +private + + def set_user + @user = User.find(params[:user_id]) + end + + def set_application + @application = Doorkeeper::Application.with_signin_permission_for(@user).not_api_only.find(params[:application_id]) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 62a7f39d2..cad8049c6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -7,9 +7,8 @@ class UsersController < ApplicationController before_action :authenticate_user! before_action :load_user, except: %i[index] - before_action :redirect_to_account_page_if_acting_on_own_user, only: %i[edit manage_permissions] + before_action :redirect_to_account_page_if_acting_on_own_user, only: %i[edit] before_action :authorize_user, except: %i[index] - before_action :allow_no_application_access, only: [:update] before_action :redirect_legacy_filters, only: [:index] helper_method :applications_and_permissions, :filter_params respond_to :html @@ -33,18 +32,9 @@ def index def edit; end - def manage_permissions - @application_permissions = all_applications_and_permissions_for(@user) - end - def update - updater = UserUpdate.new(@user, user_params, current_user, user_ip_address) - if updater.call - redirect_to users_path, notice: "Updated user #{@user.email} successfully" - else - @application_permissions = all_applications_and_permissions_for(@user) - render :manage_permissions - end + UserUpdate.new(@user, user_params, current_user, user_ip_address).call + redirect_to users_path, notice: "Updated user #{@user.email} successfully" end def event_logs @@ -79,28 +69,13 @@ def export end end - # When no permissions are selected for a user, we set the value to [] so - # a user can have no permissions - def allow_no_application_access - params[:user] ||= {} - params[:user][:supported_permission_ids] ||= [] - end - def user_params - if permitted_user_params[:skip_update_user_permissions] - permitted_user_params[:supported_permission_ids] = @user.supported_permission_ids - end - UserParameterSanitiser.new( - user_params: permitted_user_params, + user_params: params.require(:user).permit(:require_2sv).to_h, current_user_role: current_user.role.to_sym, ).sanitise end - def permitted_user_params - @permitted_user_params ||= params.require(:user).permit(:require_2sv, :skip_update_user_permissions, supported_permission_ids: []).to_h - end - def filter_params params.permit( :filter, :page, :format, :"option-select-filter", diff --git a/app/helpers/account_applications_helper.rb b/app/helpers/account_applications_helper.rb deleted file mode 100644 index c8b4df96a..000000000 --- a/app/helpers/account_applications_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -module AccountApplicationsHelper - def message_for_success(application_id) - application = Doorkeeper::Application.find_by(id: application_id) - return nil unless application - - additional_permissions = current_user.permissions_for(application).reject { |permission| permission == SupportedPermission::SIGNIN_NAME } - - if additional_permissions.any? - paragraph = tag.p("You now have the following permissions for #{application.name}:", class: "govuk-body") - list = tag.ul(class: "govuk-list govuk-list--bullet") - additional_permissions.map { |permission| list << tag.li(permission) } - else - paragraph = tag.p("You can access #{application.name} but you do not have any additional permissions.", class: "govuk-body") - list = nil - end - - paragraph + list - end -end diff --git a/app/helpers/application_permissions_helper.rb b/app/helpers/application_permissions_helper.rb new file mode 100644 index 000000000..1588c8002 --- /dev/null +++ b/app/helpers/application_permissions_helper.rb @@ -0,0 +1,25 @@ +module ApplicationPermissionsHelper + def message_for_success(application_id, user = current_user) + application = Doorkeeper::Application.find_by(id: application_id) + return nil unless application + + additional_permissions = user.permissions_for(application).reject { |permission| permission == SupportedPermission::SIGNIN_NAME } + + if additional_permissions.any? + prefix = user == current_user ? "You now have" : "#{user.name} now has" + paragraph = tag.p("#{prefix} the following permissions for #{application.name}:", class: "govuk-body") + list = tag.ul(class: "govuk-list govuk-list--bullet") + additional_permissions.map { |permission| list << tag.li(permission) } + else + string = if user == current_user + "You can access #{application.name} but you do not have any additional permissions." + else + "#{user.name} can access #{application.name} but does not have any additional permissions." + end + paragraph = tag.p(string, class: "govuk-body") + list = nil + end + + paragraph + list + end +end diff --git a/app/models/user_application_permission.rb b/app/models/user_application_permission.rb index 2a077d7be..6f455e21d 100644 --- a/app/models/user_application_permission.rb +++ b/app/models/user_application_permission.rb @@ -8,6 +8,10 @@ class UserApplicationPermission < ApplicationRecord before_validation :assign_application_id + def self.for(user, application) + new(user:, application:) + end + private # application_id is duplicated across supported_permissions and user_application_permissions diff --git a/app/policies/user_application_permission_policy.rb b/app/policies/user_application_permission_policy.rb new file mode 100644 index 000000000..3dc137fcc --- /dev/null +++ b/app/policies/user_application_permission_policy.rb @@ -0,0 +1,15 @@ +class UserApplicationPermissionPolicy < BasePolicy + def create? + return false unless Pundit.policy(current_user, user).edit? + + return true if current_user.govuk_admin? + + current_user.publishing_manager? && current_user.has_access_to?(application) && application.signin_permission.delegatable? + end + alias_method :destroy?, :create? + alias_method :delete?, :create? + alias_method :update?, :create? + alias_method :edit?, :create? + + delegate :user, :application, to: :record +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index d807618d1..30c4cccc7 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -39,7 +39,6 @@ def edit? alias_method :reset_two_step_verification?, :edit? alias_method :resend_email_change?, :edit? alias_method :cancel_email_change?, :edit? - alias_method :manage_permissions?, :edit? def assign_role? current_user.superadmin? diff --git a/app/views/users/applications/index.html.erb b/app/views/users/applications/index.html.erb new file mode 100644 index 000000000..befb64893 --- /dev/null +++ b/app/views/users/applications/index.html.erb @@ -0,0 +1,103 @@ +<% content_for :title_caption, "Manage other users" %> +<% content_for :title, "#{@user.name}'s applications" %> + +<% content_for :breadcrumbs, + render("govuk_publishing_components/components/breadcrumbs", { + collapse_on_mobile: true, + breadcrumbs: [ + { + title: "Dashboard", + url: root_path, + }, + { + title: "Users", + url: users_path, + }, + { + title: @user.name, + url: edit_user_path(@user), + }, + { + title: "#{@user.name}'s applications", + } + ] + }) +%> + +<% if flash[:application_id] %> +<%= render "govuk_publishing_components/components/success_alert", { + message: "Permissions updated", + description: message_for_success(flash[:application_id], @user), +} %> +<% end %> + + + + + + + + + + + + + <% @applications_with_signin.each do |application| %> + + + + + + + <% end %> + +
Apps <%= @user.name %> has access to
NameDescriptionPermissionsRemove access
<%= application.name %><%= application.description %> + <% if policy(UserApplicationPermission.for(@user, application)).edit? %> + <% unless application.sorted_supported_permissions_grantable_from_ui(include_signin: false).empty? %> + <%= link_to edit_user_application_permissions_path(@user, application), class: "govuk-link" do %> + Update permissions for <%= application.name %> + <% end %> + <% end %> + <% else %> + <%= link_to user_application_permissions_path(@user, application), class: "govuk-link" do %> + View permissions for <%= application.name %> + <% end %> + <% end %> + + <% if policy(UserApplicationPermission.for(@user, application)).delete? %> + <%= link_to delete_user_application_signin_permission_path(@user, application), + class: "govuk-button govuk-button--warning govuk-!-margin-0", + data: { module: "govuk-button" } do %> + Remove access to <%= application.name %> + <% end %> + <% end %> +
+ +

Apps <%= @user.name %> does not have access to

+ + + + + + + + + + + <% @applications_without_signin.each do |application| %> + + + + + + <% end %> + +
NameDescriptionGrant access
<%= application.name %><%= application.description %> + <% if policy(UserApplicationPermission.for(@user, application)).create? %> + <%= button_to user_application_signin_permission_path(@user, application), + class: "govuk-button govuk-!-margin-0", + data: { module: "govuk-button" } do %> + Grant access to <%= application.name %> + <% end %> + <% end %> +
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 9ac06a1b2..55b4663b9 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -58,7 +58,7 @@
<%= render "govuk_publishing_components/components/button", { text: "Manage permissions", - href: manage_permissions_user_path(@user) + href: user_applications_path(@user) } %>
diff --git a/app/views/users/manage_permissions.html.erb b/app/views/users/manage_permissions.html.erb deleted file mode 100644 index cfaf465ed..000000000 --- a/app/views/users/manage_permissions.html.erb +++ /dev/null @@ -1,36 +0,0 @@ -<% content_for :title, "Manage permissions [#{@user.name}]" %> - - - -

Manage permissions for “<%= @user.name %>”

- -<% if @user.errors.count > 0 %> - -<% end %> - -<%= form_for @user, :html => {:class => 'well add-top-margin'} do |f| %> -

<%= "Editable " if (current_user.publishing_manager? ) %>Permissions

- <%= render partial: "shared/user_permissions", locals: { user_object: @user }%> - - <% if current_user.publishing_manager? %> -

All Permissions for this user

- <%= render partial: "app_permissions", locals: { user_object: @user }%> - <% end %> - -
- - <%= f.submit :class => 'btn btn-primary' %> -<% end %> diff --git a/app/views/users/permissions/edit.html.erb b/app/views/users/permissions/edit.html.erb new file mode 100644 index 000000000..36ae9b0d5 --- /dev/null +++ b/app/views/users/permissions/edit.html.erb @@ -0,0 +1,47 @@ +<% content_for :title_caption, "Manage other users" %> +<% content_for :title, "Update #{@user.name}'s permissions for #{@application.name}" %> + +<% content_for :breadcrumbs, + render("govuk_publishing_components/components/breadcrumbs", { + collapse_on_mobile: true, + breadcrumbs: [ + { + title: "Dashboard", + url: root_path, + }, + { + title: "Users", + url: users_path, + }, + { + title: @user.name, + url: edit_user_path(@user), + }, + { + title: "#{@user.name}'s applications", + url: user_applications_path(@user), + }, + { + title: "Update #{@user.name}'s permissions for #{@application.name}", + } + ] + }) +%> + +<%= form_tag user_application_permissions_path(@user, @application), method: :patch do |f| %> + <%= render "govuk_publishing_components/components/checkboxes", { + name: "application[supported_permission_ids][]", + heading: "Permissions", + items: @permissions.map { |permission| { label: permission.name, value: permission.id, checked: @user.has_permission?(permission) } }, + } %> + + <%= hidden_field_tag "application[supported_permission_ids][]", @application.signin_permission.id, id: "checkboxes-signin" %> + +
+ <%= render "govuk_publishing_components/components/button", { + text: "Update permissions" + } %> + + <%= link_to "Cancel", edit_user_path(@user), class: "govuk-link govuk-link--no-visited-state" %> +
+<% end %> diff --git a/app/views/users/permissions/show.html.erb b/app/views/users/permissions/show.html.erb new file mode 100644 index 000000000..71f358d39 --- /dev/null +++ b/app/views/users/permissions/show.html.erb @@ -0,0 +1,56 @@ +<% content_for :title_caption, "Manage other users" %> +<% content_for :title, "#{@user.name}'s permissions for #{@application.name}" %> + +<% content_for :breadcrumbs, + render("govuk_publishing_components/components/breadcrumbs", { + collapse_on_mobile: true, + breadcrumbs: [ + { + title: "Dashboard", + url: root_path, + }, + { + title: "Users", + url: users_path, + }, + { + title: @user.name, + url: edit_user_path(@user), + }, + { + title: "#{@user.name}'s applications", + url: user_applications_path(@user), + }, + { + title: "#{@user.name}'s permissions for #{@application.name}", + } + ] + }) +%> + + + + + + + + + + <% @permissions.each do |permission| %> + + + + + <% end %> + +
NameHas this permission?
<%= permission.name %> + <% if @user.has_permission?(permission) %> + + Yes + + <% else %> + + No + + <% end %> +
diff --git a/app/views/users/require_2sv.html.erb b/app/views/users/require_2sv.html.erb index 9bfb25c02..8b979de0d 100644 --- a/app/views/users/require_2sv.html.erb +++ b/app/views/users/require_2sv.html.erb @@ -1,11 +1,11 @@ <% content_for :title, "2-step verification settings for new user" %> <%= form_for @user do |f| %> - <%= f.hidden_field :skip_update_user_permissions, value: "true" %> <%= render "govuk_publishing_components/components/hint", { text: "User will be prompted to set up 2-step verification again the next time they sign in." } %> + <%= f.hidden_field :require_2sv, value: 0 %> <%= render "govuk_publishing_components/components/checkboxes", { name: "user[require_2sv]", items: [ diff --git a/app/views/users/signin_permissions/delete.html.erb b/app/views/users/signin_permissions/delete.html.erb new file mode 100644 index 000000000..c3999da89 --- /dev/null +++ b/app/views/users/signin_permissions/delete.html.erb @@ -0,0 +1,41 @@ +<% content_for :title_caption, "Manage other users" %> +<% content_for :title, "Remove #{@user.name}'s access to #{@application.name}" %> + +<% content_for :breadcrumbs, + render("govuk_publishing_components/components/breadcrumbs", { + collapse_on_mobile: true, + breadcrumbs: [ + { + title: "Dashboard", + url: root_path, + }, + { + title: "Users", + url: users_path, + }, + { + title: @user.name, + url: edit_user_path(@user), + }, + { + title: "#{@user.name}'s applications", + url: user_applications_path(@user), + }, + { + title: "Remove #{@user.name}'s access to #{@application.name}", + } + ] + }) +%> + +

Are you sure you want to remove <%= @user.name %>'s access to <%= @application.name %>?

+ +<%= form_with url: user_application_signin_permission_path(@user, @application), method: :delete do |form| %> +
+ <%= render "govuk_publishing_components/components/button", { + text: "Confirm", + } %> + + <%= link_to "Cancel", edit_user_path(@user), class: "govuk-link govuk-link--no-visited-state" %> +
+<% end %> diff --git a/config/routes.rb b/config/routes.rb index f6337c61a..f6afe7ea8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,7 +37,6 @@ resources :users, except: [:show] do member do - get :manage_permissions get :event_logs get :require_2sv end @@ -52,6 +51,12 @@ resource :two_step_verification_mandation, only: %i[edit update], controller: "users/two_step_verification_mandations" resource :invitation_resend, only: %i[edit update], controller: "users/invitation_resends" resource :unlocking, only: %i[edit update], controller: "users/unlockings" + resources :applications, only: %i[index show], controller: "users/applications" do + resource :permissions, only: %i[show edit update], controller: "users/permissions" + resource :signin_permission, only: %i[create destroy], controller: "users/signin_permissions" do + get :delete + end + end end get "user", to: "oauth_users#show" diff --git a/test/controllers/users/applications_controller_test.rb b/test/controllers/users/applications_controller_test.rb new file mode 100644 index 000000000..1e2f50995 --- /dev/null +++ b/test/controllers/users/applications_controller_test.rb @@ -0,0 +1,266 @@ +require "test_helper" + +class Users::ApplicationsControllerTest < ActionController::TestCase + context "#show" do + setup do + @application = create(:application) + end + + should "prevent unauthenticated users" do + user = create(:user) + + get :show, params: { user_id: user, id: @application.id } + + assert_redirected_to "/users/sign_in" + end + + should "prevent unauthorised users" do + user = create(:user) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: false + + get :show, params: { user_id: user, id: @application.id } + + assert_not_authorised + end + + should "redirect authorised users to /accounts/applications" do + user = create(:user) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: true + + get :show, params: { user_id: user, id: @application.id } + + assert_redirected_to user_applications_path(user) + end + end + + context "#index" do + should "prevent unauthenticated users" do + user = create(:user) + + get :index, params: { user_id: user } + + assert_redirected_to "/users/sign_in" + end + + should "prevent unauthorised users" do + user = create(:user) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: false + + get :index, params: { user_id: user } + + assert_not_authorised + end + + should "display the applications the user has access to" do + user = create(:user) + application = create(:application, name: "app-name") + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "table:has( > caption[text()='Apps #{user.name} has access to'])" do + assert_select "tr td", text: "app-name" + end + end + + should "display the applications the user does not have access to" do + user = create(:user) + create(:application, name: "app-name") + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + heading_id = css_select("h2:contains('Apps #{user.name} does not have access to')").attribute("id").value + assert_select "table[aria-labelledby='#{heading_id}']" do + assert_select "tr td", text: "app-name" + end + end + + should "display a button allowing the user to grant access to applications if they're authorised to grant access" do + user = create(:user) + application = create(:application, name: "app-name") + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "form[action='#{user_application_signin_permission_path(user, application)}']" do + assert_select "button", "Grant access to app-name" + end + end + + should "not display a button allowing the user to grant access to applications if they're not authorised to grant access" do + user = create(:user) + create(:application, name: "app-name") + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: false + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "button", { text: "Grant access to app-name", count: 0 } + end + + should "display a button allowing the user to remove access to applications if they're authorised to remove access" do + user = create(:user) + application = create(:application, name: "app-name") + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, user, edit?: true + stub_policy current_user, permission, delete?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "a[href='#{delete_user_application_signin_permission_path(user, application)}']", text: "Remove access to app-name" + end + + should "not display a button allowing the user to remove access to applications if they're not authorised to remove access" do + user = create(:user) + application = create(:application, name: "app-name") + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, user, edit?: true + stub_policy current_user, permission, delete?: false + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "a[href='#{delete_user_application_signin_permission_path(user, application)}']", count: 0 + end + + should "not display a link to edit permissions if the user is authorised to edit permissions but the app only has the signin permission" do + user = create(:user) + application = create(:application, name: "app-name") + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, user, edit?: true + stub_policy current_user, permission, edit?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "a[href='#{edit_user_application_permissions_path(user, application)}']", count: 0 + end + + should "display a link to edit permissions if the user is authorised to edit permissions" do + user = create(:user) + application = create(:application, name: "app-name", with_supported_permissions: %w[foo]) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, user, edit?: true + stub_policy current_user, permission, edit?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "a[href='#{edit_user_application_permissions_path(user, application)}']", text: "Update permissions for app-name" + end + + should "display a link to view permissions if the user is not authorised to edit permissions" do + user = create(:user) + application = create(:application, name: "app-name") + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, user, edit?: true + stub_policy current_user, permission, edit?: false + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "a[href='#{user_application_permissions_path(user, application)}']", text: "View permissions for app-name" + end + + should "not display a retired application" do + user = create(:user) + create(:application, name: "retired-app-name", retired: true) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "tr td", text: "retired-app-name", count: 0 + end + + should "not display an API-only application" do + user = create(:user) + create(:application, name: "api-only-app-name", api_only: true) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: true + stub_policy_for_navigation_links current_user + + get :index, params: { user_id: user } + + assert_select "tr td", text: "api-only-app-name", count: 0 + end + end + +private + + def stub_policy_for_navigation_links(current_user) + stub_policy(current_user, User, index?: true) + end + + def stub_user_application_permission(user, application) + permission = UserApplicationPermission.new + UserApplicationPermission.stubs(:for).with(user, application).returns(permission) + permission + end +end diff --git a/test/controllers/users/permissions_controller_test.rb b/test/controllers/users/permissions_controller_test.rb new file mode 100644 index 000000000..e43e2942a --- /dev/null +++ b/test/controllers/users/permissions_controller_test.rb @@ -0,0 +1,339 @@ +require "test_helper" + +class Users::PermissionsControllerTest < ActionController::TestCase + context "#show" do + should "prevent unauthenticated users" do + application = create(:application) + user = create(:user) + + get :show, params: { user_id: user, application_id: application.id } + + assert_redirected_to "/users/sign_in" + end + + should "exclude permissions that aren't grantable from the UI" do + application = create(:application, + with_supported_permissions: %w[perm-1], + with_supported_permissions_not_grantable_from_ui: %w[perm-2]) + user = create(:user, with_signin_permissions_for: [application]) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy_for_navigation_links current_user + stub_policy current_user, user, edit?: true + + get :show, params: { user_id: user, application_id: application.id } + + assert_select "td", text: "perm-1" + assert_select "td", text: "perm-2", count: 0 + end + + should "exclude applications that the user doesn't have access to" do + sign_in create(:admin_user) + + application = create(:application) + user = create(:user) + + assert_raises(ActiveRecord::RecordNotFound) do + get :show, params: { user_id: user, application_id: application.id } + end + end + + should "exclude retired applications" do + sign_in create(:admin_user) + + application = create(:application, retired: true) + user = create(:user) + + assert_raises(ActiveRecord::RecordNotFound) do + get :show, params: { user_id: user, application_id: application.id } + end + end + + should "exclude API-only applications" do + sign_in create(:admin_user) + + application = create(:application, api_only: true) + user = create(:user) + + assert_raises(ActiveRecord::RecordNotFound) do + get :show, params: { user_id: user, application_id: application.id } + end + end + + should "order permissions by whether the user has access and then alphabetically" do + application = create(:application, + with_supported_permissions: %w[aaa bbb ttt uuu]) + user = create(:user, + with_signin_permissions_for: [application], + with_permissions: { application => %w[aaa ttt] }) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy_for_navigation_links current_user + stub_policy current_user, user, edit?: true + + get :show, params: { user_id: user, application_id: application.id } + + assert_equal %w[signin aaa ttt bbb uuu], assigns(:permissions).map(&:name) + end + + should "prevent unauthorised users" do + application = create(:application) + user = create(:user) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + stub_policy current_user, user, edit?: false + + get :show, params: { user_id: user, application_id: application.id } + + assert_not_authorised + end + end + + context "#edit" do + should "prevent unauthenticated users" do + application = create(:application) + user = create(:user) + + get :edit, params: { user_id: user, application_id: application.id } + + assert_redirected_to "/users/sign_in" + end + + should "prevent unauthorized users" do + application = create(:application) + user = create(:user) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, edit?: false + + get :edit, params: { user_id: user, application_id: application.id } + + assert_not_authorised + end + + should "display checkboxes for the grantable permissions" do + application = create(:application) + perm1 = create(:supported_permission, application:, name: "perm-1") + perm2 = create(:supported_permission, application:, name: "perm-2") + perm3 = create(:supported_permission, application:, name: "perm-3", grantable_from_ui: false) + user = create(:user, with_permissions: { application => %w[perm-1] }) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, edit?: true + + get :edit, params: { user_id: user.id, application_id: application.id } + + assert_select "input[type='checkbox'][checked='checked'][name='application[supported_permission_ids][]'][value='#{perm1.id}']" + assert_select "input[type='checkbox'][name='application[supported_permission_ids][]'][value='#{perm2.id}']" + assert_select "input[type='checkbox'][name='application[supported_permission_ids][]'][value='#{perm3.id}']", count: 0 + assert_select "input[type='checkbox'][name='application[supported_permission_ids][]'][value='#{application.signin_permission.id}']", count: 0 + end + + should "include a hidden field for the signin permission so that it is not removed" do + application = create(:application, with_supported_permissions: ["perm-1", SupportedPermission::SIGNIN_NAME]) + user = create(:user, with_permissions: { application => %w[perm-1] }) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, edit?: true + + get :edit, params: { user_id: user.id, application_id: application.id } + + assert_select "input[type='hidden'][value='#{application.signin_permission.id}']" + end + + should "exclude retired applications" do + sign_in create(:admin_user) + + application = create(:application, retired: true) + user = create(:user) + + assert_raises(ActiveRecord::RecordNotFound) do + get :edit, params: { user_id: user, application_id: application.id } + end + end + + should "exclude API-only applications" do + sign_in create(:admin_user) + + application = create(:application, api_only: true) + user = create(:user) + + assert_raises(ActiveRecord::RecordNotFound) do + get :edit, params: { user_id: user, application_id: application.id } + end + end + + should "raise an exception if the signin permission cannot be found" do + application = create(:application) + user = create(:user) + + current_user = create(:admin_user) + sign_in current_user + + assert_raises(ActiveRecord::RecordNotFound) do + get :edit, params: { user_id: user, application_id: application.id } + end + end + end + + context "#update" do + should "prevent unauthenticated users" do + application = create(:application) + user = create(:user) + + patch :update, params: { user_id: user, application_id: application.id } + + assert_redirected_to "/users/sign_in" + end + + should "replace the users permissions with new ones" do + application = create(:application, with_supported_permissions: %w[new old]) + user = create(:user, with_permissions: { application => %w[old] }) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, update?: true + + new_permission = application.supported_permissions.find_by(name: "new") + + expected_params = { supported_permission_ids: [new_permission.id] } + user_update = stub("user-update").responds_like_instance_of(UserUpdate) + user_update.expects(:call) + UserUpdate.stubs(:new).with(user, expected_params, current_user, anything).returns(user_update) + + patch :update, params: { user_id: user, application_id: application.id, application: { supported_permission_ids: [new_permission.id] } } + end + + should "redirect once the permissions have been updated" do + application = create(:application, with_supported_permissions: %w[new old]) + user = create(:user, with_permissions: { application => %w[old] }) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, update?: true + + new_permission = application.supported_permissions.find_by(name: "new") + + patch :update, params: { user_id: user, application_id: application.id, application: { supported_permission_ids: [new_permission.id] } } + + assert_redirected_to user_applications_path(user) + end + + should "retain permissions for other apps" do + other_application = create(:application, with_supported_permissions: %w[other]) + application = create(:application, with_supported_permissions: %w[new old]) + user = create(:user, with_permissions: { application => %w[old], other_application => %w[other] }) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, update?: true + + new_permission = application.supported_permissions.find_by(name: "new") + other_permission = other_application.supported_permissions.find_by(name: "other") + + expected_params = { supported_permission_ids: [other_permission.id, new_permission.id] } + user_update = stub("user-update").responds_like_instance_of(UserUpdate) + user_update.expects(:call) + UserUpdate.stubs(:new).with(user, expected_params, current_user, anything).returns(user_update) + + patch :update, params: { user_id: user, application_id: application.id, application: { supported_permission_ids: [new_permission.id] } } + end + + should "assign the application id to the application_id flash" do + application = create(:application, with_supported_permissions: %w[new old]) + user = create(:user, with_permissions: { application => %w[old] }) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, update?: true + + new_permission = application.supported_permissions.find_by(name: "new") + + patch :update, params: { user_id: user, application_id: application.id, application: { supported_permission_ids: [new_permission.id] } } + + assert_equal application.id, flash[:application_id] + end + + should "raise an exception if the user cannot be found" do + application = create(:application) + + current_user = create(:admin_user) + sign_in current_user + + assert_raises(ActiveRecord::RecordNotFound) do + patch :update, params: { user_id: "unknown-id", application_id: application.id, application: { supported_permission_ids: %w[id] } } + end + end + + should "raise an exception if the signin permission cannot be found" do + application = create(:application) + user = create(:user) + + current_user = create(:admin_user) + sign_in current_user + + assert_raises(ActiveRecord::RecordNotFound) do + patch :update, params: { user_id: user, application_id: application.id, application: { supported_permission_ids: %w[id] } } + end + end + + should "prevent unauthorised users" do + application = create(:application) + user = create(:user) + user.grant_application_signin_permission(application) + + current_user = create(:admin_user) + sign_in current_user + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, update?: false + + patch :update, params: { user_id: user, application_id: application.id, application: { supported_permission_ids: [] } } + + assert_not_authorised + end + end + +private + + def stub_policy_for_navigation_links(current_user) + stub_policy(current_user, User, index?: true) + end + + def stub_user_application_permission(user, application) + permission = UserApplicationPermission.new + UserApplicationPermission.stubs(:for).with(user, application).returns(permission) + permission + end +end diff --git a/test/controllers/users/signin_permissions_controller_test.rb b/test/controllers/users/signin_permissions_controller_test.rb new file mode 100644 index 000000000..8e10637ee --- /dev/null +++ b/test/controllers/users/signin_permissions_controller_test.rb @@ -0,0 +1,302 @@ +require "test_helper" + +class Users::SigninPermissionsControllerTest < ActionController::TestCase + context "#create" do + should "call the UserUpdate service to grant the user access to the application" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, create?: true + + expected_params = { supported_permission_ids: [application.signin_permission.id] } + user_update = stub("user-update").responds_like_instance_of(UserUpdate) + user_update.expects(:call) + UserUpdate.stubs(:new).with(user, expected_params, current_user, anything).returns(user_update) + + post :create, params: { user_id: user, application_id: application.id } + end + + should "redirect to the edit user path" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, create?: true + + post :create, params: { user_id: user, application_id: application.id } + + assert_redirected_to user_applications_path(user) + end + + should "prevent unauthenticated users" do + user = create(:user) + application = create(:application) + + post :create, params: { user_id: user, application_id: application.id } + + assert_redirected_to "/users/sign_in" + end + + should "not authorize access if UserApplicationPermissionPolicy#create? returns false" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, create?: false + + post :create, params: { user_id: user, application_id: application.id } + + assert_not_authorised + end + + should "raise exception is user isn't found" do + sign_in create(:admin_user) + + application = create(:application) + + assert_raises(ActiveRecord::RecordNotFound) do + post :create, params: { user_id: "non-existent-user-id", application_id: application.id } + end + end + + should "exclude retired applications" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application, retired: true) + + assert_raises(ActiveRecord::RecordNotFound) do + post :create, params: { user_id: user, application_id: application.id } + end + end + + should "exclude API-only applications" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application, api_only: true) + + assert_raises(ActiveRecord::RecordNotFound) do + post :create, params: { user_id: user, application_id: application.id } + end + end + end + + context "#delete" do + should "have a button to confirm deletion of the user when current_user is authorized to delete" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + user.grant_application_signin_permission(application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, delete?: true + + get :delete, params: { user_id: user, application_id: application.id } + + assert_template :delete + assert_select "form[action='#{user_application_signin_permission_path(user, application)}']" do + assert_select "button[type='submit']", text: "Confirm" + assert_select "a[href='#{edit_user_path(user)}']", text: "Cancel" + end + end + + should "prevent unauthenticated users" do + user = create(:user) + application = create(:application) + + get :delete, params: { user_id: user, application_id: application.id } + + assert_redirected_to "/users/sign_in" + end + + should "not authorize access if UserApplicationPermissionPolicy#delete? returns false" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + user.grant_application_signin_permission(application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, delete?: false + + get :delete, params: { user_id: user, application_id: application.id } + + assert_not_authorised + end + + should "raise exception if user isn't found" do + sign_in create(:admin_user) + + application = create(:application) + + assert_raises(ActiveRecord::RecordNotFound) do + get :delete, params: { user_id: "non-existent-user-id", application_id: application.id } + end + end + + should "raise exception if user doesn't have the signin permission for the app" do + sign_in create(:admin_user) + + user = create(:user) + application = create(:application) + + assert_raises(ActiveRecord::RecordNotFound) do + get :delete, params: { user_id: user, application_id: application.id } + end + end + + should "exclude retired applications" do + sign_in create(:admin_user) + + user = create(:user) + application = create(:application, retired: true) + + assert_raises(ActiveRecord::RecordNotFound) do + get :delete, params: { user_id: user, application_id: application.id } + end + end + + should "exclude API-only applications" do + sign_in create(:admin_user) + + user = create(:user) + application = create(:application, api_only: true) + user.grant_application_signin_permission(application) + + assert_raises(ActiveRecord::RecordNotFound) do + get :delete, params: { user_id: user, application_id: application.id } + end + end + end + + context "#destroy" do + should "call the UserUpdate service to remove the user's access to the application" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + user.grant_application_signin_permission(application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, destroy?: true + + expected_params = { supported_permission_ids: [] } + user_update = stub("user-update").responds_like_instance_of(UserUpdate) + user_update.expects(:call) + UserUpdate.stubs(:new).with(user, expected_params, current_user, anything).returns(user_update) + + delete :destroy, params: { user_id: user, application_id: application.id } + end + + should "redirect to the edit user path" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + user.grant_application_signin_permission(application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, destroy?: true + + delete :destroy, params: { user_id: user, application_id: application.id } + + assert_redirected_to user_applications_path(user) + end + + should "prevent unauthenticated users" do + user = create(:user) + application = create(:application) + + delete :destroy, params: { user_id: user, application_id: application.id } + + assert_redirected_to "/users/sign_in" + end + + should "not authorize access if UserApplicationPermissionPolicy#destroy? returns false" do + current_user = create(:admin_user) + sign_in current_user + + user = create(:user) + application = create(:application) + user.grant_application_signin_permission(application) + + permission = stub_user_application_permission(user, application) + stub_policy current_user, permission, destroy?: false + + delete :destroy, params: { user_id: user, application_id: application.id } + + assert_not_authorised + end + + should "raise exception is user isn't found" do + sign_in create(:admin_user) + + application = create(:application) + + assert_raises(ActiveRecord::RecordNotFound) do + delete :destroy, params: { user_id: "non-existent-user-id", application_id: application.id } + end + end + + should "raise exception is user doesn't have the signin permission for the app" do + sign_in create(:admin_user) + + user = create(:user) + application = create(:application) + + assert_raises(ActiveRecord::RecordNotFound) do + delete :destroy, params: { user_id: user, application_id: application.id } + end + end + + should "exclude retired applications" do + sign_in create(:admin_user) + + user = create(:user) + application = create(:application, retired: true) + user.grant_application_signin_permission(application) + + assert_raises(ActiveRecord::RecordNotFound) do + delete :destroy, params: { user_id: user, application_id: application.id } + end + end + + should "exclude API-only applications" do + sign_in create(:admin_user) + + user = create(:user) + application = create(:application, api_only: true) + user.grant_application_signin_permission(application) + + assert_raises(ActiveRecord::RecordNotFound) do + delete :destroy, params: { user_id: user, application_id: application.id } + end + end + end + +private + + def stub_user_application_permission(user, application) + permission = UserApplicationPermission.new + UserApplicationPermission.stubs(:for).with(user, application).returns(permission) + permission + end +end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 18e225b33..3214fb231 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -249,6 +249,20 @@ class UsersControllerTest < ActionController::TestCase end context "GET edit" do + should "display link to manage applications" do + user = create(:user) + + current_user = create(:user) + sign_in current_user + + stub_policy_for_navigation_links current_user + stub_policy current_user, user, edit?: true + + get :edit, params: { id: user } + + assert_select "a[href='#{user_applications_path(user)}']" + end + context "signed in as Admin user" do setup do @user = create(:admin_user, email: "admin@gov.uk") @@ -398,285 +412,6 @@ class UsersControllerTest < ActionController::TestCase end end - context "GET manage_permissions" do - context "signed in as Admin user" do - setup do - @user = create(:admin_user, email: "admin@gov.uk") - sign_in @user - end - - context "for the currently logged in user" do - should "redirect to the account page" do - get :manage_permissions, params: { id: @user.id } - assert_redirected_to account_path - end - end - - should "not be able to manage permissions for superadmins" do - superadmin = create(:superadmin_user) - - get :manage_permissions, params: { id: superadmin.id } - - assert_not_authorised - end - - should "can give permissions to all applications" do - delegatable_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - non_delegatable_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - delegatable_no_access_to_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - non_delegatable_no_access_to_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - - @user.grant_application_signin_permission(delegatable_app) - @user.grant_application_signin_permission(non_delegatable_app) - - user = create(:user_in_organisation) - - get :manage_permissions, params: { id: user.id } - - assert_select ".container" do - # can give permissions to a delegatable app - assert_select "td", count: 1, text: delegatable_app.name - # can give permissions to a non delegatable app - assert_select "td", count: 1, text: non_delegatable_app.name - # can give permissions to a delegatable app the admin doesn't have access to - assert_select "td", count: 1, text: delegatable_no_access_to_app.name - # can give permissions to a non-delegatable app the admin doesn't have access to - assert_select "td", count: 1, text: non_delegatable_no_access_to_app.name - end - end - end - - context "signed in as Organisation Admin user" do - setup do - @organisation_admin = create(:organisation_admin_user) - sign_in @organisation_admin - end - - should "be able to give permissions only to applications they themselves have access to and that also have delegatable signin permissions" do - delegatable_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - non_delegatable_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - delegatable_no_access_to_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - non_delegatable_no_access_to_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - - @organisation_admin.grant_application_signin_permission(delegatable_app) - @organisation_admin.grant_application_signin_permission(non_delegatable_app) - - user = create(:user_in_organisation, organisation: @organisation_admin.organisation) - - get :manage_permissions, params: { id: user.id } - - assert_select "#editable-permissions" do - # can give access to a delegatable app they have access to - assert_select "td", count: 1, text: delegatable_app.name - # can not give access to a non-delegatable app they have access to - assert_select "td", count: 0, text: non_delegatable_app.name - # can not give access to a delegatable app they do not have access to - assert_select "td", count: 0, text: delegatable_no_access_to_app.name - # can not give access to a non delegatable app they do not have access to - assert_select "td", count: 0, text: non_delegatable_no_access_to_app.name - end - end - - should "be able to see all permissions to applications for a user" do - delegatable_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME, "Editor"]) - non_delegatable_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME, "GDS Admin"]) - delegatable_no_access_to_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME, "GDS Editor"]) - non_delegatable_no_access_to_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME, "Import CSVs"]) - - @organisation_admin.grant_application_signin_permission(delegatable_app) - @organisation_admin.grant_application_signin_permission(non_delegatable_app) - - user = create( - :user_in_organisation, - organisation: @organisation_admin.organisation, - with_permissions: { delegatable_app => %w[Editor], - non_delegatable_app => [SupportedPermission::SIGNIN_NAME, "GDS Admin"], - delegatable_no_access_to_app => [SupportedPermission::SIGNIN_NAME, "GDS Editor"], - non_delegatable_no_access_to_app => [SupportedPermission::SIGNIN_NAME, "Import CSVs"] }, - ) - - get :manage_permissions, params: { id: user.id } - - assert_select "h2", "All Permissions for this user" - assert_select "#all-permissions" do - # can see permissions for a delegatable app the user has access to - assert_select "td", count: 1, text: delegatable_app.name - # can see permissions to a non-delegatable app the user has access to - assert_select "td", count: 1, text: non_delegatable_app.name - # can see permissions to a delegatable app the user has access to - assert_select "td", count: 1, text: delegatable_no_access_to_app.name - # can see permissions to a non delegatable app the user has access to - assert_select "td", count: 1, text: non_delegatable_no_access_to_app.name - # can see that user has signin permission for three of the four applications in "Has access?" column - assert_select "td", count: 3, text: "Yes" - assert_select "td", count: 1, text: "No" - # can see role - assert_select "td", count: 1, text: "Editor" - assert_select "td", count: 1, text: "GDS Admin" - assert_select "td", count: 1, text: "GDS Editor" - assert_select "td", count: 1, text: "Import CSVs" - end - end - end - - context "signed in as Super Organisation Admin user" do - setup do - @super_organisation_admin = create(:super_organisation_admin_user) - sign_in @super_organisation_admin - end - - should "be able to give permissions only to applications they themselves have access to and that also have delegatable signin permissions" do - delegatable_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - non_delegatable_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - delegatable_no_access_to_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - non_delegatable_no_access_to_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME]) - - @super_organisation_admin.grant_application_signin_permission(delegatable_app) - @super_organisation_admin.grant_application_signin_permission(non_delegatable_app) - - user = create(:user_in_organisation, organisation: @super_organisation_admin.organisation) - - get :manage_permissions, params: { id: user.id } - - assert_select "#editable-permissions" do - # can give access to a delegatable app they have access to - assert_select "td", count: 1, text: delegatable_app.name - # can not give access to a non-delegatable app they have access to - assert_select "td", count: 0, text: non_delegatable_app.name - # can not give access to a delegatable app they do not have access to - assert_select "td", count: 0, text: delegatable_no_access_to_app.name - # can not give access to a non delegatable app they do not have access to - assert_select "td", count: 0, text: non_delegatable_no_access_to_app.name - end - end - - should "be able to see all permissions to applications for a user" do - delegatable_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME, "Editor"]) - non_delegatable_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME, "GDS Admin"]) - delegatable_no_access_to_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME, "GDS Editor"]) - non_delegatable_no_access_to_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME, "Import CSVs"]) - - @super_organisation_admin.grant_application_signin_permission(delegatable_app) - @super_organisation_admin.grant_application_signin_permission(non_delegatable_app) - - user = create( - :user_in_organisation, - organisation: @super_organisation_admin.organisation, - with_permissions: { delegatable_app => %w[Editor], - non_delegatable_app => [SupportedPermission::SIGNIN_NAME, "GDS Admin"], - delegatable_no_access_to_app => [SupportedPermission::SIGNIN_NAME, "GDS Editor"], - non_delegatable_no_access_to_app => [SupportedPermission::SIGNIN_NAME, "Import CSVs"] }, - ) - - get :manage_permissions, params: { id: user.id } - - assert_select "h2", "All Permissions for this user" - assert_select "#all-permissions" do - # can see permissions for a delegatable app the user has access to - assert_select "td", count: 1, text: delegatable_app.name - # can see permissions to a non-delegatable app the user has access to - assert_select "td", count: 1, text: non_delegatable_app.name - # can see permissions to a delegatable app the user has access to - assert_select "td", count: 1, text: delegatable_no_access_to_app.name - # can see permissions to a non delegatable app the user has access to - assert_select "td", count: 1, text: non_delegatable_no_access_to_app.name - # can see that user has signin permission for three of the four applications in "Has access?" column - assert_select "td", count: 3, text: "Yes" - assert_select "td", count: 1, text: "No" - # can see role - assert_select "td", count: 1, text: "Editor" - assert_select "td", count: 1, text: "GDS Admin" - assert_select "td", count: 1, text: "GDS Editor" - assert_select "td", count: 1, text: "Import CSVs" - end - end - - should "not be able to see permissions to retired applications for a user" do - retired_app = create(:application, retired: true) - - user = create(:user_in_organisation, organisation: @super_organisation_admin.organisation) - create(:user_application_permission, application: retired_app, user:) - - get :manage_permissions, params: { id: user.id } - - assert_select "h2", "All Permissions for this user" - assert_select "#all-permissions" do - assert_select "td", text: retired_app.name, count: 0 - end - end - - should "not be able to see permissions to API-only applications for a user" do - api_only_app = create(:application, api_only: true) - - user = create(:user_in_organisation, organisation: @super_organisation_admin.organisation) - create(:user_application_permission, application: api_only_app, user:) - - get :manage_permissions, params: { id: user.id } - - assert_select "h2", "All Permissions for this user" - assert_select "#all-permissions" do - assert_select "td", text: api_only_app.name, count: 0 - end - end - end - - context "signed in as Superadmin user" do - setup do - @superadmin = create(:superadmin_user) - sign_in @superadmin - end - - should "not be able to see all permissions to applications for a user" do - delegatable_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME, "Editor"]) - non_delegatable_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME, "GDS Admin"]) - delegatable_no_access_to_app = create(:application, with_delegatable_supported_permissions: [SupportedPermission::SIGNIN_NAME, "GDS Editor"]) - non_delegatable_no_access_to_app = create(:application, with_supported_permissions: [SupportedPermission::SIGNIN_NAME, "Import CSVs"]) - - @superadmin.grant_application_signin_permission(delegatable_app) - @superadmin.grant_application_signin_permission(non_delegatable_app) - - user = create( - :user_in_organisation, - organisation: @superadmin.organisation, - with_permissions: { delegatable_app => %w[Editor], - non_delegatable_app => [SupportedPermission::SIGNIN_NAME, "GDS Admin"], - delegatable_no_access_to_app => [SupportedPermission::SIGNIN_NAME, "GDS Editor"], - non_delegatable_no_access_to_app => [SupportedPermission::SIGNIN_NAME, "Import CSVs"] }, - ) - - get :manage_permissions, params: { id: user.id } - - assert_select "h2", count: 0, text: "All Permissions for this user" - assert_select "#all-permissions", count: 0 - end - end - - context "signed in as Normal user" do - setup do - @user = create(:user, email: "normal@gov.uk") - sign_in @user - end - - context "when current user tries to manage their own permissions" do - should "redirect to the account page" do - get :manage_permissions, params: { id: @user } - - assert_redirected_to account_path - end - end - - context "when current user tries to manage another user's permissions" do - should "redirect to the dashboard and explain user does not have permission" do - another_user = create(:user) - - get :manage_permissions, params: { id: another_user } - - assert_not_authorised - end - end - end - end - context "PUT update" do context "signed in as Admin user" do setup do @@ -696,74 +431,14 @@ class UsersControllerTest < ActionController::TestCase another_user = create(:user) PermissionUpdater.expects(:perform_on).with(another_user).once - put :update, params: { id: another_user.id, user: {} } - end - - context "update application access" do - setup do - sign_in create(:admin_user) - @application = create(:application) - @another_user = create(:user) - end - - should "remove all applications access for a user" do - @another_user.grant_application_signin_permission(@application) - - put :update, params: { id: @another_user.id, user: {} } - - assert_empty @another_user.reload.application_permissions - end - - should "add application access for a user" do - put( - :update, - params: { - id: @another_user.id, - user: { - supported_permission_ids: [ - @application.supported_permissions.first.id, - ], - }, - }, - ) - - assert_equal 1, @another_user.reload.application_permissions.count - end + put :update, params: { id: another_user.id, user: { require_2sv: true } } end end + end - context "signed in as Organisation Admin user" do - setup do - @organisation_admin = create(:organisation_admin_user) - sign_in(@organisation_admin) - end - - should "redisplay the form if save fails" do - organisation = @organisation_admin.organisation - organisation_admin_for_same_organisation = create(:organisation_admin_user, organisation:) - UserUpdate.stubs(:new).returns(stub("UserUpdate", call: false)) - - put :update, params: { id: organisation_admin_for_same_organisation.id, user: {} } - - assert_select "form#edit_user_#{organisation_admin_for_same_organisation.id}" - end - end - - context "signed in as Super Organisation Admin user" do - setup do - @super_organisation_admin = create(:super_organisation_admin_user) - sign_in(@super_organisation_admin) - end - - should "redisplay the form if save fails" do - organisation = @super_organisation_admin.organisation - super_organisation_admin_for_same_organisation = create(:super_organisation_admin_user, organisation:) - UserUpdate.stubs(:new).returns(stub("UserUpdate", call: false)) - - put :update, params: { id: super_organisation_admin_for_same_organisation.id, user: {} } +private - assert_select "form#edit_user_#{super_organisation_admin_for_same_organisation.id}" - end - end + def stub_policy_for_navigation_links(current_user) + stub_policy(current_user, User, index?: true) end end diff --git a/test/helpers/account_applications_helper_test.rb b/test/helpers/application_permissions_helper_test.rb similarity index 59% rename from test/helpers/account_applications_helper_test.rb rename to test/helpers/application_permissions_helper_test.rb index e55f9105f..40751ff05 100644 --- a/test/helpers/account_applications_helper_test.rb +++ b/test/helpers/application_permissions_helper_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class AccountApplicationsHelperTest < ActionView::TestCase +class ApplicationPermissionsHelperTest < ActionView::TestCase context "#message_for_success" do setup do @application = create(:application, name: "Whitehall", with_supported_permissions: ["Permission 1"]) @@ -36,5 +36,25 @@ class AccountApplicationsHelperTest < ActionView::TestCase assert_includes message_for_success(@application.id), "You can access Whitehall but you do not have any additional permissions." end end + + context "when the user isn't the current user" do + setup do + @user = create(:user, name: "user-name", with_permissions: { @application => ["Permission 1", SupportedPermission::SIGNIN_NAME] }) + end + + should "include the application name in the message" do + assert_includes message_for_success(@application.id, @user), "user-name now has the following permissions for Whitehall" + end + end + + context "when the user isn't the current user and the user has no additional permissions" do + setup do + @user = create(:user, name: "user-name", with_permissions: { @application => [SupportedPermission::SIGNIN_NAME] }) + end + + should "indicate that the user has no additional permissions" do + assert_includes message_for_success(@application.id, @user), "user-name can access Whitehall but does not have any additional permissions." + end + end end end diff --git a/test/integration/granting_permissions_test.rb b/test/integration/granting_permissions_test.rb index 676529046..b8b399ad8 100644 --- a/test/integration/granting_permissions_test.rb +++ b/test/integration/granting_permissions_test.rb @@ -15,8 +15,7 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest visit edit_user_path(@user) click_link "Manage permissions" - check "Has access to MyApp?" - click_button "Update User" + click_button "Grant access to MyApp" assert @user.has_access_to?(app) end @@ -27,12 +26,14 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest name: "MyApp", with_supported_permissions: %w[pre-existing adding never], ) + @user.grant_application_signin_permission(app) @user.grant_application_permission(app, "pre-existing") visit edit_user_path(@user) click_link "Manage permissions" - select "adding", from: "Permissions for MyApp" - click_button "Update User" + click_link "Update permissions for MyApp" + check "adding" + click_button "Update permissions" assert_includes @user.permissions_for(app), "pre-existing" assert_includes @user.permissions_for(app), "adding" @@ -40,11 +41,15 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest end should "not be able to grant permissions that are not grantable_from_ui" do - create(:application, name: "MyApp", with_supported_permissions_not_grantable_from_ui: %w[user_update_permission]) + app = create(:application, name: "MyApp", with_supported_permissions: %w[perm], with_supported_permissions_not_grantable_from_ui: %w[user_update_permission]) + @user.grant_application_signin_permission(app) visit edit_user_path(@user) click_link "Manage permissions" - assert page.has_no_select?("Permissions for MyApp", options: %w[user_update_permission]) + click_link "Update permissions for MyApp" + + assert page.has_field?("perm") + assert page.has_no_field?("user_update_permission") end end @@ -62,8 +67,7 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest visit edit_user_path(@user) click_link "Manage permissions" - check "Has access to MyApp?" - click_button "Update User" + click_button "Grant access to MyApp" assert @user.has_access_to?(app) end @@ -74,12 +78,14 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest name: "MyApp", with_supported_permissions: %w[pre-existing adding never], ) + @user.grant_application_signin_permission(app) @user.grant_application_permission(app, "pre-existing") visit edit_user_path(@user) click_link "Manage permissions" - select "adding", from: "Permissions for MyApp" - click_button "Update User" + click_link "Update permissions for MyApp" + check "adding" + click_button "Update permissions" assert_includes @user.permissions_for(app), "pre-existing" assert_includes @user.permissions_for(app), "adding" @@ -87,11 +93,15 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest end should "not be able to grant permissions that are not grantable_from_ui" do - create(:application, name: "MyApp", with_supported_permissions_not_grantable_from_ui: %w[user_update_permission]) + app = create(:application, name: "MyApp", with_supported_permissions: %w[perm], with_supported_permissions_not_grantable_from_ui: %w[user_update_permission]) + @user.grant_application_signin_permission(app) visit edit_user_path(@user) click_link "Manage permissions" - assert page.has_no_select?("Permissions for MyApp", options: %w[user_update_permission]) + click_link "Update permissions for MyApp" + + assert page.has_field?("perm") + assert page.has_no_field?("user_update_permission") end end @@ -110,8 +120,7 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest visit edit_user_path(@user) click_link "Manage permissions" - check "Has access to MyApp?" - click_button "Update User" + click_button "Grant access to MyApp" assert @user.reload.has_access_to?(app) end @@ -124,7 +133,8 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest visit edit_user_path(@user) click_link "Manage permissions" - assert page.has_no_field? "Has access to MyApp?" + + assert page.has_no_button? "Grant access to MyApp?" end should "not support granting signin permissions to apps that the super organisation admin doesn't have access to" do @@ -132,31 +142,8 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest visit edit_user_path(@user) click_link "Manage permissions" - assert page.has_no_field? "Has access to MyApp?" - end - - should "not remove permissions the user has that the super organisation admin does not have" do - app = create(:application, name: "MyApp") - @user.grant_application_signin_permission(app) - - visit edit_user_path(@user) - click_link "Manage permissions" - click_button "Update User" - - assert @user.reload.has_access_to?(app) - end - - should "not remove permissions the user has that the super organisation admin cannot delegate" do - app = create(:application, name: "MyApp") - app.signin_permission.update!(delegatable: false) - @super_org_admin.grant_application_signin_permission(app) - @user.grant_application_signin_permission(app) - - visit edit_user_path(@user) - click_link "Manage permissions" - click_button "Update User" - assert @user.reload.has_access_to?(app) + assert page.has_no_button? "Grant access to MyApp?" end should "support granting app-specific permissions" do @@ -166,12 +153,14 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest with_supported_permissions: %w[pre-existing adding never], ) @super_org_admin.grant_application_signin_permission(app) + @user.grant_application_signin_permission(app) @user.grant_application_permission(app, "pre-existing") visit edit_user_path(@user) click_link "Manage permissions" - select "adding", from: "Permissions for MyApp" - click_button "Update User" + click_link "Update permissions for MyApp" + check "adding" + click_button "Update permissions" assert_includes @user.permissions_for(app), "pre-existing" assert_includes @user.permissions_for(app), "adding" @@ -203,8 +192,7 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest visit edit_user_path(@user) click_link "Manage permissions" - check "Has access to MyApp?" - click_button "Update User" + click_button "Grant access to MyApp" assert @user.reload.has_access_to?(app) end @@ -228,30 +216,6 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest assert page.has_no_field? "Has access to MyApp?" end - should "not remove permissions the user has that the organisation admin does not have" do - app = create(:application, name: "MyApp") - @user.grant_application_signin_permission(app) - - visit edit_user_path(@user) - click_link "Manage permissions" - click_button "Update User" - - assert @user.reload.has_access_to?(app) - end - - should "not remove permissions the user has that the organisation admin cannot delegate" do - app = create(:application, name: "MyApp") - app.signin_permission.update!(delegatable: false) - @organisation_admin.grant_application_signin_permission(app) - @user.grant_application_signin_permission(app) - - visit edit_user_path(@user) - click_link "Manage permissions" - click_button "Update User" - - assert @user.reload.has_access_to?(app) - end - should "support granting app-specific permissions" do app = create( :application, @@ -259,12 +223,14 @@ class GrantingPermissionsTest < ActionDispatch::IntegrationTest with_supported_permissions: %w[pre-existing adding never], ) @organisation_admin.grant_application_signin_permission(app) + @user.grant_application_signin_permission(app) @user.grant_application_permission(app, "pre-existing") visit edit_user_path(@user) click_link "Manage permissions" - select "adding", from: "Permissions for MyApp" - click_button "Update User" + click_link "Update permissions for MyApp" + check "adding" + click_button "Update permissions" assert_includes @user.permissions_for(app), "pre-existing" assert_includes @user.permissions_for(app), "adding" diff --git a/test/integration/user_applications_test.rb b/test/integration/user_applications_test.rb new file mode 100644 index 000000000..ce8f1d335 --- /dev/null +++ b/test/integration/user_applications_test.rb @@ -0,0 +1,72 @@ +require "test_helper" + +class UserApplicationsTest < ActionDispatch::IntegrationTest + should "allow admins to grant users access to apps" do + user = create(:user, name: "user-name") + create(:application, name: "app-name") + + admin_user = create(:admin_user) + visit new_user_session_path + signin_with admin_user + + visit user_applications_path(user) + + heading = find("h2", text: "Apps user-name does not have access to") + table = find("table[aria-labelledby='#{heading['id']}']") + assert table.has_content?("app-name") + + click_on "Grant access to app-name" + + table = find("table caption[text()='Apps user-name has access to']").ancestor("table") + assert table.has_content?("app-name") + end + + should "allow admins to remove users' access to apps" do + user = create(:user, name: "user-name") + application = create(:application, name: "app-name") + user.grant_application_signin_permission(application) + + admin_user = create(:admin_user) + visit new_user_session_path + signin_with admin_user + + visit user_applications_path(user) + + table = find("table caption[text()='Apps user-name has access to']").ancestor("table") + assert table.has_content?("app-name") + + click_on "Remove access to app-name" + click_on "Confirm" + + heading = find("h2", text: "Apps user-name does not have access to") + table = find("table[aria-labelledby='#{heading['id']}']") + assert table.has_content?("app-name") + end + + should "allow admins to update users' permissions for apps" do + application = create(:application, name: "app-name", with_supported_permissions: %w[perm1 perm2]) + application.signin_permission.update!(delegatable: true) + + user = create(:admin_user) + user.grant_application_signin_permission(application) + user.grant_application_permission(application, "perm1") + + admin_user = create(:admin_user) + visit new_user_session_path + signin_with admin_user + + visit user_applications_path(user) + + click_on "Update permissions for app-name" + + assert page.has_checked_field?("perm1") + assert page.has_unchecked_field?("perm2") + + check "perm2" + click_button "Update permissions" + + success_flash = find("div[role='alert']") + assert success_flash.has_content?("perm1") + assert success_flash.has_content?("perm2") + end +end diff --git a/test/policies/user_application_permission_policy_test.rb b/test/policies/user_application_permission_policy_test.rb new file mode 100644 index 000000000..395b83898 --- /dev/null +++ b/test/policies/user_application_permission_policy_test.rb @@ -0,0 +1,97 @@ +require "test_helper" +require "support/policy_helpers" + +class UserApplicationPermissionPolicyTest < ActiveSupport::TestCase + include PolicyHelpers + include PunditHelpers + + %i[create destroy delete update edit].each do |policy_method| + context "#{policy_method}?" do + setup do + @user = create(:user) + @application = create(:application) + @signin_permission = @user.grant_application_signin_permission(@application) + end + + should "be allowed for superadmins" do + current_user = build(:superadmin_user) + + stub_policy(current_user, @user, edit?: true) + + assert permit?(current_user, @signin_permission, policy_method) + end + + should "be allowed for admins" do + current_user = build(:admin_user) + + stub_policy(current_user, @user, edit?: true) + + assert permit?(current_user, @signin_permission, policy_method) + end + + should "be allowed for super org admins when they have access to the application" do + current_user = build(:super_organisation_admin_user) + current_user.grant_application_signin_permission(@application) + + stub_policy(current_user, @user, edit?: true) + + assert permit?(current_user, @signin_permission, policy_method) + end + + should "not be allowed for super org admins when they don't have access to the application" do + current_user = build(:super_organisation_admin_user) + + stub_policy(current_user, @user, edit?: true) + + assert forbid?(current_user, @signin_permission, policy_method) + end + + should "be allowed for org admins when they have access to the application" do + current_user = build(:organisation_admin_user) + current_user.grant_application_signin_permission(@application) + + stub_policy(current_user, @user, edit?: true) + + assert permit?(current_user, @signin_permission, policy_method) + end + + should "not be allowed for org admins when they don't have access to the application" do + current_user = build(:organisation_admin_user) + + stub_policy(current_user, @user, edit?: true) + + assert forbid?(current_user, @signin_permission, policy_method) + end + + context "when the signin permission is not delegatable" do + setup do + @application.signin_permission.update!(delegatable: false) + end + + should "not be allowed for super org admins" do + current_user = build(:super_organisation_admin_user) + + stub_policy(current_user, @user, edit?: true) + + assert forbid?(current_user, @signin_permission, policy_method) + end + + should "not be allowed for org admins" do + current_user = build(:organisation_admin_user) + + stub_policy(current_user, @user, edit?: true) + + assert forbid?(current_user, @signin_permission, policy_method) + end + end + + should "not be allowed if current user is not allowed to edit the target user" do + current_user = build(:user) + + stub_policy(current_user, @user, edit?: false) + + assert forbid?(current_user, @signin_permission, policy_method) + end + end + end +end diff --git a/test/policies/user_policy_test.rb b/test/policies/user_policy_test.rb index aab5dc5bf..abd3f12d9 100644 --- a/test/policies/user_policy_test.rb +++ b/test/policies/user_policy_test.rb @@ -13,7 +13,7 @@ class UserPolicyTest < ActiveSupport::TestCase end primary_management_actions = %i[new create assign_organisation] - user_management_actions = %i[edit update unlock suspension cancel_email_change resend_email_change event_logs reset_2sv mandate_2sv resend_invitation manage_permissions] + user_management_actions = %i[edit update unlock suspension cancel_email_change resend_email_change event_logs reset_2sv mandate_2sv resend_invitation] superadmin_actions = %i[assign_role] two_step_verification_exemption_actions = %i[exempt_from_two_step_verification] diff --git a/test/support/pundit_helpers.rb b/test/support/pundit_helpers.rb new file mode 100644 index 000000000..da29348da --- /dev/null +++ b/test/support/pundit_helpers.rb @@ -0,0 +1,7 @@ +module PunditHelpers + def stub_policy(current_user, record, method_and_return_value) + policy_class = Pundit::PolicyFinder.new(record).policy + policy = stub_everything("policy", method_and_return_value).responds_like_instance_of(policy_class) + policy_class.stubs(:new).with(current_user, record).returns(policy) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 85f39aa11..ae577cf55 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,10 +27,12 @@ class ActiveSupport::TestCase WebMock.disable_net_connect!(allow_localhost: true) require "support/confirmation_token_helpers" +require "support/pundit_helpers" class ActionController::TestCase include Devise::Test::ControllerHelpers include ConfirmationTokenHelpers + include PunditHelpers def sign_in(user, passed_mfa: true) warden.stubs(authenticate!: user)