Skip to content

Commit d0c68c0

Browse files
tuzzfloehopper
authored andcommitted
Add authorisation logic for creating projects in a school/lesson
1 parent b34f10b commit d0c68c0

File tree

5 files changed

+104
-5
lines changed

5 files changed

+104
-5
lines changed

app/controllers/api/projects_controller.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ class ProjectsController < ApiController
77
before_action :authorize_user, only: %i[create update index destroy]
88
before_action :load_project, only: %i[show update destroy]
99
before_action :load_projects, only: %i[index]
10-
after_action :pagination_link_header, only: %i[index]
1110
load_and_authorize_resource
11+
before_action :verify_lesson_belongs_to_school, only: :create
12+
after_action :pagination_link_header, only: %i[index]
1213

1314
def index
1415
@paginated_projects = @projects.page(params[:page])
@@ -47,6 +48,13 @@ def destroy
4748

4849
private
4950

51+
def verify_lesson_belongs_to_school
52+
return if base_params[:lesson_id].blank?
53+
return if school&.lessons&.pluck(:id)&.include?(base_params[:lesson_id])
54+
55+
raise ParameterError, 'lesson_id does not correspond to school_id'
56+
end
57+
5058
def load_project
5159
project_loader = ProjectLoader.new(params[:id], [params[:locale]])
5260
@project = project_loader.load
@@ -69,6 +77,7 @@ def project_params
6977
def base_params
7078
params.fetch(:project, {}).permit(
7179
:school_id,
80+
:lesson_id,
7281
:user_id,
7382
:identifier,
7483
:name,

app/models/ability.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,15 @@ def define_school_teacher_abilities(user:, organisation_id:)
6666
can(%i[read create create_batch update], :school_student)
6767
can(%i[create destroy], Lesson) { |lesson| school_teacher_can_manage_lesson?(user:, organisation_id:, lesson:) }
6868
can(%i[read create_copy], Lesson, school_id: organisation_id, visibility: %w[teachers students])
69-
can(%i[create], Project, school_id: organisation_id, user_id: user.id)
69+
can(%i[create], Project) { |project| school_teacher_can_manage_project?(user:, organisation_id:, project:) }
7070
end
7171

7272
# rubocop:disable Layout/LineLength
7373
def define_school_student_abilities(user:, organisation_id:)
7474
can(%i[read], School, id: organisation_id)
7575
can(%i[read], SchoolClass, school: { id: organisation_id }, members: { student_id: user.id })
7676
can(%i[read], Lesson, school_id: organisation_id, visibility: 'students', school_class: { members: { student_id: user.id } })
77+
can(%i[create], Project, school_id: organisation_id, user_id: user.id, lesson_id: nil)
7778
end
7879
# rubocop:enable Layout/LineLength
7980

@@ -83,4 +84,11 @@ def school_teacher_can_manage_lesson?(user:, organisation_id:, lesson:)
8384

8485
is_my_lesson && (is_my_class || !lesson.school_class)
8586
end
87+
88+
def school_teacher_can_manage_project?(user:, organisation_id:, project:)
89+
is_my_project = project.school_id == organisation_id && project.user_id == user.id
90+
is_my_lesson = project.lesson && project.lesson.user_id == user.id
91+
92+
is_my_project && (is_my_lesson || !project.lesson)
93+
end
8694
end

spec/features/lesson/creating_a_lesson_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
expect(response).to have_http_status(:unprocessable_entity)
146146
end
147147

148-
it 'responds 422 Unprocessable if school_id does not correspond to school_class_id' do
148+
it 'responds 422 Unprocessable if school_class_id does not correspond to school_id' do
149149
new_params = { lesson: params[:lesson].merge(school_id: SecureRandom.uuid) }
150150

151151
post('/api/lessons', headers:, params: new_params)

spec/features/project/creating_a_project_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@
7979
expect(response).to have_http_status(:created)
8080
end
8181

82+
it 'responds 201 Created when the user is a school-student for the school' do
83+
stub_hydra_public_api(user_index: user_index_by_role('school-student'))
84+
85+
post('/api/projects', headers:, params:)
86+
expect(response).to have_http_status(:created)
87+
end
88+
8289
it 'sets the lesson user to the specified user for school-owner users' do
8390
post('/api/projects', headers:, params:)
8491
data = JSON.parse(response.body, symbolize_names: true)
@@ -103,4 +110,79 @@
103110
expect(response).to have_http_status(:forbidden)
104111
end
105112
end
113+
114+
context 'when the project is associated with a lesson' do
115+
let(:school) { create(:school) }
116+
let(:lesson) { create(:lesson, school:) }
117+
let(:teacher_index) { user_index_by_role('school-teacher') }
118+
let(:teacher_id) { user_id_by_index(teacher_index) }
119+
120+
let(:params) do
121+
{
122+
project: {
123+
name: 'Test Project',
124+
components: [],
125+
school_id: school.id,
126+
lesson_id: lesson.id,
127+
user_id: teacher_id
128+
}
129+
}
130+
end
131+
132+
it 'responds 201 Created' do
133+
post('/api/projects', headers:, params:)
134+
expect(response).to have_http_status(:created)
135+
end
136+
137+
it 'responds 201 Created when the current user is the owner of the lesson' do
138+
stub_hydra_public_api(user_index: teacher_index)
139+
lesson.update!(user_id: user_id_by_index(teacher_index))
140+
141+
post('/api/projects', headers:, params:)
142+
expect(response).to have_http_status(:created)
143+
end
144+
145+
it 'responds 422 Unprocessable when when the user_id is not the owner of the lesson' do
146+
new_params = { project: params[:project].merge(user_id: SecureRandom.uuid) }
147+
148+
post('/api/projects', headers:, params: new_params)
149+
expect(response).to have_http_status(:unprocessable_entity)
150+
end
151+
152+
it 'responds 422 Unprocessable when lesson_id is provided but school_id is missing' do
153+
new_params = { project: params[:project].without(:school_id) }
154+
155+
post('/api/projects', headers:, params: new_params)
156+
expect(response).to have_http_status(:unprocessable_entity)
157+
end
158+
159+
it 'responds 422 Unprocessable when lesson_id does not correspond to school_id' do
160+
new_params = { project: params[:project].merge(lesson_id: SecureRandom.uuid) }
161+
162+
post('/api/projects', headers:, params: new_params)
163+
expect(response).to have_http_status(:unprocessable_entity)
164+
end
165+
166+
it 'responds 403 Forbidden when the user is a school-owner for a different school' do
167+
new_params = { project: params[:project].without(:lesson_id).merge(school_id: SecureRandom.uuid) }
168+
169+
post('/api/projects', headers:, params: new_params)
170+
expect(response).to have_http_status(:forbidden)
171+
end
172+
173+
it 'responds 403 Forbidden when the current user is not the owner of the lesson' do
174+
stub_hydra_public_api(user_index: teacher_index)
175+
lesson.update!(user_id: SecureRandom.uuid)
176+
177+
post('/api/projects', headers:, params:)
178+
expect(response).to have_http_status(:forbidden)
179+
end
180+
181+
it 'responds 403 Forbidden when the user is a school-student' do
182+
stub_hydra_public_api(user_index: user_index_by_role('school-student'))
183+
184+
post('/api/projects', headers:, params:)
185+
expect(response).to have_http_status(:forbidden)
186+
end
187+
end
106188
end

spec/models/lesson_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
lesson.update!(school: create(:school))
7171
end
7272

73-
it 'requires a user that has the school-owner or school-teacher role for the school' do
73+
it 'requires that the user that has the school-owner or school-teacher role for the school' do
7474
lesson.user_id = '22222222-2222-2222-2222-222222222222' # school-student
7575
expect(lesson).to be_invalid
7676
end
@@ -81,7 +81,7 @@
8181
lesson.update!(school_class: create(:school_class))
8282
end
8383

84-
it 'requires a user that is the school-teacher for the school_class' do
84+
it 'requires that the user that is the school-teacher for the school_class' do
8585
lesson.user_id = '00000000-0000-0000-0000-000000000000' # school-owner
8686
expect(lesson).to be_invalid
8787
end

0 commit comments

Comments
 (0)