diff --git a/config/feature_flags/development/manage_project_access_tokens.yml b/config/feature_flags/development/manage_project_access_tokens.yml new file mode 100644 index 0000000000000000000000000000000000000000..6a91e1fc140f3d67e84ac5c6c53849e6b20d1902 --- /dev/null +++ b/config/feature_flags/development/manage_project_access_tokens.yml @@ -0,0 +1,8 @@ +--- +name: manage_project_access_tokens +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132342 +rollout_issue_url: +milestone: '16.5' +type: development +group: group::authentication and authorization +default_enabled: false diff --git a/db/migrate/20231015111533_add_manage_project_access_tokens_to_member_roles.rb b/db/migrate/20231015111533_add_manage_project_access_tokens_to_member_roles.rb new file mode 100644 index 0000000000000000000000000000000000000000..a3c0a39b74874eb4657302f7b69fb1de25deceea --- /dev/null +++ b/db/migrate/20231015111533_add_manage_project_access_tokens_to_member_roles.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddManageProjectAccessTokensToMemberRoles < Gitlab::Database::Migration[2.1] + def change + add_column :member_roles, :manage_project_access_tokens, :boolean, default: false, null: false + end +end diff --git a/db/schema_migrations/20231015111533 b/db/schema_migrations/20231015111533 new file mode 100644 index 0000000000000000000000000000000000000000..fd249195e63eab286454d8006902eca7e30f4ac5 --- /dev/null +++ b/db/schema_migrations/20231015111533 @@ -0,0 +1 @@ +d4fd2e43046da8892341767256fd7d5d1480dc57ce77699937243d04060985d4 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0d6de24b54541fb747c901be75a7799e7ff67bdd..226c93d793859439ece28c8ba07da8cdb60138e6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18146,6 +18146,7 @@ CREATE TABLE member_roles ( description text, admin_merge_request boolean DEFAULT false NOT NULL, admin_group_member boolean DEFAULT false NOT NULL, + manage_project_access_tokens boolean DEFAULT false NOT NULL, CONSTRAINT check_4364846f58 CHECK ((char_length(description) <= 255)), CONSTRAINT check_9907916995 CHECK ((char_length(name) <= 255)) ); diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md index 36f9b4694b95aeef32eefb061257430de5f64544..dc31c99f9e826024a5f9640c982aa0fd89d9be13 100644 --- a/doc/api/member_roles.md +++ b/doc/api/member_roles.md @@ -14,6 +14,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - [Name and description fields added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126423) in GitLab 16.3. > - [Admin merge request introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `admin_merge_request`. Disabled by default. > - [Admin group members introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131914) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `admin_group_member`. Disabled by default. +> - [Manage project access tokens introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132342) in GitLab 16.5 in [with a flag](../administration/feature_flags.md) named `manage_project_access_tokens`. Disabled by default. FLAG: On self-managed GitLab, by default these two features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_merge_request` and `admin_member_custom_role`. @@ -33,19 +34,20 @@ GET /groups/:id/member_roles If successful, returns [`200`](rest/index.md#status-codes) and the following response attributes: -| Attribute | Type | Description | -|:-------------------------|:--------|:----------------------| -| `[].id` | integer | The ID of the member role. | -| `[].name` | string | The name of the member role. | -| `[].description` | string | The description of the member role. | -| `[].group_id` | integer | The ID of the group that the member role belongs to. | -| `[].base_access_level` | integer | Base access level for member role. Valid values are 10 (Guest), 20 (Reporter), 30 (Developer), 40 (Maintainer), or 50 (Owner).| -| `[].admin_merge_request` | boolean | Permission to admin project merge requests and enables the ability to `download_code`. | -| `[].admin_vulnerability` | boolean | Permission to admin project vulnerabilities. | -| `[].read_code` | boolean | Permission to read project code. | -| `[].read_dependency` | boolean | Permission to read project dependencies. | -| `[].read_vulnerability` | boolean | Permission to read project vulnerabilities. | -| `[].admin_group_member` | boolean | Permission to admin members of a group. | +| Attribute | Type | Description | +|:-----------------------------------|:--------|:----------------------| +| `[].id` | integer | The ID of the member role. | +| `[].name` | string | The name of the member role. | +| `[].description` | string | The description of the member role. | +| `[].group_id` | integer | The ID of the group that the member role belongs to. | +| `[].base_access_level` | integer | Base access level for member role. Valid values are 10 (Guest), 20 (Reporter), 30 (Developer), 40 (Maintainer), or 50 (Owner).| +| `[].admin_merge_request` | boolean | Permission to admin project merge requests and enables the ability to `download_code`. | +| `[].admin_vulnerability` | boolean | Permission to admin project vulnerabilities. | +| `[].read_code` | boolean | Permission to read project code. | +| `[].read_dependency` | boolean | Permission to read project dependencies. | +| `[].read_vulnerability` | boolean | Permission to read project vulnerabilities. | +| `[].admin_group_member` | boolean | Permission to admin members of a group. | +| `[].manage_project_access_tokens` | boolean | Permission to manage project access tokens. | Example request: @@ -67,7 +69,8 @@ Example response: "admin_vulnerability": false, "read_code": true, "read_dependency": false, - "read_vulnerability": false + "read_vulnerability": false, + "manage_project_access_tokens": false }, { "id": 3, @@ -79,7 +82,8 @@ Example response: "admin_vulnerability": true, "read_code": false, "read_dependency": true, - "read_vulnerability": true + "read_vulnerability": true, + "manage_project_access_tokens": false } ] ``` diff --git a/doc/user/custom_roles.md b/doc/user/custom_roles.md index 53cb835eed1e7e10ec35e8079907d07b110ffb12..1cd5ec67284736960ddfe633148a73e9a6689f53 100644 --- a/doc/user/custom_roles.md +++ b/doc/user/custom_roles.md @@ -14,6 +14,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - [Feature flag `custom_roles_vulnerability` removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124049) in GitLab 16.2. > - Ability to create and remove a custom role with the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393235) in GitLab 16.4. > - Ability to manage group members [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17364) in GitLab 16.5 under `admin_group_member` Feature flag. +> - Ability to manage project access tokens [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421778) in GitLab 16.5 under `manage_project_access_tokens` Feature flag. Custom roles allow group members who are assigned the Owner role to create roles specific to the needs of their organization. @@ -102,6 +103,7 @@ You can see the abilities requirements in the following table. | `admin_merge_request` | - | | `admin_vulnerability` | `read_vulnerability` | | `admin_group_member` | - | +| `manage_project_access_tokens` | - | ## Associate a custom role with an existing group member diff --git a/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue b/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue index 49a0a881a3655b916214a520bd90d8a666bfff9b..ed63fd7c9a720ac35a93cb2428a3ddbb30826c84 100644 --- a/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue +++ b/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue @@ -54,9 +54,19 @@ export default { name: '', nameValid: true, permissions: [], + availablePermissions: [], permissionsValid: null, }; }, + mounted() { + if (!gon.features.manageProjectAccessTokens) { + this.availablePermissions = Object.values(PERMISSIONS).filter( + (permission) => permission.value !== 'manage_project_access_tokens', + ); + } else { + this.availablePermissions = Object.values(PERMISSIONS); + } + }, methods: { areFieldsValid() { this.baseRoleValid = true; @@ -135,7 +145,6 @@ export default { label: I18N_NEW_ROLE_PERMISSIONS_LABEL, }, }, - permissions: Object.values(PERMISSIONS), }; @@ -185,7 +194,7 @@ export default { diff --git a/ee/app/assets/javascripts/roles_and_permissions/constants.js b/ee/app/assets/javascripts/roles_and_permissions/constants.js index fa86f15cb61d83a6fb3026fd636cdbf580b74f3d..149118d0074c208e5e40c5c624b3760c4c0f780a 100644 --- a/ee/app/assets/javascripts/roles_and_permissions/constants.js +++ b/ee/app/assets/javascripts/roles_and_permissions/constants.js @@ -21,6 +21,7 @@ export const BASE_ROLES = Object.freeze( export const READ_CODE = 'read_code'; export const READ_VULNERABILITY = 'read_vulnerability'; export const ADMIN_VULNERABILITY = 'admin_vulnerability'; +export const MANAGE_PROJECT_ACCESS_TOKENS = 'manage_project_access_tokens'; export const PERMISSIONS = Object.freeze({ [READ_CODE]: { @@ -40,6 +41,13 @@ export const PERMISSIONS = Object.freeze({ text: s__('MemberRoles|Admin vulnerability'), value: ADMIN_VULNERABILITY, }, + [MANAGE_PROJECT_ACCESS_TOKENS]: { + help: s__( + "MemberRoles|Allows manage access to the project access tokens. Select 'Manage Project Access Tokens' for this to take effect.", + ), + text: s__('MemberRoles|Manage Project Access Tokens'), + value: MANAGE_PROJECT_ACCESS_TOKENS, + }, }); export const FIELDS = [ diff --git a/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb b/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb index e12c9f8fe92dc4451be25f3eb21457df9b659376..6ed3c81fdeba7462e5ebf193a0913a49f8ddc546 100644 --- a/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb +++ b/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb @@ -8,6 +8,9 @@ class RolesAndPermissionsController < Groups::ApplicationController before_action :authorize_admin_member_roles! before_action :ensure_root_group! before_action :ensure_custom_roles_available! + before_action do + push_frontend_feature_flag(:manage_project_access_tokens, group) + end private diff --git a/ee/app/models/auth/member_role_ability_loader.rb b/ee/app/models/auth/member_role_ability_loader.rb index 2b3e147d9ba0b62d0dfbc847f97e80f262fab6d3..d91560bf2704bdd5b99227c14f9b39285ab4d6d6 100644 --- a/ee/app/models/auth/member_role_ability_loader.rb +++ b/ee/app/models/auth/member_role_ability_loader.rb @@ -12,9 +12,9 @@ def has_ability? return false unless user.is_a?(User) roles = if resource.is_a?(::Project) - preloaded_member_roles_for_project.fetch(resource.id) + preloaded_member_roles_for_project[resource.id] else # Group - preloaded_member_roles_for_group.fetch(resource.id) + preloaded_member_roles_for_group[resource.id] end roles&.include?(ability) diff --git a/ee/app/models/members/member_role.rb b/ee/app/models/members/member_role.rb index 456d5d5b01056c886ae282c7971a6b58a808b0f6..26d7eb7a2e98c2f8f7a90290a6a65b66af6c2e90 100644 --- a/ee/app/models/members/member_role.rb +++ b/ee/app/models/members/member_role.rb @@ -21,6 +21,9 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass }, read_vulnerability: { description: 'Allows read-only access to the vulnerability reports.' + }, + manage_project_access_tokens: { + description: 'Allows manage access to the project access tokens' } }.freeze ALL_CUSTOMIZABLE_PROJECT_PERMISSIONS = [ @@ -28,7 +31,8 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass :read_dependency, :read_vulnerability, :admin_merge_request, - :admin_vulnerability + :admin_vulnerability, + :manage_project_access_tokens ].freeze ALL_CUSTOMIZABLE_GROUP_PERMISSIONS = [ :read_dependency, diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index adbe613bfbad0d04918b3ecdb047edd94c5a2048..336d5a6b5118b55e4a9c5d66e0031efbfb29b74a 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -596,6 +596,19 @@ module ProjectPolicy ::Feature.enabled?(:dependency_scanning_on_advisory_ingestion) end + condition(:manage_project_access_tokens_custom_roles_enabled) do + ::Feature.enabled?(:manage_project_access_tokens, @subject.root_ancestor) + end + + desc "Custom role on project that enables manage project access tokens" + condition(:role_enables_manage_project_access_tokens) do + ::Auth::MemberRoleAbilityLoader.new( + user: @user, + resource: project, + ability: :manage_project_access_tokens + ).has_ability? + end + rule { needs_new_sso_session }.policy do prevent :read_project end @@ -701,6 +714,13 @@ module ProjectPolicy enable :create_resource_access_tokens end + rule { role_enables_manage_project_access_tokens & resource_access_token_feature_available & resource_access_token_creation_allowed & manage_project_access_tokens_custom_roles_enabled }.policy do + enable :read_resource_access_tokens + enable :create_resource_access_tokens + enable :destroy_resource_access_tokens + enable :manage_resource_access_tokens + end + rule { security_policy_bot }.policy do enable :create_pipeline enable :create_bot_pipeline diff --git a/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js b/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js index d9182d54bf1c87b3322691d571141129742d7ef4..4487a45933541aed2580545762d6beb2a98820ad 100644 --- a/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js +++ b/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js @@ -44,6 +44,7 @@ describe('CreateMemberRole', () => { }; beforeEach(() => { + window.gon.features = {}; createComponent(); }); @@ -80,6 +81,21 @@ describe('CreateMemberRole', () => { ); }); + describe('manage_project_access_token feature flag is on', () => { + beforeEach(() => { + window.gon.features = { manageProjectAccessTokens: true }; + createComponent(); + }); + + it('renders manage project access token permission', () => { + const checkboxFourText = findCheckboxes().at(3).text(); + expect(checkboxFourText).toContain('Manage Project Access Tokens'); + expect(checkboxFourText).toContain( + "Allows manage access to the project access tokens. Select 'Manage Project Access Tokens' for this to take effect.", + ); + }); + }); + it('emits cancel event', () => { expect(wrapper.emitted('cancel')).toBeUndefined(); diff --git a/ee/spec/lib/ee/api/entities/member_role_spec.rb b/ee/spec/lib/ee/api/entities/member_role_spec.rb index 23bb6c24a668a77a3e22bf8cb2c42c5728cb4f8f..9fe99b298de87747977ad46fe4d02ab7c2a15b33 100644 --- a/ee/spec/lib/ee/api/entities/member_role_spec.rb +++ b/ee/spec/lib/ee/api/entities/member_role_spec.rb @@ -20,6 +20,7 @@ expect(subject[:read_code]).to eq member_role.read_code expect(subject[:read_vulnerability]).to eq member_role.read_vulnerability expect(subject[:admin_vulnerability]).to eq member_role.admin_vulnerability + expect(subject[:manage_project_access_tokens]).to eq member_role.admin_vulnerability expect(subject[:group_id]).to eq(member_role.namespace.id) end end diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb index 700b4fa20484c8b6ab80f74b36b6cdb2934c9142..747f87d198aeed9c8b5fca822c323606a0e4afbf 100644 --- a/ee/spec/models/ee/user_spec.rb +++ b/ee/spec/models/ee/user_spec.rb @@ -1181,7 +1181,8 @@ OR admin_merge_request = true OR admin_vulnerability = true OR read_dependency = true - OR read_vulnerability = true\)\)\)\)'.squish # allow_cross_joins_across_databases + OR read_vulnerability = true + OR manage_project_access_tokens = true\)\)\)\)'.squish # allow_cross_joins_across_databases end before do diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 620e1cfa03210776b2670f74752f4882ba4e222a..84f787acea8073b8e47f9dfc1abee202ae5cc403 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -2656,6 +2656,29 @@ def create_member_role(member, abilities = member_role_abilities) it { is_expected.to be_disallowed(:read_merge_request, :admin_merge_request, :download_code) } end end + + context 'for a member role with manage_project_access_tokens true' do + let(:member_role_abilities) { { manage_project_access_tokens: true } } + let(:allowed_abilities) { [:manage_resource_access_tokens] } + + context 'with manage_project_access_tokens FF enabled' do + before do + stub_feature_flags(manage_project_access_tokens: [project.group]) + end + + it_behaves_like 'custom roles abilities' + end + + context 'with manage_project_access_tokens FF disabled' do + before do + stub_feature_flags(manage_project_access_tokens: false) + end + + let(:disallowed_abilities) { [:manage_resource_access_tokens] } + + it { is_expected.to be_disallowed(*disallowed_abilities) } + end + end end describe 'permissions for suggested reviewers bot', :saas do diff --git a/ee/spec/requests/api/member_roles_spec.rb b/ee/spec/requests/api/member_roles_spec.rb index b426249efb5d26a15230219abf5ec3af581b8905..06ef35355f0b363a02a1924e4b8a6d036eba1a5c 100644 --- a/ee/spec/requests/api/member_roles_spec.rb +++ b/ee/spec/requests/api/member_roles_spec.rb @@ -105,6 +105,7 @@ "admin_group_member" => false, "admin_merge_request" => false, "admin_vulnerability" => false, + "manage_project_access_tokens" => false, "group_id" => group_id }, { @@ -118,6 +119,7 @@ "admin_group_member" => false, "admin_merge_request" => true, "admin_vulnerability" => false, + "manage_project_access_tokens" => false, "group_id" => group_id } ] diff --git a/ee/spec/requests/custom_roles/manage_project_access_tokens/request_spec.rb b/ee/spec/requests/custom_roles/manage_project_access_tokens/request_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c002cbc30076aec8abf65f896a444513c0859332 --- /dev/null +++ b/ee/spec/requests/custom_roles/manage_project_access_tokens/request_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User with read_dependency custom role', feature_category: :system_access do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, :in_group) } + + before do + stub_licensed_features(custom_roles: true) + sign_in(user) + end + + describe Projects::Settings::AccessTokensController do + let_it_be(:role) { create(:member_role, :guest, namespace: project.group, manage_project_access_tokens: true) } + let_it_be(:member) { create(:project_member, :guest, member_role: role, user: user, project: project) } + + describe 'GET /:namespace/:project/-/settings/access_tokens' do + it 'user has access via custom role' do + get project_settings_access_tokens_path(project) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + end + + describe ProjectsController do + it 'user has access via custom role' do + get project_path(project) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to include('Access Token') + end + end + end +end diff --git a/ee/spec/requests/projects/issues_controller_spec.rb b/ee/spec/requests/projects/issues_controller_spec.rb index afcbdb0435fc5324e2d207edf91de5619744c047..ef3036910942613e6158bf0087baee0c172dd7f9 100644 --- a/ee/spec/requests/projects/issues_controller_spec.rb +++ b/ee/spec/requests/projects/issues_controller_spec.rb @@ -27,6 +27,7 @@ def get_show it 'does not cause extra queries when multiple blocking issues are present' do create(:issue_link, source: blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS) + project.reload control = ActiveRecord::QueryRecorder.new { get_show } other_project_issue = create(:issue) diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb index 142d803037b17146a4ada39a2a697b0acb483a44..8fed1c46425eaccbc36a5eff869c06639a023067 100644 --- a/lib/sidebars/projects/menus/settings_menu.rb +++ b/lib/sidebars/projects/menus/settings_menu.rb @@ -6,19 +6,11 @@ module Menus class SettingsMenu < ::Sidebars::Menu override :configure_menu_items def configure_menu_items - return false unless can?(context.current_user, :admin_project, context.project) - - add_item(general_menu_item) - add_item(integrations_menu_item) - add_item(webhooks_menu_item) - add_item(access_tokens_menu_item) - add_item(repository_menu_item) - add_item(merge_requests_menu_item) - add_item(ci_cd_menu_item) - add_item(packages_and_registries_menu_item) - add_item(monitor_menu_item) - add_item(usage_quotas_menu_item) + return false if enabled_menu_items.empty? + enabled_menu_items.each do |menu_item| + add_item(menu_item) + end true end @@ -51,6 +43,29 @@ def separated? private + def enabled_menu_items + if can?(context.current_user, :admin_project, context.project) + [ + general_menu_item, + integrations_menu_item, + webhooks_menu_item, + access_tokens_menu_item, + repository_menu_item, + merge_requests_menu_item, + ci_cd_menu_item, + packages_and_registries_menu_item, + monitor_menu_item, + usage_quotas_menu_item + ] + elsif context.current_user && can?(context.current_user, :manage_resource_access_tokens, context.project) + [ + access_tokens_menu_item + ] + else + [] + end + end + def general_menu_item ::Sidebars::MenuItem.new( title: _('General'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fcd8c86969ebba0fb3a7bde1028ec967cd47c451..ed53b17580e6e75b9de3342492fc6666905a9644 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -29119,6 +29119,9 @@ msgstr "" msgid "MemberRoles|Allows admin access to the vulnerability reports. Select 'Read vulnerability' for this to take effect." msgstr "" +msgid "MemberRoles|Allows manage access to the project access tokens. Select 'Manage Project Access Tokens' for this to take effect." +msgstr "" + msgid "MemberRoles|Allows read-only access to the source code." msgstr "" @@ -29167,6 +29170,9 @@ msgstr "" msgid "MemberRoles|Make sure the group is in the Ultimate tier." msgstr "" +msgid "MemberRoles|Manage Project Access Tokens" +msgstr "" + msgid "MemberRoles|Name" msgstr "" diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb index 605cec8be5e3003aa36f75fbaa10b031e6c8eb28..81ca9670ac6f152a0803f0a45671b9d47c5ba00c 100644 --- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb @@ -59,6 +59,18 @@ let(:item_id) { :access_tokens } it_behaves_like 'access rights checks' + + describe 'when the user is not an admin but has manage_resource_access_tokens' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :manage_resource_access_tokens, project).and_return(true) + end + + it 'includes access token menu item' do + expect(subject.title).to eql('Access Tokens') + end + end end describe 'Repository' do