forked from instructure/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeature_flags_controller.rb
More file actions
308 lines (292 loc) · 12.6 KB
/
Copy pathfeature_flags_controller.rb
File metadata and controls
308 lines (292 loc) · 12.6 KB
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
#
# 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/>.
# @API Feature Flags
#
# Manage optional features in Canvas.
#
# _Deprecated_[2016-01-15] FeatureFlags previously had a locking_account_id field;
# it was never used, and has been removed. It is still included in API responses
# for backwards compatibility reasons. Its value is always null.
#
# @model Feature
# {
# "id": "Feature",
# "description": "",
# "properties": {
# "name": {
# "description": "The symbolic name of the feature, used in FeatureFlags",
# "example": "fancy_wickets",
# "type": "string"
# },
# "display_name": {
# "description": "The user-visible name of the feature",
# "example": "Fancy Wickets",
# "type": "string"
# },
# "applies_to": {
# "description": "The type of object the feature applies to (RootAccount, Account, Course, or User):\n * RootAccount features may only be controlled by flags on root accounts.\n * Account features may be controlled by flags on accounts and their parent accounts.\n * Course features may be controlled by flags on courses and their parent accounts.\n * User features may be controlled by flags on users and site admin only.",
# "example": "Course",
# "type": "string",
# "allowableValues": {
# "values": [
# "Course",
# "RootAccount",
# "Account",
# "User"
# ]
# }
# },
# "enable_at": {
# "description": "The date this feature will be globally enabled, or null if this is not planned. (This information is subject to change.)",
# "example": "2014-01-01T00:00:00Z",
# "type": "datetime"
# },
# "feature_flag": {
# "description": "The FeatureFlag that applies to the caller",
# "example": {"feature": "fancy_wickets", "state": "allowed"},
# "$ref": "FeatureFlag"
# },
# "root_opt_in": {
# "description": "If true, a feature that is 'allowed' globally will be 'off' by default in root accounts. Otherwise, root accounts inherit the global 'allowed' setting, which allows sub-accounts and courses to turn features on with no root account action.",
# "example": true,
# "type": "boolean"
# },
# "beta": {
# "description": "Whether the feature is a beta feature. If true, the feature may not be fully polished and may be subject to change in the future.",
# "example": true,
# "type": "boolean"
# },
# "autoexpand": {
# "description": "Whether the details of the feature are autoexpanded on page load vs. the user clicking to expand.",
# "example": true,
# "type": "boolean"
# },
# "development": {
# "description": "Whether the feature is in active development. Features in this state are only visible in test and beta instances and are not yet available for production use.",
# "example": false,
# "type": "boolean"
# },
# "release_notes_url": {
# "description": "A URL to the release notes describing the feature",
# "example": "http://canvas.example.com/release_notes#fancy_wickets",
# "type": "string"
# }
# }
# }
# @model FeatureFlag
# {
# "id": "FeatureFlag",
# "description": "",
# "properties": {
# "context_type": {
# "description": "The type of object to which this flag applies (Account, Course, or User). (This field is not present if this FeatureFlag represents the global Canvas default)",
# "example": "Account",
# "type": "string",
# "allowableValues": {
# "values": [
# "Course",
# "Account",
# "User"
# ]
# }
# },
# "context_id": {
# "description": "The id of the object to which this flag applies (This field is not present if this FeatureFlag represents the global Canvas default)",
# "example": 1038,
# "type": "integer"
# },
# "feature": {
# "description": "The feature this flag controls",
# "example": "fancy_wickets",
# "type": "string"
# },
# "state": {
# "description": "The policy for the feature at this context. can be 'off', 'allowed', or 'on'.",
# "example": "allowed",
# "type": "string",
# "allowableValues": {
# "values": [
# "off",
# "allowed",
# "on"
# ]
# }
# },
# "locked": {
# "description": "If set, this feature flag cannot be changed in the caller's context because the flag is set 'off' or 'on' in a higher context",
# "type": "boolean",
# "example": false
# }
# }
# }
#
class FeatureFlagsController < ApplicationController
include Api::V1::FeatureFlag
before_action :get_context
# @API List features
#
# List all features that apply to a given Account, Course, or User.
#
# @example_request
#
# curl 'http://<canvas>/api/v1/courses/1/features' \
# -H "Authorization: Bearer "
#
# @returns [Feature]
def index
if authorized_action(@context, @current_user, :read)
route = polymorphic_url([:api_v1, @context, :features])
features = Feature.applicable_features(@context)
features = Api.paginate(features, self, route)
flags = features.map { |fd|
@context.lookup_feature_flag(fd.feature, Account.site_admin.grants_right?(@current_user, session, :read))
}.compact
render json: flags.map { |flag| feature_with_flag_json(flag, @context, @current_user, session) }
end
end
# @API List enabled features
#
# List all features that are enabled on a given Account, Course, or User.
# Only the feature names are returned.
#
# @example_request
#
# curl 'http://<canvas>/api/v1/courses/1/features/enabled' \
# -H "Authorization: Bearer "
#
# @example_response
#
# ["fancy_wickets", "automatic_essay_grading", "telepathic_navigation"]
def enabled_features
if authorized_action(@context, @current_user, :read)
features = Feature.applicable_features(@context).map { |fd| @context.lookup_feature_flag(fd.feature) }.compact.
select { |ff| ff.enabled? }.map(&:feature)
render json: features
end
end
# @API Get feature flag
#
# Get the feature flag that applies to a given Account, Course, or User.
# The flag may be defined on the object, or it may be inherited from a parent
# account. You can look at the context_id and context_type of the returned object
# to determine which is the case. If these fields are missing, then the object
# is the global Canvas default.
#
# @example_request
#
# curl 'http://<canvas>/api/v1/courses/1/features/flags/fancy_wickets' \
# -H "Authorization: Bearer "
#
# @returns FeatureFlag
def show
if authorized_action(@context, @current_user, :read)
return render json: { message: "missing feature parameter" }, status: :bad_request unless params[:feature].present?
flag = @context.lookup_feature_flag(params[:feature], Account.site_admin.grants_right?(@current_user, session, :read))
raise ActiveRecord::RecordNotFound unless flag
render json: feature_flag_json(flag, @context, @current_user, session)
end
end
# @API Set feature flag
#
# Set a feature flag for a given Account, Course, or User. This call will fail if a parent account sets
# a feature flag for the same feature in any state other than "allowed".
#
# @argument state [String, "off"|"allowed"|"on"]
# "off":: The feature is not available for the course, user, or account and sub-accounts.
# "allowed":: (valid only on accounts) The feature is off in the account, but may be enabled in
# sub-accounts and courses by setting a feature flag on the sub-account or course.
# "on":: The feature is turned on unconditionally for the user, course, or account and sub-accounts.
#
# @example_request
#
# curl -X PUT 'http://<canvas>/api/v1/courses/1/features/flags/fancy_wickets' \
# -H "Authorization: Bearer " \
# -F "state=on"
#
# @returns FeatureFlag
def update
if authorized_action(@context, @current_user, :manage_feature_flags)
return render json: { message: "must specify feature" }, status: :bad_request unless params[:feature].present?
feature_def = Feature.definitions[params[:feature]]
return render json: { message: "invalid feature" }, status: :bad_request unless feature_def
# check whether the feature is locked
MultiCache.delete(@context.feature_flag_cache_key(params[:feature]))
current_flag = @context.lookup_feature_flag(params[:feature])
if current_flag
return render json: { message: "higher account disallows setting feature flag" }, status: :forbidden if current_flag.locked?(@context)
prior_state = current_flag.state
end
# if this is a hidden feature, require site admin privileges to create (but not update) a root account flag
if !current_flag && feature_def.hidden?
return render json: { message: "invalid feature" }, status: :bad_request unless ((@context.is_a?(Account) && @context.root_account?) || @context.is_a?(User)) && Account.site_admin.grants_right?(@current_user, session, :read)
prior_state = 'hidden'
end
new_attrs = { feature: params[:feature] }
# check transition
if params[:state].present?
transitions = Feature.transitions(params[:feature], @current_user, @context, prior_state)
if transitions[params[:state]] && transitions[params[:state]]['locked']
return render json: { message: "state change not allowed" }, status: :forbidden
end
new_attrs[:state] = params[:state]
end
new_flag, saved = create_or_update_feature_flag(new_attrs, current_flag)
if saved
if prior_state != new_flag.state && feature_def.after_state_change_proc.is_a?(Proc)
feature_def.after_state_change_proc.call(@current_user, @context, prior_state, new_flag.state)
end
render json: feature_flag_json(new_flag, @context, @current_user, session)
else
render json: new_flag.errors, status: :bad_request
end
end
end
# @API Remove feature flag
#
# Remove feature flag for a given Account, Course, or User. (Note that the flag must
# be defined on the Account, Course, or User directly.) The object will then inherit
# the feature flags from a higher account, if any exist. If this flag was 'on' or 'off',
# then lower-level account flags that were masked by this one will apply again.
#
# @example_request
#
# curl -X DELETE 'http://<canvas>/api/v1/courses/1/features/flags/fancy_wickets' \
# -H "Authorization: Bearer "
#
# @returns FeatureFlag
def delete
if authorized_action(@context, @current_user, :manage_feature_flags)
return render json: { message: "must specify feature" }, status: :bad_request unless params[:feature].present?
flag = @context.feature_flags.where(feature: params[:feature]).first!
return render json: { message: "flag is locked" }, status: :forbidden if flag.locked?(@context)
flag.destroy
render json: feature_flag_json(flag, @context, @current_user, session)
end
end
private
def create_or_update_feature_flag(attributes, current_flag = nil)
FeatureFlag.unique_constraint_retry do
new_flag = @context.feature_flags.find(current_flag.id) if current_flag &&
!current_flag.default? && !current_flag.new_record? &&
current_flag.context_type == @context.class.name && current_flag.context_id == @context.id
new_flag ||= @context.feature_flags.build
new_flag.assign_attributes(attributes)
result = new_flag.save
[new_flag, result]
end
end
end