diff --git a/app/services/project_access_tokens/rotate_service.rb b/app/services/resource_access_tokens/rotate_service.rb similarity index 56% rename from app/services/project_access_tokens/rotate_service.rb rename to app/services/resource_access_tokens/rotate_service.rb index 63d8d2a82cc69d57b717d0979dc4c47f78b3738c..a55ddcb795f4a34f43788db21cb6fc0c5aaee2f7 100644 --- a/app/services/project_access_tokens/rotate_service.rb +++ b/app/services/resource_access_tokens/rotate_service.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true -module ProjectAccessTokens +module ResourceAccessTokens class RotateService < ::PersonalAccessTokens::RotateService extend ::Gitlab::Utils::Override def initialize(current_user, token, resource = nil) @current_user = current_user @token = token - @project = resource + @resource = resource end def execute(params = {}) super end - attr_reader :project + attr_reader :resource private @@ -44,15 +44,34 @@ def update_bot_membership(target_user, expires_at) end def valid_access_level? - return true if current_user.can_admin_all_resources? - return false unless current_user.can?(:manage_resource_access_tokens, project) + return true if admin_all_resources? + return false unless can_manage_tokens? - token_access_level = project.team.max_member_access(token.user.id).to_i - current_user_access_level = project.team.max_member_access(current_user.id).to_i + token_access_level <= current_user_access_level + end + + def admin_all_resources? + current_user.can_admin_all_resources? + end + + def can_manage_tokens? + current_user.can?(:manage_resource_access_tokens, resource) + end - return true if token_access_level.to_i <= current_user_access_level + def token_access_level + if resource.is_a? Project + resource.team.max_member_access(token.user.id).to_i + else + resource.max_member_access_for_user(token.user).to_i + end + end - false + def current_user_access_level + if resource.is_a? Project + resource.team.max_member_access(current_user.id).to_i + else + resource.max_member_access_for_user(current_user).to_i + end end end end diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb index 1e1b5d77cfdab4a2f0f54903df065299dfbc6ce0..7a477c6fed559546c2f0003bad027c8b26638de1 100644 --- a/lib/api/resource_access_tokens.rb +++ b/lib/api/resource_access_tokens.rb @@ -153,13 +153,8 @@ class ResourceAccessTokens < ::API::Base token = find_token(resource, params[:token_id]) if resource_accessible if token - response = if source_type == "project" - ::ProjectAccessTokens::RotateService.new(current_user, token, resource) + response = ::ResourceAccessTokens::RotateService.new(current_user, token, resource) .execute(declared_params) - else - ::PersonalAccessTokens::RotateService.new(current_user, token) - .execute(declared_params) - end if response.success? status :ok diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb index 70d665383758b07366d2d9569dc29dcdcc8fe59c..7497add0adc6662cb21c674f91955299c4656039 100644 --- a/spec/requests/api/resource_access_tokens_spec.rb +++ b/spec/requests/api/resource_access_tokens_spec.rb @@ -575,21 +575,14 @@ context 'when service raises an error' do let(:error_message) { 'boom!' } - let(:personal_token_service) { PersonalAccessTokens::RotateService } - let(:project_token_service) { ProjectAccessTokens::RotateService } + let(:resource_token_service) { ResourceAccessTokens::RotateService } before do resource.add_maintainer(project_bot) resource.add_owner(user) - if source_type == 'project' - allow_next_instance_of(project_token_service) do |service| - allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message)) - end - else - allow_next_instance_of(personal_token_service) do |service| - allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message)) - end + allow_next_instance_of(resource_token_service) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message)) end end diff --git a/spec/services/project_access_tokens/rotate_service_spec.rb b/spec/services/project_access_tokens/rotate_service_spec.rb deleted file mode 100644 index 10e29be4979f109ffdb89f6d99af89edd32a8cb6..0000000000000000000000000000000000000000 --- a/spec/services/project_access_tokens/rotate_service_spec.rb +++ /dev/null @@ -1,189 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -RSpec.describe ProjectAccessTokens::RotateService, feature_category: :system_access do - describe '#execute' do - let_it_be(:token, reload: true) { create(:personal_access_token) } - let(:current_user) { create(:user) } - let(:project) { create(:project, group: create(:group)) } - let(:error_message) { 'Not eligible to rotate token with access level higher than the user' } - - subject(:response) { described_class.new(current_user, token, project).execute } - - shared_examples_for 'rotates token succesfully' do - it "rotates user's own token", :freeze_time do - expect(response).to be_success - - new_token = response.payload[:personal_access_token] - - expect(new_token.token).not_to eq(token.token) - expect(new_token.expires_at).to eq(Date.today + 1.week) - expect(new_token.user).to eq(token.user) - end - end - - context 'when user tries to rotate token with different access level' do - before do - project.add_guest(token.user) - end - - context 'when current user is an owner' do - before do - project.add_owner(current_user) - end - - it_behaves_like "rotates token succesfully" - - context 'when creating the new token fails' do - let(:error_message) { 'boom!' } - - before do - allow_next_instance_of(PersonalAccessToken) do |token| - allow(token).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return(error_message) - allow(token).to receive_message_chain(:errors, :clear) - allow(token).to receive_message_chain(:errors, :empty?).and_return(false) - end - end - - it 'returns an error' do - expect(response).to be_error - expect(response.message).to eq(error_message) - end - - it 'reverts the changes' do - expect { response }.not_to change { token.reload.revoked? }.from(false) - end - end - end - - context 'when current user is not an owner' do - context 'when current user is maintainer' do - before do - project.add_maintainer(current_user) - end - - context 'when access level is not owner' do - it_behaves_like "rotates token succesfully" - end - - context 'when access level is owner' do - before do - project.add_owner(token.user) - end - - it "does not rotate token with higher priviledge" do - response - - expect(response).to be_error - expect(response.message).to eq(error_message) - end - end - end - - context 'when current user is not maintainer' do - before do - project.add_developer(current_user) - end - - it 'does not rotate the token' do - response - - expect(response).to be_error - expect(response.message).to eq(error_message) - end - end - end - - context 'when current user is admin' do - let(:current_user) { create(:admin) } - - context 'when admin mode enabled', :enable_admin_mode do - it_behaves_like "rotates token succesfully" - end - - context 'when admin mode not enabled' do - it 'does not rotate the token' do - response - - expect(response).to be_error - expect(response.message).to eq(error_message) - end - end - end - - context 'when nested membership' do - let_it_be(:project_bot) { create(:user, :project_bot) } - let(:token) { create(:personal_access_token, user: project_bot) } - let(:top_level_group) { create(:group) } - let(:sub_group) { create(:group, parent: top_level_group) } - let(:project) { create(:project, group: sub_group) } - - before do - project.add_maintainer(project_bot) - end - - context 'when current user is an owner' do - before do - project.add_owner(current_user) - end - - it_behaves_like "rotates token succesfully" - - context 'when its a bot user' do - let_it_be(:bot_user) { create(:user, :project_bot) } - let_it_be(:bot_user_membership) do - create(:project_member, :developer, user: bot_user, project: create(:project)) - end - - let_it_be(:token, reload: true) { create(:personal_access_token, user: bot_user) } - - it 'updates membership expires at' do - response - - new_token = response.payload[:personal_access_token] - expect(bot_user_membership.reload.expires_at).to eq(new_token.expires_at) - end - end - end - - context 'when current user is not an owner' do - context 'when current user is maintainer' do - before do - project.add_maintainer(current_user) - end - - context 'when access level is not owner' do - it_behaves_like "rotates token succesfully" - end - - context 'when access level is owner' do - before do - project.add_owner(token.user) - end - - it "does not rotate token with higher priviledge" do - response - - expect(response).to be_error - expect(response.message).to eq(error_message) - end - end - end - - context 'when current user is not maintainer' do - before do - project.add_developer(current_user) - end - - it 'does not rotate the token' do - response - - expect(response).to be_error - expect(response.message).to eq(error_message) - end - end - end - end - end - end -end diff --git a/spec/services/resource_access_tokens/rotate_service_spec.rb b/spec/services/resource_access_tokens/rotate_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..67ec8816c9c5caeb70b161a273168113d9b3b679 --- /dev/null +++ b/spec/services/resource_access_tokens/rotate_service_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceAccessTokens::RotateService, feature_category: :system_access do + shared_examples_for 'rotates token successfully' do + it "rotates user's own token", :freeze_time do + expect(response).to be_success + + new_token = response.payload[:personal_access_token] + + expect(new_token.token).not_to eq(token.token) + expect(new_token.expires_at).to eq(Date.today + 1.week) + expect(new_token.user).to eq(token.user) + end + + it 'updates membership expires_at' do + response + + new_token = response.payload[:personal_access_token] + expect(bot_user.members.first.reload.expires_at).to eq(new_token.expires_at) + end + end + + shared_examples 'token rotation access level check' do |source_type| + before do + resource.add_guest(token.user) + end + + context 'when current user is an owner' do + before do + resource.add_owner(current_user) + end + + it_behaves_like "rotates token successfully" + + context 'when creating the new token fails' do + let(:error_message) { 'boom!' } + + before do + allow_next_instance_of(PersonalAccessToken) do |token| + allow(token).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return(error_message) + allow(token).to receive_message_chain(:errors, :clear) + allow(token).to receive_message_chain(:errors, :empty?).and_return(false) + end + end + + it 'returns an error' do + expect(response).to be_error + expect(response.message).to eq(error_message) + end + + it 'reverts the changes' do + expect { response }.not_to change { token.reload.revoked? }.from(false) + end + end + end + + context 'when current user is maintainer' do + before do + resource.add_maintainer(current_user) + end + + context 'and token user is not owner' do + if source_type == 'project' + it_behaves_like "rotates token successfully" + elsif source_type == 'group' + it 'cannot rotate the token' do + response + + expect(response).to be_error + expect(response.message).to eq(error_message) + end + end + end + + context 'and token user is owner' do + before do + resource.add_owner(token.user) + end + + it "cannot rotate token with higher privilege" do + response + + expect(response).to be_error + expect(response.message).to eq(error_message) + end + end + end + + context 'when current user is neither owner or maintainer' do + before do + resource.add_developer(current_user) + end + + it 'cannot rotate the token' do + response + + expect(response).to be_error + expect(response.message).to eq(error_message) + end + end + + context 'when current user is admin' do + let(:current_user) { create(:admin) } + + context 'with admin mode', :enable_admin_mode do + it_behaves_like "rotates token successfully" + end + + context 'without admin mode' do + it 'cannot rotate the token' do + response + + expect(response).to be_error + expect(response.message).to eq(error_message) + end + end + end + end + + describe '#execute' do + subject(:response) { described_class.new(current_user, token, resource).execute } + + let(:current_user) { create(:user) } + let(:error_message) { 'Not eligible to rotate token with access level higher than the user' } + let(:bot_user) { create(:user, :project_bot) } + let(:token) { create(:personal_access_token, user: bot_user) } + + context 'for project' do + let_it_be(:resource) { create(:project, group: create(:group)) } + + it_behaves_like 'token rotation access level check', 'project' + + context 'with a nested membership' do + let(:top_level_group) { create(:group) } + let(:sub_group) { create(:group, parent: top_level_group) } + let(:resource) { create(:project, group: sub_group) } + + it_behaves_like 'token rotation access level check', 'project' + end + end + + context 'for group' do + let(:resource) { create(:group) } + + it_behaves_like 'token rotation access level check', 'group' + + context 'with a nested membership' do + let(:top_level_group) { create(:group) } + let(:resource) { create(:group, parent: top_level_group) } + + it_behaves_like 'token rotation access level check', 'group' + end + end + end +end