Skip to content

Commit 7fdca65

Browse files
authored
Ensure safeguarding flags before Profile student calls (#866)
## Status - Closes RaspberryPiFoundation/digital-editor-issues#1475 ## What's changed? This change ensures safeguarding flags are created before editor-api calls Profile’s protected school-student endpoints. Instead of relying on individual controllers to create the flags, flag creation now happens at the `SchoolStudent` / Profile API boundary. This covers controller and non-controller callers, including class member listing, project student-name lookups, SSO student imports, batch student creation jobs, and student removal. The duplicated safeguarding flag logic has been removed from `SchoolStudentsController` and `SchoolMembersController`, and a new `SafeguardingFlagService` centralizes owner/teacher flag creation. Member listing now also propagates `SchoolStudent::List` failures instead of treating failed student lookups as empty lists. This prevents school/class member endpoints from returning successful but incomplete responses when safeguarding flag creation or Profile API calls fail. ## Steps to perform after deploying to production _If the production environment requires any extra work after this PR has been deployed detail it here. This could be running a Rake task, a migration, or upgrading a Gem. That kind of thing._
1 parent d7b36cf commit 7fdca65

33 files changed

Lines changed: 351 additions & 75 deletions

app/controllers/api/school_members_controller.rb

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ class SchoolMembersController < ApiController
66
load_and_authorize_resource :school
77
authorize_resource :school_member, class: false
88

9-
before_action :create_safeguarding_flags
10-
119
def index
1210
result = SchoolMember::List.call(school: @school, token: current_user.token)
1311

@@ -18,34 +16,5 @@ def index
1816
render json: { error: result[:error] }, status: :unprocessable_content
1917
end
2018
end
21-
22-
private
23-
24-
def create_safeguarding_flags
25-
create_teacher_safeguarding_flag
26-
create_owner_safeguarding_flag
27-
end
28-
29-
def create_teacher_safeguarding_flag
30-
return unless current_user.school_teacher?(@school)
31-
32-
ProfileApiClient.create_safeguarding_flag(
33-
token: current_user.token,
34-
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher],
35-
email: current_user.email,
36-
school_id: @school.id
37-
)
38-
end
39-
40-
def create_owner_safeguarding_flag
41-
return unless current_user.school_owner?(@school)
42-
43-
ProfileApiClient.create_safeguarding_flag(
44-
token: current_user.token,
45-
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner],
46-
email: current_user.email,
47-
school_id: @school.id
48-
)
49-
end
5019
end
5120
end

app/controllers/api/school_students_controller.rb

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ class SchoolStudentsController < ApiController
1010
load_and_authorize_resource :school
1111
authorize_resource :school_student, class: false
1212

13-
before_action :create_safeguarding_flags
14-
1513
def index
1614
result = SchoolStudent::List.call(school: @school, token: current_user.token)
1715

@@ -183,32 +181,5 @@ def school_students_params
183181
def student_ids_params
184182
params.fetch(:student_ids, [])
185183
end
186-
187-
def create_safeguarding_flags
188-
create_teacher_safeguarding_flag
189-
create_owner_safeguarding_flag
190-
end
191-
192-
def create_teacher_safeguarding_flag
193-
return unless current_user.school_teacher?(@school)
194-
195-
ProfileApiClient.create_safeguarding_flag(
196-
token: current_user.token,
197-
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:teacher],
198-
email: current_user.email,
199-
school_id: @school.id
200-
)
201-
end
202-
203-
def create_owner_safeguarding_flag
204-
return unless current_user.school_owner?(@school)
205-
206-
ProfileApiClient.create_safeguarding_flag(
207-
token: current_user.token,
208-
flag: ProfileApiClient::SAFEGUARDING_FLAGS[:owner],
209-
email: current_user.email,
210-
school_id: @school.id
211-
)
212-
end
213184
end
214185
end

app/controllers/concerns/identifiable.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ module Identifiable
1010

1111
def load_current_user
1212
token = request.headers['Authorization']
13-
@current_user = User.from_token(token:) if token
13+
return if token.blank?
14+
15+
@current_user = User.from_token(token:)
16+
return if @current_user.blank?
17+
return unless RequestStore.respond_to?(:active?) && RequestStore.active?
18+
19+
RequestStore.store[:safeguarding_flag_users_by_token] ||= {}
20+
RequestStore.store[:safeguarding_flag_users_by_token][token] = @current_user
1421
end
1522
end

app/jobs/create_students_job.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ def self.attempt_perform_later(school_id:, students:, token:)
3131
end
3232

3333
def perform(school_id:, students:, token:)
34+
school = School.find(school_id)
3435
decrypted_students = StudentHelpers.decrypt_students(students)
36+
SafeguardingFlagService.create_for_token(token:, school:)
3537
responses = ProfileApiClient.create_school_students(token:, students: decrypted_students, school_id:)
3638
return if responses[:created].blank?
3739

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
class SafeguardingFlagService
4+
class << self
5+
def create_for_school_roles(user:, school:)
6+
return if user.blank? || school.blank?
7+
8+
create_for_roles(token: user.token, email: user.email, school:, roles: user.school_roles(school))
9+
end
10+
11+
def create_for_token(token:, school:)
12+
return if token.blank? || school.blank?
13+
14+
create_for_school_roles(user: user_for_token(token), school:)
15+
end
16+
17+
def create_for_roles(token:, email:, school:, roles:)
18+
return if token.blank? || email.blank? || school.blank?
19+
20+
Array(roles).each do |role|
21+
flag = ProfileApiClient::SAFEGUARDING_FLAGS[role.to_sym]
22+
next if flag.blank?
23+
24+
ProfileApiClient.create_safeguarding_flag(
25+
token:,
26+
flag:,
27+
email:,
28+
school_id: school.id
29+
)
30+
end
31+
end
32+
33+
private
34+
35+
def user_for_token(token)
36+
return User.from_token(token:) unless request_store_active?
37+
38+
cache = RequestStore.store[:safeguarding_flag_users_by_token] ||= {}
39+
return cache[token] if cache.key?(token)
40+
41+
cache[token] = User.from_token(token:)
42+
end
43+
44+
def request_store_active?
45+
RequestStore.respond_to?(:active?) && RequestStore.active?
46+
end
47+
end
48+
end

