Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/graphql/types/user_deletion_restriction_enum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Types
class UserDeletionRestrictionEnum < Types::BaseEnum
description 'The reason why a user cannot be deleted.'

value 'LAST_ADMINISTRATOR',
'The user is the last administrator of the instance.',
value: :last_administrator
value 'ACTIVE_SUBSCRIPTION',
'The current user has an active subscription.',
value: :active_subscription
end
end
7 changes: 7 additions & 0 deletions app/graphql/types/user_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class UserType < Types::BaseObject
null: true,
description: 'Global admin status of the user',
authorize: :read_admin_status
field :deletion_restriction, Types::UserDeletionRestrictionEnum,
null: true,
description: 'The reason why this user cannot be deleted'
field :email, String, null: true, description: 'Email of the user', authorize: :read_email
field :email_verified_at, Types::TimeType,
null: true,
Expand Down Expand Up @@ -72,5 +75,9 @@ def mfa_status
backup_codes_count: object.backup_codes.size,
}
end

def deletion_restriction
Users::DeleteService.new(current_authentication, object).deletion_restriction
end
end
end
1 change: 1 addition & 0 deletions app/services/error_code.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def self.error_codes
missing_permission: { description: 'The user is not permitted to perform this operation' },
missing_parameter: { description: 'Not all required parameters are present' },
cannot_remove_last_administrator: { description: 'This action would remove the last administrator' },
cannot_delete_last_administrator: { description: 'The last instance administrator cannot be deleted' },
cannot_remove_last_admin_ability: { description: 'This action would remove the last administrative ability' },
cannot_delete_last_admin_role: { description: 'This action would remove the last administrative role' },
inconsistent_namespace: { description: 'Resources are from different namespaces' },
Expand Down
20 changes: 20 additions & 0 deletions app/services/users/delete_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def execute
return ServiceResponse.error(message: 'Missing permission', error_code: :missing_permission)
end

deletion_error = validate_deletion
return deletion_error if deletion_error

transactional do |t|
user.destroy

Expand All @@ -38,5 +41,22 @@ def execute
ServiceResponse.success(message: 'Deleted user', payload: user)
end
end

def deletion_restriction
:last_administrator if user.admin? && !User.where.not(id: user.id).exists?(admin: true)
end

protected

def validate_deletion
return unless deletion_restriction == :last_administrator

ServiceResponse.error(
message: 'The last instance administrator cannot be deleted',
error_code: :cannot_delete_last_administrator
)
end
end
end

Users::DeleteService.prepend_extensions
2 changes: 2 additions & 0 deletions docs/graphql/enum/errorcodeenum.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Represents the available error responses

| Value | Description |
|-------|-------------|
| `CANNOT_DELETE_LAST_ADMINISTRATOR` | The last instance administrator cannot be deleted |
| `CANNOT_DELETE_LAST_ADMIN_ROLE` | This action would remove the last administrative role |
| `CANNOT_DELETE_USER_WITH_ACTIVE_SUBSCRIPTION` | A user with an active subscription cannot delete itself |
| `CANNOT_MODIFY_ADMIN` | Only administrators can modify admin status of users |
| `CANNOT_MODIFY_OWN_ADMIN` | Users cannot modify their own admin status |
| `CANNOT_REMOVE_LAST_ADMINISTRATOR` | This action would remove the last administrator |
Expand Down
10 changes: 10 additions & 0 deletions docs/graphql/enum/userdeletionrestriction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: UserDeletionRestriction
---

The reason why a user cannot be deleted.

| Value | Description |
|-------|-------------|
| `ACTIVE_SUBSCRIPTION` | The current user has an active subscription. |
| `LAST_ADMINISTRATOR` | The user is the last administrator of the instance. |
1 change: 1 addition & 0 deletions docs/graphql/object/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Represents a user
| `admin` | [`Boolean`](../scalar/boolean.md) | Global admin status of the user |
| `avatarPath` | [`String`](../scalar/string.md) | The avatar if present of the user |
| `createdAt` | [`Time!`](../scalar/time.md) | Time when this User was created |
| `deletionRestriction` | [`UserDeletionRestriction`](../enum/userdeletionrestriction.md) | The reason why this user cannot be deleted |
| `email` | [`String`](../scalar/string.md) | Email of the user |
| `emailVerifiedAt` | [`Time`](../scalar/time.md) | Email verification date of the user if present |
| `firstname` | [`String`](../scalar/string.md) | Firstname of the user |
Expand Down
22 changes: 22 additions & 0 deletions extensions/cloud/app/services/cloud/error_code.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module CLOUD
module ErrorCode
extend ActiveSupport::Concern

