1# frozen_string_literal: true
2
3class DeployToken < ApplicationRecord
4  include Expirable
5  include TokenAuthenticatable
6  include PolicyActor
7  include Gitlab::Utils::StrongMemoize
8  add_authentication_token_field :token, encrypted: :optional
9
10  AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
11                        read_package_registry write_package_registry).freeze
12  GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
13  REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze
14
15  default_value_for(:expires_at) { Forever.date }
16
17  has_many :project_deploy_tokens, inverse_of: :deploy_token
18  has_many :projects, through: :project_deploy_tokens
19
20  has_many :group_deploy_tokens, inverse_of: :deploy_token
21  has_many :groups, through: :group_deploy_tokens
22
23  validate :no_groups, unless: :group_type?
24  validate :no_projects, unless: :project_type?
25  validate :ensure_at_least_one_scope
26  validates :username,
27    length: { maximum: 255 },
28    allow_nil: true,
29    format: {
30      with: /\A[a-zA-Z0-9\.\+_-]+\z/,
31      message: "can contain only letters, digits, '_', '-', '+', and '.'"
32    }
33
34  validates :deploy_token_type, presence: true
35  enum deploy_token_type: {
36    group_type: 1,
37    project_type: 2
38  }
39
40  before_save :ensure_token
41
42  accepts_nested_attributes_for :project_deploy_tokens
43
44  scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
45
46  def self.gitlab_deploy_token
47    active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
48  end
49
50  def valid_for_dependency_proxy?
51    group_type? &&
52      active? &&
53      REQUIRED_DEPENDENCY_PROXY_SCOPES.all? { |scope| scope.in?(scopes) }
54  end
55
56  def revoke!
57    update!(revoked: true)
58  end
59
60  def active?
61    !revoked && !expired?
62  end
63
64  def deactivated?
65    !active?
66  end
67
68  def scopes
69    AVAILABLE_SCOPES.select { |token_scope| read_attribute(token_scope) }
70  end
71
72  def username
73    super || default_username
74  end
75
76  def has_access_to?(requested_project)
77    return false unless active?
78    return false unless holder
79
80    holder.has_access_to?(requested_project)
81  end
82
83  def has_access_to_group?(requested_group)
84    return false unless active?
85    return false unless group_type?
86    return false unless holder
87
88    holder.has_access_to_group?(requested_group)
89  end
90
91  # This is temporal. Currently we limit DeployToken
92  # to a single project or group, later we're going to
93  # extend that to be for multiple projects and namespaces.
94  def project
95    strong_memoize(:project) do
96      projects.first
97    end
98  end
99
100  def group
101    strong_memoize(:group) do
102      groups.first
103    end
104  end
105
106  def accessible_projects
107    if project_type?
108      projects
109    elsif group_type?
110      group.all_projects
111    end
112  end
113
114  def holder
115    strong_memoize(:holder) do
116      if project_type?
117        project_deploy_tokens.first
118      elsif group_type?
119        group_deploy_tokens.first
120      end
121    end
122  end
123
124  def expires_at
125    expires_at = read_attribute(:expires_at)
126    expires_at != Forever.date ? expires_at : nil
127  end
128
129  def expires_at=(value)
130    write_attribute(:expires_at, value.presence || Forever.date)
131  end
132
133  private
134
135  def expired?
136    return false unless expires_at
137
138    expires_at < Date.today
139  end
140
141  def ensure_at_least_one_scope
142    errors.add(:base, _("Scopes can't be blank")) unless scopes.any?
143  end
144
145  def default_username
146    "gitlab+deploy-token-#{id}" if persisted?
147  end
148
149  def no_groups
150    errors.add(:deploy_token, 'cannot have groups assigned') if group_deploy_tokens.any?
151  end
152
153  def no_projects
154    errors.add(:deploy_token, 'cannot have projects assigned') if project_deploy_tokens.any?
155  end
156end
157