app/services/student_removal_service.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def initialize(students:, school:, remove_from_profile: false, token: nil)
1212
def remove_students
1313
results = []
1414

15+
ensure_safeguarding_flag if remove_from_profile?
16+
1517
@students.each do |user_id|
1618
student_roles = Role.student.where(user_id:, school_id: @school.id)
1719
if student_roles.empty?
@@ -33,8 +35,8 @@ def remove_students
3335
# Remove roles
3436
student_roles.destroy_all
3537

36-
# Remove from profile if requested - inside transaction so it can be rolled back
37-
ProfileApiClient.delete_school_student(token: @token, school_id: @school.id, student_id: user_id) if @remove_from_profile && @token.present?
38+
# Keep local DB changes uncommitted until Profile confirms deletion.
39+
delete_from_profile(user_id) if remove_from_profile?
3840
end
3941
rescue StandardError => e
4042
result[:error] = "#{e.class}: #{e.message}"
@@ -43,4 +45,18 @@ def remove_students
4345
end
4446
results
4547
end
48+
49+
private
50+
51+
def delete_from_profile(user_id)
52+
ProfileApiClient.delete_school_student(token: @token, school_id: @school.id, student_id: user_id)
53+
end
54+
55+
def ensure_safeguarding_flag
56+
SafeguardingFlagService.create_for_token(token: @token, school: @school)
57+
end
58+
59+
def remove_from_profile?
60+
@remove_from_profile && @token.present?
61+
end
4662
end

lib/concepts/class_member/operations/list.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ def call(school_class:, class_students:, token:)
1010
begin
1111
school = school_class.school
1212
student_ids = class_students.pluck(:student_id)
13-
students = SchoolStudent::List.call(school:, token:, student_ids:).fetch(:school_students, [])
13+
students_response = SchoolStudent::List.call(school:, token:, student_ids:)
14+
if students_response.failure?
15+
response[:error] = students_response[:error]
16+
return response
17+
end
18+
19+
students = students_response.fetch(:school_students, [])
1420
class_students.each do |member|
1521
member.student = students.find { |student| student.id == member.student_id }
1622
end

lib/concepts/school_member/list.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ def call(school:, token:)
1515
response[:school_members] = []
1616

1717
begin
18-
students = fetch_students(school:, token:)
18+
students_response = fetch_students(school:, token:)
19+
if students_response.failure?
20+
response[:error] = students_response[:error]
21+
return response
22+
end
23+
24+
students = build_student_members(students_response.fetch(:school_students, []))
1925
teachers = fetch_teachers(school:)
2026
owners = fetch_owners(school:)
2127

@@ -36,11 +42,13 @@ def call(school:, token:)
3642
private
3743

3844
def fetch_students(school:, token:)
39-
student_roles = Role.student.where(school:)
45+
return OperationResponse[school_students: []] unless Role.student.exists?(school:)
4046

41-
students_response = student_roles.any? ? SchoolStudent::List.call(school:, token:).fetch(:school_students, []) : []
47+
SchoolStudent::List.call(school:, token:)
48+
end
4249

43-
students_response.map do |student|
50+
def build_student_members(students)
51+
students.map do |student|
4452
SchoolMember.new(student.id, student.name, student.username, student.email, :student, student.sso_providers)
4553
end
4654
end

lib/concepts/school_student/create.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def create_student(school, school_student_params, token)
2828
name:
2929
)
3030

31+
SafeguardingFlagService.create_for_token(token:, school:)
3132
response = ProfileApiClient.create_school_student(token:, username:, password:, name:, school_id:)
3233
user_id = response[:created].first
3334
Role.student.create!(school:, user_id:)

lib/concepts/school_student/create_batch_sso.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class << self
1515
def call(school:, school_students_params:, current_user:)
1616
response = OperationResponse.new
1717
response[:school_students] = []
18-
response[:school_students] = create_batch_sso(school, school_students_params, current_user.token)
18+
response[:school_students] = create_batch_sso(school, school_students_params, current_user)
1919
response
2020
rescue ValidationError => e
2121
response[:error] = "Error creating one or more students - see 'errors' key for details"
@@ -31,8 +31,10 @@ def call(school:, school_students_params:, current_user:)
3131

3232
private
3333

34-
def create_batch_sso(school, students, token)
34+
def create_batch_sso(school, students, current_user)
3535
students = normalize_student_params(students)
36+
token = current_user.token
37+
SafeguardingFlagService.create_for_school_roles(user: current_user, school:)
3638
responses = ProfileApiClient.create_school_students_sso(token:, students:, school_id: school.id)
3739

3840
create_student_roles(school, responses)

0 commit comments

Comments
 (0)