command_tower 0.12.0 → 0.13.1
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.12.0.md +1 -1
- data/docs/upgrades/0.13.0.md +18 -0
- data/docs/upgrades/0.13.1.md +24 -0
- data/docs/upgrades/README.md +2 -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/intervention/severity.rb +20 -0
- data/lib/command_tower/version.rb +1 -1
- data/lib/command_tower.rb +1 -0
- metadata +15 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5243f531ae8a96ca6373b331d30665a0417078f9b799ca0491c78a26231b639e
|
|
4
|
+
data.tar.gz: 6d6ed873a0a3012fd8b09b952be2b4a32652980fea5ba6871c8089ffdba71ade
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94aa74a9fe97771c5b690db1fa00298dd551ba9a0a62430097a3ffac583e4be5c66198768e8262f670863533fa90d18ddc5ab89e34a64fd5967d00bbf550f93b
|
|
7
|
+
data.tar.gz: 57431bc097a11ea361fe5e56d7ef797a6c07afbd076219745a0687fb33d456e809bf693785eb7d794e0a0cf9443f414d78522dc742e8d8bd5219a508155ef41d
|
|
@@ -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
|
data/docs/upgrades/0.12.0.md
CHANGED
|
@@ -9,7 +9,7 @@ Minor release: canonical action-intervention envelope ser/de, and Admin Scope re
|
|
|
9
9
|
|
|
10
10
|
| Change | Host impact |
|
|
11
11
|
|--------|-------------|
|
|
12
|
-
| Intervention envelope | `CommandTower::Serializers::Intervention::EnvelopeSerializer` / `EnvelopeDeserializer` (plus blocker/remediation serializers). Canonical `{ action, allowed, blockers[] }` with ordered blockers; **no** presentation-mode fields. Host/CT FE owns `callout` / `intercept` / `sheet` / `blocking_region`. |
|
|
12
|
+
| Intervention envelope | `CommandTower::Serializers::Intervention::EnvelopeSerializer` / `EnvelopeDeserializer` (plus blocker/remediation serializers). Canonical `{ action, allowed, blockers[] }` with ordered blockers; **no** presentation-mode fields. Host/CT FE owns `callout` / `intercept` / `sheet` / `blocking_region`. Optional blocker `severity` strings: prefer `CommandTower::Intervention::Severity` (`blocking`, `warning`, `informational`) — transport hints only; FE `tone` remains host-owned. |
|
|
13
13
|
| Admin Scope hook requirements | Every `admin_scope.register` still requires `options`, `validate`, and `availability`. Resource-narrowing hooks (`narrow_users`, `narrow_audit`, `affected_users_in_scope`) are required for CT-owned **`users`** / **`audit`**, or whenever any narrowing hook is set. Host product tools may register the three base hooks only. |
|
|
14
14
|
|
|
15
15
|
## Host actions
|
|
@@ -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`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Upgrade: CommandTower 0.13.1
|
|
2
|
+
|
|
3
|
+
**From:** `0.13.0`
|
|
4
|
+
**To:** `0.13.1`
|
|
5
|
+
|
|
6
|
+
Patch release: canonical host-authored intervention severity constants. No public HTTP or persistence break.
|
|
7
|
+
|
|
8
|
+
## Host-visible changes
|
|
9
|
+
|
|
10
|
+
| Change | Host impact |
|
|
11
|
+
|--------|-------------|
|
|
12
|
+
| `CommandTower::Intervention::Severity` | Named constants for optional blocker `severity` strings: `BLOCKING` (`"blocking"`), `WARNING` (`"warning"`), `INFORMATIONAL` (`"informational"`). Transport hints only — CommandTower does not serialize FE `tone`. Hosts map severity → `@commandtower/frontend` `ActionIntervention` tone. |
|
|
13
|
+
|
|
14
|
+
## Host actions
|
|
15
|
+
|
|
16
|
+
1. Bump gem to `0.13.1` (or `>= 0.13.1`).
|
|
17
|
+
2. **No new migration.**
|
|
18
|
+
3. Prefer `CommandTower::Intervention::Severity::*` when setting `BlockerSerializer` `severity:` instead of ad-hoc strings.
|
|
19
|
+
4. Existing envelopes with free-form `severity` strings remain valid.
|
|
20
|
+
|
|
21
|
+
## Not in this release
|
|
22
|
+
|
|
23
|
+
- Frontend `ActionIntervention` `tone` (ships in `@commandtower/frontend` **v0.1.6**)
|
|
24
|
+
- New HTTP routes or RBAC entities
|
data/docs/upgrades/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Host-facing upgrade / change summaries for CommandTower releases.
|
|
|
4
4
|
|
|
5
5
|
| Version | Summary |
|
|
6
6
|
|---------|---------|
|
|
7
|
+
| [0.13.1](0.13.1.md) | Canonical `Intervention::Severity` constants (`blocking`, `warning`, `informational`) |
|
|
8
|
+
| [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 |
|
|
7
9
|
| [0.12.0](0.12.0.md) | Intervention envelope serializers/deserializers; product-tool admin_scope without Users/Audit narrowing |
|
|
8
10
|
| [0.11.1](0.11.1.md) | Audit Event `attribute :json` (MariaDB); audit migration without `utf8mb4_0900_ai_ci` |
|
|
9
11
|
| [0.11.0](0.11.0.md) | Execution context, audit ledger, Admin Workspace, principal capabilities, impersonation, Admin Users |
|
|
@@ -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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Intervention
|
|
5
|
+
# Canonical blocker severity strings for host-authored action interventions.
|
|
6
|
+
# Transport-only: CT FE presentation (`tone`) is host-owned and is not
|
|
7
|
+
# serialized on the envelope. Hosts may map these severities to FE tones.
|
|
8
|
+
#
|
|
9
|
+
# - BLOCKING — policy / hard stop (typical FE tone: destructive)
|
|
10
|
+
# - WARNING — attention without framing as an error (typical FE tone: warning)
|
|
11
|
+
# - INFORMATIONAL — soft notice (typical FE tone: informational)
|
|
12
|
+
module Severity
|
|
13
|
+
BLOCKING = "blocking"
|
|
14
|
+
WARNING = "warning"
|
|
15
|
+
INFORMATIONAL = "informational"
|
|
16
|
+
|
|
17
|
+
ALL = [BLOCKING, WARNING, INFORMATIONAL].freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/command_tower.rb
CHANGED
|
@@ -19,6 +19,7 @@ require "command_tower/configuration/config"
|
|
|
19
19
|
require "command_tower/install/baseline"
|
|
20
20
|
require "command_tower/install/doctor"
|
|
21
21
|
require "command_tower/credential_resolution"
|
|
22
|
+
require "command_tower/intervention/severity"
|
|
22
23
|
require "command_tower/jwt/authorization_helper"
|
|
23
24
|
require "command_tower/jwt/csrf_helper"
|
|
24
25
|
require "command_tower/password_recovery/authorization_helper"
|
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.1
|
|
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-30 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,8 @@ 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
|
|
778
|
+
- docs/upgrades/0.13.1.md
|
|
768
779
|
- docs/upgrades/README.md
|
|
769
780
|
- lib/command_tower.rb
|
|
770
781
|
- lib/command_tower/admin_scope.rb
|
|
@@ -787,6 +798,7 @@ files:
|
|
|
787
798
|
- lib/command_tower/authorization/effective_entity_grants.rb
|
|
788
799
|
- lib/command_tower/authorization/entity.rb
|
|
789
800
|
- lib/command_tower/authorization/role.rb
|
|
801
|
+
- lib/command_tower/configuration/account_deletion/config.rb
|
|
790
802
|
- lib/command_tower/configuration/admin/config.rb
|
|
791
803
|
- lib/command_tower/configuration/admin_scope/config.rb
|
|
792
804
|
- lib/command_tower/configuration/admin_scope/tool_registration.rb
|
|
@@ -845,6 +857,7 @@ files:
|
|
|
845
857
|
- lib/command_tower/impersonation/establish_identity.rb
|
|
846
858
|
- lib/command_tower/install/baseline.rb
|
|
847
859
|
- lib/command_tower/install/doctor.rb
|
|
860
|
+
- lib/command_tower/intervention/severity.rb
|
|
848
861
|
- lib/command_tower/jwt/authorization_helper.rb
|
|
849
862
|
- lib/command_tower/jwt/csrf_helper.rb
|
|
850
863
|
- lib/command_tower/logging/lifecycle_declaration.rb
|