summaryrefslogtreecommitdiff
path: root/app/models/project_feature.rb
blob: 52e623db7b0f7a9c3eed70e048ccc0e9f5751b66 (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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# frozen_string_literal: true

class ProjectFeature < ApplicationRecord
  include Featurable
  extend Gitlab::ConfigHelper
  extend ::Gitlab::Utils::Override

  # When updating this array, make sure to update rubocop/cop/gitlab/feature_available_usage.rb as well.
  FEATURES = %i[
    issues
    forking
    merge_requests
    wiki
    snippets
    builds
    repository
    pages
    metrics_dashboard
    analytics
    monitor
    operations
    security_and_compliance
    container_registry
    package_registry
    environments
    feature_flags
    releases
    infrastructure
  ].freeze

  EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze

  set_available_features(FEATURES)

  PRIVATE_FEATURES_MIN_ACCESS_LEVEL = {
    merge_requests: Gitlab::Access::REPORTER,
    metrics_dashboard: Gitlab::Access::REPORTER,
    container_registry: Gitlab::Access::REPORTER,
    package_registry: Gitlab::Access::REPORTER,
    environments: Gitlab::Access::REPORTER
  }.freeze
  PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze

  class << self
    def required_minimum_access_level(feature)
      feature = ensure_feature!(feature)

      PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST)
    end

    # Guest users can perform certain features on public and internal projects, but not private projects.
    def required_minimum_access_level_for_private_project(feature)
      feature = ensure_feature!(feature)

      PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT.fetch(feature) do
        required_minimum_access_level(feature)
      end
    end
  end

  belongs_to :project

  validates :project, presence: true

  validate :repository_children_level

  attribute :builds_access_level, default: ENABLED
  attribute :issues_access_level, default: ENABLED
  attribute :forking_access_level, default: ENABLED
  attribute :merge_requests_access_level, default: ENABLED
  attribute :snippets_access_level, default: ENABLED
  attribute :wiki_access_level, default: ENABLED
  attribute :repository_access_level, default: ENABLED
  attribute :analytics_access_level, default: ENABLED
  attribute :metrics_dashboard_access_level, default: PRIVATE
  attribute :operations_access_level, default: ENABLED
  attribute :security_and_compliance_access_level, default: PRIVATE
  attribute :monitor_access_level, default: ENABLED
  attribute :infrastructure_access_level, default: ENABLED
  attribute :feature_flags_access_level, default: ENABLED
  attribute :environments_access_level, default: ENABLED

  attribute :package_registry_access_level, default: -> do
    if ::Gitlab.config.packages.enabled
      ENABLED
    else
      DISABLED
    end
  end

  attribute :container_registry_access_level, default: -> do
    if gitlab_config_features.container_registry
      ENABLED
    else
      DISABLED
    end
  end

  after_initialize :set_pages_access_level, if: :new_record?
  after_initialize :set_default_values, unless: :new_record?

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
    feature_access_level_attribute = arel_table[access_level_attribute(feature)]
    enabled_feature = feature_access_level_attribute.gt(DISABLED).or(feature_access_level_attribute.eq(nil))

    where(enabled_feature)
  }

  # Picks a feature where the level is exactly that given.
  scope :with_feature_access_level, ->(feature, level) {
    feature_access_level_attribute = access_level_attribute(feature)
    where(project_features: { feature_access_level_attribute => level })
  }

  # project features may be "disabled", "internal", "enabled" or "public". If "internal",
  # they are only available to team members. This scope returns features where
  # the feature is either public, enabled, or internal with permission for the user.
  # Note: this scope doesn't enforce that the user has access to the projects, it just checks
  # that the user has access to the feature. It's important to use this scope with others
  # that checks project authorizations first (e.g. `filter_by_feature_visibility`).
  #
  # This method uses an optimized version of `with_feature_access_level` for
  # logged in users to more efficiently get private projects with the given
  # feature.
  def self.with_feature_available_for_user(feature, user)
    visible = [ENABLED, PUBLIC]

    if user&.can_read_all_resources?
      with_feature_enabled(feature)
    elsif user
      min_access_level = required_minimum_access_level(feature)
      column = quoted_access_level_column(feature)

      where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
           {
             public_visible: visible,
             private_visible: PRIVATE,
             authorizations: user.authorizations_for_projects(min_access_level: min_access_level, related_project_column: 'project_features.project_id')
           })
    else
      # This has to be added to include features whose value is nil in the db
      visible << nil
      with_feature_access_level(feature, visible)
    end
  end

  def public_pages?
    return true unless Gitlab.config.pages.access_control

    return false if ::Gitlab::Pages.access_control_is_forced?

    pages_access_level == PUBLIC || pages_access_level == ENABLED && project.public?
  end

  def private_pages?
    !public_pages?
  end

  def package_registry_access_level=(value)
    super(value).tap do
      project.packages_enabled = self.package_registry_access_level != DISABLED if project
    end
  end

  def public_packages?
    return false unless Gitlab.config.packages.enabled

    package_registry_access_level == PUBLIC || project.public?
  end

  private

  def set_pages_access_level
    self.pages_access_level ||= if ::Gitlab::Pages.access_control_is_forced?
                                  PRIVATE
                                else
                                  self.project&.public? ? ENABLED : PRIVATE
                                end
  end

  def set_default_values
    self.class.column_names.each do |column_name|
      next unless has_attribute?(column_name)
      next unless read_attribute(column_name).nil?

      write_attribute(column_name, self.class.column_defaults[column_name])
    end
  end

  # Validates builds and merge requests access level
  # which cannot be higher than repository access level
  def repository_children_level
    validator = lambda do |field|
      level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend
      not_allowed = level > repository_access_level
      self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
    end

    %i(merge_requests_access_level builds_access_level).each(&validator)
  end

  def feature_validation_exclusion
    %i(pages package_registry)
  end

  override :resource_member?
  def resource_member?(user, feature)
    project.team.member?(user, ProjectFeature.required_minimum_access_level(feature))
  end
end

ProjectFeature.prepend_mod_with('ProjectFeature')