forked from instructure/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
/
feature_flags.rb
186 lines (159 loc) · 6.48 KB
/
feature_flags.rb
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
# frozen_string_literal: true
#
# Copyright (C) 2013 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module FeatureFlags
def self.included(base)
base.has_many :feature_flags, as: :context, dependent: :destroy
end
def feature_enabled?(feature)
flag = lookup_feature_flag(feature)
return flag.enabled? if flag
false
end
def feature_allowed?(feature)
flag = lookup_feature_flag(feature)
return false unless flag
flag.enabled? || flag.can_override?
end
def set_feature_flag!(feature, state)
feature = feature.to_s
flag = self.feature_flags.where(feature: feature).first
flag ||= self.feature_flags.build(feature: feature)
flag.state = state
@feature_flag_cache ||= {}
@feature_flag_cache[feature] = flag
flag.save!
end
def allow_feature!(feature)
set_feature_flag!(feature, Feature::STATE_DEFAULT_OFF)
end
def enable_feature!(feature)
set_feature_flag!(feature, Feature::STATE_ON)
end
def disable_feature!(feature)
set_feature_flag!(feature, Feature::STATE_OFF)
end
def reset_feature!(feature)
self.feature_flags.where(feature: feature.to_s).destroy_all
end
def feature_flag_cache_key(feature)
['feature_flag3', self.class.name, self.global_id, feature.to_s].cache_key
end
def feature_flag_cache
Rails.cache
end
# return the feature flag for the given feature that is defined on this object, if any.
# (helper method. use lookup_feature_flag to test policy.)
def feature_flag(feature, skip_cache: false)
return nil unless self.id
self.shard.activate do
if self.feature_flags.loaded?
self.feature_flags.detect{|ff| ff.feature == feature.to_s}
elsif skip_cache
self.feature_flags.where(feature: feature.to_s).first
else
result = RequestCache.cache("feature_flag", self, feature) do
feature_flag_cache.fetch(feature_flag_cache_key(feature)) do
# keep have the context association unloaded in case we can't marshal it
FeatureFlag.where(feature: feature.to_s).polymorphic_where(:context => self).first
end
end
result.context = self if result
result
end
end
end
# each account that needs to be searched for a feature flag, in priority order,
# starting with site admin
def feature_flag_account_ids
return [Account.site_admin.global_id] if is_a?(User)
return [] if self.is_a?(Account) && self.site_admin?
cache = self.is_a?(Account) && root_account? ? MultiCache.cache : Rails.cache
RequestCache.cache('feature_flag_account_ids', self) do
shard.activate do
cache.fetch(['feature_flag_account_ids', self].cache_key) do
chain = account_chain(include_site_admin: true)
chain.shift if is_a?(Account)
chain.reverse.map(&:global_id)
end
end
end
end
# find the feature flag setting that applies to this object
# it may be defined on the object or inherited
def lookup_feature_flag(feature, override_hidden: false, skip_cache: false, hide_inherited_enabled: false, inherited_only: false)
feature = feature.to_s
feature_def = Feature.definitions[feature]
raise "no such feature - #{feature}" unless feature_def
return nil unless feature_def.applies_to_object(self)
return nil if feature_def.visible_on.is_a?(Proc) && !feature_def.visible_on.call(self)
return return_flag(feature_def, hide_inherited_enabled) unless feature_def.can_override? || feature_def.hidden?
is_root_account = self.is_a?(Account) && self.root_account?
is_site_admin = self.is_a?(Account) && self.site_admin?
# inherit the feature definition as a default unless it's a hidden feature
retval = feature_def.clone_for_cache unless feature_def.hidden? && !is_site_admin && !override_hidden
@feature_flag_cache ||= {}
return return_flag(@feature_flag_cache[feature], hide_inherited_enabled) if @feature_flag_cache.key?(feature) && !inherited_only
# find the highest flag that doesn't allow override,
# or the most specific flag otherwise
accounts = feature_flag_account_ids.map do |id|
account = Account.new
account.id = id
account.shard = Shard.shard_for(id, self.shard)
account.readonly!
account
end
all_contexts = (accounts + [self]).uniq
all_contexts -= [self] if inherited_only
all_contexts.each_with_index do |context, idx|
flag = context.feature_flag(feature, skip_cache: context == self && skip_cache)
next unless flag
retval = flag
break unless flag.can_override?
end
# if this feature requires root account opt-in, reject a default or site admin flag
# if the context is beneath a root account
if retval && (retval.state == Feature::STATE_DEFAULT_OFF || retval.hidden?) && feature_def.root_opt_in && !is_site_admin &&
(retval.default? || retval.context_type == 'Account' && retval.context_id == Account.site_admin.id)
if is_root_account
# create a virtual feature flag in corresponding default state state
retval = self.feature_flags.temp_record feature: feature, state: 'off' unless retval.hidden?
else
# the feature doesn't exist beneath the root account until the root account opts in
if inherited_only
return nil
else
return @feature_flag_cache[feature] = nil
end
end
end
@feature_flag_cache[feature] = retval unless inherited_only
return_flag(retval, hide_inherited_enabled)
end
def return_flag(retval, hide_inherited_enabled)
return nil unless retval
unless hide_inherited_enabled && retval.enabled? && !retval.can_override? && (
# Hide feature flag configs if they belong to a different context
(!retval.default? && (retval.context_type != self.class.name || retval.context_id != self.id)) ||
# Hide flags that are forced on in config as well
retval.default?
)
retval
end
end
end