summaryrefslogtreecommitdiff
path: root/app/services/resource_access_tokens/create_service.rb
blob: c6948536053cec993dd1c7254e7ad9889fc985fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# frozen_string_literal: true

module ResourceAccessTokens
  class CreateService < BaseService
    def initialize(current_user, resource, params = {})
      @resource_type = resource.class.name.downcase
      @resource = resource
      @current_user = current_user
      @params = params.dup
    end

    def execute
      return error("User does not have permission to create #{resource_type} access token") unless has_permission_to_create?

      access_level = params[:access_level] || Gitlab::Access::MAINTAINER
      return error("Could not provision owner access to project access token") if do_not_allow_owner_access_level_for_project_bot?(access_level)

      user = create_user

      return error(user.errors.full_messages.to_sentence) unless user.persisted?

      user.update!(external: true) if current_user.external?

      member = create_membership(resource, user, access_level)

      unless member.persisted?
        delete_failed_user(user)
        return error("Could not provision #{Gitlab::Access.human_access(access_level).downcase} access to project access token")
      end

      token_response = create_personal_access_token(user)

      if token_response.success?
        log_event(token_response.payload[:personal_access_token])
        success(token_response.payload[:personal_access_token])
      else
        delete_failed_user(user)
        error(token_response.message)
      end
    end

    private

    attr_reader :resource_type, :resource

    def has_permission_to_create?
      %w(project group).include?(resource_type) && can?(current_user, :create_resource_access_tokens, resource)
    end

    def create_user
      # Even project maintainers/owners can create project access tokens, which in turn
      # creates a bot user, and so it becomes necessary to  have `skip_authorization: true`
      # since someone like a project maintainer/owner does not inherently have the ability
      # to create a new user in the system.

      ::Users::AuthorizedCreateService.new(current_user, default_user_params).execute
    end

    def delete_failed_user(user)
      DeleteUserWorker.perform_async(current_user.id, user.id, hard_delete: true, 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: :project_bot,
        skip_confirmation: true # Bot users should always have their emails confirmed.
      }
    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@noreply.#{Gitlab.config.gitlab.host}"

      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(
        current_user: user, target_user: user, params: personal_access_token_params
      ).execute
    end

    def personal_access_token_params
      {
        name: params[:name] || "#{resource_type}_bot",
        impersonation: false,
        scopes: params[:scopes] || default_scopes,
        expires_at: params[:expires_at] || nil
      }
    end

    def default_scopes
      Gitlab::Auth.resource_bot_scopes
    end

    def create_membership(resource, user, access_level)
      resource.add_member(user, access_level, expires_at: params[:expires_at])
    end

    def log_event(token)
      ::Gitlab::AppLogger.info "PROJECT ACCESS TOKEN CREATION: created_by: #{current_user.username}, project_id: #{resource.id}, token_user: #{token.user.name}, token_id: #{token.id}"
    end

    def error(message)
      ServiceResponse.error(message: message)
    end

    def success(access_token)
      ServiceResponse.success(payload: { access_token: access_token })
    end

    def do_not_allow_owner_access_level_for_project_bot?(access_level)
      resource.is_a?(Project) &&
        access_level == Gitlab::Access::OWNER &&
        !current_user.can?(:manage_owners, resource)
    end
  end
end

ResourceAccessTokens::CreateService.prepend_mod_with('ResourceAccessTokens::CreateService')