diff --git a/db/migrate/20231218172621_add_manage_group_access_tokens_to_member_roles.rb b/db/migrate/20231218172621_add_manage_group_access_tokens_to_member_roles.rb
new file mode 100644
index 0000000000000000000000000000000000000000..58f9f94a880c3ba8f3f36dbf6f398264675a7c4f
--- /dev/null
+++ b/db/migrate/20231218172621_add_manage_group_access_tokens_to_member_roles.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddManageGroupAccessTokensToMemberRoles < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ enable_lock_retries!
+
+ def change
+ add_column :member_roles, :manage_group_access_tokens, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema_migrations/20231218172621 b/db/schema_migrations/20231218172621
new file mode 100644
index 0000000000000000000000000000000000000000..c5fff4c007238998eb617f37426c092715106657
--- /dev/null
+++ b/db/schema_migrations/20231218172621
@@ -0,0 +1 @@
+9a5a5ecda3186fb4ef642ff56d5e3125bfe888e1a903bfdb8cfcef6827c41df6
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2ce0b1daa76c7920c44dfcc934769dca822d4d35..7d1cdf75cf1cd62a599cfef8a65114faadb7c1a5 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18621,6 +18621,7 @@ CREATE TABLE member_roles (
admin_group_member boolean DEFAULT false NOT NULL,
manage_project_access_tokens boolean DEFAULT false NOT NULL,
archive_project boolean DEFAULT false NOT NULL,
+ manage_group_access_tokens boolean DEFAULT false NOT NULL,
remove_project 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/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 621ac03078362ba5ea4f629b7268aa199a84a027..2b43d0a7c567ffdd5b9b2271cc281bd8003c0dfb 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -5377,6 +5377,7 @@ Input type: `MemberRoleCreateInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `description` | [`String`](#string) | Description of the member role. |
| `groupPath` | [`ID`](#id) | Group the member role to mutate is in. Required for SaaS. |
+| `manageGroupAccessTokens` | [`Boolean`](#boolean) | Permission to admin group access tokens. |
| `manageProjectAccessTokens` | [`Boolean`](#boolean) | Permission to admin project access tokens. |
| `name` | [`String`](#string) | Name of the member role. |
| `permissions` | [`[MemberRolePermission!]`](#memberrolepermission) | List of all customizable permissions. |
@@ -21452,6 +21453,7 @@ Represents a member role.
| `description` | [`String`](#string) | Description of the member role. |
| `enabledPermissions` **{warning-solid}** | [`[MemberRolePermission!]`](#memberrolepermission) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Array of all permissions enabled for the custom role. |
| `id` | [`MemberRoleID!`](#memberroleid) | ID of the member role. |
+| `manageGroupAccessTokens` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.8. This feature is an Experiment. It can be changed or removed at any time. Permission to admin group access tokens. |
| `manageProjectAccessTokens` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin project access tokens. |
| `membersCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Total number of members with the custom role. |
| `name` | [`String!`](#string) | Name of the member role. |
@@ -30807,6 +30809,7 @@ Member role permission.
| `ADMIN_MERGE_REQUEST` | Allows to approve merge requests. |
| `ADMIN_VULNERABILITY` | Allows admin access to the vulnerability reports. |
| `ARCHIVE_PROJECT` | Allows to archive projects. |
+| `MANAGE_GROUP_ACCESS_TOKENS` | Allows manage access to the group access tokens. |
| `MANAGE_PROJECT_ACCESS_TOKENS` | Allows manage access to the project access tokens. |
| `READ_CODE` | Allows read-only access to the source code. |
| `READ_DEPENDENCY` | Allows read-only access to the dependencies. |
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index a482195b94e39f5cf9f6ce7099ca8115dbbafb12..2fd10d99fda89dda472645f32500b3913bbca8e7 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -18,6 +18,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [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.
> - [Archive project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134998) in GitLab 16.7.
> - [Delete project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139696) in GitLab 16.8.
+> - [Manage group access tokens introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140115) in GitLab 16.8.
FLAG:
On self-managed GitLab, by default these features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_group_member` and `manage_project_access_tokens`.
@@ -53,6 +54,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and the following res
| `[].manage_project_access_tokens` | boolean | Permission to manage project access tokens. |
| `[].archive_project` | boolean | Permission to archive projects. |
| `[].remove_project` | boolean | Permission to delete projects. |
+| `[].manage_group_access_tokens` | boolean | Permission to manage group access tokens. |
Example request:
@@ -75,6 +77,7 @@ Example response:
"read_code": true,
"read_dependency": false,
"read_vulnerability": false,
+ "manage_group_access_tokens": false,
"manage_project_access_tokens": false,
"archive_project": false,
"remove_project": false
@@ -90,6 +93,7 @@ Example response:
"read_code": false,
"read_dependency": true,
"read_vulnerability": true,
+ "manage_group_access_tokens": false,
"manage_project_access_tokens": false,
"archive_project": false,
"remove_project": false
diff --git a/doc/user/custom_roles.md b/doc/user/custom_roles.md
index ac04e7cca0f1c6f1d18b174055931fd688ba8fe2..3ab4e31859c30e7e57fa85ed38e284543c6ff30c 100644
--- a/doc/user/custom_roles.md
+++ b/doc/user/custom_roles.md
@@ -18,6 +18,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Ability to archive projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425957) in GitLab 16.7.
> - Ability to use the UI to add a user to your group with a custom role, change a user's custom role, or remove a custom role from a group member [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393239) in GitLab 16.7.
> - Ability to delete projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425959) in GitLab 16.8.
+> - Ability to manage group access tokens [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/428353) in GitLab 16.8.
Custom roles allow group Owners or instance administrators to create roles
specific to the needs of their organization.
@@ -117,6 +118,7 @@ These requirements are documented in the `Required permission` column in the fol
| `admin_group_member` | GitLab 16.5 and later | Not applicable | Add or remove [group members](group/manage.md). |
| `archive_project` | GitLab 16.7 and later | Not applicable | [Archive and unarchive projects](project/settings/migrate_projects.md#archive-a-project). |
| `remove_project` | GitLab 16.8 and later | Not applicable | [Delete projects](project/working_with_projects.md#delete-a-project). |
+| `manage_group_access_tokens` | GitLab 16.8 and later | Not applicable | [Create, delete, and list group access tokens](group/settings/group_access_tokens.md). |
## Billing and seat usage
diff --git a/ee/app/graphql/mutations/member_roles/create.rb b/ee/app/graphql/mutations/member_roles/create.rb
index 46ee30ca84ea6c7969b3dd889d332262f9c64c9f..45f2ae412fe16efc7b308d29452bbb7bbddf2a8a 100644
--- a/ee/app/graphql/mutations/member_roles/create.rb
+++ b/ee/app/graphql/mutations/member_roles/create.rb
@@ -30,6 +30,10 @@ class Create < Base
argument :group_path, GraphQL::Types::ID,
required: ::Gitlab::Saas.feature_available?(:gitlab_saas_subscriptions),
description: 'Group the member role to mutate is in. Required for SaaS.'
+ argument :manage_group_access_tokens,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: 'Permission to admin group access tokens.'
argument :manage_project_access_tokens,
GraphQL::Types::Boolean,
required: false,
diff --git a/ee/app/graphql/types/member_roles/member_role_type.rb b/ee/app/graphql/types/member_roles/member_role_type.rb
index 8d02826b1e70efa53c167b833dda7ecd6f6a6f36..809e320a4bca723babcb6501a1a9853e81d11816 100644
--- a/ee/app/graphql/types/member_roles/member_role_type.rb
+++ b/ee/app/graphql/types/member_roles/member_role_type.rb
@@ -53,6 +53,12 @@ class MemberRoleType < BaseObject
alpha: { milestone: '16.8' },
description: 'Permission to delete projects.'
+ field :manage_group_access_tokens,
+ GraphQL::Types::Boolean,
+ null: true,
+ alpha: { milestone: '16.8' },
+ description: 'Permission to admin group access tokens.'
+
field :manage_project_access_tokens,
GraphQL::Types::Boolean,
null: true,
diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb
index 33f0c8dc91b5e859b87e00a90c920dcf35ff0aba..26155b4c656048f864822b6594b68fb75719aa86 100644
--- a/ee/app/policies/ee/group_policy.rb
+++ b/ee/app/policies/ee/group_policy.rb
@@ -227,6 +227,15 @@ module GroupPolicy
).has_ability?
end
+ desc "Custom role on group that enables manage group access tokens"
+ condition(:role_enables_manage_group_access_tokens) do
+ ::Auth::MemberRoleAbilityLoader.new(
+ user: @user,
+ resource: @subject,
+ ability: :manage_group_access_tokens
+ ).has_ability?
+ end
+
rule { owner & unique_project_download_limit_enabled }.policy do
enable :ban_group_member
end
@@ -497,6 +506,16 @@ module GroupPolicy
enable :destroy_group_member
end
+ rule { custom_roles_allowed & role_enables_manage_group_access_tokens & resource_access_token_feature_available }.policy do
+ enable :read_resource_access_tokens
+ enable :destroy_resource_access_tokens
+ end
+
+ rule { custom_roles_allowed & role_enables_manage_group_access_tokens & resource_access_token_creation_allowed }.policy do
+ enable :create_resource_access_tokens
+ enable :manage_resource_access_tokens
+ end
+
rule { custom_roles_allowed & owner }.policy do
enable :admin_member_role
end
diff --git a/ee/config/custom_abilities/manage_group_access_tokens.yml b/ee/config/custom_abilities/manage_group_access_tokens.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5305ce1bae43f578bc5086ee635a94ee767e1747
--- /dev/null
+++ b/ee/config/custom_abilities/manage_group_access_tokens.yml
@@ -0,0 +1,10 @@
+---
+name: manage_group_access_tokens
+description: Allows manage access to the group access tokens.
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/428353
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140115
+feature_category: system_access
+milestone: '16.8'
+group_ability: true
+project_ability: false
+requirement: ''
diff --git a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb
index 1bacaf2b6e67b916863afd54c69ba01ac5cfe8d7..b0877743c6b641a3c828a81bdfd7063da9bd2ee4 100644
--- a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb
+++ b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb
@@ -21,6 +21,11 @@ def configure_menu_items
add_item(billing_menu_item)
add_item(reporting_menu_item)
else
+ if can?(context.current_user, :read_resource_access_tokens, context.group)
+ # Managing group acccess tokens is a custom ability independent of the access level.
+ add_item(access_tokens_menu_item)
+ end
+
if can?(context.current_user, :change_push_rules, context.group)
# Push Rules are the only group setting that can also be edited by maintainers.
# They only get the Repository settings which only show the Push Rules section for maintainers.
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 6b1fe24b342650e7d168b29683135337870511e9..8bdc4f85897ad3c5b7dbf09953c831f3e40cedbb 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_group_access_tokens]).to eq member_role.manage_group_access_tokens
expect(subject[:manage_project_access_tokens]).to eq member_role.manage_project_access_tokens
expect(subject[:archive_project]).to eq member_role.archive_project
expect(subject[:remove_project]).to eq member_role.remove_project
diff --git a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb
index 23a456116822a20db72239a797264f37156958b3..82a25f8f6575556b8ff33b418b99321768a1a226 100644
--- a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb
+++ b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb
@@ -272,5 +272,27 @@
end
end
end
+
+ context 'for user with `read_resource_access_tokens` custom permission', feature_category: :permissions do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:role) { create(:member_role, :guest, namespace: group, manage_group_access_tokens: true) }
+ let_it_be(:member) { create(:group_member, :guest, member_role: role, user: user, group: group) }
+
+ subject { menu.renderable_items.find { |e| e.item_id == item_id } }
+
+ before do
+ stub_licensed_features(custom_roles: true)
+ end
+
+ describe 'Access Tokens menu item' do
+ let(:item_id) { :access_tokens }
+
+ it { is_expected.to be_present }
+
+ it 'does not show any other menu items' do
+ expect(menu.renderable_items.length).to equal(1)
+ end
+ end
+ end
end
end
diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb
index a70da766584c49363a611906f4e619e50211ea6f..001a295da47d952ccb3c511f48aee9848f0c1b76 100644
--- a/ee/spec/models/ee/user_spec.rb
+++ b/ee/spec/models/ee/user_spec.rb
@@ -1224,6 +1224,7 @@
OR admin_merge_request = true
OR admin_vulnerability = true
OR archive_project = true
+ OR manage_group_access_tokens = true
OR manage_project_access_tokens = true
OR read_dependency = true
OR read_vulnerability = true
diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb
index bc126e75b5934a0a953b8d97f3fe95813fc32c43..dcb84e5949d7f2b5e3e966b041eab6afef0d194b 100644
--- a/ee/spec/policies/group_policy_spec.rb
+++ b/ee/spec/policies/group_policy_spec.rb
@@ -2979,6 +2979,37 @@ def create_member_role(member, abilities = member_role_abilities)
it_behaves_like 'custom roles abilities'
end
+
+ context 'for a member role with manage_group_access_tokens true' do
+ let(:member_role_abilities) { { manage_group_access_tokens: true } }
+ let(:allowed_abilities) do
+ [:read_resource_access_tokens, :destroy_resource_access_tokens,
+ :create_resource_access_tokens, :manage_resource_access_tokens]
+ end
+
+ it_behaves_like 'custom roles abilities'
+
+ context 'when resource access token creation is not allowed' do
+ before do
+ create_member_role(group_member_guest)
+ stub_licensed_features(custom_roles: true)
+ group.root_ancestor.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it { is_expected.to be_allowed(:read_resource_access_tokens, :destroy_resource_access_tokens) }
+ it { is_expected.to be_disallowed(:create_resource_access_tokens, :manage_resource_access_tokens) }
+ end
+
+ context 'when resource access tokens feature is unavailable' do
+ before do
+ create_member_role(group_member_guest)
+ stub_licensed_features(custom_roles: true)
+ stub_ee_application_setting(personal_access_tokens_disabled?: true)
+ end
+
+ it { is_expected.to be_disallowed(*allowed_abilities) }
+ end
+ end
end
context 'for :read_limit_alert' do
diff --git a/ee/spec/requests/api/member_roles_spec.rb b/ee/spec/requests/api/member_roles_spec.rb
index d451f3b125ab26a83c0342350301fe8c34f76a21..cacfa8f9b5e1b0157cd9c2107cbe249fe3de3096 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_group_access_tokens" => false,
"manage_project_access_tokens" => false,
"archive_project" => false,
"remove_project" => false,
@@ -121,6 +122,7 @@
"admin_group_member" => false,
"admin_merge_request" => true,
"admin_vulnerability" => false,
+ "manage_group_access_tokens" => false,
"manage_project_access_tokens" => false,
"archive_project" => false,
"remove_project" => false,
diff --git a/ee/spec/requests/custom_roles/manage_group_access_tokens/request_spec.rb b/ee/spec/requests/custom_roles/manage_group_access_tokens/request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb606a902c373e3010f7eec326b7133179c17646
--- /dev/null
+++ b/ee/spec/requests/custom_roles/manage_group_access_tokens/request_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User with manage_group_access_tokens custom role', feature_category: :permissions do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ before do
+ stub_licensed_features(custom_roles: true)
+ sign_in(user)
+ end
+
+ describe Groups::Settings::AccessTokensController do
+ let_it_be(:role) { create(:member_role, :guest, namespace: group, manage_group_access_tokens: true) }
+ let_it_be(:member) { create(:group_member, :guest, member_role: role, user: user, group: group) }
+
+ describe '#index' do
+ it 'user has access via custom role' do
+ get group_settings_access_tokens_path(group)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ end
+ end
+
+ describe '#create' do
+ let_it_be(:access_token_params) { { name: 'TestToken', scopes: ['api'], expires_at: Date.today + 1.month } }
+ let_it_be(:resource) { group }
+
+ subject(:request) do
+ post group_settings_access_tokens_path(group, params: { resource_access_token: access_token_params })
+ end
+
+ it 'user has access via a custom role' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'POST resource access tokens available'
+ end
+
+ describe '#revoke' do
+ let_it_be(:access_token_user) { create(:user, :project_bot) }
+ let_it_be(:group_member) { create(:group_member, user: access_token_user, group: group) }
+ let_it_be(:resource_access_token) { create(:personal_access_token, user: access_token_user) }
+
+ subject(:request) { put revoke_group_settings_access_token_path(group, resource_access_token) }
+
+ it 'user has access via a custom role' do
+ request
+
+ expect(resource_access_token.reload).to be_revoked
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+
+ describe GroupsController do
+ it 'user has access via custom role' do
+ get group_path(group)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to include('Access Tokens')
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb
index ece6460bb8952513691d64c5f270c16034b89349..293383f8eacec72629a7bd6fe8779a58c87d6531 100644
--- a/lib/sidebars/groups/menus/settings_menu.rb
+++ b/lib/sidebars/groups/menus/settings_menu.rb
@@ -25,6 +25,10 @@ def configure_menu_items
# Billing is the only group setting that is visible to auditors.
# Create an empty sub-menu here and EE adds Settings menu item (with only Billing).
return true
+ elsif Gitlab.ee? && can?(context.current_user, :read_resource_access_tokens, context.group)
+ # Managing group acccess tokens is a custom ability independent of the access level.
+ # Create an empty sub-menu here and EE adds Settings menu item (with only Billing).
+ return true
end
false