-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
csp_settings_controller.rb
214 lines (193 loc) · 8.09 KB
/
csp_settings_controller.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
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
# frozen_string_literal: true
#
# Copyright (C) 2018 - 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 Content Security Policy Settings
# @beta
#
# API for enabling/disabling the use of Content Security Policy headers and
# configuring allowed domains
class CspSettingsController < ApplicationController
before_action :require_context, :require_user
before_action :require_read_permissions, only: [:get_csp_settings, :csp_log]
before_action :require_permissions, except: [:get_csp_settings, :csp_log]
before_action :get_domain, only: [:add_domain, :remove_domain]
# @API Get current settings for account or course
#
# Update multiple modules in an account.
#
# @response_field enabled Whether CSP is enabled.
# @response_field inherited Whether the current CSP settings are inherited from a parent account.
# @response_field settings_locked Whether current CSP settings can be overridden by sub-accounts and courses.
# @response_field effective_whitelist If enabled, lists the currently allowed domains
# (includes domains automatically allowed through external tools).
# @response_field tools_whitelist (Account-only) Lists the automatically allowed domains with
# their respective external tools
# @response_field current_account_whitelist (Account-only) Lists the current list of domains
# explicitly allowed by this account. (Note: this list will not take effect unless
# CSP is explicitly enabled on this account)
def get_csp_settings
render json: csp_settings_json
end
# @API Enable, disable, or clear explicit CSP setting
#
# Either explicitly sets CSP to be on or off for courses and sub-accounts,
# or clear the explicit settings to default to those set by a parent account
#
# Note: If "inherited" and "settings_locked" are both true for this account or course,
# then the CSP setting cannot be modified.
#
# @argument status [Required, String, "enabled"|"disabled"|"inherited"]
# If set to "enabled" for an account, CSP will be enabled for all its courses and sub-accounts (that
# have not explicitly enabled or disabled it), using the allowed domains set on this account.
# If set to "disabled", CSP will be disabled for this account or course and for all sub-accounts
# that have not explicitly re-enabled it.
# If set to "inherited", this account or course will reset to the default state where CSP settings
# are inherited from the first parent account to have them explicitly set.
#
def set_csp_setting
if ["enabled", "disabled"].include?(params[:status]) && @context.csp_inherited? && @context.csp_locked?
return render json: { message: "cannot set when locked by parent account" }, status: :bad_request
end
case params[:status]
when "enabled"
if @context.is_a?(Course)
if @context.account.csp_enabled?
@context.inherit_csp! # just un-disable
else
return render json: { message: "must be enabled on account-level first" }, status: :bad_request
end
else
@context.enable_csp!
end
when "disabled"
@context.disable_csp!
when "inherited"
@context.inherit_csp!
else
return render json: { message: "invalid setting" }, status: :bad_request
end
RequestCache.clear # clear inherited account settings
render json: csp_settings_json
end
# @API Lock or unlock current CSP settings for sub-accounts and courses
#
# Can only be set if CSP is explicitly enabled or disabled on this account (i.e. "inherited" is false).
#
# @argument settings_locked [Required, Boolean]
# Whether sub-accounts and courses will be prevented from overriding settings inherited from this account.
#
def set_csp_lock
if @context.csp_inherited?
return render json: { message: "CSP must be explicitly set on this account" }, status: :bad_request
end
if value_to_boolean(params.require(:settings_locked))
@context.lock_csp!
else
@context.unlock_csp!
end
render json: csp_settings_json
end
# @API Add an allowed domain to account
#
# Adds an allowed domain for the current account. Note: this will not take effect
# unless CSP is explicitly enabled on this account.
#
# @argument domain [Required, String]
def add_domain
if @context.add_domain!(@domain)
render json: { current_account_whitelist: @context.csp_domains.active.pluck(:domain).sort }
else
render json: { message: "invalid domain" }, status: :bad_request
end
end
# @API Add multiple allowed domains to an account
#
# Adds multiple allowed domains for the current account. Note: this will not take effect
# unless CSP is explicitly enabled on this account.
#
# @argument domains [Required, Array]
def add_multiple_domains
domains = params.require(:domains)
invalid_domains = domains.reject { |domain| URI.parse(domain) rescue nil }
unless invalid_domains.empty?
render json: { message: "invalid domains: #{invalid_domains.join(", ")}" }, status: :bad_request
return false
end
unsuccessful_domains = []
domains.each do |domain|
unsuccessful_domains << domain unless @context.add_domain!(domain)
end
if unsuccessful_domains.empty?
render json: { current_account_whitelist: @context.csp_domains.active.pluck(:domain).sort }
else
render json: { message: "failed adding some domains: #{unsuccessful_domains.join(", ")}" }, status: :bad_request
end
end
# @API Retrieve reported CSP Violations for account
#
# Must be called on a root account.
def csp_log
return render status: :bad_request, json: { message: "must be called on a root account" } unless @context.root_account?
return render status: :service_unavailable, json: { message: "CSP logging is not configured on the server" } unless (ss = @context.csp_logging_config["shared_secret"])
render json: CanvasHttp.get("#{@context.csp_logging_config["host"]}report/#{@context.global_id}", { "Authorization" => "Bearer #{ss}" }).body
end
# @API Remove a domain from account
#
# Removes an allowed domain from the current account.
#
# @argument domain [Required, String]
def remove_domain
@context.remove_domain!(@domain)
render json: { current_account_whitelist: @context.csp_domains.active.pluck(:domain).sort }
end
protected
def require_read_permissions
!!authorized_action(@context, @current_user, :read_as_admin)
end
def require_permissions
account = @context.is_a?(Course) ? @context.account : @context
!!authorized_action(account, @current_user, :manage_account_settings)
end
def get_domain
@domain = params.require(:domain)
unless @domain.is_a?(String) # could do stricter checking someday maybe
render json: { message: "invalid domain" }, status: :bad_request
return false
end
@domain
end
def csp_settings_json
json = {
enabled: @context.csp_enabled?,
inherited: @context.csp_inherited?,
settings_locked: @context.csp_locked?,
}
json[:effective_whitelist] = @context.csp_whitelisted_domains(request, include_files: false, include_tools: true) if @context.csp_enabled?
if @context.is_a?(Account)
tools_whitelist = {}
@context.csp_tools_grouped_by_domain.each do |domain, tools|
tools_whitelist[domain] = tools.map do |tool|
{ id: tool.id, name: tool.name, account_id: tool.context_id }
end
end
json[:tools_whitelist] = tools_whitelist
json[:current_account_whitelist] = @context.csp_domains.active.pluck(:domain).sort
end
json
end
end