summaryrefslogtreecommitdiff
path: root/app/models/concerns/featurable.rb
blob: 08189d83534a04c70d256526327d52d6eb7153ed (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
# frozen_string_literal: true

# == Featurable concern
#
# This concern adds features (tools) functionality to Project and Group
# To enable features you need to call `set_available_features`
#
# Example:
#
# class ProjectFeature
#   include Featurable
#   set_available_features %i(wiki merge_request)

module Featurable
  extend ActiveSupport::Concern

  # Can be enabled only for members, everyone or disabled
  # Access control is made only for non private containers.
  #
  # Permission levels:
  #
  # Disabled: not enabled for anyone
  # Private:  enabled only for team members
  # Enabled:  enabled for everyone able to access the project
  # Public:   enabled for everyone (only allowed for pages)
  DISABLED = 0
  PRIVATE  = 10
  ENABLED  = 20
  PUBLIC   = 30

  STRING_OPTIONS = HashWithIndifferentAccess.new({
    'disabled' => DISABLED,
    'private'  => PRIVATE,
    'enabled'  => ENABLED,
    'public'   => PUBLIC
  }).freeze

  class_methods do
    def set_available_features(available_features = [])
      @available_features ||= []
      @available_features += available_features

      class_eval do
        available_features.each do |feature|
          define_method("#{feature}_enabled?") do
            public_send("#{feature}_access_level") > DISABLED # rubocop:disable GitlabSecurity/PublicSend
          end
        end
      end
    end

    def available_features
      @available_features || []
    end

    def access_level_attribute(feature)
      feature = ensure_feature!(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

    def access_level_from_str(level)
      STRING_OPTIONS.fetch(level)
    end

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

    def required_minimum_access_level(feature)
      ensure_feature!(feature)

      Gitlab::Access::GUEST
    end

    def ensure_feature!(feature)
      feature = feature.model_name.plural if feature.respond_to?(:model_name)
      feature = feature.to_sym
      raise ArgumentError, "invalid feature: #{feature}" unless available_features.include?(feature)

      feature
    end
  end

  included do
    validate :allowed_access_levels
  end

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

  def feature_available?(feature, user = nil)
    has_permission?(user, feature)
  end

  def string_access_level(feature)
    self.class.str_from_access_level(access_level(feature))
  end

  private

  def allowed_access_levels
    validator = lambda do |field|
      level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend
      not_allowed = level > ENABLED
      self.errors.add(field, "cannot have public visibility level") if not_allowed
    end

    (self.class.available_features - feature_validation_exclusion).each {|f| validator.call("#{f}_access_level")}
  end

  # Features that we should exclude from the validation
  def feature_validation_exclusion
    []
  end

  def has_permission?(user, feature)
    case access_level(feature)
    when DISABLED
      false
    when PRIVATE
      member?(user, feature)
    when ENABLED
      true
    when PUBLIC
      true
    else
      true
    end
  end

  def member?(user, feature)
    return false unless user
    return true if user.can_read_all_resources?

    resource_member?(user, feature)
  end

  def resource_member?(user, feature)
    raise NotImplementedError
  end
end