summaryrefslogtreecommitdiff
path: root/app/models/project_feature.rb
blob: 346db2847d868dc2e22c8efa7d9b1ecf4059e682 (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
# frozen_string_literal: true

class ProjectFeature < ActiveRecord::Base
  # == 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
  #

  # Permission levels
  DISABLED = 0
  PRIVATE  = 10
  ENABLED  = 20

  FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
  STATISTICS_ATTRIBUTE = 'wikis_count'.freeze

  class << self
    def access_level_attribute(feature)
      feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
      raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)

      "#{feature}_access_level".to_sym
    end

    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
  end

  # 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) }

  validates :project, presence: true

  validate :repository_children_level

  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
  default_value_for :repository_access_level,     value: ENABLED, allows_nil: false

  after_create ->(model) { SiteStatistic.track(STATISTICS_ATTRIBUTE) if model.wiki_enabled? }
  after_update :update_site_statistics

  def feature_available?(feature, user)
    # 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)

    get_permission(user, access_level(feature))
  end

  def access_level(feature)
    public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
  end

  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

  def issues_enabled?
    issues_access_level > DISABLED
  end

  # This is a workaround for the removal hooks not been triggered when removing a Project.
  #
  # ProjectFeature is removed using database cascade index rule.
  # This method is called by Project model when deletion starts.
  def untrack_statistics_for_deletion!
    return unless wiki_enabled?

    SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
  end

  private

  def update_site_statistics
    return unless wiki_access_level_changed?

    if self.wiki_access_level_was == DISABLED
      # possible new states are PRIVATE / ENABLED, both should be tracked
      SiteStatistic.track(STATISTICS_ATTRIBUTE)
    elsif self.wiki_access_level == DISABLED
      # old state was either PRIVATE / ENABLED, only untrack if new state is DISABLED
      SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
    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) || ProjectFeature::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 get_permission(user, level)
    case level
    when DISABLED
      false
    when PRIVATE
      user && (project.team.member?(user) || user.full_private_access?)
    when ENABLED
      true
    else
      true
    end
  end
end