Skip to content

Fix missing authorization check before REST project remix creation#864

Merged
abcampo-iry merged 1 commit into
mainfrom
issues/1468
Jun 11, 2026
Merged

Fix missing authorization check before REST project remix creation#864
abcampo-iry merged 1 commit into
mainfrom
issues/1468

Conversation

@abcampo-iry

@abcampo-iry abcampo-iry commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Status

Why

The REST project remix endpoint authenticated the caller but did not verify that the caller could view the original project before cloning it. That meant a user who knew a project
identifier could create a remix from a project they were not authorized to read.

This now aligns the REST remix flow with the existing Scratch and GraphQL remix authorization behavior.

What Changed

  • Added an explicit authorize! :show, project check before Project::CreateRemix.call.
  • Added regression coverage for:
    • remixing another user’s private project
    • a student remixing a teacher-only lesson project they cannot view
  • The regression specs assert the request is forbidden, no remix is created, and the clone operation is not called.

Follow-up: consider enabling CanCanCan check_authorization at the API controller layer so missing authorization becomes fail-closed by default. This should be handled separately because it requires auditing existing controllers and adding explicit skip_authorization_check for intentionally public or non-CanCan endpoints.

@cla-bot cla-bot Bot added the cla-signed label Jun 10, 2026
@abcampo-iry abcampo-iry changed the title update remixes controller Fix missing authorization check before REST project remix creation Jun 10, 2026
@github-actions

Copy link
Copy Markdown

Test coverage

91.46% line coverage reported by SimpleCov.
Run: https://github.com/RaspberryPiFoundation/editor-api/actions/runs/27263546453

@abcampo-iry abcampo-iry marked this pull request as ready for review June 10, 2026 08:32
Copilot AI review requested due to automatic review settings June 10, 2026 08:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an explicit CanCanCan authorization gate to the REST project remix creation endpoint to ensure callers can view the original project before a remix is created, closing an authorization bypass described in digital-editor-issues#1468 and aligning REST behavior with existing GraphQL/Scratch remix flows.

Changes:

  • Enforced authorize! :show, project before invoking Project::CreateRemix in the REST remixes controller.
  • Added request specs covering unauthorized remix attempts (other user’s project; student vs teacher-only lesson project), asserting 403 Forbidden, no remix created, and no clone operation invoked.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
app/controllers/api/projects/remixes_controller.rb Adds an explicit :show authorization check before remix creation to prevent unauthorized cloning.
spec/requests/projects/remix_spec.rb Adds regression coverage for forbidden remix creation scenarios and asserts Project::CreateRemix is not called.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +171 to +212
context 'when the original project belongs to another user' do
let!(:original_project) { create(:project, user_id: create(:user).id) }

it 'returns forbidden without creating a remix' do
allow(Project::CreateRemix).to receive(:call).and_call_original

expect do
post("/api/projects/#{original_project.identifier}/remix", params: { project: project_params }, headers:)
end.not_to change(Project, :count)

expect(response).to have_http_status(:forbidden)
expect(Project::CreateRemix).not_to have_received(:call)
end
end

context 'when a student cannot view the teacher-only original project' do
let(:student) { create(:student, school:) }
let(:teacher) { create(:teacher, school:) }
let(:school_class) { create(:school_class, school:, teacher_ids: [teacher.id]) }
let(:lesson) { create(:lesson, school:, school_class:, user_id: teacher.id, visibility: 'teachers') }
let!(:original_project) do
lesson.project.tap do |project|
project.update!(school:, user_id: teacher.id, instructions: 'Teacher-only instructions')
end
end

before do
create(:class_student, school_class:, student_id: student.id)
authenticated_in_hydra_as(student)
end

it 'returns forbidden without creating a remix' do
allow(Project::CreateRemix).to receive(:call).and_call_original

expect do
post("/api/projects/#{original_project.identifier}/remix", params: { project: project_params }, headers:)
end.not_to change(Project, :count)

expect(response).to have_http_status(:forbidden)
expect(Project::CreateRemix).not_to have_received(:call)
end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about the testing around this (not just for the PR but for request/feature specs).

A lot of the detail in this test is actually testing the behaviour of the ability rather than the controller and it requires a lot of setup to do that. I agree that there should be at least one test at this level that is checking a 'forbidden' state but maybe any more cases should be pushed down to the ability.

What I think would be more valuable, is configuring our controllers so we we can't forget to check an ability for an action.

@abcampo-iry abcampo-iry Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw in the ticket.
Agreed.
I can remove the test from there and then trying to go down the road that you described here:

As part of this we might want to consider adding check_authorization to our application controller. This is a cancancan feature that prevents us from 'forgetting' to add in authorization to a controller and would require us to explicitly decide if a controller shouldn't authorize resources (you can do a similar thing with authorize user, although that's not the problem here.)
https://github.com/CanCanCommunity/cancancan/blob/develop/docs/changing_defaults.md#check_authorization

I can create a ticket and add a different PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can create a ticket and add a different PR

Thanks. Might be one for next sprint.

@zetter-rpf zetter-rpf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one on fixing this, I've added some thoughts I've had about how we setup and test abilities (up to you if you think it's worth changing as part of this).

@abcampo-iry abcampo-iry merged commit ec60963 into main Jun 11, 2026
6 checks passed
@abcampo-iry abcampo-iry deleted the issues/1468 branch June 11, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants