summaryrefslogtreecommitdiff
path: root/app/models/deploy_token.rb
blob: 360a9ffbc53ce2d4f4a50ff73714341761bf1ef0 (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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# frozen_string_literal: true

class DeployToken < ApplicationRecord
  include Expirable
  include TokenAuthenticatable
  include PolicyActor
  include Gitlab::Utils::StrongMemoize
  add_authentication_token_field :token, encrypted: :optional

  AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
                        read_package_registry write_package_registry).freeze
  GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
  REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze

  default_value_for(:expires_at) { Forever.date }

  # Do NOT use this `user` for the authentication/authorization of the deploy tokens.
  # It's for the auditing purpose on Credential Inventory, only.
  # See https://gitlab.com/gitlab-org/gitlab/-/issues/353467#note_859774246 for more information.
  belongs_to :user, foreign_key: :creator_id, optional: true

  has_many :project_deploy_tokens, inverse_of: :deploy_token
  has_many :projects, through: :project_deploy_tokens

  has_many :group_deploy_tokens, inverse_of: :deploy_token
  has_many :groups, through: :group_deploy_tokens

  validate :no_groups, unless: :group_type?
  validate :no_projects, unless: :project_type?
  validate :ensure_at_least_one_scope
  validates :username,
    length: { maximum: 255 },
    allow_nil: true,
    format: {
      with: /\A[a-zA-Z0-9\.\+_-]+\z/,
      message: "can contain only letters, digits, '_', '-', '+', and '.'"
    }

  validates :deploy_token_type, presence: true
  enum deploy_token_type: {
    group_type: 1,
    project_type: 2
  }

  before_save :ensure_token

  accepts_nested_attributes_for :project_deploy_tokens

  scope :active, -> { where("revoked = false AND expires_at >= NOW()") }

  def self.gitlab_deploy_token
    active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
  end

  def valid_for_dependency_proxy?
    group_type? &&
      active? &&
      REQUIRED_DEPENDENCY_PROXY_SCOPES.all? { |scope| scope.in?(scopes) }
  end

  def revoke!
    update!(revoked: true)
  end

  def active?
    !revoked && !expired?
  end

  def deactivated?
    !active?
  end

  def scopes
    AVAILABLE_SCOPES.select { |token_scope| read_attribute(token_scope) }
  end

  def username
    super || default_username
  end

  def has_access_to?(requested_project)
    return false unless active?
    return false unless holder

    holder.has_access_to?(requested_project)
  end

  def has_access_to_group?(requested_group)
    return false unless active?
    return false unless group_type?
    return false unless holder

    holder.has_access_to_group?(requested_group)
  end

  # This is temporal. Currently we limit DeployToken
  # to a single project or group, later we're going to
  # extend that to be for multiple projects and namespaces.
  def project
    strong_memoize(:project) do
      projects.first
    end
  end

  def group
    strong_memoize(:group) do
      groups.first
    end
  end

  def accessible_projects
    if project_type?
      projects
    elsif group_type?
      group.all_projects
    end
  end

  def holder
    strong_memoize(:holder) do
      if project_type?
        project_deploy_tokens.first
      elsif group_type?
        group_deploy_tokens.first
      end
    end
  end

  def expires_at
    expires_at = read_attribute(:expires_at)
    expires_at != Forever.date ? expires_at : nil
  end

  def expires_at=(value)
    write_attribute(:expires_at, value.presence || Forever.date)
  end

  private

  def expired?
    return false unless expires_at

    expires_at < Date.today
  end

  def ensure_at_least_one_scope
    errors.add(:base, _("Scopes can't be blank")) unless scopes.any?
  end

  def default_username
    "gitlab+deploy-token-#{id}" if persisted?
  end

  def no_groups
    errors.add(:deploy_token, 'cannot have groups assigned') if group_deploy_tokens.any?
  end

  def no_projects
    errors.add(:deploy_token, 'cannot have projects assigned') if project_deploy_tokens.any?
  end
end