From a41c970e1bbdd6aec323e58a07c2b5bb7c2145f2 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 25 Jun 2026 13:10:04 +0200 Subject: [PATCH 1/8] feat: added ghost user --- app/models/concerns/namespace_parent.rb | 2 +- app/models/user.rb | 31 ++++++++++ app/services/users/delete_service.rb | 3 +- db/fixtures/01_application_settings.rb | 7 +++ db/fixtures/production/01_users.rb | 2 +- .../graphql/query/users_query_spec.rb | 9 +-- spec/services/users/delete_service_spec.rb | 62 +++++++++++++++++++ 7 files changed, 109 insertions(+), 7 deletions(-) diff --git a/app/models/concerns/namespace_parent.rb b/app/models/concerns/namespace_parent.rb index f5c021f6f..71bc864a7 100644 --- a/app/models/concerns/namespace_parent.rb +++ b/app/models/concerns/namespace_parent.rb @@ -4,7 +4,7 @@ module NamespaceParent extend ActiveSupport::Concern included do - has_one :namespace, as: :parent + has_one :namespace, as: :parent, dependent: :destroy end def ensure_namespace diff --git a/app/models/user.rb b/app/models/user.rb index 9ba788551..e69a9de1a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,9 @@ class User < ApplicationRecord include NamespaceParent + GHOST_USERNAME = 'ghost' + GHOST_EMAIL = 'ghost@code0.tech' + has_secure_password validates :username, length: { maximum: 50 }, @@ -31,6 +34,17 @@ class User < ApplicationRecord has_one_attached :avatar + before_destroy :prevent_destroy_ghost_user, prepend: true + before_destroy :reassign_authored_audit_events_to_ghost_user + + def self.ghost + find_by!(username: GHOST_USERNAME) + end + + def ghost? + username == GHOST_USERNAME + end + def mfa_enabled? totp_secret != nil end @@ -68,6 +82,23 @@ def validate_mfa!(mfa) generates_token_for :password_reset, expires_in: 15.minutes do password_digest&.last(20) end + + private + + def prevent_destroy_ghost_user + return unless ghost? + + errors.add(:base, :invalid, message: 'Cannot delete ghost user') + throw :abort + end + + def reassign_authored_audit_events_to_ghost_user + ghost_user = self.class.ghost + + authored_audit_events.find_each do |audit_event| + audit_event.update!(author: ghost_user) + end + end end User.prepend_extensions diff --git a/app/services/users/delete_service.rb b/app/services/users/delete_service.rb index a14035d02..224da4397 100644 --- a/app/services/users/delete_service.rb +++ b/app/services/users/delete_service.rb @@ -18,6 +18,7 @@ def execute transactional do |t| namespace = user.namespace + audit_author_id = user == current_authentication.user ? User.ghost.id : current_authentication.user.id user.destroy @@ -41,7 +42,7 @@ def execute AuditService.audit( :user_deleted, - author_id: current_authentication.user.id, + author_id: audit_author_id, entity: user, target: AuditEvent::GLOBAL_TARGET, details: {} diff --git a/db/fixtures/01_application_settings.rb b/db/fixtures/01_application_settings.rb index df16d5270..54c5f749e 100644 --- a/db/fixtures/01_application_settings.rb +++ b/db/fixtures/01_application_settings.rb @@ -1,5 +1,12 @@ # frozen_string_literal: true +User.seed_once :username do |u| + u.username = User::GHOST_USERNAME + u.email = User::GHOST_EMAIL + u.password = SecureRandom.hex + u.admin = false +end + ApplicationSetting.seed_once :setting do |s| s.setting = :user_registration_enabled s.value = false diff --git a/db/fixtures/production/01_users.rb b/db/fixtures/production/01_users.rb index cee5f4120..003d9098e 100644 --- a/db/fixtures/production/01_users.rb +++ b/db/fixtures/production/01_users.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -return unless User.none? +return unless User.where.not(username: User::GHOST_USERNAME).none? initial_root_email = ENV.fetch('INITIAL_ROOT_MAIL', nil) initial_root_password = ENV.fetch('INITIAL_ROOT_PASSWORD', SecureRandom.hex) diff --git a/spec/requests/graphql/query/users_query_spec.rb b/spec/requests/graphql/query/users_query_spec.rb index 0db57b59c..7ffae8678 100644 --- a/spec/requests/graphql/query/users_query_spec.rb +++ b/spec/requests/graphql/query/users_query_spec.rb @@ -19,9 +19,7 @@ end before do - create(:user) - create(:user) - create(:user) + create_list(:user, 3) post_graphql query, current_user: current_user end @@ -46,7 +44,10 @@ let(:current_user) { create(:user, :admin) } it 'returns all users' do - expect(graphql_data_at(:users, :nodes)).to have_attributes(length: 4) + expected_ids = User.all.map { |user| user.to_gid.to_s } + returned_ids = graphql_data_at(:users, :nodes).pluck('id') + + expect(returned_ids).to match_array(expected_ids) end end end diff --git a/spec/services/users/delete_service_spec.rb b/spec/services/users/delete_service_spec.rb index 649f9e9d5..0d7ed182f 100644 --- a/spec/services/users/delete_service_spec.rb +++ b/spec/services/users/delete_service_spec.rb @@ -9,6 +9,9 @@ let(:user) { create(:user) } let(:current_user) { create(:user, :admin) } + let!(:ghost_user) do + create(:user, username: User::GHOST_USERNAME, email: User::GHOST_EMAIL) + end it 'deletes the user successfully' do namespace_id = user.ensure_namespace.id @@ -31,6 +34,65 @@ ) end + context 'when the user authored audit events' do + let!(:audit_event) do + create(:audit_event, + author: user, + action_type: :user_logged_in, + entity: user, + target: AuditEvent::GLOBAL_TARGET) + end + + it 'reassigns authored audit events to the ghost user' do + expect { service_response }.to change { User.exists?(user.id) }.from(true).to(false) + expect(service_response).to be_success + expect(audit_event.reload.author).to eq(ghost_user) + end + end + + context 'when the user owns a namespace' do + let!(:namespace) { user.ensure_namespace } + let!(:project) { create(:namespace_project, namespace: namespace) } + let!(:flow) { create(:flow, project: project) } + + it 'deletes the owned namespace and associated projects and flows' do + expect { service_response } + .to change { Namespace.exists?(namespace.id) }.from(true).to(false) + .and change { NamespaceProject.exists?(project.id) }.from(true).to(false) + .and change { Flow.exists?(flow.id) }.from(true).to(false) + + expect(service_response).to be_success + end + end + + context 'when deleting the current user' do + let(:user) { current_user } + + it 'creates the deletion audit event with the ghost user as author' do + expect(service_response).to be_success + + is_expected.to create_audit_event( + :user_deleted, + author_id: ghost_user.id, + entity_type: 'User', + entity_id: user.id, + details: {}, + target_type: 'global', + target_id: 0 + ) + end + end + + context 'when deleting the ghost user' do + let(:user) { ghost_user } + + it 'returns an invalid user error' do + expect(service_response).not_to be_success + expect(service_response.payload[:error_code]).to eq(:invalid_user) + expect(User.exists?(ghost_user.id)).to be true + end + end + context 'when current user lacks permission' do let(:current_user) { create(:user) } From 8dad579e3d6625064ca252ca2696a08402dac62d Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 29 Jun 2026 09:21:20 +0200 Subject: [PATCH 2/8] feat: added requested changes from review --- app/models/user.rb | 34 ++++--------------- app/policies/user_policy.rb | 4 ++- app/services/users/delete_service.rb | 4 ++- db/fixtures/01_application_settings.rb | 7 ---- db/fixtures/02_users.rb | 9 +++++ db/fixtures/production/01_users.rb | 2 +- .../20260629120000_add_user_type_to_users.rb | 7 ++++ db/schema_migrations/20260629120000 | 1 + db/structure.sql | 1 + spec/factories/users.rb | 4 +++ spec/policies/user_policy_spec.rb | 12 +++++++ spec/services/users/delete_service_spec.rb | 8 ++--- 12 files changed, 51 insertions(+), 42 deletions(-) create mode 100644 db/fixtures/02_users.rb create mode 100644 db/migrate/20260629120000_add_user_type_to_users.rb create mode 100644 db/schema_migrations/20260629120000 diff --git a/app/models/user.rb b/app/models/user.rb index e69a9de1a..63093bd4a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,11 +3,15 @@ class User < ApplicationRecord include NamespaceParent - GHOST_USERNAME = 'ghost' - GHOST_EMAIL = 'ghost@code0.tech' + USER_TYPES = { + regular: 0, + ghost: 1, + }.freeze has_secure_password + enum :user_type, USER_TYPES + validates :username, length: { maximum: 50 }, presence: true, allow_blank: false, @@ -34,15 +38,8 @@ class User < ApplicationRecord has_one_attached :avatar - before_destroy :prevent_destroy_ghost_user, prepend: true - before_destroy :reassign_authored_audit_events_to_ghost_user - def self.ghost - find_by!(username: GHOST_USERNAME) - end - - def ghost? - username == GHOST_USERNAME + find_by!(user_type: :ghost) end def mfa_enabled? @@ -82,23 +79,6 @@ def validate_mfa!(mfa) generates_token_for :password_reset, expires_in: 15.minutes do password_digest&.last(20) end - - private - - def prevent_destroy_ghost_user - return unless ghost? - - errors.add(:base, :invalid, message: 'Cannot delete ghost user') - throw :abort - end - - def reassign_authored_audit_events_to_ghost_user - ghost_user = self.class.ghost - - authored_audit_events.find_each do |audit_event| - audit_event.update!(author: ghost_user) - end - end end User.prepend_extensions diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 149c94e35..245a1f12b 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -3,6 +3,7 @@ class UserPolicy < BasePolicy condition(:user_is_self) { subject.id == user&.id } condition(:user_is_admin) { user&.admin? || false } + condition(:user_is_regular) { subject.regular? } condition(:admin_status_visible) { ApplicationSetting.current[:admin_status_visible] } rule { ~anonymous }.enable :read_user @@ -12,11 +13,12 @@ class UserPolicy < BasePolicy enable :read_user_identity enable :update_attachment_avatar enable :read_email - enable :delete_user enable :read_admin_status enable :read_mfa_status end + rule { user_is_admin & user_is_regular }.enable :delete_user + rule { admin_status_visible & ~anonymous }.enable :read_admin_status rule { user_is_self }.policy do diff --git a/app/services/users/delete_service.rb b/app/services/users/delete_service.rb index 224da4397..23181dedd 100644 --- a/app/services/users/delete_service.rb +++ b/app/services/users/delete_service.rb @@ -18,8 +18,10 @@ def execute transactional do |t| namespace = user.namespace - audit_author_id = user == current_authentication.user ? User.ghost.id : current_authentication.user.id + ghost_user = User.ghost + audit_author_id = user == current_authentication.user ? ghost_user.id : current_authentication.user.id + user.authored_audit_events.update_all(author_id: ghost_user.id) # rubocop:disable Rails/SkipsModelValidations user.destroy if user.persisted? diff --git a/db/fixtures/01_application_settings.rb b/db/fixtures/01_application_settings.rb index 54c5f749e..df16d5270 100644 --- a/db/fixtures/01_application_settings.rb +++ b/db/fixtures/01_application_settings.rb @@ -1,12 +1,5 @@ # frozen_string_literal: true -User.seed_once :username do |u| - u.username = User::GHOST_USERNAME - u.email = User::GHOST_EMAIL - u.password = SecureRandom.hex - u.admin = false -end - ApplicationSetting.seed_once :setting do |s| s.setting = :user_registration_enabled s.value = false diff --git a/db/fixtures/02_users.rb b/db/fixtures/02_users.rb new file mode 100644 index 000000000..510754e9d --- /dev/null +++ b/db/fixtures/02_users.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +User.seed_once :user_type do |u| + u.username = 'ghost' + u.email = 'ghost@code0.tech' + u.password = SecureRandom.hex + u.admin = false + u.user_type = :ghost +end diff --git a/db/fixtures/production/01_users.rb b/db/fixtures/production/01_users.rb index 003d9098e..0eee7418d 100644 --- a/db/fixtures/production/01_users.rb +++ b/db/fixtures/production/01_users.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -return unless User.where.not(username: User::GHOST_USERNAME).none? +return unless User.regular.none? initial_root_email = ENV.fetch('INITIAL_ROOT_MAIL', nil) initial_root_password = ENV.fetch('INITIAL_ROOT_PASSWORD', SecureRandom.hex) diff --git a/db/migrate/20260629120000_add_user_type_to_users.rb b/db/migrate/20260629120000_add_user_type_to_users.rb new file mode 100644 index 000000000..2296e4257 --- /dev/null +++ b/db/migrate/20260629120000_add_user_type_to_users.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUserTypeToUsers < Code0::ZeroTrack::Database::Migration[1.0] + def change + add_column :users, :user_type, :integer, default: 0, null: false + end +end diff --git a/db/schema_migrations/20260629120000 b/db/schema_migrations/20260629120000 new file mode 100644 index 000000000..0e03ce5f8 --- /dev/null +++ b/db/schema_migrations/20260629120000 @@ -0,0 +1 @@ +68074a1fb7f8bdf3087c3b6823eaef35175c480f05d4bc73c190ef7b2811cf68 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 4448f8343..1af1e36a1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1165,6 +1165,7 @@ CREATE TABLE users ( email_verified_at timestamp with time zone, readme text, blocked_at timestamp with time zone, + user_type integer DEFAULT 0 NOT NULL, CONSTRAINT check_11461c37fb CHECK ((char_length(readme) <= 5000)), CONSTRAINT check_3bedaaa612 CHECK ((char_length(email) <= 255)), CONSTRAINT check_56606ce552 CHECK ((char_length(username) <= 50)), diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 8d7ac759c..20b696694 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -22,6 +22,10 @@ blocked_at { Time.zone.now } end + trait :ghost do + user_type { :ghost } + end + trait :with_namespace do after :build, &:ensure_namespace end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index c3a33169c..c1285e4d7 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -18,4 +18,16 @@ it { is_expected.not_to be_allowed(:read_user) } end + + context 'when the current user is an admin' do + let(:current_user) { create(:user, :admin) } + + it { is_expected.to be_allowed(:delete_user) } + + context 'when the user is internal' do + let(:user) { create(:user, :ghost) } + + it { is_expected.not_to be_allowed(:delete_user) } + end + end end diff --git a/spec/services/users/delete_service_spec.rb b/spec/services/users/delete_service_spec.rb index 0d7ed182f..2a50f98f5 100644 --- a/spec/services/users/delete_service_spec.rb +++ b/spec/services/users/delete_service_spec.rb @@ -9,9 +9,7 @@ let(:user) { create(:user) } let(:current_user) { create(:user, :admin) } - let!(:ghost_user) do - create(:user, username: User::GHOST_USERNAME, email: User::GHOST_EMAIL) - end + let!(:ghost_user) { create(:user, :ghost) } it 'deletes the user successfully' do namespace_id = user.ensure_namespace.id @@ -86,9 +84,9 @@ context 'when deleting the ghost user' do let(:user) { ghost_user } - it 'returns an invalid user error' do + it 'returns a missing permission error' do expect(service_response).not_to be_success - expect(service_response.payload[:error_code]).to eq(:invalid_user) + expect(service_response.payload[:error_code]).to eq(:missing_permission) expect(User.exists?(ghost_user.id)).to be true end end From 9be6392e9e659b7072a36797374744a24889b37b Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 29 Jun 2026 09:35:19 +0200 Subject: [PATCH 3/8] fix: correct spec context --- spec/requests/graphql/mutation/users/delete_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/requests/graphql/mutation/users/delete_spec.rb b/spec/requests/graphql/mutation/users/delete_spec.rb index 8ef3f1ec3..522b7ab03 100644 --- a/spec/requests/graphql/mutation/users/delete_spec.rb +++ b/spec/requests/graphql/mutation/users/delete_spec.rb @@ -31,6 +31,7 @@ let(:current_user) { create(:user, :admin) } before do + create(:user, :ghost) post_graphql mutation, variables: variables, current_user: current_user end From a788016ba1b4afe0509e5a87a809a7a6e9edbc70 Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 10 Jul 2026 20:26:00 +0200 Subject: [PATCH 4/8] drop: removed destroy for namespace parent --- app/models/concerns/namespace_parent.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/namespace_parent.rb b/app/models/concerns/namespace_parent.rb index 71bc864a7..f5c021f6f 100644 --- a/app/models/concerns/namespace_parent.rb +++ b/app/models/concerns/namespace_parent.rb @@ -4,7 +4,7 @@ module NamespaceParent extend ActiveSupport::Concern included do - has_one :namespace, as: :parent, dependent: :destroy + has_one :namespace, as: :parent end def ensure_namespace From 02626cf509559913cfef167c35d3455fc75ffcd7 Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 10 Jul 2026 20:38:12 +0200 Subject: [PATCH 5/8] feat: correct structure sql --- db/structure.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/structure.sql b/db/structure.sql index 1af1e36a1..f5a027332 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1164,8 +1164,8 @@ CREATE TABLE users ( totp_secret text, email_verified_at timestamp with time zone, readme text, - blocked_at timestamp with time zone, user_type integer DEFAULT 0 NOT NULL, + blocked_at timestamp with time zone, CONSTRAINT check_11461c37fb CHECK ((char_length(readme) <= 5000)), CONSTRAINT check_3bedaaa612 CHECK ((char_length(email) <= 255)), CONSTRAINT check_56606ce552 CHECK ((char_length(username) <= 50)), From 43f070d0e9b253c7f730a37dec1616c36c57ae37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20G=C3=B6tz?= <52959657+raphael-goetz@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:49:19 +0200 Subject: [PATCH 6/8] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niklas van Schrick Signed-off-by: Raphael Götz <52959657+raphael-goetz@users.noreply.github.com> --- app/policies/user_policy.rb | 5 +++-- db/fixtures/02_users.rb | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 245a1f12b..ffc6993f7 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -3,7 +3,7 @@ class UserPolicy < BasePolicy condition(:user_is_self) { subject.id == user&.id } condition(:user_is_admin) { user&.admin? || false } - condition(:user_is_regular) { subject.regular? } + condition(:subject_is_regular) { subject.regular? } condition(:admin_status_visible) { ApplicationSetting.current[:admin_status_visible] } rule { ~anonymous }.enable :read_user @@ -13,11 +13,12 @@ class UserPolicy < BasePolicy enable :read_user_identity enable :update_attachment_avatar enable :read_email + enable :delete_user enable :read_admin_status enable :read_mfa_status end - rule { user_is_admin & user_is_regular }.enable :delete_user + rule { ~subject_is_regular }.prevent :delete_user rule { admin_status_visible & ~anonymous }.enable :read_admin_status diff --git a/db/fixtures/02_users.rb b/db/fixtures/02_users.rb index 510754e9d..1de5bad13 100644 --- a/db/fixtures/02_users.rb +++ b/db/fixtures/02_users.rb @@ -6,4 +6,5 @@ u.password = SecureRandom.hex u.admin = false u.user_type = :ghost + u.readme = 'This user will hold the activity of the users that have been deleted' end From e31186151fe68b6706abd2f76e08ca05f41dc40f Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Fri, 10 Jul 2026 21:02:41 +0200 Subject: [PATCH 7/8] Prevent login of non-regular users --- app/policies/global_policy.rb | 3 +++ app/services/users/identity/login_service.rb | 10 ++++++++++ app/services/users/login_service.rb | 6 ++++++ spec/services/users/identity/login_service_spec.rb | 12 ++++++++++++ spec/services/users/login_service_spec.rb | 14 ++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 0d513afe9..7db5c970c 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -3,6 +3,7 @@ class GlobalPolicy < BasePolicy condition(:organization_creation_restricted) { ApplicationSetting.current[:organization_creation_restricted] } condition(:admin) { user&.admin } + condition(:user_is_regular) { user&.regular? } rule { ~anonymous }.enable :create_organization rule { organization_creation_restricted & ~admin }.prevent :create_organization @@ -15,6 +16,8 @@ class GlobalPolicy < BasePolicy enable :read_velorum_config end + rule { user_is_regular }.enable :create_user_session + rule { admin }.policy do enable :read_application_setting enable :update_application_setting diff --git a/app/services/users/identity/login_service.rb b/app/services/users/identity/login_service.rb index b507bbc2b..572170070 100644 --- a/app/services/users/identity/login_service.rb +++ b/app/services/users/identity/login_service.rb @@ -50,6 +50,16 @@ def execute details: user_session.errors) end + unless Ability.allowed?(Sagittarius::Authentication.new(:session, user_session), :create_user_session) + logger.warn( + message: 'User was not allowed to create user session', + user_id: user.id, + username: user.username + ) + t.rollback_and_return! ServiceResponse.error(message: 'Not allowed to create user session', + error_code: :invalid_login_data) + end + AuditService.audit( :user_logged_in, author_id: user.id, diff --git a/app/services/users/login_service.rb b/app/services/users/login_service.rb index 6b2dda312..1bff707d6 100644 --- a/app/services/users/login_service.rb +++ b/app/services/users/login_service.rb @@ -45,6 +45,12 @@ def execute details: user_session.errors) end + unless Ability.allowed?(Sagittarius::Authentication.new(:session, user_session), :create_user_session) + logger.warn(message: 'User was not allowed to create user session', user_id: user.id, username: user.username) + t.rollback_and_return! ServiceResponse.error(message: 'Not allowed to create user session', + error_code: :invalid_login_data) + end + AuditService.audit( :user_logged_in, author_id: user.id, diff --git a/spec/services/users/identity/login_service_spec.rb b/spec/services/users/identity/login_service_spec.rb index 916aa6284..b9bc1c6de 100644 --- a/spec/services/users/identity/login_service_spec.rb +++ b/spec/services/users/identity/login_service_spec.rb @@ -56,6 +56,18 @@ def setup_identity_provider(identity) expect(service_response.payload[:error_code]).to eq(:user_blocked) end end + + context 'when user is ghost' do + before do + current_user.update!(user_type: :ghost) + end + + it do + is_expected.not_to create_audit_event + expect(service_response).to be_error + expect(service_response.payload[:error_code]).to eq(:invalid_login_data) + end + end end context 'when user identity validation fails' do diff --git a/spec/services/users/login_service_spec.rb b/spec/services/users/login_service_spec.rb index fcf751b8c..0d992c246 100644 --- a/spec/services/users/login_service_spec.rb +++ b/spec/services/users/login_service_spec.rb @@ -67,6 +67,20 @@ end end + context 'when user is ghost' do + let(:params) { { username: username, password: password } } + + before do + current_user.update!(user_type: :ghost) + end + + it 'returns an error response' do + expect(service_response).to be_error + expect(service_response.payload[:error_code]).to eq(:invalid_login_data) + is_expected.not_to create_audit_event + end + end + context 'when using mfa' do context 'when mfa is not activated' do let(:params) do From fe64f9a91bce0b97342b399a2bdaae38d3bc7be8 Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Fri, 10 Jul 2026 21:08:05 +0200 Subject: [PATCH 8/8] Exclude non-regular users from license seat limit --- .../ee/app/services/ee/users/validate_user_limit.rb | 2 +- .../ee/spec/services/ee/users/create_service_spec.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/extensions/ee/app/services/ee/users/validate_user_limit.rb b/extensions/ee/app/services/ee/users/validate_user_limit.rb index 5e372fb01..4d9604b58 100644 --- a/extensions/ee/app/services/ee/users/validate_user_limit.rb +++ b/extensions/ee/app/services/ee/users/validate_user_limit.rb @@ -8,7 +8,7 @@ module ValidateUserLimit def validate_user_limit!(t) license = License.current return if license.nil? || !license.restricted?(:user_count) - return if User.count <= license.restrictions[:user_count] + return if User.regular.count <= license.restrictions[:user_count] t.rollback_and_return! ServiceResponse.error( message: 'No free user seats in license', diff --git a/extensions/ee/spec/services/ee/users/create_service_spec.rb b/extensions/ee/spec/services/ee/users/create_service_spec.rb index 446149d85..b0637fc0f 100644 --- a/extensions/ee/spec/services/ee/users/create_service_spec.rb +++ b/extensions/ee/spec/services/ee/users/create_service_spec.rb @@ -26,4 +26,14 @@ it { expect { service_response }.not_to change { User.count } } it { expect(service_response.payload[:error_code]).to eq(:no_free_license_seats) } end + + context 'when non-regular users exist' do + before do + current_user + create(:user, :ghost) + create(:license, restrictions: { user_count: 2 }) + end + + it { is_expected.to be_success } + end end