command_tower 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/command_tower/me/account_controller.rb +35 -0
- data/app/deserializers/command_tower/deserializers/me/delete_account_deserializer.rb +31 -0
- data/app/errors/command_tower/errors/auth/account_deleted_error.rb +21 -0
- data/app/models/user.rb +20 -2
- data/app/serializers/command_tower/serializers/me/delete_account_response_serializer.rb +13 -0
- data/app/services/command_tower/jwt/authenticate_user.rb +1 -1
- data/app/services/command_tower/services/admin/users.rb +2 -2
- data/app/services/command_tower/services/auth/email_availability.rb +1 -1
- data/app/services/command_tower/services/auth/plain_text/login.rb +1 -1
- data/app/services/command_tower/services/auth/purge_user_associated_data.rb +51 -0
- data/app/services/command_tower/services/auth/tombstone_user.rb +47 -0
- data/app/services/command_tower/services/me/capabilities.rb +6 -5
- data/app/services/command_tower/services/me/delete_account.rb +92 -0
- data/app/workflows/command_tower/workflows/me/delete_account_workflow.rb +34 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20260826140000_add_deleted_at_to_users.rb +8 -0
- data/docs/upgrades/0.13.0.md +18 -0
- data/docs/upgrades/README.md +1 -0
- data/lib/command_tower/authorization/default.yml +3 -0
- data/lib/command_tower/configuration/account_deletion/config.rb +22 -0
- data/lib/command_tower/configuration/config.rb +6 -0
- data/lib/command_tower/configuration/registry/audit/config.rb +14 -0
- data/lib/command_tower/install/baseline.rb +1 -0
- data/lib/command_tower/version.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7116094a133d9fc9edaf8cc0851bde09a3c2bebacded3ea64b1f4a7b4f445db
|
|
4
|
+
data.tar.gz: e4880f05a6c601fb07349d44d27d3a746e10051e8b4ef2b701dc961998862fc4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3633188b6f62b92945cf142ea65fff20a596420b9c0cc4f907d4688732b4d03f283843277e36ea662091b097f71d39d348f4bd06822604f2dace486b3484df38
|
|
7
|
+
data.tar.gz: 749c9b86dd4919c072e1a29af779a84bcd65892f1c95b80f06ded36b4e30844c9598eabf050902d13679cfe911c6b6214898236afd6472ef61fb17370a8b7757
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Me
|
|
5
|
+
class AccountController < CommandTower::ApplicationController
|
|
6
|
+
include CommandTower::Auth::AuthenticationBoundary
|
|
7
|
+
include CommandTower::Auth::AuthorizationBoundary
|
|
8
|
+
|
|
9
|
+
before_action :authenticate_request!
|
|
10
|
+
before_action :authorize_request!
|
|
11
|
+
|
|
12
|
+
def destroy
|
|
13
|
+
deserialized = CommandTower::Deserializers::Me::DeleteAccountDeserializer.call(params)
|
|
14
|
+
return render_deserializer_errors unless deserialized.success?
|
|
15
|
+
|
|
16
|
+
result = CommandTower::Workflows::Me::DeleteAccountWorkflow.call(
|
|
17
|
+
current_user: current_user,
|
|
18
|
+
password: deserialized.input.password
|
|
19
|
+
)
|
|
20
|
+
render_application_result(result)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render_deserializer_errors
|
|
26
|
+
render_application_result(
|
|
27
|
+
CommandTower::Workflows::WorkflowResult.failure(
|
|
28
|
+
errors: [CommandTower::Errors::ValidationError.new(details: { base: "Missing required fields" })],
|
|
29
|
+
http_status: :unprocessable_entity
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Me
|
|
6
|
+
class DeleteAccountDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
7
|
+
Input = Data.define(:password)
|
|
8
|
+
|
|
9
|
+
def call(params)
|
|
10
|
+
password = params[:password]
|
|
11
|
+
|
|
12
|
+
if blank?(password)
|
|
13
|
+
return failure(errors: { message: "missing_required_fields" })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
unless password.is_a?(String)
|
|
17
|
+
return failure(errors: { message: "invalid_field_types" })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
success(Input.new(password:))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def blank?(value)
|
|
26
|
+
value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
module Auth
|
|
6
|
+
class AccountDeletedError < CommandTower::Errors::UnauthorizedError
|
|
7
|
+
def code
|
|
8
|
+
"account_deleted"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"Account has been deleted"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def log_level
|
|
16
|
+
:info
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/app/models/user.rb
CHANGED
|
@@ -33,8 +33,14 @@ require "securerandom"
|
|
|
33
33
|
class User < CommandTower::ApplicationRecord
|
|
34
34
|
has_secure_password
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
TOMBSTONE_EMAIL_DOMAIN = "@invalid.local"
|
|
37
|
+
TOMBSTONE_EMAIL_PREFIX = "deleted+"
|
|
38
|
+
TOMBSTONE_USERNAME_PREFIX = "deleted_"
|
|
39
|
+
|
|
40
|
+
scope :not_deleted, -> { where(deleted_at: nil) }
|
|
41
|
+
|
|
42
|
+
validates :username, uniqueness: { conditions: -> { not_deleted } }
|
|
43
|
+
validates :email, uniqueness: { conditions: -> { not_deleted } }
|
|
38
44
|
|
|
39
45
|
###
|
|
40
46
|
# Serialize the roles column to check for inclusion easily
|
|
@@ -49,6 +55,18 @@ class User < CommandTower::ApplicationRecord
|
|
|
49
55
|
"#{first_name} #{last_name}"
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
def deleted?
|
|
59
|
+
deleted_at.present?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.tombstone_email_for(id)
|
|
63
|
+
"#{TOMBSTONE_EMAIL_PREFIX}#{id}#{TOMBSTONE_EMAIL_DOMAIN}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.tombstone_username_for(id)
|
|
67
|
+
"#{TOMBSTONE_USERNAME_PREFIX}#{id}"
|
|
68
|
+
end
|
|
69
|
+
|
|
52
70
|
def reset_verifier_token!
|
|
53
71
|
value = SecureRandom.alphanumeric(32)
|
|
54
72
|
update!(verifier_token: value, verifier_token_last_reset: Time.now)
|
|
@@ -27,7 +27,7 @@ module CommandTower::Jwt
|
|
|
27
27
|
return invalid_token if expires_at.nil?
|
|
28
28
|
|
|
29
29
|
user = User.find_by(id: payload[:user_id])
|
|
30
|
-
if user.nil?
|
|
30
|
+
if user.nil? || user.deleted?
|
|
31
31
|
log_warn("user_id [#{payload[:user_id]}] was not found. Cannot Continue")
|
|
32
32
|
return invalid_token
|
|
33
33
|
end
|
|
@@ -12,7 +12,7 @@ module CommandTower
|
|
|
12
12
|
validate :scope_context, is_a: [CommandTower::AdminScope::ScopeContext, NilClass], required: false
|
|
13
13
|
|
|
14
14
|
def call
|
|
15
|
-
relation = ::User.order(id: :desc)
|
|
15
|
+
relation = ::User.not_deleted.order(id: :desc)
|
|
16
16
|
relation = CommandTower::AdminScope::ApplyUsersNarrowing.call(
|
|
17
17
|
relation:,
|
|
18
18
|
scope_context:,
|
|
@@ -48,7 +48,7 @@ module CommandTower
|
|
|
48
48
|
validate :scope_context, is_a: [CommandTower::AdminScope::ScopeContext, NilClass], required: false
|
|
49
49
|
|
|
50
50
|
def call
|
|
51
|
-
relation = ::User.where(id:)
|
|
51
|
+
relation = ::User.not_deleted.where(id:)
|
|
52
52
|
relation = CommandTower::AdminScope::ApplyUsersNarrowing.call(
|
|
53
53
|
relation:,
|
|
54
54
|
scope_context:,
|
|
@@ -9,7 +9,7 @@ module CommandTower
|
|
|
9
9
|
validate :password, is_a: String, required: true, sensitive: true
|
|
10
10
|
|
|
11
11
|
def call
|
|
12
|
-
user = User.where(username: identifier).or(User.where(email: identifier)).first
|
|
12
|
+
user = User.not_deleted.where(username: identifier).or(User.not_deleted.where(email: identifier)).first
|
|
13
13
|
if user.nil?
|
|
14
14
|
audit_login_failed(affected_user: nil, outcome: "unknown_identifier")
|
|
15
15
|
invalid_credentials!
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Auth
|
|
6
|
+
class PurgeUserAssociatedData < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :user, is_a: User, required: true
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
purge_user_secrets!
|
|
11
|
+
purge_messaging_inbox!
|
|
12
|
+
purge_messaging_endpoints!
|
|
13
|
+
revoke_impersonation_sessions!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def purge_user_secrets!
|
|
19
|
+
UserSecret.where(user_id: user.id).delete_all
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def purge_messaging_inbox!
|
|
23
|
+
item_ids = CommandTower::Messaging::Inbox::Scope.for_recipient(user.id).pluck(:id)
|
|
24
|
+
return if item_ids.empty?
|
|
25
|
+
|
|
26
|
+
CommandTower::Messaging::Inbox.bulk_delete(recipient_id: user.id, inbox_item_ids: item_ids)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def purge_messaging_endpoints!
|
|
30
|
+
return unless defined?(CommandTower::Messaging::Endpoint)
|
|
31
|
+
|
|
32
|
+
CommandTower::Messaging::Endpoint.where(user_id: user.id).find_each(&:destroy!)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def revoke_impersonation_sessions!
|
|
36
|
+
CommandTower::Services::Impersonation::TerminateOpenSessions.call(
|
|
37
|
+
actor_user_id: user.id,
|
|
38
|
+
reason: "revoked"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
CommandTower::Impersonation::Session.open.where(target_user_id: user.id).find_each do |session|
|
|
42
|
+
CommandTower::Services::Impersonation::End.call(
|
|
43
|
+
session_id: session.id,
|
|
44
|
+
reason: "revoked"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Auth
|
|
6
|
+
class TombstoneUser < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :user, is_a: User, required: true
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
if user.deleted?
|
|
11
|
+
context.user = user
|
|
12
|
+
context.changed = false
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
user_id = user.id
|
|
17
|
+
scrub_password = SecureRandom.hex(32)
|
|
18
|
+
user.assign_attributes(
|
|
19
|
+
deleted_at: Time.current,
|
|
20
|
+
first_name: "",
|
|
21
|
+
last_name: "",
|
|
22
|
+
email: User.tombstone_email_for(user_id),
|
|
23
|
+
username: User.tombstone_username_for(user_id),
|
|
24
|
+
phone_number: nil,
|
|
25
|
+
phone_number_validated: false,
|
|
26
|
+
email_validated: false,
|
|
27
|
+
roles: []
|
|
28
|
+
)
|
|
29
|
+
user.password = scrub_password
|
|
30
|
+
user.password_confirmation = scrub_password
|
|
31
|
+
|
|
32
|
+
unless user.save(validate: false)
|
|
33
|
+
context.fail!(
|
|
34
|
+
application_error: CommandTower::Errors::InternalError.new
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
user.reset_verifier_token!
|
|
40
|
+
|
|
41
|
+
context.user = user.reload
|
|
42
|
+
context.changed = true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -15,14 +15,15 @@ module CommandTower
|
|
|
15
15
|
|
|
16
16
|
def project
|
|
17
17
|
{
|
|
18
|
-
editName: { enabled:
|
|
18
|
+
editName: { enabled: !user.deleted? },
|
|
19
19
|
editUsername: { enabled: false },
|
|
20
20
|
changeEmail: { enabled: false },
|
|
21
|
-
changePassword: { enabled:
|
|
22
|
-
editPhone: { enabled: sms_enabled? },
|
|
23
|
-
editPushover: { enabled: pushover_enabled? },
|
|
21
|
+
changePassword: { enabled: !user.deleted? },
|
|
22
|
+
editPhone: { enabled: sms_enabled? && !user.deleted? },
|
|
23
|
+
editPushover: { enabled: pushover_enabled? && !user.deleted? },
|
|
24
24
|
logoutAllDevices: { enabled: false },
|
|
25
|
-
verifyEmail: { enabled: !user.email_validated }
|
|
25
|
+
verifyEmail: { enabled: !user.deleted? && !user.email_validated },
|
|
26
|
+
deleteAccount: { enabled: !user.deleted? }
|
|
26
27
|
}
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Me
|
|
6
|
+
class DeleteAccount < CommandTower::Services::ApplicationService
|
|
7
|
+
FIELD_KEYS = {
|
|
8
|
+
password: :password
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
validate :user, is_a: User, required: true
|
|
12
|
+
validate :password, is_a: String, required: true, sensitive: true
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
if user.deleted?
|
|
16
|
+
context.fail!(
|
|
17
|
+
application_error: CommandTower::Errors::Auth::AccountDeletedError.new
|
|
18
|
+
)
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
reject!(password: "Incorrect password") unless user.authenticate(password)
|
|
23
|
+
|
|
24
|
+
infrastructure_failure = false
|
|
25
|
+
|
|
26
|
+
ActiveRecord::Base.transaction do
|
|
27
|
+
unless invoke_host_finalizer!
|
|
28
|
+
infrastructure_failure = true
|
|
29
|
+
raise ActiveRecord::Rollback
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
purge_result = CommandTower::Services::Auth::PurgeUserAssociatedData.call(user:)
|
|
33
|
+
unless purge_result.success?
|
|
34
|
+
infrastructure_failure = true
|
|
35
|
+
raise ActiveRecord::Rollback
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
tombstone_result = CommandTower::Services::Auth::TombstoneUser.call(user:)
|
|
39
|
+
unless tombstone_result.success?
|
|
40
|
+
infrastructure_failure = true
|
|
41
|
+
raise ActiveRecord::Rollback
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
user.reload
|
|
45
|
+
audit(
|
|
46
|
+
:account_deleted,
|
|
47
|
+
subject: user,
|
|
48
|
+
affected_user: user,
|
|
49
|
+
changes: {},
|
|
50
|
+
metadata: { mechanism: "self_service" }
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if infrastructure_failure
|
|
55
|
+
context.fail!(application_error: CommandTower::Errors::InternalError.new)
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
user.reload
|
|
60
|
+
unless user.deleted?
|
|
61
|
+
log_error("Account deletion transaction did not persist for user [#{user.id}]")
|
|
62
|
+
context.fail!(application_error: CommandTower::Errors::InternalError.new)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
log_info("Account deleted for user [#{user.id}]")
|
|
66
|
+
context.message = "Your account has been deleted"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def invoke_host_finalizer!
|
|
72
|
+
finalizer = CommandTower.config.account_deletion.host_finalizer
|
|
73
|
+
return true if finalizer.nil?
|
|
74
|
+
|
|
75
|
+
outcome = finalizer.call(user:)
|
|
76
|
+
return true if outcome.nil?
|
|
77
|
+
return true if outcome.respond_to?(:success?) && outcome.success?
|
|
78
|
+
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def reject!(errors)
|
|
83
|
+
details = errors.each_with_object({}) do |(key, message), mapped|
|
|
84
|
+
mapped[FIELD_KEYS.fetch(key.to_sym, key)] = message
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context.fail!(application_error: CommandTower::Errors::ValidationError.new(details:))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Me
|
|
6
|
+
class DeleteAccountWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
7
|
+
retry_strategy :none
|
|
8
|
+
|
|
9
|
+
def call(current_user:, password:)
|
|
10
|
+
delete_result = CommandTower::Services::Me::DeleteAccount.call(
|
|
11
|
+
user: current_user,
|
|
12
|
+
password:
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
unless delete_result.success?
|
|
16
|
+
error = delete_result.errors.first
|
|
17
|
+
return failure(
|
|
18
|
+
errors: delete_result.errors,
|
|
19
|
+
http_status: CommandTower::Workflows::Me::ErrorStatus.http_status_for(error)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
success(
|
|
24
|
+
payload: CommandTower::Serializers::Me::DeleteAccountResponseSerializer.serialize(
|
|
25
|
+
message: delete_result.data[:message]
|
|
26
|
+
),
|
|
27
|
+
http_status: :ok,
|
|
28
|
+
response_effects: { clear_token: true }
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -60,6 +60,7 @@ CommandTower::Engine.routes.draw do
|
|
|
60
60
|
namespace :me do
|
|
61
61
|
patch "name", to: "name#update"
|
|
62
62
|
patch "password", to: "password#update"
|
|
63
|
+
delete "account", to: "account#destroy"
|
|
63
64
|
|
|
64
65
|
resources :inbox, only: [:index, :show, :destroy], controller: "inbox" do
|
|
65
66
|
collection do
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# CommandTower 0.13.0
|
|
2
|
+
|
|
3
|
+
Account self-service deletion (CANON-020 / M1).
|
|
4
|
+
|
|
5
|
+
## Host actions
|
|
6
|
+
|
|
7
|
+
1. `rails command_tower:install:migrations` (adds `users.deleted_at`)
|
|
8
|
+
2. `rails db:migrate`
|
|
9
|
+
3. Grant `me_account` to authenticated member role in `config/rbac_groups.yml`
|
|
10
|
+
4. Optional: register `config.account_deletion.host_finalizer` for product cleanup
|
|
11
|
+
|
|
12
|
+
## API
|
|
13
|
+
|
|
14
|
+
- `DELETE /api/me/account` — body `{ "password": "..." }`; clears session on success
|
|
15
|
+
|
|
16
|
+
## RBAC
|
|
17
|
+
|
|
18
|
+
New entity: `me_account` → `CommandTower::Me::AccountController#destroy`
|
data/docs/upgrades/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Host-facing upgrade / change summaries for CommandTower releases.
|
|
4
4
|
|
|
5
5
|
| Version | Summary |
|
|
6
|
+
| [0.13.0](0.13.0.md) | Account self-service deletion (`DELETE /api/me/account`); tombstone + PII scrub; `me_account` RBAC; `users.deleted_at` migration |
|
|
6
7
|
|---------|---------|
|
|
7
8
|
| [0.12.0](0.12.0.md) | Intervention envelope serializers/deserializers; product-tool admin_scope without Users/Audit narrowing |
|
|
8
9
|
| [0.11.1](0.11.1.md) | Audit Event `attribute :json` (MariaDB); audit migration without `utf8mb4_0900_ai_ci` |
|
|
@@ -25,6 +25,9 @@ entities:
|
|
|
25
25
|
- name: me_password
|
|
26
26
|
controller: CommandTower::Me::PasswordController
|
|
27
27
|
only: update
|
|
28
|
+
- name: me_account
|
|
29
|
+
controller: CommandTower::Me::AccountController
|
|
30
|
+
only: destroy
|
|
28
31
|
- name: me_inbox
|
|
29
32
|
controller: CommandTower::Me::InboxController
|
|
30
33
|
only:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "class_composer"
|
|
4
|
+
require "command_tower/configuration/base"
|
|
5
|
+
|
|
6
|
+
module CommandTower
|
|
7
|
+
module Configuration
|
|
8
|
+
module AccountDeletion
|
|
9
|
+
class Config < ::CommandTower::Configuration::Base
|
|
10
|
+
include ClassComposer::Generator
|
|
11
|
+
|
|
12
|
+
add_composer :host_finalizer,
|
|
13
|
+
desc: "Optional host-owned callable invoked synchronously during account deletion " \
|
|
14
|
+
"before the user row is tombstoned. Receives keyword `user:` (non-deleted User). " \
|
|
15
|
+
"Return value is ignored; raise to abort the deletion transaction.",
|
|
16
|
+
allowed: [Proc, NilClass],
|
|
17
|
+
default: nil,
|
|
18
|
+
default_shown: "nil"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "singleton"
|
|
4
4
|
require "class_composer"
|
|
5
|
+
require "command_tower/configuration/account_deletion/config"
|
|
5
6
|
require "command_tower/configuration/admin/config"
|
|
6
7
|
require "command_tower/configuration/admin_scope/config"
|
|
7
8
|
require "command_tower/configuration/application/config"
|
|
@@ -91,6 +92,11 @@ module CommandTower
|
|
|
91
92
|
allowed: Configuration::Messaging::Config,
|
|
92
93
|
default: Configuration::Messaging::Config.new
|
|
93
94
|
|
|
95
|
+
add_composer :account_deletion,
|
|
96
|
+
desc: "Account self-service deletion host hooks",
|
|
97
|
+
allowed: Configuration::AccountDeletion::Config,
|
|
98
|
+
default: Configuration::AccountDeletion::Config.new
|
|
99
|
+
|
|
94
100
|
add_composer :admin,
|
|
95
101
|
desc: "Admin configuration for the app",
|
|
96
102
|
allowed: Configuration::Admin::Config,
|
|
@@ -64,6 +64,20 @@ module CommandTower
|
|
|
64
64
|
affected_user_required: true,
|
|
65
65
|
global_visible_in_host_scope: true
|
|
66
66
|
},
|
|
67
|
+
account_deleted: {
|
|
68
|
+
label: "Account deleted",
|
|
69
|
+
tags: %w[identity account deletion security],
|
|
70
|
+
enabled: true,
|
|
71
|
+
enablement_configurable: false,
|
|
72
|
+
user_history: true,
|
|
73
|
+
sensitive_fields: [],
|
|
74
|
+
allowed_changes: [],
|
|
75
|
+
retention: :permanent,
|
|
76
|
+
subject_required: true,
|
|
77
|
+
subject_type: "User",
|
|
78
|
+
affected_user_required: true,
|
|
79
|
+
global_visible_in_host_scope: true
|
|
80
|
+
},
|
|
67
81
|
email_verified: {
|
|
68
82
|
label: "Email verified",
|
|
69
83
|
tags: %w[identity verification email],
|
|
@@ -14,6 +14,7 @@ module CommandTower
|
|
|
14
14
|
20260816000001_create_command_tower_audit_events.rb
|
|
15
15
|
20260817000001_add_scope_columns_to_command_tower_audit_events.rb
|
|
16
16
|
20260817000003_create_command_tower_impersonation_sessions.rb
|
|
17
|
+
20260826140000_add_deleted_at_to_users.rb
|
|
17
18
|
].freeze
|
|
18
19
|
|
|
19
20
|
module_function
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: command_tower
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- matt-taylor
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: phonelib
|
|
@@ -209,6 +209,7 @@ files:
|
|
|
209
209
|
- app/controllers/command_tower/auth/session_controller.rb
|
|
210
210
|
- app/controllers/command_tower/auth/signup_session/create_controller.rb
|
|
211
211
|
- app/controllers/command_tower/auth/username/availability_controller.rb
|
|
212
|
+
- app/controllers/command_tower/me/account_controller.rb
|
|
212
213
|
- app/controllers/command_tower/me/audit_events_controller.rb
|
|
213
214
|
- app/controllers/command_tower/me/inbox_controller.rb
|
|
214
215
|
- app/controllers/command_tower/me/name_controller.rb
|
|
@@ -251,6 +252,7 @@ files:
|
|
|
251
252
|
- app/deserializers/command_tower/deserializers/failure.rb
|
|
252
253
|
- app/deserializers/command_tower/deserializers/intervention/envelope_deserializer.rb
|
|
253
254
|
- app/deserializers/command_tower/deserializers/me/change_password_deserializer.rb
|
|
255
|
+
- app/deserializers/command_tower/deserializers/me/delete_account_deserializer.rb
|
|
254
256
|
- app/deserializers/command_tower/deserializers/me/phone_verification/verify_deserializer.rb
|
|
255
257
|
- app/deserializers/command_tower/deserializers/me/pushover/credentials_deserializer.rb
|
|
256
258
|
- app/deserializers/command_tower/deserializers/me/update_name_deserializer.rb
|
|
@@ -272,6 +274,7 @@ files:
|
|
|
272
274
|
- app/errors/command_tower/errors/account/pushover_verification_failed_error.rb
|
|
273
275
|
- app/errors/command_tower/errors/account/sms_capability_unavailable_error.rb
|
|
274
276
|
- app/errors/command_tower/errors/application_error.rb
|
|
277
|
+
- app/errors/command_tower/errors/auth/account_deleted_error.rb
|
|
275
278
|
- app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb
|
|
276
279
|
- app/errors/command_tower/errors/auth/csrf_mismatch_error.rb
|
|
277
280
|
- app/errors/command_tower/errors/auth/csrf_missing_error.rb
|
|
@@ -359,6 +362,7 @@ files:
|
|
|
359
362
|
- app/serializers/command_tower/serializers/intervention/remediation_serializer.rb
|
|
360
363
|
- app/serializers/command_tower/serializers/me/account_serializer.rb
|
|
361
364
|
- app/serializers/command_tower/serializers/me/change_password_response_serializer.rb
|
|
365
|
+
- app/serializers/command_tower/serializers/me/delete_account_response_serializer.rb
|
|
362
366
|
- app/serializers/command_tower/serializers/me/pushover_serializer.rb
|
|
363
367
|
- app/serializers/command_tower/serializers/messaging/inbox.rb
|
|
364
368
|
- app/serializers/command_tower/serializers/messaging/preferences/catalog_serializer.rb
|
|
@@ -619,6 +623,7 @@ files:
|
|
|
619
623
|
- app/services/command_tower/services/auth/password_reset/validate.rb
|
|
620
624
|
- app/services/command_tower/services/auth/plain_text/login.rb
|
|
621
625
|
- app/services/command_tower/services/auth/principal_capabilities/project.rb
|
|
626
|
+
- app/services/command_tower/services/auth/purge_user_associated_data.rb
|
|
622
627
|
- app/services/command_tower/services/auth/register.rb
|
|
623
628
|
- app/services/command_tower/services/auth/signup_rate_limits/check_availability.rb
|
|
624
629
|
- app/services/command_tower/services/auth/signup_rate_limits/check_register.rb
|
|
@@ -628,6 +633,7 @@ files:
|
|
|
628
633
|
- app/services/command_tower/services/auth/signup_session/decode.rb
|
|
629
634
|
- app/services/command_tower/services/auth/signup_session/encode.rb
|
|
630
635
|
- app/services/command_tower/services/auth/signup_session/validate.rb
|
|
636
|
+
- app/services/command_tower/services/auth/tombstone_user.rb
|
|
631
637
|
- app/services/command_tower/services/auth/username_availability.rb
|
|
632
638
|
- app/services/command_tower/services/impersonation/create.rb
|
|
633
639
|
- app/services/command_tower/services/impersonation/end.rb
|
|
@@ -635,6 +641,7 @@ files:
|
|
|
635
641
|
- app/services/command_tower/services/impersonation/terminate_open_sessions.rb
|
|
636
642
|
- app/services/command_tower/services/me/capabilities.rb
|
|
637
643
|
- app/services/command_tower/services/me/change_password.rb
|
|
644
|
+
- app/services/command_tower/services/me/delete_account.rb
|
|
638
645
|
- app/services/command_tower/services/me/pushover_product_gate.rb
|
|
639
646
|
- app/services/command_tower/services/me/sms_product_gate.rb
|
|
640
647
|
- app/services/command_tower/services/me/update_name.rb
|
|
@@ -708,6 +715,7 @@ files:
|
|
|
708
715
|
- app/workflows/command_tower/workflows/impersonation/stop_workflow.rb
|
|
709
716
|
- app/workflows/command_tower/workflows/me/change_password_workflow.rb
|
|
710
717
|
- app/workflows/command_tower/workflows/me/clear_phone_workflow.rb
|
|
718
|
+
- app/workflows/command_tower/workflows/me/delete_account_workflow.rb
|
|
711
719
|
- app/workflows/command_tower/workflows/me/error_mapping.rb
|
|
712
720
|
- app/workflows/command_tower/workflows/me/error_status.rb
|
|
713
721
|
- app/workflows/command_tower/workflows/me/phone_verification/send_workflow.rb
|
|
@@ -739,6 +747,7 @@ files:
|
|
|
739
747
|
- db/migrate/20260816000001_create_command_tower_audit_events.rb
|
|
740
748
|
- db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb
|
|
741
749
|
- db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb
|
|
750
|
+
- db/migrate/20260826140000_add_deleted_at_to_users.rb
|
|
742
751
|
- docs/admin_workspace.md
|
|
743
752
|
- docs/api_reference.md
|
|
744
753
|
- docs/architecture.md
|
|
@@ -765,6 +774,7 @@ files:
|
|
|
765
774
|
- docs/upgrades/0.11.0.md
|
|
766
775
|
- docs/upgrades/0.11.1.md
|
|
767
776
|
- docs/upgrades/0.12.0.md
|
|
777
|
+
- docs/upgrades/0.13.0.md
|
|
768
778
|
- docs/upgrades/README.md
|
|
769
779
|
- lib/command_tower.rb
|
|
770
780
|
- lib/command_tower/admin_scope.rb
|
|
@@ -787,6 +797,7 @@ files:
|
|
|
787
797
|
- lib/command_tower/authorization/effective_entity_grants.rb
|
|
788
798
|
- lib/command_tower/authorization/entity.rb
|
|
789
799
|
- lib/command_tower/authorization/role.rb
|
|
800
|
+
- lib/command_tower/configuration/account_deletion/config.rb
|
|
790
801
|
- lib/command_tower/configuration/admin/config.rb
|
|
791
802
|
- lib/command_tower/configuration/admin_scope/config.rb
|
|
792
803
|
- lib/command_tower/configuration/admin_scope/tool_registration.rb
|