Skip to content

feat: Implement API to GDPR delete users#3224

Open
Janis4411 wants to merge 1 commit intomainfrom
jv/SODEV-2997-Implement-API-to-GDPR-delete-users
Open

feat: Implement API to GDPR delete users#3224
Janis4411 wants to merge 1 commit intomainfrom
jv/SODEV-2997-Implement-API-to-GDPR-delete-users

Conversation

@Janis4411
Copy link
Copy Markdown
Contributor

This PR introduces a possible way to GDPR delete users via a API.

If we delete a user on openHPI we also have to delete the user on codeocean. Previously this was handled via a rake task that has to be triggered manually.

Part of SODEV-2997

@Janis4411 Janis4411 requested a review from arkirchner April 16, 2026 13:25
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 85f5e83 to 4c3c10a Compare April 16, 2026 13:26
Comment thread app/controllers/api/internal/users/deletions_controller.rb Fixed
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
Comment thread app/jobs/user_cleanup_job.rb Outdated
@Janis4411 Janis4411 marked this pull request as draft April 16, 2026 13:34
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 89.47368% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.39%. Comparing base (7876111) to head (1d4b374).

Files with missing lines Patch % Lines
lib/tasks/gdpr_delete.rake 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3224      +/-   ##
==========================================
+ Coverage   70.33%   70.39%   +0.06%     
==========================================
  Files         214      216       +2     
  Lines        6839     6857      +18     
==========================================
+ Hits         4810     4827      +17     
- Misses       2029     2030       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch 3 times, most recently from 3238780 to 9988915 Compare April 20, 2026 07:57
Comment thread app/controllers/api/api_controller.rb Fixed
Comment thread app/controllers/api/api_controller.rb Fixed
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 9988915 to cb82a61 Compare April 20, 2026 08:23
@Janis4411 Janis4411 self-assigned this Apr 20, 2026
@Janis4411 Janis4411 marked this pull request as ready for review April 20, 2026 09:53
Comment thread app/jobs/user_cleanup_job.rb Outdated
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
Comment thread app/controllers/api/api_controller.rb Outdated
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
Comment thread app/jobs/user_cleanup_job.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from cb82a61 to b7bfbd7 Compare April 20, 2026 11:57
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch 2 times, most recently from 62fa41c to 014d4d7 Compare April 21, 2026 07:14
Comment thread app/models/external_user.rb Outdated
Comment on lines +11 to +12
def soft_delete!
update(name: 'Deleted User', email: nil)
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 think it is better to blow up if the update did not work. soft_delete does not need a !. The ! indicates a volatile version of a method. Since we do not implement two versions of this method, we should not use !.

Suggested change
def soft_delete!
update(name: 'Deleted User', email: nil)
def soft_delete
update!(name: 'Deleted User', email: nil)

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.

Maybe we can add a new column deleted_at and add a timestamp when the user was deleted. This is good for debugging.

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.

Is the updated_at not serving the pretty much same purpose? Once a ExternalUser is updated with (name: 'Deleted User', email: nil) it won't be updated again, right?

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.

It might be. The username Deleted User could be used by a user. No email could be a side effect from some bug. The deleted_at just makes it clearer and is a common practice for soft deletions.

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 added the deleted_at column and update it now in the soft_delete method.

Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
Comment thread spec/request/api/internal/users/deletions_spec.rb Outdated
Comment thread spec/request/api/internal/users/destroy_spec.rb
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 014d4d7 to 667a61b Compare April 21, 2026 09:27
Comment thread lib/tasks/gdpr_delete.rake Outdated
Comment thread app/controllers/api/internal/users/deletions_controller.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 667a61b to 37a665c Compare April 21, 2026 12:03
Comment thread config/routes.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch 2 times, most recently from 1b514dc to 71bbf3d Compare April 21, 2026 12:37
Comment thread app/controllers/api/internal/users_controller.rb Outdated
Comment thread app/controllers/api/internal/users_controller.rb Outdated
Comment thread config/routes.rb Outdated
Comment thread app/controllers/api/internal/users_controller.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 71bbf3d to 142fae9 Compare April 21, 2026 13:14
Comment thread app/controllers/api/internal/users_controller.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 142fae9 to 79b8847 Compare April 21, 2026 13:55
Comment thread app/controllers/api/internal/users_controller.rb
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch 3 times, most recently from 4f7469b to 4379562 Compare April 21, 2026 15:33
Comment thread app/controllers/api/api_controller.rb Outdated
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 4379562 to 29ef0d1 Compare April 21, 2026 15:53
Comment thread app/controllers/api/api_controller.rb Outdated
Comment thread app/controllers/api/api_controller.rb Outdated
Comment on lines +5 to +15
before_action :verify_token!

def verify_token!
token = request.headers['Authorization']&.remove('Bearer ')

