diff --git a/app/controllers/application_controller/authorizes.rb b/app/controllers/application_controller/authorizes.rb index 7c246ac0d..3485d66ee 100644 --- a/app/controllers/application_controller/authorizes.rb +++ b/app/controllers/application_controller/authorizes.rb @@ -22,39 +22,6 @@ module ApplicationController::Authorizes [:controllers, self] end - # We need a special UserContext when authorizing in controller context - # because of Token authentication which has it's own permissions - # See: https://github.com/varvet/pundit#additional-context - # We use a Delegator here to have transparent / DuckType access - # to the underlying User instance in the Policy - class UserContext < Delegator - - def initialize(user, token) - @user = user - @token = token - end - - def __getobj__ - @user - end - - def permissions!(permissions) - raise Exceptions::NotAuthorized, 'authentication failed' if !@user - raise Exceptions::NotAuthorized, 'Not authorized (user)!' if !@user.permissions?(permissions) - return if !@token - return if @token.permissions?(permissions) - - raise Exceptions::NotAuthorized, 'Not authorized (token)!' - end - - def permissions?(permissions) - permissions!(permissions) - true - rescue Exceptions::NotAuthorized - false - end - end - def pundit_user @pundit_user ||= UserContext.new(current_user, @_token) end diff --git a/app/models/token.rb b/app/models/token.rb index 0c84aca5a..b7acdb2d3 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -109,11 +109,22 @@ cleanup old token end def permissions?(names) - return false if !user.permissions?(names) + return false if !effective_user.permissions?(names) super(names) end + # allows to evaluate token permissions in context of given user instead of owner + # @param [User] user to use as context for the given block + # @param block to evaluate in given context + def with_context(user:, &block) + @effective_user = user + + instance_eval(&block) if block_given? + ensure + @effective_user = nil + end + private def generate_token @@ -123,4 +134,9 @@ cleanup old token end true end + + # token owner or user set by #with_context + def effective_user + @effective_user || user + end end diff --git a/lib/user_context.rb b/lib/user_context.rb new file mode 100644 index 000000000..0533c667d --- /dev/null +++ b/lib/user_context.rb @@ -0,0 +1,32 @@ +# We need a special UserContext when authorizing in controller context +# because of Token authentication which has it's own permissions +# See: https://github.com/varvet/pundit#additional-context +# We use a Delegator here to have transparent / DuckType access +# to the underlying User instance in the Policy +class UserContext < Delegator + + def initialize(user, token) + @user = user + @token = token + end + + def __getobj__ + @user + end + + def permissions!(permissions) + raise Exceptions::NotAuthorized, 'authentication failed' if !@user + raise Exceptions::NotAuthorized, 'Not authorized (user)!' if !@user.permissions?(permissions) + return if !@token + return if @token.with_context(user: @user) { permissions?(permissions) } + + raise Exceptions::NotAuthorized, 'Not authorized (token)!' + end + + def permissions?(permissions) + permissions!(permissions) + true + rescue Exceptions::NotAuthorized + false + end +end diff --git a/spec/factories/role.rb b/spec/factories/role.rb index ca056a213..6b2842562 100644 --- a/spec/factories/role.rb +++ b/spec/factories/role.rb @@ -7,5 +7,9 @@ FactoryBot.define do factory :agent_role do permissions { Permission.where(name: 'ticket.agent') } end + + trait :admin do + permissions { Permission.where(name: 'admin') } + end end end diff --git a/spec/lib/user_context_spec.rb b/spec/lib/user_context_spec.rb new file mode 100644 index 000000000..642483d5f --- /dev/null +++ b/spec/lib/user_context_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe UserContext do + subject(:user_context) { described_class.new(user, token) } + + describe '#permissions?' do + context 'when user with ticket.agent permission' do + let(:user) { create(:user, roles: [create(:agent_role)]) } + let(:token) { nil } + + it { is_expected.to be_permissions('ticket.agent') } + it { is_expected.not_to be_permissions('admin') } + end + + # https://github.com/zammad/zammad/issues/3186 + context 'when user with ticket.agent permission and token created by user who doesn\'t' do + let(:user) { create(:user, roles: [create(:agent_role)]) } + let(:token_owner) { create(:user, roles: [create(:role, :admin)]) } + let(:token) { create(:token, user: token_owner, preferences: { permission: %w[ticket.agent] }) } + + it { is_expected.to be_permissions('ticket.agent') } + it { is_expected.not_to be_permissions('admin') } + end + end +end