Commit af5a5366 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'project-access-token' into 'master'

Service to create Project Access Token

See merge request gitlab-org/gitlab!28621
parents 59056421 0932e1b4
......@@ -1727,7 +1727,7 @@ class User < ApplicationRecord
# override, from Devise::Validatable
def password_required?
return false if internal?
return false if internal? || project_bot?
super
end
......
# frozen_string_literal: true
module PersonalAccessTokens
class CreateService < BaseService
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
end
def execute
personal_access_token = current_user.personal_access_tokens.create(params.slice(*allowed_params))
if personal_access_token.persisted?
ServiceResponse.success(payload: { personal_access_token: personal_access_token })
else
ServiceResponse.error(message: personal_access_token.errors.full_messages.to_sentence)
end
end
private
def allowed_params
[
:name,
:impersonation,
:scopes,
:expires_at
]
end
end
end
# frozen_string_literal: true
module Resources
class CreateAccessTokenService < BaseService
attr_accessor :resource_type, :resource
def initialize(resource_type, resource, user, params = {})
@resource_type = resource_type
@resource = resource
@current_user = user
@params = params.dup
end
def execute
return unless feature_enabled?
return error("User does not have permission to create #{resource_type} Access Token") unless has_permission_to_create?
# We skip authorization by default, since the user creating the bot is not an admin
# and project/group bot users are not created via sign-up
user = create_user
return error(user.errors.full_messages.to_sentence) unless user.persisted?
return error("Failed to provide maintainer access") unless provision_access(resource, user)
token_response = create_personal_access_token(user)
if token_response.success?
success(token_response.payload[:personal_access_token])
else
error(token_response.message)
end
end
private
def feature_enabled?
::Feature.enabled?(:resource_access_token, resource)
end
def has_permission_to_create?
case resource_type
when 'project'
can?(current_user, :admin_project, resource)
when 'group'
can?(current_user, :admin_group, resource)
else
false
end
end
def create_user
Users::CreateService.new(current_user, default_user_params).execute(skip_authorization: true)
end
def default_user_params
{
name: params[:name] || "#{resource.name.to_s.humanize} bot",
email: generate_email,
username: generate_username,
user_type: "#{resource_type}_bot".to_sym
}
end
def generate_username
base_username = "#{resource_type}_#{resource.id}_bot"
uniquify.string(base_username) { |s| User.find_by_username(s) }
end
def generate_email
email_pattern = "#{resource_type}#{resource.id}_bot%s@example.com"
uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
User.find_by_email(s)
end
end
def uniquify
Uniquify.new
end
def create_personal_access_token(user)
PersonalAccessTokens::CreateService.new(user, personal_access_token_params).execute
end
def personal_access_token_params
{
name: "#{resource_type}_bot",
impersonation: false,
scopes: params[:scopes] || default_scopes,
expires_at: params[:expires_at] || nil
}
end
def default_scopes
Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
end
def provision_access(resource, user)
resource.add_maintainer(user)
end
def error(message)
ServiceResponse.error(message: message)
end
def success(access_token)
ServiceResponse.success(payload: { access_token: access_token })
end
end
end
......@@ -81,7 +81,8 @@ module Users
:private_profile,
:organization,
:location,
:public_email
:public_email,
:user_type
]
end
......@@ -95,7 +96,8 @@ module Users
:first_name,
:last_name,
:password,
:username
:username,
:user_type
]
end
......@@ -127,6 +129,8 @@ module Users
user_params[:external] = user_external?
end
user_params.delete(:user_type) unless project_bot?(user_params[:user_type])
user_params
end
......@@ -137,6 +141,10 @@ module Users
def user_external?
user_default_internal_regex_instance.match(params[:email]).nil?
end
def project_bot?(user_type)
user_type&.to_sym == :project_bot
end
end
end
......
......@@ -4659,4 +4659,30 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to be :locked }
end
end
describe '#password_required?' do
let_it_be(:user) { create(:user) }
shared_examples 'does not require password to be present' do
it { expect(user).not_to validate_presence_of(:password) }
it { expect(user).not_to validate_presence_of(:password_confirmation) }
end
context 'when user is an internal user' do
before do
user.update(user_type: 'alert_bot')
end
it_behaves_like 'does not require password to be present'
end
context 'when user is a project bot user' do
before do
user.update(user_type: 'project_bot')
end
it_behaves_like 'does not require password to be present'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe PersonalAccessTokens::CreateService do
describe '#execute' do
context 'with valid params' do
it 'creates personal access token record' do
user = create(:user)
params = { name: 'Test token', impersonation: true, scopes: [:api], expires_at: Date.today + 1.month }
response = described_class.new(user, params).execute
personal_access_token = response.payload[:personal_access_token]
expect(response.success?).to be true
expect(personal_access_token.name).to eq(params[:name])
expect(personal_access_token.impersonation).to eq(params[:impersonation])
expect(personal_access_token.scopes).to eq(params[:scopes])
expect(personal_access_token.expires_at).to eq(params[:expires_at])
expect(personal_access_token.user).to eq(user)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resources::CreateAccessTokenService do
subject { described_class.new(resource_type, resource, user, params).execute }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:params) { {} }
describe '#execute' do
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'fails when user does not have the permission to create a Resource Bot' do
before do
resource.add_developer(user)
end
it 'returns error' do
response = subject
expect(response.error?).to be true
expect(response.message).to eq("User does not have permission to create #{resource_type} Access Token")
end
end
shared_examples 'fails when flag is disabled' do
before do
stub_feature_flags(resource_access_token: false)
end
it 'returns nil' do
expect(subject).to be nil
end
end
shared_examples 'allows creation of bot with valid params' do
it { expect { subject }.to change { User.count }.by(1) }
it 'creates resource bot user' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.user.reload.user_type).to eq("#{resource_type}_bot")
end
context 'bot name' do
context 'when no value is passed' do
it 'uses default value' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.user.name).to eq("#{resource.name.to_s.humanize} bot")
end
end
context 'when user provides value' do
let(:params) { { name: 'Random bot' } }
it 'overrides the default value' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.user.name).to eq(params[:name])
end
end
end
it 'adds the bot user as a maintainer in the resource' do
response = subject
access_token = response.payload[:access_token]
bot_user = access_token.user
expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id)
end
context 'personal access token' do
it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
context 'when user does not provide scope' do
it 'has default scopes' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.scopes).to eq(Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user])
end
end
context 'when user provides scope explicitly' do
let(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
it 'overrides the default value' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.scopes).to eq(Gitlab::Auth::REPOSITORY_SCOPES)
end
end
context 'expires_at' do
context 'when no value is passed' do
it 'uses default value' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.expires_at).to eq(nil)
end
end
context 'when user provides value' do
let(:params) { { expires_at: Date.today + 1.month } }
it 'overrides the default value' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.expires_at).to eq(params[:expires_at])
end
end
context 'when invalid scope is passed' do
let(:params) { { scopes: [:invalid_scope] } }
it 'returns error' do
response = subject
expect(response.error?).to be true
end
end
end
end
context 'when access provisioning fails' do
before do
allow(resource).to receive(:add_maintainer).and_return(nil)
end
it 'returns error' do
response = subject
expect(response.error?).to be true
end
end
end
context 'when resource is a project' do
let(:resource_type) { 'project' }
let(:resource) { project }
it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
it_behaves_like 'fails when flag is disabled'
context 'user with valid permission' do
before do
resource.add_maintainer(user)
end
it_behaves_like 'allows creation of bot with valid params'
end
end
end
end
......@@ -157,6 +157,26 @@ describe Users::BuildService do
end
end
context 'when user_type is provided' do
subject(:user) { service.execute }
context 'when project_bot' do
before do
params.merge!({ user_type: :project_bot })
end
it { expect(user.project_bot?).to be true}
end
context 'when not a project_bot' do
before do
params.merge!({ user_type: :alert_bot })
end
it { expect(user.user_type).to be nil }
end
end
context 'with "user_default_external" application setting' do
using RSpec::Parameterized::TableSyntax
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment