Skip to content
Snippets Groups Projects
Commit 599fa02b authored by Max Fan's avatar Max Fan :two:
Browse files

Adding ci_pipeline_bots as a new user type

This bot can be used to run pipelines and by
doing so decrease the coupling between system pipeline
runs and human runs
parent a2666fe5
No related branches found
No related tags found
3 merge requests!181325Fix ambiguous `created_at` in project.rb,!180727Resolve "Extend job archival mechanism to the whole pipeline",!179899Introduce CreateService for PipelineBots
Showing
with 314 additions and 4 deletions
......@@ -21,7 +21,8 @@ module HasUserType
llm_bot: 14,
placeholder: 15,
duo_code_review_bot: 16,
import_user: 17
import_user: 17,
ci_pipeline_bot: 18
}.with_indifferent_access.freeze
BOT_USER_TYPES = %w[
......@@ -38,6 +39,7 @@ module HasUserType
service_account
llm_bot
duo_code_review_bot
ci_pipeline_bot
].freeze
# `service_account` allows instance/namespaces to configure a user for external integrations/automations
......
......@@ -42499,6 +42499,7 @@ Possible types of user.
| <a id="usertypeadmin_bot"></a>`ADMIN_BOT` | Admin bot. |
| <a id="usertypealert_bot"></a>`ALERT_BOT` | Alert bot. |
| <a id="usertypeautomation_bot"></a>`AUTOMATION_BOT` | Automation bot. |
| <a id="usertypeci_pipeline_bot"></a>`CI_PIPELINE_BOT` | Ci pipeline bot. |
| <a id="usertypeduo_code_review_bot"></a>`DUO_CODE_REVIEW_BOT` | Duo code review bot. |
| <a id="usertypeghost"></a>`GHOST` | Ghost. |
| <a id="usertypehuman"></a>`HUMAN` | Human. |
......@@ -186,6 +186,7 @@ class Features
default_roles_assignees
ci_component_usages_in_projects
branch_rule_squash_options
ci_pipeline_bots
].freeze
ULTIMATE_FEATURES = %i[
......
......@@ -12,6 +12,8 @@ module ProjectPolicy
desc "User is a security policy bot on the project"
condition(:security_policy_bot) { user&.security_policy_bot? && team_member? }
condition(:ci_pipeline_bot) { user&.ci_pipeline_bot? && team_member? }
with_scope :subject
condition(:repository_mirrors_enabled) { @subject.feature_available?(:repository_mirrors) }
......@@ -612,6 +614,7 @@ module ProjectPolicy
enable :manage_project_security_exclusions
enable :read_project_security_exclusions
enable :manage_security_settings
enable :admin_ci_pipeline_bots
end
rule { ~runner_performance_insights_available }.prevent :read_runner_usage
......@@ -982,6 +985,10 @@ module ProjectPolicy
enable :build_download_code
end
rule { ci_pipeline_bot & can?(:developer_access) }.policy do
enable :create_bot_pipeline
end
desc "SPP project access to read policy config for pipeline execution policy"
condition(:spp_repository_access_allowed) do
Security::OrchestrationPolicyConfiguration.policy_management_project?(project) &&
......
# frozen_string_literal: true
module Ci
module PipelineBots
class CreateService
include Gitlab::Utils::StrongMemoize
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
@params = params
end
def execute
if Feature.disabled?(:create_and_use_ci_pipeline_bots, project)
return ServiceResponse.error(message: "Feature flag create_and_use_ci_pipeline_bots is disabled")
end
return feature_not_available unless project.licensed_feature_available?(:ci_pipeline_bots)
unless current_user.can?(:admin_ci_pipeline_bots, project)
return ServiceResponse.error(
message: "User does not have permission to create pipeline bots",
reason: :unauthorized
)
end
return ServiceResponse.error(message: "Bot must have either Developer or Maintainer permissions") unless [
Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER
].include?(params[:access_level])
user_response = ::Users::AuthorizedCreateService.new(current_user, default_ci_user_params).execute
return ServiceResponse.error(message: user_response.message) if user_response.error?
created_user = user_response.payload[:user]
member = project.add_member(created_user, params[:access_level])
if member.persisted?
ServiceResponse.success(payload: { user: created_user })
else
delete_failed_user(created_user)
ServiceResponse.error(
message: "Could not associate pipeline bot to project. ERROR: #{member.errors.full_messages.to_sentence}"
)
end
end
private
attr_accessor :project, :current_user, :params
def feature_not_available
ServiceResponse.error(message: "Pipeline bots feature not available")
end
def delete_failed_user(user)
DeleteUserWorker.perform_async(
current_user.id,
user.id,
hard_delete: true,
skip_authorization: true,
reason_for_deletion: "Pipeline bot creation failed"
)
end
def username_and_email_generator
Gitlab::Utils::UsernameAndEmailGenerator.new(
username_prefix: "project_#{project.id}_pipeline_bot",
email_domain: "noreply.#{Gitlab.config.gitlab.host}"
)
end
strong_memoize_attr :username_and_email_generator
def default_ci_user_params
{
name: params[:name] || "ci pipelines bot",
email: username_and_email_generator.email,
username: username_and_email_generator.username,
user_type: :ci_pipeline_bot,
skip_confirmation: true, # Bot users should always have their emails confirmed.
organization_id: project.organization_id,
bot_namespace: project.project_namespace
}
end
end
end
end
......@@ -52,7 +52,11 @@ def assign_common_user_params
override :allowed_user_type?
def allowed_user_type?
super || service_account?
super || service_account? || pipeline_bot?
end
def pipeline_bot?
user_params[:user_type]&.to_sym == :ci_pipeline_bot
end
def service_account?
......
---
name: create_and_use_ci_pipeline_bots
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/404931
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179899
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516256
milestone: '17.9'
group: group::pipeline execution
type: wip
default_enabled: false
......@@ -147,6 +147,25 @@
)
end
end
context 'when the build author is a ci_pipeline_bot' do
let_it_be(:pipeline_bot) { create(:user, :ci_pipeline_bot) }
before do
build.update!(user: pipeline_bot)
end
it 'recognises project level pipeline_bot access token' do
project.add_maintainer(build.user)
expect(subject).to have_attributes(
actor: build.user,
project: build.project,
type: :build,
authentication_abilities: described_class.build_authentication_abilities
)
end
end
end
end
end
......
......@@ -3391,6 +3391,30 @@ def create_member_role(member, abilities = member_role_abilities)
end
end
end
context 'and user is a ci pipelines bot' do
let(:current_user) { create(:user, user_type: :ci_pipeline_bot) }
it { is_expected.not_to be_allowed(:create_bot_pipeline) }
context 'and user is a member of the project' do
context 'with reporter permissions' do
before do
project.add_reporter(current_user)
end
it { is_expected.not_to be_allowed(:create_bot_pipeline) }
end
context 'with developer permissions' do
before do
project.add_developer(current_user)
end
it { is_expected.to be_allowed(:create_bot_pipeline) }
end
end
end
end
end
......@@ -4576,6 +4600,31 @@ def create_member_role(member, abilities = member_role_abilities)
end
end
describe 'admin_ci_pipeline_bots' do
let(:policy) { :admin_ci_pipeline_bots }
where(:role, :allowed) do
:guest | false
:planner | false
:reporter | false
:developer | false
:maintainer | true
:auditor | false
:owner | true
:admin | true
end
with_them do
let(:current_user) { public_send(role) }
before do
enable_admin_mode!(current_user) if role == :admin
end
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
describe 'read_security_settings' do
let(:policy) { :read_security_settings }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::PipelineBots::CreateService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let(:params) { { access_level: Gitlab::Access::DEVELOPER } }
subject(:service) { described_class.new(project, current_user, params) }
context 'when the current user is a maintainer' do
let_it_be(:current_user) { create(:user, maintainer_of: project) }
it 'rejects if the feature is not licensed' do
service_response = service.execute
expect(service_response.message).to eq("Pipeline bots feature not available")
end
context 'when the project has pipeline bot license' do
before do
stub_licensed_features(ci_pipeline_bots: true)
end
it 'creates a pipeline bot user' do
service_response = service.execute
expect(service_response.status).to eq(:success)
user_payload = service_response.payload[:user]
expect(user_payload.username).to start_with("project_#{project.id}_pipeline_bot")
expect(user_payload.email).to start_with(user_payload.username)
expect(user_payload.max_member_access_for_project(project.id)).to eq(Gitlab::Access::DEVELOPER)
expect(user_payload).to have_attributes(
name: "ci pipelines bot",
confirmed?: true,
user_type: "ci_pipeline_bot",
external: false,
password: nil
)
end
context 'when name and access level parameters are passed in' do
let(:params) { { access_level: Gitlab::Access::MAINTAINER, name: "nice bot" } }
it 'creates a pipeline bot with given name' do
service_response = service.execute
expect(service_response.payload[:user].name).to eq("nice bot")
expect(
service_response.payload[:user].max_member_access_for_project(project.id)
).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when an invalid access level parameter is passed in' do
let(:params) { { access_level: Gitlab::Access::REPORTER } }
it 'rejects pipeline bot creation' do
service_response = service.execute
expect(service_response.message).to eq("Bot must have either Developer or Maintainer permissions")
end
end
context 'when something goes wrong with member creation' do
before do
member_double = instance_double(ProjectMember, persisted?: false)
allow(member_double).to receive_message_chain(:errors, :full_messages, :to_sentence)
.and_return("Could not add to project")
allow(project).to receive(:add_member).and_return(member_double)
end
it 'triggers deletion of the user and returns and error response' do
expect(DeleteUserWorker).to receive(:perform_async).with(
current_user.id,
anything,
{
hard_delete: true,
reason_for_deletion: "Pipeline bot creation failed",
skip_authorization: true
}
)
service_response = service.execute
expect(service_response.message)
.to eq("Could not associate pipeline bot to project. ERROR: Could not add to project")
end
end
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(create_and_use_ci_pipeline_bots: false)
end
it 'returns service error' do
service_response = service.execute
expect(service_response.message).to eq("Feature flag create_and_use_ci_pipeline_bots is disabled")
end
end
end
context 'when the current user does not have permission' do
let_it_be(:current_user) { create(:user, developer_of: project) }
before do
stub_licensed_features(ci_pipeline_bots: true)
end
it 'returns insufficient permission error' do
service_response = service.execute
expect(service_response.message).to eq("User does not have permission to create pipeline bots")
end
end
end
......@@ -86,6 +86,18 @@
end
end
context 'with a pipeline_bot user type' do
before do
params.merge!(user_type: :ci_pipeline_bot)
end
it 'allows provisioned by group id to be set' do
user = service.execute
expect(user.user_type).to eq('ci_pipeline_bot')
end
end
context 'with composite_identity_enforced as allowed params' do
let(:params) { super().merge(composite_identity_enforced: true) }
......
......@@ -283,7 +283,7 @@ def can_read_project?(user, project)
end
def bot_user_can_read_project?(user, project)
(user.project_bot? || user.service_account? || user.security_policy_bot?) && can_read_project?(user, project)
(user.project_bot? || user.ci_pipeline_bot? || user.service_account? || user.security_policy_bot?) && can_read_project?(user, project)
end
def valid_oauth_token?(token)
......
......@@ -136,6 +136,10 @@
user_type { :llm_bot }
end
trait :ci_pipeline_bot do
user_type { :ci_pipeline_bot }
end
trait :duo_code_review_bot do
user_type { :duo_code_review_bot }
end
......
......@@ -14,7 +14,7 @@
expect(described_class::USER_TYPES.keys)
.to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot
visual_review_bot migration_bot automation_bot security_policy_bot admin_bot suggested_reviewers_bot
service_account llm_bot placeholder duo_code_review_bot import_user])
service_account llm_bot placeholder duo_code_review_bot import_user ci_pipeline_bot])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment