Skip to content

Commit a41c970

Browse files
committed
feat: added ghost user
1 parent 4b00919 commit a41c970

7 files changed

Lines changed: 109 additions & 7 deletions

File tree

app/models/concerns/namespace_parent.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module NamespaceParent
44
extend ActiveSupport::Concern
55

66
included do
7-
has_one :namespace, as: :parent
7+
has_one :namespace, as: :parent, dependent: :destroy
88
end
99

1010
def ensure_namespace

app/models/user.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
class User < ApplicationRecord
44
include NamespaceParent
55

6+
GHOST_USERNAME = 'ghost'
7+
GHOST_EMAIL = 'ghost@code0.tech'
8+
69
has_secure_password
710

811
validates :username, length: { maximum: 50 },
@@ -31,6 +34,17 @@ class User < ApplicationRecord
3134

3235
has_one_attached :avatar
3336

37+
before_destroy :prevent_destroy_ghost_user, prepend: true
38+
before_destroy :reassign_authored_audit_events_to_ghost_user
39+
40+
def self.ghost
41+
find_by!(username: GHOST_USERNAME)
42+
end
43+
44+
def ghost?
45+
username == GHOST_USERNAME
46+
end
47+
3448
def mfa_enabled?
3549
totp_secret != nil
3650
end
@@ -68,6 +82,23 @@ def validate_mfa!(mfa)
6882
generates_token_for :password_reset, expires_in: 15.minutes do
6983
password_digest&.last(20)
7084
end
85+
86+
private
87+
88+
def prevent_destroy_ghost_user
89+
return unless ghost?
90+
91+
errors.add(:base, :invalid, message: 'Cannot delete ghost user')
92+
throw :abort
93+
end
94+
95+
def reassign_authored_audit_events_to_ghost_user
96+
ghost_user = self.class.ghost
97+
98+
authored_audit_events.find_each do |audit_event|
99+
audit_event.update!(author: ghost_user)
100+
end
101+
end
71102
end
72103

73104
User.prepend_extensions

app/services/users/delete_service.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def execute
1818

1919
transactional do |t|
2020
namespace = user.namespace
21+
audit_author_id = user == current_authentication.user ? User.ghost.id : current_authentication.user.id
2122

2223
user.destroy
2324

@@ -41,7 +42,7 @@ def execute
4142

4243
AuditService.audit(
4344
:user_deleted,
44-
author_id: current_authentication.user.id,
45+
author_id: audit_author_id,
4546
entity: user,
4647
target: AuditEvent::GLOBAL_TARGET,
4748
details: {}

db/fixtures/01_application_settings.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# frozen_string_literal: true
22

3+
User.seed_once :username do |u|
4+
u.username = User::GHOST_USERNAME
5+
u.email = User::GHOST_EMAIL
6+
u.password = SecureRandom.hex
7+
u.admin = false
8+
end
9+
310
ApplicationSetting.seed_once :setting do |s|
411
s.setting = :user_registration_enabled
512
s.value = false

db/fixtures/production/01_users.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
return unless User.none?
3+
return unless User.where.not(username: User::GHOST_USERNAME).none?
44

55
initial_root_email = ENV.fetch('INITIAL_ROOT_MAIL', nil)
66
initial_root_password = ENV.fetch('INITIAL_ROOT_PASSWORD', SecureRandom.hex)

spec/requests/graphql/query/users_query_spec.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
end
2020

2121
before do
22-
create(:user)
23-
create(:user)
24-
create(:user)
22+
create_list(:user, 3)
2523

2624
post_graphql query, current_user: current_user
2725
end
@@ -46,7 +44,10 @@
4644
let(:current_user) { create(:user, :admin) }
4745

4846
it 'returns all users' do
49-
expect(graphql_data_at(:users, :nodes)).to have_attributes(length: 4)
47+
expected_ids = User.all.map { |user| user.to_gid.to_s }
48+
returned_ids = graphql_data_at(:users, :nodes).pluck('id')
49+
50+
expect(returned_ids).to match_array(expected_ids)
5051
end
5152
end
5253
end

spec/services/users/delete_service_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
let(:user) { create(:user) }
1111
let(:current_user) { create(:user, :admin) }
12+
let!(:ghost_user) do
13+
create(:user, username: User::GHOST_USERNAME, email: User::GHOST_EMAIL)
14+
end
1215

1316
it 'deletes the user successfully' do
1417
namespace_id = user.ensure_namespace.id
@@ -31,6 +34,65 @@
3134
)
3235
end
3336

37+
context 'when the user authored audit events' do
38+
let!(:audit_event) do
39+
create(:audit_event,
40+
author: user,
41+
action_type: :user_logged_in,
42+
entity: user,
43+
target: AuditEvent::GLOBAL_TARGET)
44+
end
45+
46+
it 'reassigns authored audit events to the ghost user' do
47+
expect { service_response }.to change { User.exists?(user.id) }.from(true).to(false)
48+
expect(service_response).to be_success
49+
expect(audit_event.reload.author).to eq(ghost_user)
50+
end
51+
end
52+
53+
context 'when the user owns a namespace' do
54+
let!(:namespace) { user.ensure_namespace }
55+
let!(:project) { create(:namespace_project, namespace: namespace) }
56+
let!(:flow) { create(:flow, project: project) }
57+
58+
it 'deletes the owned namespace and associated projects and flows' do
59+
expect { service_response }
60+
.to change { Namespace.exists?(namespace.id) }.from(true).to(false)
61+
.and change { NamespaceProject.exists?(project.id) }.from(true).to(false)
62+
.and change { Flow.exists?(flow.id) }.from(true).to(false)
63+
64+
expect(service_response).to be_success
65+
end
66+
end
67+
68+
context 'when deleting the current user' do
69+
let(:user) { current_user }
70+
71+
it 'creates the deletion audit event with the ghost user as author' do
72+
expect(service_response).to be_success
73+
74+
is_expected.to create_audit_event(
75+
:user_deleted,
76+
author_id: ghost_user.id,
77+
entity_type: 'User',
78+
entity_id: user.id,
79+
details: {},
80+
target_type: 'global',
81+
target_id: 0
82+
)
83+
end
84+
end
85+
86+
context 'when deleting the ghost user' do
87+
let(:user) { ghost_user }
88+
89+
it 'returns an invalid user error' do
90+
expect(service_response).not_to be_success
91+
expect(service_response.payload[:error_code]).to eq(:invalid_user)
92+
expect(User.exists?(ghost_user.id)).to be true
93+
end
94+
end
95+
3496
context 'when current user lacks permission' do
3597
let(:current_user) { create(:user) }
3698

0 commit comments

Comments
 (0)