summaryrefslogtreecommitdiff
path: root/app/services/auth/container_registry_authentication_service.rb
blob: 98da65639475438a96ca575f7a6360bd7fd7927d (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
module Auth
  class ContainerRegistryAuthenticationService < BaseService
    include Gitlab::CurrentSettings

    AUDIENCE = 'container_registry'

    def execute(authentication_abilities:)
      @authentication_abilities = authentication_abilities || []

      return error('not found', 404) unless registry.enabled

      unless current_user || project
        return error('forbidden', 403) unless scope
      end

      { token: authorized_token(scope).encoded }
    end

    def self.full_access_token(*names)
      registry = Gitlab.config.registry
      token = JSONWebToken::RSAToken.new(registry.key)
      token.issuer = registry.issuer
      token.audience = AUDIENCE
      token.expire_time = token_expire_at

      token[:access] = names.map do |name|
        { type: 'repository', name: name, actions: %w(*) }
      end

      token.encoded
    end

    def self.token_expire_at
      Time.now + current_application_settings.container_registry_token_expire_delay.minutes
    end

    private

    def authorized_token(*accesses)
      token = JSONWebToken::RSAToken.new(registry.key)
      token.issuer = registry.issuer
      token.audience = params[:service]
      token.subject = current_user.try(:username)
      token.expire_time = self.class.token_expire_at
      token[:access] = accesses.compact
      token
    end

    def scope
      return unless params[:scope]

      @scope ||= process_scope(params[:scope])
    end

    def process_scope(scope)
      type, name, actions = scope.split(':', 3)
      actions = actions.split(',')
      return unless type == 'repository'

      process_repository_access(type, name, actions)
    end

    def process_repository_access(type, name, actions)
      requested_project = Project.find_with_namespace(name)
      return unless requested_project

      actions = actions.select do |action|
        can_access?(requested_project, action)
      end

      { type: type, name: name, actions: actions } if actions.present?
    end

    def can_access?(requested_project, requested_action)
      return false unless requested_project.container_registry_enabled?

      case requested_action
      when 'pull'
        requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
      when 'push'
        build_can_push?(requested_project) || user_can_push?(requested_project)
      else
        false
      end
    end

    def registry
      Gitlab.config.registry
    end

    def build_can_pull?(requested_project)
      # Build can:
      # 1. pull from its own project (for ex. a build)
      # 2. read images from dependent projects if creator of build is a team member
      @authentication_abilities.include?(:build_read_container_image) &&
        (requested_project == project || can?(current_user, :build_read_container_image, requested_project))
    end

    def user_can_pull?(requested_project)
      @authentication_abilities.include?(:read_container_image) &&
        can?(current_user, :read_container_image, requested_project)
    end

    def build_can_push?(requested_project)
      # Build can push only to the project from which it originates
      @authentication_abilities.include?(:build_create_container_image) &&
        requested_project == project
    end

    def user_can_push?(requested_project)
      @authentication_abilities.include?(:create_container_image) &&
        can?(current_user, :create_container_image, requested_project)
    end
  end
end