Skip to content

Commit 6c6f9e9

Browse files
authored
use school id for safeguarding flags (#666)
## Status - Related to [profile#2006](RaspberryPiFoundation/profile#2006 ) and [profile#2007](RaspberryPiFoundation/profile#2007) and [#270](RaspberryPiFoundation/digital-platform-capabilities#270) ## What's changed When creating or reading safeguarding flags from profile api, include the school id. This should go live so that all new safeguarding flags get the school id, and that profile can backfill the information. After that we can improve the profile API to verify user actions target the correct school. Also separated out the 401 unauthorized handling partially to stop them getting sent to sentry. Left a TODO for actual further API changes, so this change does not change what the api returns for now.
1 parent c689980 commit 6c6f9e9

15 files changed

Lines changed: 124 additions & 40 deletions

app/controllers/api/school_members_controller.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def create_teacher_safeguarding_flag
3232
ProfileApiClient.create_safeguarding_flag(
3333
token: current_user.token,
3434
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher],
35-
email: current_user.email
35+
email: current_user.email,
36+
school_id: @school.id
3637
)
3738
end
3839

@@ -42,7 +43,8 @@ def create_owner_safeguarding_flag
4243
ProfileApiClient.create_safeguarding_flag(
4344
token: current_user.token,
4445
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner],
45-
email: current_user.email
46+
email: current_user.email,
47+
school_id: @school.id
4648
)
4749
end
4850
end

app/controllers/api/school_students_controller.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ def create_teacher_safeguarding_flag
195195
ProfileApiClient.create_safeguarding_flag(
196196
token: current_user.token,
197197
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher],
198-
email: current_user.email
198+
email: current_user.email,
199+
school_id: @school.id
199200
)
200201
end
201202

@@ -205,7 +206,8 @@ def create_owner_safeguarding_flag
205206
ProfileApiClient.create_safeguarding_flag(
206207
token: current_user.token,
207208
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner],
208-
email: current_user.email
209+
email: current_user.email,
210+
school_id: @school.id
209211
)
210212
end
211213
end

app/services/school_onboarding_service.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ def onboard(token:)
1414

1515
ProfileApiClient.create_school(token:, id: school.id, code: school.code)
1616
end
17+
rescue ProfileApiClient::UnauthorizedError
18+
# Do not log noise to sentry.
19+
# TODO: consider returning a separate error here to distinguish from other errors and return 401 from the API, not 422
20+
Rails.logger.warn { "Failed to onboard school #{@school.id}: user is unauthorized" }
21+
false
1722
rescue StandardError => e
1823
Sentry.capture_exception(e)
1924
Rails.logger.error { "Failed to onboard school #{@school.id}: #{e.message}" }

lib/profile_api_client.rb

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class ProfileApiClient
88

99
# rubocop:disable Naming/MethodName
1010
School = Data.define(:id, :schoolCode, :updatedAt, :createdAt, :discardedAt)
11-
SafeguardingFlag = Data.define(:id, :userId, :flag, :email, :createdAt, :updatedAt, :discardedAt)
11+
SafeguardingFlag = Data.define(:id, :userId, :schoolId, :flag, :email, :createdAt, :updatedAt, :discardedAt)
1212
Student = Data.define(:id, :schoolId, :name, :username, :createdAt, :updatedAt, :discardedAt, :email, :ssoProviders)
1313
# rubocop:enable Naming/MethodName
1414

@@ -27,6 +27,8 @@ def initialize(errors)
2727
end
2828
end
2929

30+
class UnauthorizedError < Error; end
31+
3032
class UnexpectedResponse < Error
3133
attr_reader :response_status, :response_headers, :response_body
3234

@@ -50,6 +52,7 @@ def create_school(token:, id:, code:)
5052
}
5153
end
5254

55+
unauthorized!(response)
5356
raise UnexpectedResponse, response unless response.status == 201
5457

5558
School.new(**response.body)
@@ -58,6 +61,7 @@ def create_school(token:, id:, code:)
5861
def school_student(token:, school_id:, student_id:)
5962
response = connection(token).get("/api/v1/schools/#{school_id}/students/#{student_id}")
6063

64+
unauthorized!(response)
6165
raise UnexpectedResponse, response unless response.status == 200
6266

6367
build_student(response.body)
@@ -70,6 +74,7 @@ def list_school_students(token:, school_id:, student_ids:)
7074
request.body = student_ids
7175
end
7276

77+
unauthorized!(response)
7378
raise UnexpectedResponse, response unless response.status == 200
7479

7580
response.body.map { |attrs| build_student(attrs) }
@@ -86,6 +91,7 @@ def create_school_student(token:, username:, password:, name:, school_id:)
8691
}]
8792
end
8893

94+
unauthorized!(response)
8995
raise UnexpectedResponse, response unless response.status == 201
9096

