Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.1 on 2022-01-26 at 16:00 UTC+1 (a few minutes of down time)

project_feature.rb 5.34 KB
Newer Older
1
2
# frozen_string_literal: true

3
class ProjectFeature < ApplicationRecord
Felipe Artur's avatar
Felipe Artur committed
4
5
6
7
8
9
10
11
12
13
14
15
  # == Project features permissions
  #
  # Grants access level to project tools
  #
  # Tools can be enabled only for users, everyone or disabled
  # Access control is made only for non private projects
  #
  # levels:
  #
  # Disabled: not enabled for anyone
  # Private:  enabled only for team members
  # Enabled:  enabled for everyone able to access the project
16
  # Public:   enabled for everyone (only allowed for pages)
Felipe Artur's avatar
Felipe Artur committed
17
18
  #

19
  # Permission levels
Felipe Artur's avatar
Felipe Artur committed
20
21
22
  DISABLED = 0
  PRIVATE  = 10
  ENABLED  = 20
23
  PUBLIC   = 30
Felipe Artur's avatar
Felipe Artur committed
24

25
  FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
26
  PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
27
28
29
30
31
32
  STRING_OPTIONS = HashWithIndifferentAccess.new({
    'disabled' => DISABLED,
    'private'  => PRIVATE,
    'enabled'  => ENABLED,
    'public'   => PUBLIC
  }).freeze
Felipe Artur's avatar
Felipe Artur committed
33

34
35
  class << self
    def access_level_attribute(feature)
36
      feature = ensure_feature!(feature)
37
38
39

      "#{feature}_access_level".to_sym
    end
40
41
42
43
44
45
46

    def quoted_access_level_column(feature)
      attribute = connection.quote_column_name(access_level_attribute(feature))
      table = connection.quote_table_name(table_name)

      "#{table}.#{attribute}"
    end
47
48
49
50
51
52
53

    def required_minimum_access_level(feature)
      feature = ensure_feature!(feature)

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

54
55
56
57
58
59
60
61
    def access_level_from_str(level)
      STRING_OPTIONS.fetch(level)
    end

    def str_from_access_level(level)
      STRING_OPTIONS.key(level)
    end

62
63
64
    private

    def ensure_feature!(feature)
65
66
      feature = feature.model_name.plural if feature.respond_to?(:model_name)
      feature = feature.to_sym
67
68
69
70
      raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)

      feature
    end
71
72
  end

73
74
75
76
  # Default scopes force us to unscope here since a service may need to check
  # permissions for a project in pending_delete
  # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
  belongs_to :project, -> { unscope(where: :pending_delete) }
Felipe Artur's avatar
Felipe Artur committed
77

78
79
  validates :project, presence: true

80
  validate :repository_children_level
81
  validate :allowed_access_levels
82

83
84
85
86
87
  default_value_for :builds_access_level,         value: ENABLED, allows_nil: false
  default_value_for :issues_access_level,         value: ENABLED, allows_nil: false
  default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
  default_value_for :snippets_access_level,       value: ENABLED, allows_nil: false
  default_value_for :wiki_access_level,           value: ENABLED, allows_nil: false
88
  default_value_for :repository_access_level,     value: ENABLED, allows_nil: false
89

90
91
  default_value_for(:pages_access_level, allows_nil: false) { |feature| feature.project&.public? ? ENABLED : PRIVATE }

Felipe Artur's avatar
Felipe Artur committed
92
  def feature_available?(feature, user)
93
94
95
    # This feature might not be behind a feature flag at all, so default to true
    return false unless ::Feature.enabled?(feature, user, default_enabled: true)

96
    get_permission(user, feature)
97
98
99
  end

  def access_level(feature)
100
    public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
Felipe Artur's avatar
Felipe Artur committed
101
102
  end

103
104
105
106
  def string_access_level(feature)
    ProjectFeature.str_from_access_level(access_level(feature))
  end

Felipe Artur's avatar
Felipe Artur committed
107
108
109
110
111
112
113
114
115
116
117
118
  def builds_enabled?
    builds_access_level > DISABLED
  end

  def wiki_enabled?
    wiki_access_level > DISABLED
  end

  def merge_requests_enabled?
    merge_requests_access_level > DISABLED
  end

119
120
121
122
  def issues_enabled?
    issues_access_level > DISABLED
  end

123
124
125
126
127
128
129
130
131
132
  def pages_enabled?
    pages_access_level > DISABLED
  end

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

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

133
134
135
136
  def private_pages?
    !public_pages?
  end

Felipe Artur's avatar
Felipe Artur committed
137
138
  private

139
140
141
142
  # Validates builds and merge requests access level
  # which cannot be higher than repository access level
  def repository_children_level
    validator = lambda do |field|
143
      level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend
144
145
146
147
148
149
150
      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

151
152
153
154
155
156
157
158
159
160
161
  # Validates access level for other than pages cannot be PUBLIC
  def allowed_access_levels
    validator = lambda do |field|
      level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend
      not_allowed = level > ProjectFeature::ENABLED
      self.errors.add(field, "cannot have public visibility level") if not_allowed
    end

    (FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")}
  end

162
163
  def get_permission(user, feature)
    case access_level(feature)
Felipe Artur's avatar
Felipe Artur committed
164
165
166
    when DISABLED
      false
    when PRIVATE
167
      team_access?(user, feature)
Felipe Artur's avatar
Felipe Artur committed
168
169
    when ENABLED
      true
170
171
    when PUBLIC
      true
Felipe Artur's avatar
Felipe Artur committed
172
173
174
175
    else
      true
    end
  end
176
177
178
179
180
181
182

  def team_access?(user, feature)
    return unless user
    return true if user.full_private_access?

    project.team.member?(user, ProjectFeature.required_minimum_access_level(feature))
  end
Felipe Artur's avatar
Felipe Artur committed
183
end
184
185

ProjectFeature.prepend_if_ee('EE::ProjectFeature')