Add a new service to mock permission checks for stubbed users

This commit is contained in:
Klaus Zanders
2023-09-27 18:02:01 +02:00
parent a256fd85ac
commit cb759bd810
2 changed files with 424 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# Provides a nice DSL to mock permissions on the user. It also validates that the permissions are
# mocked in the right context. So when you try to mock a project permission globally, it will complain.
# For examples on usage, see spec/support_spec/mocked_permission_helper_spec.rb
class PermissionMock
attr_reader :user, :permitted_entities, :allow_all_permissions
def initialize(user)
@user = user
@permitted_entities = Hash.new do |hash, entity_project_or_global|
hash[entity_project_or_global] = Array.new
end
@allow_all_permissions = false
end
def all_permissions_allowed!
@allow_all_permissions = true
end
def forbid_everything!
@allow_all_permissions = false
@permitted_entites = {}
end
def in_project(*permissions, project:)
return if project.nil?
permissions.each do |permission|
Authorization.contextual_permissions(permission, :project, raise_on_unknown: true)
end
permitted_entities[project] += permissions
end
def in_work_package(*permissions, work_package:)
return if work_package.nil?
permissions.each do |permission|
Authorization.contextual_permissions(permission, :work_package, raise_on_unknown: true)
end
permitted_entities[work_package] += permissions
end
def globally(*permissions)
permissions.each do |permission|
Authorization.contextual_permissions(permission, :global, raise_on_unknown: true)
end
permitted_entities[:global] += permissions
end
end
module MockedPermissionHelper
def mock_permissions_for(user) # rubocop:disable Metrics/PerceivedComplexity
permission_mock = PermissionMock.new(user)
yield permission_mock if block_given?
# Instead of mocking directly on the user, we mock on the UserPermissibleService
# Advantage is that we can handle the `allowed_in_entity?` calls correctly without needing to write
# a mock for each of them
permissible_service = user.send(:user_permissible_service) # access the private instance
# Permission is allowed globally, when it has been given globally
allow(permissible_service).to receive(:allowed_globally?) do |permission_or_action|
next true if permission_mock.allow_all_permissions
permissions = Authorization.permissions_for(permission_or_action).map(&:name)
permission_mock.permitted_entities[:global].intersect?(permissions)
end
# Permission allowed on one (or more) projects, when it has been given to all of them
allow(permissible_service).to receive(:allowed_in_project?) do |permission_or_action, project_or_projects|
next true if permission_mock.allow_all_permissions
projects = Array(project_or_projects)
permissions = Authorization.permissions_for(permission_or_action).map(&:name)
projects.all? do |project|
permission_mock.permitted_entities[project].intersect?(permissions)
end
end
# Permission allowed on any project, if it has been given to any project
allow(permissible_service).to receive(:allowed_in_any_project?) do |permission_or_action|
next true if permission_mock.allow_all_permissions
permissions = Authorization.permissions_for(permission_or_action).map(&:name)
permission_mock.permitted_entities
.select { |k, _| k.is_a?(Project) }
.values
.flatten
.intersect?(permissions)
end
# Permission is allowed on any entity, if
# - filtering for one specific project, when
# - the permission has been given to that project
# - the permission has been given to any work package belonging to that project
# - NOT filtering for one specific project, when
# - the permission has been given to any project
# - the permission has been given to any entity
allow(permissible_service).to receive(:allowed_in_any_entity?) do |permission_or_action, entity_class, in_project:|
next true if permission_mock.allow_all_permissions
permissions = Authorization.permissions_for(permission_or_action).map(&:name)
next true if in_project && permission_mock.permitted_entities[in_project].intersect?(permissions)
filtered_entities = if in_project
permission_mock.permitted_entities.select do |k, _|
k.is_a?(entity_class) && k.respond_to?(:project) && k.project == in_project
end
else
permission_mock.permitted_entities.select { |k, _| k.is_a?(entity_class) || k.is_a?(Project) }
end
filtered_entities
.values
.flatten
.intersect?(permissions)
end
# Permission is allowed on a specific entity, if
# - the permission has been given to the project the entity belongs to
# - the permission has been given to the entity itself
allow(permissible_service).to receive(:allowed_in_entity?) do |permission_or_action, entity|
next true if permission_mock.allow_all_permissions
permissions = Authorization.permissions_for(permission_or_action).map(&:name)
(entity.respond_to?(:project) && permission_mock.permitted_entities[entity.project].intersect?(permissions)) ||
permission_mock.permitted_entities[entity].intersect?(permissions)
end
# Also mock the legacy interface using the `allowed_to?` method
allow(user).to receive(:allowed_to?) do |permission_or_action, project, global: false|
next true if permission_mock.allow_all_permissions
permissions = Authorization.permissions_for(permission_or_action).map(&:name)
if global
# global permission is true, when it is either allowed globally (for global permissions) or
# when it is allowed in any project (for project permissions).
permission_mock.permitted_entities[:global].intersect?(permissions) ||
permission_mock.permitted_entities
.select { |k, _| k.is_a?(Project) }
.values
.flatten
.intersect?(permissions)
elsif project
permission_mock.permitted_entities[project].intersect?(permissions)
end
end
end
end
RSpec.configure do |config|
config.include MockedPermissionHelper
end
@@ -0,0 +1,237 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
RSpec.describe MockedPermissionHelper do
let(:user) { build(:user) }
let(:project) { build(:project) }
let(:other_project) { build(:project) }
let(:work_package_in_project) { build(:work_package, project:) }
let(:other_work_package_in_project) { build(:work_package, project:) }
let(:other_work_package) { build(:work_package) }
context 'when trying to mock a permission that does not exist' do
it 'raises UnknownPermissionError exception' do
expect do
mock_permissions_for(user) do |mock|
mock.globally :this_permission_does_not_exist
end
end.to raise_error(Authorization::UnknownPermissionError)
end
end
context 'when trying to mock a permission in the wrong context' do
it 'raises IllegalPermissionContext exception' do
expect do
mock_permissions_for(user) do |mock|
mock.globally :view_work_packages # this is a project/work_package permission
end
end.to raise_error(Authorization::IllegalPermissionContextError)
end
end
context 'when not mocking any permissions' do
before do
mock_permissions_for(user)
end
it 'does not allow anything' do
expect(user).not_to be_allowed_globally(:add_project)
expect(user).not_to be_allowed_in_project(:add_work_packages, project)
expect(user).not_to be_allowed_in_any_project(:add_work_packages)
expect(user).not_to be_allowed_in_work_package(:add_work_packages, work_package_in_project)
expect(user).not_to be_allowed_in_any_work_package(:add_work_packages)
end
end
context 'when explicitly forbidding everything' do
before do
mock_permissions_for(user) do |mock|
mock.all_permissions_allowed!
mock.in_project :add_work_packages, project:
# this removes all permissions previously set
mock.forbid_everything!
end
end
it 'does not allow anything' do
expect(user).not_to be_allowed_globally(:add_project)
expect(user).not_to be_allowed_in_project(:add_work_packages, project)
expect(user).not_to be_allowed_in_any_project(:add_work_packages)
expect(user).not_to be_allowed_in_work_package(:add_work_packages, work_package_in_project)
expect(user).not_to be_allowed_in_any_work_package(:add_work_packages)
end
end
context 'when mocking all permissions' do
before do
mock_permissions_for(user, &:all_permissions_allowed!)
end
it 'allows everything' do
expect(user).to be_allowed_globally(:add_project)
expect(user).to be_allowed_in_project(:add_work_packages, project)
expect(user).to be_allowed_in_any_project(:add_work_packages)
expect(user).to be_allowed_in_work_package(:add_work_packages, work_package_in_project)
expect(user).to be_allowed_in_any_work_package(:add_work_packages)
# legacy interface
expect(user).to be_allowed_to_globally(:add_project)
expect(user).to be_allowed_to_in_project(:add_work_packages, project)
expect(user).to be_allowed_to_globally(:add_work_packages)
end
end
context 'when mocking a global permission' do
before do
mock_permissions_for(user) do |mock|
mock.globally :add_project
end
end
it 'allows the global permission' do
expect(user).to be_allowed_globally(:add_project)
end
it 'allows the global permission when querying with controller and action hash' do
expect(user).to be_allowed_globally({ controller: 'projects', action: 'new' })
end
it 'allows the global permission using the deprecated interface' do
expect(user).to be_allowed_to_globally(:add_project)
expect(user).to be_allowed_to(:add_project, nil, global: true)
end
end
context 'when mocking a permission in the project' do
before do
mock_permissions_for(user) do |mock|
mock.in_project :view_work_packages, :add_work_packages, project:
end
end
it 'allows the permissions when asking for the project' do
expect(user).to be_allowed_in_project(:view_work_packages, project)
expect(user).not_to be_allowed_in_project(:view_work_packages, other_project)
expect(user).to be_allowed_in_project(:add_work_packages, project)
expect(user).not_to be_allowed_in_project(:add_work_packages, other_project)
end
it 'allows the project permission when querying with controller and action hash' do
expect(user).to be_allowed_in_project({ controller: 'work_packages', action: 'index', project_id: project.id })
expect(user).to be_allowed_in_any_project({ controller: 'work_packages', action: 'index' })
end
it 'allows the permission when using the deprecated interface' do
expect(user).to be_allowed_to_in_project(:view_work_packages, project)
expect(user).to be_allowed_to(:view_work_packages, project)
expect(user).to be_allowed_to_globally(:view_work_packages)
end
it 'allows the permissions when asking for any project' do
expect(user).to be_allowed_in_any_project(:view_work_packages)
expect(user).to be_allowed_in_any_project(:add_work_packages)
end
it 'allows the permissions when asking for any work package within the project' do
expect(user).to be_allowed_in_any_work_package(:view_work_packages, in_project: project)
expect(user).not_to be_allowed_in_any_work_package(:view_work_packages, in_project: other_project)
expect(user).to be_allowed_in_any_work_package(:add_work_packages, in_project: project)
expect(user).not_to be_allowed_in_any_work_package(:add_work_packages, in_project: other_project)
end
it 'allows the permissions when asking for any work package' do
expect(user).to be_allowed_in_any_work_package(:view_work_packages)
expect(user).to be_allowed_in_any_work_package(:add_work_packages)
expect(user).not_to be_allowed_in_any_work_package(:copy_work_packages)
end
it 'allows the permission when asking for a specific work package within the project' do
expect(user).to be_allowed_in_work_package(:view_work_packages, work_package_in_project)
expect(user).not_to be_allowed_in_work_package(:view_work_packages, other_work_package)
end
end
context 'when mocking a permission in the work package' do
before do
mock_permissions_for(user) do |mock|
mock.in_work_package :view_work_packages, work_package: work_package_in_project
mock.in_work_package :view_work_packages, :edit_work_packages, work_package: other_work_package_in_project
end
end
it 'does not allow the permissions when asking for the project' do
expect(user).not_to be_allowed_in_project(:view_work_packages, project)
end
it 'does not allow the permissions when asking for any project' do
expect(user).not_to be_allowed_in_any_project(:view_work_packages)
expect(user).not_to be_allowed_in_any_project(:edit_work_packages)
end
it 'allows the permissions when asking for any work package within the project' do
expect(user).to be_allowed_in_any_work_package(:view_work_packages, in_project: project)
expect(user).not_to be_allowed_in_any_work_package(:view_work_packages, in_project: other_project)
expect(user).to be_allowed_in_any_work_package(:edit_work_packages, in_project: project)
expect(user).not_to be_allowed_in_any_work_package(:edit_work_packages, in_project: other_project)
expect(user).not_to be_allowed_in_any_work_package(:copy_work_packages, in_project: project)
end
it 'allows the work package permission when querying with controller and action hash' do
expect(user).to be_allowed_in_work_package({ controller: 'work_packages', action: 'index', project_id: project.id },
work_package_in_project)
expect(user).to be_allowed_in_any_work_package({ controller: 'work_packages', action: 'index', project_id: project.id })
expect(user).to be_allowed_in_any_work_package({ controller: 'work_packages', action: 'index', project_id: project.id },
in_project: project)
end
it 'allows the permissions when asking for any work package' do
expect(user).to be_allowed_in_any_work_package(:view_work_packages)
expect(user).to be_allowed_in_any_work_package(:edit_work_packages)
expect(user).not_to be_allowed_in_any_work_package(:copy_work_packages)
end
it 'allows the permission when asking for a specific work package within the project' do
expect(user).to be_allowed_in_work_package(:view_work_packages, work_package_in_project)
expect(user).to be_allowed_in_work_package(:view_work_packages, other_work_package_in_project)
expect(user).not_to be_allowed_in_work_package(:view_work_packages, other_work_package)
expect(user).not_to be_allowed_in_work_package(:edit_work_packages, work_package_in_project)
expect(user).to be_allowed_in_work_package(:edit_work_packages, other_work_package_in_project)
expect(user).not_to be_allowed_in_work_package(:edit_work_packages, other_work_package)
end
end
end