9197
response.body.deep_symbolize_keys
@@ -103,6 +109,7 @@ def validate_school_students(token:, students:, school_id:)
103109
request.headers['Content-Type'] = 'application/json'
104110
end
105111

112+
unauthorized!(response)
106113
raise UnexpectedResponse, response unless response.status == 200
107114
rescue Faraday::UnprocessableEntityError => e
108115
raise Student422Error, JSON.parse(e.response_body)['errors']
@@ -119,6 +126,7 @@ def create_school_students(token:, students:, school_id:, preflight: false)
119126
request.headers['Content-Type'] = 'application/json'
120127
end
121128

129+
unauthorized!(response)
122130
raise UnexpectedResponse, response unless [200, 201].include?(response.status)
123131

124132
response.body.deep_symbolize_keys
@@ -136,6 +144,7 @@ def create_school_students_sso(token:, students:, school_id:)
136144
request.headers['Content-Type'] = 'application/json'
137145
end
138146

147+
unauthorized!(response)
139148
raise UnexpectedResponse, response unless [200, 201].include?(response.status)
140149

141150
response.body.map(&:deep_symbolize_keys)
@@ -154,6 +163,7 @@ def update_school_student(token:, school_id:, student_id:, name: nil, username:
154163
}.compact
155164
end
156165

166+
unauthorized!(response)
157167
raise UnexpectedResponse, response unless response.status == 200
158168

159169
build_student(response.body)
@@ -166,28 +176,32 @@ def delete_school_student(token:, school_id:, student_id:)
166176

167177
response = connection(token).delete("/api/v1/schools/#{school_id}/students/#{student_id}")
168178

179+
unauthorized!(response)
169180
raise UnexpectedResponse, response unless response.status == 204
170181
end
171182

172183
def safeguarding_flags(token:)
173184
response = connection(token).get('/api/v1/safeguarding-flags')
174185

186+
unauthorized!(response)
175187
raise UnexpectedResponse, response unless response.status == 200
176188

177189
response.body.map { |flag| SafeguardingFlag.new(**flag.symbolize_keys) }
178190
end
179191

180-
def create_safeguarding_flag(token:, flag:, email:)
192+
def create_safeguarding_flag(token:, flag:, email:, school_id:)
181193
response = connection(token).post('/api/v1/safeguarding-flags') do |request|
182-
request.body = { flag:, email: }
194+
request.body = { flag:, email:, schoolId: school_id }
183195
end
184196

197+
unauthorized!(response)
185198
raise UnexpectedResponse, response unless [201, 303].include?(response.status)
186199
end
187200

188201
def delete_safeguarding_flag(token:, flag:)
189202
response = connection(token).delete("/api/v1/safeguarding-flags/#{flag}")
190203

204+
unauthorized!(response)
191205
raise UnexpectedResponse, response unless response.status == 204
192206
end
193207

@@ -197,7 +211,7 @@ def connection(token)
197211
Faraday.new(ENV.fetch('IDENTITY_URL')) do |faraday|
198212
faraday.request :json
199213
faraday.response :json
200-
faraday.response :raise_error
214+
faraday.response :raise_error, allowed_statuses: [401]
201215
faraday.headers = {
202216
'Accept' => 'application/json',
203217
'Authorization' => "Bearer #{token}",
@@ -229,5 +243,10 @@ def build_student(attrs)
229243

230244
Student.new(**symbolized_attrs)
231245
end
246+
247+
def unauthorized!(response)
248+
# The API is only available to verified non-student users that are over 13. Others get a 401.
249+
raise UnauthorizedError, 'Profile API unauthorized' if response.status == 401
250+
end
232251
end
233252
end

spec/features/school/creating_a_school_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
RSpec.describe 'Creating a school', type: :request do
66
before do
7-
authenticated_in_hydra_as(owner)
7+
authenticated_in_hydra_as(user)
8+
stub_profile_api_create_school
89
end
910

1011
let(:headers) { { Authorization: UserProfileMock::TOKEN } }
11-
let(:school) { create(:school) }
12-
let(:owner) { create(:owner, school:) }
12+
let(:user) { create(:user) }
1313

1414
let(:params) do
1515
{

spec/features/school_member/listing_school_members_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@
9494

9595
it 'creates the school owner safeguarding flag' do
9696
get("/api/schools/#{school.id}/students", headers:)
97-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
97+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
9898
end
9999

100100
it 'does not create the school teacher safeguarding flag' do
101101
get("/api/schools/#{school.id}/students", headers:)
102-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email)
102+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email, school_id: school.id)
103103
end
104104

105105
it "responds with nil attributes for students if the user profile doesn't exist" do
@@ -136,14 +136,14 @@
136136
authenticated_in_hydra_as(teacher)
137137

138138
get("/api/schools/#{school.id}/students", headers:)
139-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
139+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
140140
end
141141