unless ActiveSupport::SecurityUtils.secure_compare(
token.to_s,
ENV.fetch('OPENHPI_API_TOKEN', nil)
)
head :unauthorized
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.

We can use some more Rails helper methods here.

Suggested change
before_action :verify_token!
def verify_token!
token = request.headers['Authorization']&.remove('Bearer ')
unless ActiveSupport::SecurityUtils.secure_compare(
token.to_s,
ENV.fetch('OPENHPI_API_TOKEN', nil)
)
head :unauthorized
end
before_action :authenticate
def authenticate
authenticate_or_request_with_http_token do |token, _options|
ActiveSupport::SecurityUtils.secure_compare(
token,
ENV.fetch('OPENHPI_API_TOKEN', nil)
)
end
end

Copy link
Copy Markdown
Contributor Author

@Janis4411 Janis4411 Apr 22, 2026

Choose a reason for hiding this comment

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

I get this error:

undefined method 'authenticate_or_request_with_http_token' for #Api::Internal::UsersController:0x000000013e17c458

It should also be available for ActionController::API, I'll have another look.

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 had to include the ActionController::HttpAuthentication::Token::ControllerMethods as they are set in the ActionController::API by default.

@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from 29ef0d1 to a30b4f7 Compare April 23, 2026 07:15
This is a possible way to GDPR delete users via a API. The background
is that if we delete a user on openhpi we also want to delete the user
on codeocean.

Part of SODEV-2997
@Janis4411 Janis4411 force-pushed the jv/SODEV-2997-Implement-API-to-GDPR-delete-users branch from a30b4f7 to 1d4b374 Compare April 23, 2026 07:16
end
end

context 'without token' do
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.

Can you add one more test where OPENHPI_API_TOKEN is not present? I want to see if no secret configuration is set and the API is still unreachable.

Comment on lines +6 to +36
let(:user_id) { '123456' }
let(:token) { 'supersecrettoken' }
let(:consumer) { create(:consumer, id: 1, name: 'openHPI') }
let!(:user) { create(:external_user, external_id: user_id, consumer_id: consumer.id, name: 'Test User', email: 'testmail@gmail.com') }

let(:body) do
{user_id: user_id}.to_json
end

let(:headers) do
{
'Authorization' => "Bearer #{token}",
'Content-Type' => 'application/json',
}
end

describe 'DELETE /api/internal/users' do
context 'with valid token and signature' do
it 'anonymizes the user name to "Deleted User"' do
expect { delete "/api/internal/users/#{user_id}", headers: headers }
.to change { user.reload.name }.to('Deleted User').and change { user.reload.email }.to(nil)
end

it 'returns 200 OK' do
delete "/api/internal/users/#{user_id}",
headers: headers

expect(response).to have_http_status(:ok)
end
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.

Please keep the tests simple. Your test setup is quite complex with all these let statements. One-off record creation should be done in the test directly and not assigned as let. This makes test easier to understand.

Suggested change
let(:user_id) { '123456' }
let(:token) { 'supersecrettoken' }
let(:consumer) { create(:consumer, id: 1, name: 'openHPI') }
let!(:user) { create(:external_user, external_id: user_id, consumer_id: consumer.id, name: 'Test User', email: 'testmail@gmail.com') }
let(:body) do
{user_id: user_id}.to_json
end
let(:headers) do
{
'Authorization' => "Bearer #{token}",
'Content-Type' => 'application/json',
}
end
describe 'DELETE /api/internal/users' do
context 'with valid token and signature' do
it 'anonymizes the user name to "Deleted User"' do
expect { delete "/api/internal/users/#{user_id}", headers: headers }
.to change { user.reload.name }.to('Deleted User').and change { user.reload.email }.to(nil)
end
it 'returns 200 OK' do
delete "/api/internal/users/#{user_id}",
headers: headers
expect(response).to have_http_status(:ok)
end
end
end
let(:headers) { { 'Authorization' => "Bearer #{token}" } }
describe 'DELETE /api/internal/users' do
it 'soft deletes a user' do
user = create(:external_user, external_id: '123456')
freeze_time
expect { delete "/api/internal/users/123456", headers: headers }
.to change { user.reload.deleted_at }.to(Time.current)
end
it 'returns an error if the user is not found' do
delete "/api/internal/users/123456",
headers: headers
expect(response).to have_http_status(:not_found)
end
end
end

Comment on lines +19 to +20
context 'with valid token' do
it 'allows the request' do
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.

[nits] A context with only one example is not really worth it. I think a single it assertion here is clearer and more readable.

Suggested change
context 'with valid token' do
it 'allows the request' do
it 'allows the request with valid token' do


require 'rails_helper'

RSpec.describe Api::Internal::UsersController do
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.

We have a request test for this API. We don't need a controller test as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants