Skip to content

Commit 6d155c4

Browse files
committed
permission context in addressbook service call
closes CNVS-33774 when making an addressbook service call, include information about the courses and accounts in which the sender (if any) has (or lacks) relevant permissions. test-plan: - have canvas communicating with the address book service - set up a user who's: - a teacher in one course (grants send_messages permission) - an observer in another course (lacks send_messages permission) - an admin in a separate subaccount (grants read_roster permission) - visit the inbox and search for a recipient (search details irrelevant) - look in the request logs of the address book service; the requests for your search should include: - restricted_course_ids parameter that: - includes the id of the course in which the user's an observer - does not include the id of the course in which the user's a teacher - visible_account_ids parameter that: - includes the id of the account in which the user's an admin - does not include the id of the other accounts the user's associated with Change-Id: I2c1c3dcb9d69a4053e19c8803f27024d4b3d653c Reviewed-on: https://gerrit.instructure.com/100599 Reviewed-by: Andrew Huff <ahuff@instructure.com> Reviewed-by: Jonathan Featherstone <jfeatherstone@instructure.com> QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com> Tested-by: Jenkins Product-Review: Jacob Fugal <jacob@instructure.com>
1 parent e04667a commit 6d155c4

2 files changed

Lines changed: 85 additions & 43 deletions

File tree

lib/services/address_book.rb

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,28 +135,36 @@ def query_params(params={})
135135
query_params[:cursor] = params[:cursor] if params[:cursor]
136136
query_params[:per_page] = params[:per_page] if params[:per_page]
137137
query_params[:search] = params[:search] if params[:search]
138-
query_params[:for_sender] = serialize_user(params[:sender]) if params[:sender]
138+
if params[:sender]
139+
sender = params[:sender]
140+
sender = User.find(sender) unless sender.is_a?(User)
141+
visible_accounts = sender.associated_accounts.select{ |account| account.grants_right?(sender, :read_roster) }
142+
restricted_courses = sender.all_courses.reject{ |course| course.grants_right?(sender, :send_messages) }
143+
query_params[:for_sender] = serialize_item(sender)
144+
query_params[:visible_account_ids] = serialize_list(visible_accounts) unless visible_accounts.empty?
145+
query_params[:restricted_course_ids] = serialize_list(restricted_courses) unless restricted_courses.empty?
146+
end
139147
query_params[:in_context] = serialize_context(params[:context]) if params[:context]
140-
query_params[:user_ids] = serialize_users(params[:user_ids]) if params[:user_ids]
141-
query_params[:exclude_ids] = serialize_users(params[:exclude_ids]) if params[:exclude_ids]
148+
query_params[:user_ids] = serialize_list(params[:user_ids]) if params[:user_ids]
149+
query_params[:exclude_ids] = serialize_list(params[:exclude_ids]) if params[:exclude_ids]
142150
query_params[:weak_checks] = 1 if params[:weak_checks]
143151
query_params
144152
end
145153

146-
def serialize_user(user)
147-
Shard.global_id_for(user)
154+
def serialize_item(item)
155+
Shard.global_id_for(item)
148156
end
149157

150-
def serialize_users(users)
151-
users.map{ |user| serialize_user(user) }.join(',')
158+
def serialize_list(list) # can be either IDs or objects (e.g. User)
159+
list.map{ |item| serialize_item(item) }.join(',')
152160
end
153161

154162
def serialize_context(context)
155163
if context.respond_to?(:global_asset_string)
156164
context.global_asset_string
157165
else
158166
context_type, context_id, scope = context.split('_', 3)
159-
global_context_id = Shard.global_id_for(context_id)
167+
global_context_id = serialize_item(context_id)
160168
asset_string = "#{context_type}_#{global_context_id}"
161169
asset_string += "_#{scope}" if scope
162170
asset_string

spec/lib/services/address_book_spec.rb

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ def not_match(*args)
4747
::RSpec::Matchers::AliasedNegatedMatcher.new(match(*args), ->{})
4848
end
4949