class_methods do
include Sagittarius::Override

override :error_codes
def error_codes
super.merge(
{
cannot_delete_user_with_active_subscription: {
description: 'A user with an active subscription cannot delete itself',
},
}
)
end
end
end
end
33 changes: 33 additions & 0 deletions extensions/cloud/app/services/cloud/users/delete_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module CLOUD
module Users
module DeleteService
include Sagittarius::Override

override :deletion_restriction
def deletion_restriction
restriction = super
return restriction if restriction
return unless user == current_authentication.user

current_license = user.namespace&.current_license
license_data = current_license&.license
:active_subscription if license_data&.options&.[](:subscription)
end

protected

override :validate_deletion
def validate_deletion
restriction = deletion_restriction
return super unless restriction == :active_subscription

ServiceResponse.error(
message: 'A user with an active subscription cannot delete itself',
error_code: :cannot_delete_user_with_active_subscription
)
end
end
end
end
42 changes: 42 additions & 0 deletions extensions/cloud/spec/services/cloud/users/delete_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Users::DeleteService do
subject(:service_response) do
described_class.new(create_authentication(current_user), user).execute
end

let(:current_user) { create(:user) }
let(:user) { current_user }
let(:namespace) { current_user.ensure_namespace }

before do
current_user.update!(admin: true)
create(:user, :admin)
end

context 'when the current user has an active subscription' do
before { create(:license, namespace: namespace, options: { subscription: true }) }

it 'does not delete the user' do
expect(described_class.new(create_authentication(current_user), user).deletion_restriction)
.to eq(:active_subscription)
expect(service_response).not_to be_success
expect(service_response.payload[:error_code]).to eq(:cannot_delete_user_with_active_subscription)
expect(User.exists?(user.id)).to be true
is_expected.not_to create_audit_event(:user_deleted)
end
end

context 'when another user has an active subscription' do
let(:user) { create(:user) }

before { create(:license, namespace: user.ensure_namespace, options: { subscription: true }) }

it 'allows an administrator to delete the user' do
expect { service_response }.to change { User.exists?(user.id) }.from(true).to(false)
expect(service_response).to be_success
end
end
end
1 change: 1 addition & 0 deletions spec/graphql/types/user_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
identities
mfaStatus
userAbilities
deletionRestriction
createdAt
updatedAt
]
Expand Down
7 changes: 7 additions & 0 deletions spec/requests/graphql/query/users_query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
nodes {
id
username
deletionRestriction
}
}
}
Expand Down Expand Up @@ -48,5 +49,11 @@
it 'returns all users' do
expect(graphql_data_at(:users, :nodes)).to have_attributes(length: 4)
end

it 'returns the deletion restriction for the last administrator' do
admin = graphql_data_at(:users, :nodes).find { |user| user['id'] == current_user.to_global_id.to_s }

expect(admin['deletionRestriction']).to eq('LAST_ADMINISTRATOR')
end
end
end
20 changes: 20 additions & 0 deletions spec/services/users/delete_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@
)
end

context 'when the user is the last administrator' do
let(:user) { current_user }

it 'does not delete the user' do
expect(service_response).not_to be_success
expect(service_response.payload[:error_code]).to eq(:cannot_delete_last_administrator)
expect(User.exists?(user.id)).to be true
is_expected.not_to create_audit_event(:user_deleted)
end
end

context 'when the user is an administrator and another administrator exists' do
let(:user) { create(:user, :admin) }

it 'deletes the user successfully' do
expect { service_response }.to change { User.exists?(user.id) }.from(true).to(false)
expect(service_response).to be_success
end
end

context 'when current user lacks permission' do
let(:current_user) { create(:user) }

Expand Down