diff --git a/app/graphql/mutations/ci/job_token_scope/autopopulate_allowlist.rb b/app/graphql/mutations/ci/job_token_scope/autopopulate_allowlist.rb
new file mode 100644
index 0000000000000000000000000000000000000000..202d153e9d018a4c5627b5d5af280e068e7f3432
--- /dev/null
+++ b/app/graphql/mutations/ci/job_token_scope/autopopulate_allowlist.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module JobTokenScope
+ class AutopopulateAllowlist < BaseMutation
+ graphql_name 'CiJobTokenScopeAutopopulateAllowlist'
+
+ include FindsProject
+
+ authorize :admin_project
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Project in which to autopopulate the allowlist.'
+
+ field :status,
+ GraphQL::Types::String,
+ null: false,
+ description: "Status of the autopopulation process."
+
+ def resolve(project_path:)
+ project = authorized_find!(project_path)
+
+ result = ::Ci::JobToken::ClearAutopopulatedAllowlistService.new(project, current_user).execute
+ result = ::Ci::JobToken::AutopopulateAllowlistService.new(project, current_user).execute if result.success?
+
+ if result.success?
+ { status: "complete", errors: [] }
+ else
+ { status: "error", errors: [result.message] }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job_token_scope/clear_allowlist_autopopulations.rb b/app/graphql/mutations/ci/job_token_scope/clear_allowlist_autopopulations.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d083c20172b16cccf98909d841f12cf68ea2f3ca
--- /dev/null
+++ b/app/graphql/mutations/ci/job_token_scope/clear_allowlist_autopopulations.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module JobTokenScope
+ class ClearAllowlistAutopopulations < BaseMutation
+ graphql_name 'CiJobTokenScopeClearAllowlistAutopopulations'
+
+ include FindsProject
+
+ authorize :admin_project
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Project in which to autopopulate the allowlist.'
+
+ field :status,
+ GraphQL::Types::String,
+ null: false,
+ description: "Status of the autopopulation process."
+
+ def resolve(project_path:)
+ project = authorized_find!(project_path)
+
+ result = ::Ci::JobToken::ClearAutopopulatedAllowlistService.new(project, current_user).execute
+
+ if result.success?
+ { status: "complete", errors: [] }
+ else
+ { status: "error", errors: [result.message] }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 7549ca5e147a4e5233ca9810703a632fd400aea1..81cc7dd220257be122d0ee506bcd55c9655baa52 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -180,6 +180,8 @@ class MutationType < BaseObject
mount_mutation Mutations::Ci::JobTokenScope::RemoveGroup
mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
mount_mutation Mutations::Ci::JobTokenScope::UpdateJobTokenPolicies, experiment: { milestone: '17.6' }
+ mount_mutation Mutations::Ci::JobTokenScope::AutopopulateAllowlist, experiment: { milestone: '17.9' }
+ mount_mutation Mutations::Ci::JobTokenScope::ClearAllowlistAutopopulations, experiment: { milestone: '17.9' }
mount_mutation Mutations::Ci::Pipeline::Cancel
mount_mutation Mutations::Ci::Pipeline::Create
mount_mutation Mutations::Ci::Pipeline::Destroy
diff --git a/app/models/ci/job_token/allowlist.rb b/app/models/ci/job_token/allowlist.rb
index 80a640b791964753eddde4960840586886873420..15498a3ac70d987d021d07bb6e6d9380a8883dc9 100644
--- a/app/models/ci/job_token/allowlist.rb
+++ b/app/models/ci/job_token/allowlist.rb
@@ -81,6 +81,7 @@ def bulk_add_projects!(target_projects, user:, autopopulated: false, policies: [
autopopulated: autopopulated,
added_by: user,
job_token_policies: job_token_policies,
+ direction: @direction,
created_at: now
)
end
diff --git a/app/services/ci/job_token/autopopulate_allowlist_service.rb b/app/services/ci/job_token/autopopulate_allowlist_service.rb
index f11e6d4921b5ebf1979cfee57d6f47805992e02c..d23a8b95a486ba17e7e6c5ff1279136b2dbd9257 100644
--- a/app/services/ci/job_token/autopopulate_allowlist_service.rb
+++ b/app/services/ci/job_token/autopopulate_allowlist_service.rb
@@ -24,6 +24,15 @@ def execute
allowlist.bulk_add_groups!(groups, user: @user, autopopulated: true)
allowlist.bulk_add_projects!(projects, user: @user, autopopulated: true)
end
+
+ ServiceResponse.success
+ rescue Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError,
+ Gitlab::Utils::TraversalIdCompactor::RedundantCompactionEntry,
+ Gitlab::Utils::TraversalIdCompactor::UnexpectedCompactionEntry,
+ Ci::JobToken::AuthorizationsCompactor::UnexpectedCompactionEntry,
+ Ci::JobToken::AuthorizationsCompactor::RedundantCompactionEntry => e
+ Gitlab::ErrorTracking.log_exception(e, { project_id: @project.id, user_id: @user.id })
+ ServiceResponse.error(message: e.message)
end
private
diff --git a/app/services/ci/job_token/clear_autopopulated_allowlist_service.rb b/app/services/ci/job_token/clear_autopopulated_allowlist_service.rb
index 24e1761bc285736485fbdde83a899b3f02d654b5..1ba8c2c2f5fa9e91fe569033c33650d201119380 100644
--- a/app/services/ci/job_token/clear_autopopulated_allowlist_service.rb
+++ b/app/services/ci/job_token/clear_autopopulated_allowlist_service.rb
@@ -17,6 +17,8 @@ def execute
allowlist.project_links.autopopulated.delete_all
allowlist.group_links.autopopulated.delete_all
end
+
+ ServiceResponse.success
end
private
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a3db407a3f814e26df97df3e34b632a5cb48dce2..47cbb3dc6333c54763ca80f2cb683138d05d2c13 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3212,6 +3212,52 @@ Input type: `CiJobTokenScopeAddProjectInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.ciJobTokenScopeAutopopulateAllowlist`
+
+DETAILS:
+**Introduced** in GitLab 17.9.
+**Status**: Experiment.
+
+Input type: `CiJobTokenScopeAutopopulateAllowlistInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `projectPath` | [`ID!`](#id) | Project in which to autopopulate the allowlist. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `status` | [`String!`](#string) | Status of the autopopulation process. |
+
+### `Mutation.ciJobTokenScopeClearAllowlistAutopopulations`
+
+DETAILS:
+**Introduced** in GitLab 17.9.
+**Status**: Experiment.
+
+Input type: `CiJobTokenScopeClearAllowlistAutopopulationsInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `projectPath` | [`ID!`](#id) | Project in which to autopopulate the allowlist. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `status` | [`String!`](#string) | Status of the autopopulation process. |
+
### `Mutation.ciJobTokenScopeRemoveGroup`
Input type: `CiJobTokenScopeRemoveGroupInput`
diff --git a/spec/graphql/mutations/ci/job_token_scope/autopopulate_allowlist_spec.rb b/spec/graphql/mutations/ci/job_token_scope/autopopulate_allowlist_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..424d5c8b3fdba32bb05e3ae3d743941cb0752a85
--- /dev/null
+++ b/spec/graphql/mutations/ci/job_token_scope/autopopulate_allowlist_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Ci::JobTokenScope::AutopopulateAllowlist, feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let(:mutation) do
+ described_class.new(object: nil, context: query_context, field: nil)
+ end
+
+ describe '#resolve' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:origin_project) { create(:project) }
+ let(:mutation_args) { { project_path: project.full_path } }
+
+ subject(:resolver) do
+ mutation.resolve(**mutation_args)
+ end
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it 'raises error' do
+ expect { resolver }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when user is logged in' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'when user does not have permissions to admin the project' do
+ it 'raises error' do
+ expect { resolver }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when user has permissions to admin the project' do
+ before_all do
+ project.add_maintainer(current_user)
+
+ create(:ci_job_token_authorization, origin_project: origin_project, accessed_project: project,
+ last_authorized_at: 1.day.ago)
+ end
+
+ it 'adds target project to the inbound job token scope by default' do
+ expect do
+ expect(resolver).to include(errors: be_empty)
+ end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1)
+ end
+
+ context 'when the clear service returns an error' do
+ let(:service) { instance_double(::Ci::JobToken::ClearAutopopulatedAllowlistService) }
+
+ it 'returns an error response' do
+ expect(::Ci::JobToken::ClearAutopopulatedAllowlistService).to receive(:new).with(project,
+ current_user).and_return(service)
+ expect(service).to receive(:execute)
+ .and_return(ServiceResponse.error(message: 'Clear service error message'))
+
+ expect(resolver.fetch(:errors)).to include("Clear service error message")
+ end
+ end
+
+ context 'when the autopopulate service returns an error' do
+ let(:service) { instance_double(::Ci::JobToken::AutopopulateAllowlistService) }
+
+ it 'returns an error response' do
+ expect(::Ci::JobToken::AutopopulateAllowlistService).to receive(:new).with(project,
+ current_user).and_return(service)
+ expect(service).to receive(:execute)
+ .and_return(ServiceResponse.error(message: 'Autopopulates service error message'))
+
+ expect(resolver.fetch(:errors)).to include("Autopopulates service error message")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/ci/job_token_scope/clear_allowlist_autopopulations_spec.rb b/spec/graphql/mutations/ci/job_token_scope/clear_allowlist_autopopulations_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..670d6e785388ac9132dd914bb1ac6985e4776289
--- /dev/null
+++ b/spec/graphql/mutations/ci/job_token_scope/clear_allowlist_autopopulations_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Ci::JobTokenScope::ClearAllowlistAutopopulations, feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let(:mutation) do
+ described_class.new(object: nil, context: query_context, field: nil)
+ end
+
+ describe '#resolve' do
+ let_it_be(:project) { create(:project) }
+ let(:mutation_args) { { project_path: project.full_path } }
+
+ subject(:resolver) do
+ mutation.resolve(**mutation_args)
+ end
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it 'raises error' do
+ expect { resolver }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when user is logged in' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'when user does not have permissions to admin the project' do
+ it 'raises error' do
+ expect { resolver }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when user has permissions to admin the project' do
+ before_all do
+ project.add_maintainer(current_user)
+ end
+
+ context 'when group scope links have been autopopulated' do
+ before do
+ create(:ci_job_token_group_scope_link, source_project: project, autopopulated: true)
+ create(:ci_job_token_group_scope_link, source_project: project, autopopulated: false)
+ end
+
+ it 'removes the autopopulated group links' do
+ expect do
+ expect(resolver).to include(errors: be_empty)
+ end.to change { Ci::JobToken::GroupScopeLink.autopopulated.count }.by(-1)
+ .and not_change { Ci::JobToken::ProjectScopeLink.autopopulated.count }
+ end
+ end
+
+ context 'when project scope links have been autopopulated' do
+ before do
+ create(:ci_job_token_project_scope_link, source_project: project, direction: :inbound,
+ autopopulated: true)
+ create(:ci_job_token_project_scope_link, source_project: project, direction: :inbound,
+ autopopulated: false)
+ end
+
+ it 'removes the autopopulated project links' do
+ expect do
+ expect(resolver).to include(errors: be_empty)
+ end.to change { Ci::JobToken::ProjectScopeLink.autopopulated.count }.by(-1)
+ .and not_change { Ci::JobToken::GroupScopeLink.autopopulated.count }
+ end
+ end
+
+ context 'when the service returns an error' do
+ let(:service) { instance_double(::Ci::JobToken::ClearAutopopulatedAllowlistService) }
+
+ it 'returns an error response' do
+ expect(::Ci::JobToken::ClearAutopopulatedAllowlistService).to receive(:new).with(project,
+ current_user).and_return(service)
+ expect(service).to receive(:execute)
+ .and_return(ServiceResponse.error(message: 'The error message'))
+
+ expect(resolver.fetch(:errors)).to include("The error message")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/job_token/autopopulate_allowlist_service_spec.rb b/spec/services/ci/job_token/autopopulate_allowlist_service_spec.rb
index e449711fa014383da25dcebe29df64017e316b51..ce607aba21061ffc311ac78de7cbcd903bc05556 100644
--- a/spec/services/ci/job_token/autopopulate_allowlist_service_spec.rb
+++ b/spec/services/ci/job_token/autopopulate_allowlist_service_spec.rb
@@ -62,7 +62,9 @@
end
it 'creates the expected group and project links for the given limit' do
- service.execute
+ result = service.execute
+
+ expect(result).to be_success
expect(Ci::JobToken::GroupScopeLink.autopopulated.pluck(:target_group_id)).to match_array([ns2.id, ns4.id])
expect(Ci::JobToken::ProjectScopeLink.autopopulated.pluck(:target_project_id)).to match_array([pns1.project.id,
@@ -73,8 +75,9 @@
let(:compaction_limit) { 3 }
it 'creates the expected group and project links' do
- service.execute
+ result = service.execute
+ expect(result).to be_success
expect(Ci::JobToken::GroupScopeLink.autopopulated.pluck(:target_group_id)).to match_array([ns1.id])
expect(Ci::JobToken::ProjectScopeLink.autopopulated.pluck(:target_project_id)).to match_array([pns8.project.id])
end
@@ -84,9 +87,10 @@
let(:compaction_limit) { 1 }
it 'logs a CompactionLimitCannotBeAchievedError error' do
- expect do
- service.execute
- end.to raise_error(Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError)
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.message).to eq('Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError')
expect(Ci::JobToken::GroupScopeLink.count).to be(0)
expect(Ci::JobToken::ProjectScopeLink.count).to be(0)
@@ -102,7 +106,10 @@
original_response << [1, 2, 3]
end
- expect { service.execute }.to raise_error(Gitlab::Utils::TraversalIdCompactor::UnexpectedCompactionEntry)
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.message).to eq('Gitlab::Utils::TraversalIdCompactor::UnexpectedCompactionEntry')
end
it 'logs a RedundantCompactionEntry error' do
@@ -111,7 +118,10 @@
original_response << original_response.last.first(2)
end
- expect { service.execute }.to raise_error(Gitlab::Utils::TraversalIdCompactor::RedundantCompactionEntry)
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.message).to eq('Gitlab::Utils::TraversalIdCompactor::RedundantCompactionEntry')
end
end
@@ -137,9 +147,15 @@
let(:compaction_limit) { 2 }
it 'raises when the limit cannot be achieved' do
- expect do
- service.execute
- end.to raise_error(Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError)
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ kind_of(Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError),
+ { project_id: accessed_project.id, user_id: maintainer.id }
+ )
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.message).to eq('Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError')
+
expect(Ci::JobToken::GroupScopeLink.count).to be(0)
expect(Ci::JobToken::ProjectScopeLink.count).to be(0)
end