50+
matcher :with_param do |param, value|
51+
match do |url|
52+
if value.is_a?(Array)
53+
return false unless url =~ %r{[?&]#{param}=(\d+(%2C\d+)*)(?:&|$)}
54+
actual = $1.split('%2C').map(&:to_i)
55+
return actual.sort == value.sort
56+
else
57+
url =~ %r{[?&]#{param}=#{value}(?:&|$)}
58+
end
59+
end
60+
end
61+
62+
matcher :with_param_present do |param|
63+
match do |url|
64+
url =~ %r{[?&]#{param}=}
65+
end
66+
end
67+
5068
describe "jwt" do
5169
it "signs with the base64 decoded secret from the configuration" do
5270
jwt = Services::AddressBook.jwt
@@ -77,65 +95,81 @@ def not_match(*args)
7795
end
7896

7997
it "normalizes sender from a User to its global ID as for_sender param" do
80-
expect_request(%r{for_sender=#{@sender.global_id}})
98+
expect_request(with_param(:for_sender, @sender.global_id))
8199
Services::AddressBook.recipients(sender: @sender)
82100
end
83101

84102
it "normalizes sender from an ID to a global ID as for_sender param" do
85-
expect_request(%r{for_sender=#{@sender.global_id}})
103+
expect_request(with_param(:for_sender, @sender.global_id))
86104
Services::AddressBook.recipients(sender: @sender.id)
87105
end
88106

107+
it "includes the sender's visible accounts" do
108+
account1 = account_model
109+
account2 = account_model
110+
account_admin_user(user: @sender, account: account1)
111+
account_admin_user(user: @sender, account: account2)
112+
expect_request(with_param(:visible_account_ids, [account1.global_id, account2.global_id]))
113+
Services::AddressBook.recipients(sender: @sender)
114+
end
115+
116+
it "includes the sender's restricted courses" do
117+
course1 = course_with_observer(user: @sender, active_all: true).course
118+
course2 = course_with_observer(user: @sender, active_all: true).course
119+
expect_request(with_param(:restricted_course_ids, [course1.global_id, course2.global_id]))
120+
Services::AddressBook.recipients(sender: @sender)
121+
end
122+
89123
it "normalizes context from e.g. a Course to its global asset string as in_context param" do
90-
expect_request(%r{in_context=#{@course.global_asset_string}})
124+
expect_request(with_param(:in_context, @course.global_asset_string))
91125
Services::AddressBook.recipients(context: @course)
92126
end
93127

94128
it "normalizes context from an asset string to a global asset string as in_context param" do
95-
expect_request(%r{in_context=#{@course.global_asset_string}})
129+
expect_request(with_param(:in_context, @course.global_asset_string))
96130
Services::AddressBook.recipients(context: @course.asset_string)
97131
end
98132

99133
it "normalizes context from a scoped asset string to a scoped global asset string as in_context param" do
100-
expect_request(%r{in_context=#{@course.global_asset_string}_students})
134+
expect_request(with_param(:in_context, "#{@course.global_asset_string}_students"))
101135
Services::AddressBook.recipients(context: "#{@course.asset_string}_students")
102136
end
103137

104138
it "normalizes user_ids from Users to a comma-separated list of their global IDs as user_ids param" do
105139
recipient1 = user_model
106140
recipient2 = user_model
107-
expect_request(%r{user_ids=#{recipient1.global_id}%2C#{recipient2.global_id}})
141+
expect_request(with_param(:user_ids, [recipient1.global_id, recipient2.global_id]))
108142
Services::AddressBook.recipients(user_ids: [recipient1, recipient2])
109143
end
110144

111145
it "normalizes user_ids from IDs to a comma-separated list of global IDs as user_ids param" do
112146
recipient1 = user_model
113147
recipient2 = user_model
114-
expect_request(%r{user_ids=#{recipient1.global_id}%2C#{recipient2.global_id}})
148+
expect_request(with_param(:user_ids, [recipient1.global_id, recipient2.global_id]))
115149
Services::AddressBook.recipients(user_ids: [recipient1.id, recipient2.id])
116150
end
117151

118152
it "normalizes exclude_ids from Users to a comma-separated list of their global IDs as exclude_ids param" do
119153
recipient1 = user_model
120154
recipient2 = user_model
121-
expect_request(%r{exclude_ids=#{recipient1.global_id}%2C#{recipient2.global_id}})
155+
expect_request(with_param(:exclude_ids, [recipient1.global_id, recipient2.global_id]))
122156
Services::AddressBook.recipients(exclude_ids: [recipient1, recipient2])
123157
end
124158

125159
it "normalizes exclude_ids from IDs to a comma-separated list of global IDs as exclude_ids param" do
126160
recipient1 = user_model
127161
recipient2 = user_model
128-
expect_request(%r{exclude_ids=#{recipient1.global_id}%2C#{recipient2.global_id}})
162+
expect_request(with_param(:exclude_ids, [recipient1.global_id, recipient2.global_id]))
129163
Services::AddressBook.recipients(exclude_ids: [recipient1.id, recipient2.id])
130164
end
131165

132166
it "normalizes weak_checks to 1 if truthy" do
133-
expect_request(%r{weak_checks=1})
167+
expect_request(with_param(:weak_checks, 1))
134168
Services::AddressBook.recipients(weak_checks: true)
135169
end
136170

137171
it "omits weak_checks if falsey" do
138-
expect_request(not_match(%r{weak_checks=}))
172+
expect_request(not_match(with_param_present(:weak_checks)))
139173
Services::AddressBook.recipients(weak_checks: false)
140174
end
141175

@@ -191,12 +225,12 @@ def not_match(*args)
191225
end
192226

193227
it "normalizes sender same as recipients" do
194-
expect_request(%r{for_sender=#{@sender.global_id}}, body: @response)
228+
expect_request(with_param(:for_sender, @sender.global_id), body: @response)
195229
Services::AddressBook.count_recipients(sender: @sender)
196230
end
197231

198232
it "normalizes context same as recipients" do
199-
expect_request(%r{in_context=#{@course.global_asset_string}}, body: @response)
233+
expect_request(with_param(:in_context, @course.global_asset_string), body: @response)
200234
Services::AddressBook.count_recipients(context: @course)
201235
end
202236

@@ -214,40 +248,40 @@ def not_match(*args)
214248
end
215249

216250
it "passes the sender to the recipients call" do
217-
expect_request(%r{sender=})
251+
expect_request(with_param_present(:for_sender))
218252
Services::AddressBook.common_contexts(@sender, [1, 2, 3])
219253
end
220254

221255
it "passes the user_ids to the recipients call" do
222256
recipient1 = user_model
223257
recipient2 = user_model
224-
expect_request(%r{user_ids=#{recipient1.global_id}%2C#{recipient2.global_id}})
258+
expect_request(with_param(:user_ids, [recipient1.global_id, recipient2.global_id]))
225259
Services::AddressBook.common_contexts(@sender, [recipient1.id, recipient2.id])
226260
end
227261
end
228262

229263
describe "roles_in_context" do
230264
it "makes a recipient request with no sender" do
231265
expect_request(%r{/recipients\?})
232-
expect_request(%r{sender=}).never
266+
expect_request(with_param_present(:for_sender)).never
233267
Services::AddressBook.roles_in_context(@course, [1, 2, 3])
234268
end
235269

236270
it "passes the user_ids to the recipients call" do
237271
recipient1 = user_model
238272
recipient2 = user_model
239273
expect(recipient1.global_id).to eql(Shard.global_id_for(recipient1.id))
240-
expect_request(%r{user_ids=#{recipient1.global_id}%2C#{recipient2.global_id}})
274+
expect_request(with_param(:user_ids, [recipient1.global_id, recipient2.global_id]))
241275
Services::AddressBook.roles_in_context(@course, [recipient1.id, recipient2.id])
242276
end
243277

244278
it "passes the context to the recipients call" do
245-
expect_request(%r{in_context=#{@course.global_asset_string}})
279+
expect_request(with_param(:in_context, @course.global_asset_string))
246280
Services::AddressBook.roles_in_context(@course, [1, 2, 3])
247281
end
248282

249283
it "uses the course as the context for a course section" do
250-
expect_request(%r{in_context=#{@course.global_asset_string}})
284+
expect_request(with_param(:in_context, @course.global_asset_string))
251285
Services::AddressBook.roles_in_context(@course.default_section, [1, 2, 3])
252286
end
253287
end
@@ -259,17 +293,17 @@ def not_match(*args)
259293
end
260294

261295
it "passes the sender to the recipients call if not is_admin" do
262-
expect_request(%r{sender=#{@sender.global_id}})
296+
expect_request(with_param(:for_sender, @sender.global_id))
263297
Services::AddressBook.known_in_context(@sender, @course.asset_string)
264298
end
265299

266300
it "omits the sender form the recipients call if is_admin" do
267-
expect_request(not_match(%r{sender=}))
301+
expect_request(not_match(with_param_present(:for_sender)))
268302
Services::AddressBook.known_in_context(@sender, @course.asset_string, true)
269303
end
270304

271305
it "passes the context to the recipients call" do
272-
expect_request(%r{in_context=#{@course.global_asset_string}})
306+
expect_request(with_param(:in_context, @course.global_asset_string))
273307
Services::AddressBook.known_in_context(@sender, @course.asset_string)
274308
end
275309

@@ -302,12 +336,12 @@ def not_match(*args)
302336
end
303337

304338
it "passes the sender to the count_recipients call" do
305-
expect_request(%r{sender=#{@sender.global_id}})
339+
expect_request(with_param(:for_sender, @sender.global_id))
306340
Services::AddressBook.count_in_context(@sender, @course.asset_string)
307341
end
308342

309343
it "passes the context to the count_recipients call" do
310-
expect_request(%r{in_context=#{@course.global_asset_string}})
344+
expect_request(with_param(:in_context, @course.global_asset_string))
311345
Services::AddressBook.count_in_context(@sender, @course.asset_string)
312346
end
313347

@@ -325,35 +359,35 @@ def not_match(*args)
325359
end
326360

327361
it "only has search parameter by default" do
328-
expect_request(%r{search=bob})
329-
expect_request(%r{in_context=}).never
330-
expect_request(%r{exclude_ids=}).never
331-
expect_request(%r{weak_checks=}).never
362+
expect_request(with_param(:search, 'bob'))
363+
expect_request(with_param_present(:in_context)).never
364+
expect_request(with_param_present(:exclude_ids)).never
365+
expect_request(with_param_present(:weak_checks)).never
332366
Services::AddressBook.search_users(@sender, search: 'bob')
333367
end
334368

335369
it "passes context to recipients call" do
336-
expect_request(%r{in_context=})
370+
expect_request(with_param_present(:in_context))
337371
Services::AddressBook.search_users(@sender, search: 'bob', context: @course)
338372
end
339373

340374
it "includes sender if is_admin but no context given" do
341-
expect_request(%r{sender=})
375+
expect_request(with_param_present(:for_sender))
342376
Services::AddressBook.search_users(@sender, search: 'bob', is_admin: true)
343377
end
344378

345379
it "omits sender if is_admin specified with context" do
346-
expect_request(not_match(%r{sender=}))
380+
expect_request(not_match(with_param_present(:sender)))
347381
Services::AddressBook.search_users(@sender, search: 'bob', context: @course, is_admin: true)
348382
end
349383

350384
it "passes exclude_ids to recipients call" do
351-
expect_request(%r{exclude_ids=})
385+
expect_request(with_param_present(:exclude_ids))
352386
Services::AddressBook.search_users(@sender, search: 'bob', exclude_ids: [1, 2, 3])
353387
end
354388

355389
it "passes weak_checks flag along to recipients" do
356-
expect_request(%r{weak_checks=})
390+
expect_request(with_param_present(:weak_checks))
357391
Services::AddressBook.search_users(@sender, search: 'bob', weak_checks: true)
358392
end
359393

@@ -375,12 +409,12 @@ def not_match(*args)
375409
end
376410

377411
it "passes cursor parameter to the service" do
378-
expect_request(%r{cursor=12})
412+
expect_request(with_param(:cursor, 12))
379413
Services::AddressBook.search_users(@sender, {search: 'bob'}, {cursor: 12})
380414
end
381415

382416
it "passes per_page parameter to the service" do
383-
expect_request(%r{per_page=20})
417+
expect_request(with_param(:per_page, 20))
384418
Services::AddressBook.search_users(@sender, {search: 'bob'}, {per_page: 20})
385419
end
386420
end

0 commit comments

Comments
 (0)