142142
it 'creates the school teacher safeguarding flag when the user is a school teacher' do
143143
teacher = create(:teacher, school:)
144144
authenticated_in_hydra_as(teacher)
145145

146146
get("/api/schools/#{school.id}/students", headers:)
147-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email)
147+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email, school_id: school.id)
148148
end
149149
end

spec/features/school_student/creating_a_batch_of_school_students_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@
5656

5757
it 'creates the school owner safeguarding flag' do
5858
post("/api/schools/#{school.id}/students/batch", headers:, params:)
59-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
59+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
6060
end
6161

6262
it 'does not create the school teacher safeguarding flag' do
6363
post("/api/schools/#{school.id}/students/batch", headers:, params:)
64-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email)
64+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email, school_id: school.id)
6565
end
6666

6767
it 'responds 202 Accepted' do
@@ -113,15 +113,15 @@
113113
authenticated_in_hydra_as(teacher)
114114

115115
post("/api/schools/#{school.id}/students/batch", headers:, params:)
116-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
116+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
117117
end
118118

119119
it 'creates the school teacher safeguarding flag when the user is a school-teacher' do
120120
teacher = create(:teacher, school:)
121121
authenticated_in_hydra_as(teacher)
122122

123123
post("/api/schools/#{school.id}/students/batch", headers:, params:)
124-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email)
124+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email, school_id: school.id)
125125
end
126126

127127
it 'responds 422 Unprocessable Entity when params are invalid' do

spec/features/school_student/creating_a_school_student_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525

2626
it 'creates the school owner safeguarding flag' do
2727
post("/api/schools/#{school.id}/students", headers:, params:)
28-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
28+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
2929
end
3030

3131
it 'does not create the school teacher safeguarding flag' do
3232
post("/api/schools/#{school.id}/students", headers:, params:)
33-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email)
33+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email, school_id: school.id)
3434
end
3535

3636
it 'responds 204 No Content' do
@@ -51,15 +51,15 @@
5151
authenticated_in_hydra_as(teacher)
5252

5353
post("/api/schools/#{school.id}/students", headers:, params:)
54-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
54+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
5555
end
5656

5757
it 'creates the school teacher safeguarding flag when the user is a school teacher' do
5858
teacher = create(:teacher, school:)
5959
authenticated_in_hydra_as(teacher)
6060

6161
post("/api/schools/#{school.id}/students", headers:, params:)
62-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email)
62+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email, school_id: school.id)
6363
end
6464

6565
it 'responds 400 Bad Request when params are missing' do

spec/features/school_student/deleting_a_school_student_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222

2323
it 'creates the school owner safeguarding flag' do
2424
delete("/api/schools/#{school.id}/students/#{student_id}", headers:)
25-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
25+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
2626
end
2727

2828
it 'does not create the school teacher safeguarding flag' do
2929
delete("/api/schools/#{school.id}/students/#{student_id}", headers:)
30-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email)
30+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email, school_id: school.id)
3131
end
3232

3333
it 'responds 200 OK' do
@@ -61,15 +61,15 @@
6161
authenticated_in_hydra_as(teacher)
6262

6363
delete("/api/schools/#{school.id}/students/#{student_id}", headers:)
64-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
64+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
6565
end
6666

6767
it 'does not create the school teacher safeguarding flag when logged in as a teacher' do
6868
teacher = create(:teacher, school:)
6969
authenticated_in_hydra_as(teacher)
7070

7171
delete("/api/schools/#{school.id}/students/#{student_id}", headers:)
72-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email)
72+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email, school_id: school.id)
7373
end
7474

7575
it 'responds 403 Forbidden when the user is a school-student' do

spec/features/school_student/listing_school_students_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222

2323
it 'creates the school owner safeguarding flag' do
2424
get("/api/schools/#{school.id}/students", headers:)
25-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
25+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
2626
end
2727

2828
it 'does not create the school teacher safeguarding flag' do
2929
get("/api/schools/#{school.id}/students", headers:)
30-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email)
30+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: owner.email, school_id: school.id)
3131
end
3232

3333
it 'responds 200 OK when the user is a school-teacher' do
@@ -43,15 +43,15 @@
4343
authenticated_in_hydra_as(teacher)
4444

4545
get("/api/schools/#{school.id}/students", headers:)
46-
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email)
46+
expect(ProfileApiClient).not_to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner], email: owner.email, school_id: school.id)
4747
end
4848

4949
it 'creates the school teacher safeguarding flag when the user is a school teacher' do
5050
teacher = create(:teacher, school:)
5151
authenticated_in_hydra_as(teacher)
5252

5353
get("/api/schools/#{school.id}/students", headers:)
54-
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email)
54+
expect(ProfileApiClient).to have_received(:create_safeguarding_flag).with(token: UserProfileMock::TOKEN, flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher], email: teacher.email, school_id: school.id)
5555
end
5656

5757
it 'responds with the school students JSON' do

0 commit comments

Comments
 (0)