command_tower 0.10.0 → 0.11.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/README.md +7 -3
- data/app/auth/command_tower/auth/auth_context.rb +10 -4
- data/app/controllers/command_tower/admin/application_controller.rb +27 -0
- data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
- data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
- data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
- data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
- data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
- data/app/controllers/command_tower/admin/users_controller.rb +46 -0
- data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
- data/app/controllers/command_tower/application_controller.rb +17 -1
- data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
- data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
- data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
- data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
- data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
- data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
- data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
- data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
- data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
- data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
- data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
- data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
- data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
- data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
- data/app/jobs/command_tower/application_job.rb +7 -0
- data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
- data/app/mailers/command_tower/application_mailer.rb +11 -1
- data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
- data/app/models/command_tower/audit/event.rb +36 -0
- data/app/models/command_tower/impersonation/session.rb +51 -0
- data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
- data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
- data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
- data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
- data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
- data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
- data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
- data/app/services/command_tower/README.md +4 -10
- data/app/services/command_tower/authorize/validate.rb +7 -4
- data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
- data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
- data/app/services/command_tower/jwt/login_create.rb +7 -4
- data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
- data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
- data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
- data/app/services/command_tower/service_base.rb +30 -43
- data/app/services/command_tower/service_logging.rb +2 -36
- data/app/services/command_tower/services/account/clear_phone.rb +8 -0
- data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
- data/app/services/command_tower/services/account/update_phone.rb +9 -1
- data/app/services/command_tower/services/admin/users.rb +377 -0
- data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
- data/app/services/command_tower/services/application_service.rb +37 -0
- data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
- data/app/services/command_tower/services/audit/events.rb +199 -0
- data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
- data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
- data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
- data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
- data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
- data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
- data/app/services/command_tower/services/impersonation/create.rb +29 -0
- data/app/services/command_tower/services/impersonation/end.rb +28 -0
- data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
- data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
- data/app/services/command_tower/services/me/change_password.rb +12 -0
- data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
- data/app/workflows/command_tower/transactional.rb +65 -0
- data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
- data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
- data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
- data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
- data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
- data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
- data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
- data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
- data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
- data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
- data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
- data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
- data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
- data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
- data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
- data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
- data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
- data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
- data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
- data/config/routes.rb +20 -0
- data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
- data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
- data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
- data/docs/admin_workspace.md +158 -0
- data/docs/api_reference.md +175 -14
- data/docs/architecture.md +5 -0
- data/docs/audit.md +195 -0
- data/docs/authentication.md +14 -0
- data/docs/authentication_authorization_guide.md +15 -11
- data/docs/authorization.md +25 -4
- data/docs/controllers.md +7 -1
- data/docs/eventing.md +179 -0
- data/docs/extending.md +32 -1
- data/docs/host_integration_guide.md +103 -12
- data/docs/initializing.md +2 -2
- data/docs/messaging_integration_guide.md +1 -1
- data/docs/models.md +4 -0
- data/docs/pagination.md +2 -2
- data/docs/principal_capabilities.md +89 -0
- data/docs/upgrades/0.10.0.md +2 -2
- data/docs/upgrades/0.11.0.md +92 -0
- data/docs/upgrades/README.md +1 -0
- data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
- data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
- data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
- data/lib/command_tower/admin_scope/resolve.rb +31 -0
- data/lib/command_tower/admin_scope/scope_context.rb +7 -0
- data/lib/command_tower/admin_scope/scope_option.rb +7 -0
- data/lib/command_tower/admin_scope.rb +20 -0
- data/lib/command_tower/admin_workspace.rb +16 -0
- data/lib/command_tower/audit/attribution.rb +90 -0
- data/lib/command_tower/audit/emit.rb +113 -0
- data/lib/command_tower/audit/masking.rb +41 -0
- data/lib/command_tower/audit/payload.rb +128 -0
- data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
- data/lib/command_tower/audit.rb +27 -0
- data/lib/command_tower/authorization/assignable_roles.rb +36 -0
- data/lib/command_tower/authorization/default.yml +99 -5
- data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
- data/lib/command_tower/authorization/entity.rb +15 -5
- data/lib/command_tower/authorization/role.rb +5 -4
- data/lib/command_tower/authorization.rb +71 -10
- data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
- data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
- data/lib/command_tower/configuration/authorization/config.rb +6 -0
- data/lib/command_tower/configuration/config.rb +20 -0
- data/lib/command_tower/configuration/impersonation/config.rb +38 -0
- data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
- data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
- data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
- data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
- data/lib/command_tower/configuration/registry/config.rb +34 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
- data/lib/command_tower/current.rb +12 -0
- data/lib/command_tower/engine.rb +19 -4
- data/lib/command_tower/events.rb +184 -0
- data/lib/command_tower/execution.rb +111 -0
- data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
- data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
- data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
- data/lib/command_tower/impersonation/establish_identity.rb +44 -0
- data/lib/command_tower/install/baseline.rb +3 -0
- data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
- data/lib/command_tower/logging/projection.rb +67 -0
- data/lib/command_tower/logging/subscriber.rb +103 -0
- data/lib/command_tower/principal_capabilities.rb +15 -0
- data/lib/command_tower/version.rb +1 -1
- data/lib/command_tower.rb +12 -0
- data/spec/factories/impersonation_session.rb +17 -0
- data/spec/factories/user.rb +16 -0
- metadata +112 -2
- data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
|
@@ -3,25 +3,122 @@
|
|
|
3
3
|
module CommandTower
|
|
4
4
|
module Workflows
|
|
5
5
|
class ApplicationWorkflow
|
|
6
|
+
include CommandTower::Transactional
|
|
7
|
+
include CommandTower::Execution::ContextAccess
|
|
8
|
+
include CommandTower::Logging::LifecycleDeclaration
|
|
9
|
+
include CommandTower::Impersonation::ActivityDeclaration
|
|
10
|
+
log_lifecycle!
|
|
11
|
+
|
|
12
|
+
TransactionFailure = CommandTower::Transactional::TransactionFailure
|
|
13
|
+
InvalidTransactionResult = CommandTower::Transactional::InvalidTransactionResult
|
|
14
|
+
|
|
15
|
+
ALLOWED_RETRY_STRATEGIES = %i[none delayed_continuation scheduled_cadence].freeze
|
|
16
|
+
|
|
17
|
+
protected :transaction, :fail_transaction!
|
|
18
|
+
|
|
19
|
+
# Programmer error: deferred result used with a strategy that forbids continuation.
|
|
20
|
+
class InvalidDeferredResult < StandardError; end
|
|
21
|
+
|
|
22
|
+
# Programmer error: delayed continuation requires a job instance to replay.
|
|
23
|
+
class DelayedContinuationRequiresJob < StandardError; end
|
|
24
|
+
|
|
6
25
|
class << self
|
|
7
26
|
def call(**args)
|
|
8
27
|
validate_retry_strategy!
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
CommandTower::Events.around_execution(layer: :workflow, subject: name, log_lifecycle: lifecycle_loggable?) do |record|
|
|
29
|
+
begin
|
|
30
|
+
record[:result] = new.call(**args)
|
|
31
|
+
rescue TransactionFailure => e
|
|
32
|
+
record[:result] = e.result
|
|
33
|
+
rescue InvalidTransactionResult
|
|
34
|
+
raise
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
record[:unexpected] = e
|
|
37
|
+
record[:result] = WorkflowResult.failure(
|
|
38
|
+
errors: [CommandTower::Errors::InternalError.new(cause: e)],
|
|
39
|
+
http_status: :internal_server_error
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
mark_impersonation_activity!(record[:result])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Job transport entry. Unexpected exceptions re-raise. Deferred continuation
|
|
47
|
+
# is scheduled by CommandTower when the strategy is :delayed_continuation.
|
|
48
|
+
def call_from_job(job: nil, continuation_attempt: 1, **args)
|
|
49
|
+
validate_retry_strategy!
|
|
50
|
+
result = invoke_for_job(**args)
|
|
51
|
+
handle_job_result(
|
|
52
|
+
result,
|
|
53
|
+
job: job,
|
|
54
|
+
continuation_attempt: Integer(continuation_attempt),
|
|
55
|
+
**args
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def retry_strategy(strategy, max_attempts: nil)
|
|
60
|
+
@retry_strategy = strategy&.to_sym
|
|
61
|
+
@continuation_max_attempts = max_attempts
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def declared_retry_strategy
|
|
65
|
+
@retry_strategy
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def continuation_max_attempts
|
|
69
|
+
@continuation_max_attempts
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mark_impersonation_activity!(result)
|
|
73
|
+
return unless impersonation_activity?
|
|
74
|
+
return unless result.is_a?(CommandTower::Workflows::WorkflowResult) && result.success?
|
|
75
|
+
|
|
76
|
+
CommandTower::Current.impersonation_activity_recorded = true
|
|
77
|
+
end
|
|
78
|
+
private :mark_impersonation_activity!
|
|
79
|
+
|
|
80
|
+
def validate_retry_strategy!
|
|
81
|
+
raise "#{name} must declare retry_strategy (:none, :delayed_continuation, or :scheduled_cadence)" if @retry_strategy.nil?
|
|
82
|
+
|
|
83
|
+
if @retry_strategy == :sidekiq
|
|
84
|
+
raise "#{name} retry_strategy :sidekiq is not supported; use :delayed_continuation"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
unless ALLOWED_RETRY_STRATEGIES.include?(@retry_strategy)
|
|
88
|
+
raise "#{name} retry_strategy #{@retry_strategy.inspect} is invalid " \
|
|
89
|
+
"(allowed: :none, :delayed_continuation, :scheduled_cadence)"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if @retry_strategy == :delayed_continuation
|
|
93
|
+
max = @continuation_max_attempts
|
|
94
|
+
unless max.is_a?(Integer) && max >= 1
|
|
95
|
+
raise "#{name} retry_strategy :delayed_continuation requires max_attempts: >= 1"
|
|
96
|
+
end
|
|
97
|
+
elsif !@continuation_max_attempts.nil?
|
|
98
|
+
raise "#{name} retry_strategy #{@retry_strategy.inspect} cannot declare max_attempts"
|
|
17
99
|
end
|
|
18
100
|
end
|
|
19
101
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def invoke_for_job(**args)
|
|
105
|
+
CommandTower::Events.around_execution(layer: :workflow, subject: name, log_lifecycle: lifecycle_loggable?) do |record|
|
|
106
|
+
begin
|
|
107
|
+
record[:result] = new.call(**args)
|
|
108
|
+
rescue TransactionFailure => e
|
|
109
|
+
record[:result] = e.result
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
record[:unexpected] = e
|
|
112
|
+
raise
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def handle_job_result(result, job:, continuation_attempt:, **args)
|
|
118
|
+
if result.deferred?
|
|
119
|
+
return handle_deferred(result, job: job, continuation_attempt: continuation_attempt, **args)
|
|
120
|
+
end
|
|
121
|
+
|
|
25
122
|
if result.failure? && result.meta[:propagate_to_job]
|
|
26
123
|
error = Array(result.errors).first
|
|
27
124
|
raise error if error
|
|
@@ -32,21 +129,29 @@ module CommandTower
|
|
|
32
129
|
result
|
|
33
130
|
end
|
|
34
131
|
|
|
35
|
-
def
|
|
36
|
-
@retry_strategy
|
|
37
|
-
|
|
132
|
+
def handle_deferred(result, job:, continuation_attempt:, **args)
|
|
133
|
+
unless @retry_strategy == :delayed_continuation
|
|
134
|
+
raise InvalidDeferredResult,
|
|
135
|
+
"#{name} returned deferred but retry_strategy is #{@retry_strategy.inspect}"
|
|
136
|
+
end
|
|
38
137
|
|
|
39
|
-
|
|
40
|
-
|
|
138
|
+
max_attempts = Integer(@continuation_max_attempts)
|
|
139
|
+
if continuation_attempt >= max_attempts
|
|
140
|
+
raise CommandTower::Errors::ContinuationExhaustedError.new(
|
|
141
|
+
details: { attempt: continuation_attempt, max_attempts: max_attempts }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
41
144
|
|
|
42
|
-
|
|
43
|
-
|
|
145
|
+
if job.nil?
|
|
146
|
+
raise DelayedContinuationRequiresJob,
|
|
147
|
+
"#{name} deferred continuation requires call_from_job(job:)"
|
|
148
|
+
end
|
|
44
149
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"#{exception.backtrace&.join("\n")}"
|
|
150
|
+
wait = [Integer(result.retry_after), 1].max
|
|
151
|
+
job.class.set(wait: wait.seconds).perform_later(
|
|
152
|
+
**args.merge(continuation_attempt: continuation_attempt + 1)
|
|
49
153
|
)
|
|
154
|
+
result
|
|
50
155
|
end
|
|
51
156
|
end
|
|
52
157
|
|
|
@@ -63,6 +168,40 @@ module CommandTower
|
|
|
63
168
|
def failure(errors:, http_status:, response_effects: nil, meta: {})
|
|
64
169
|
WorkflowResult.failure(errors:, http_status:, response_effects:, meta:)
|
|
65
170
|
end
|
|
171
|
+
|
|
172
|
+
def deferred(reason:, retry_after:, payload: {}, http_status: :accepted, response_effects: nil, meta: {})
|
|
173
|
+
WorkflowResult.deferred(
|
|
174
|
+
reason:,
|
|
175
|
+
retry_after:,
|
|
176
|
+
payload:,
|
|
177
|
+
http_status:,
|
|
178
|
+
response_effects:,
|
|
179
|
+
meta:
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def acceptable_success_result?(result)
|
|
186
|
+
result.is_a?(WorkflowResult) && result.success?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def acceptable_failure_result?(result)
|
|
190
|
+
result.is_a?(WorkflowResult) && result.failure?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def fail_transaction_type_error_message
|
|
194
|
+
"fail_transaction! requires a failed WorkflowResult"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def invalid_success_result_message(result)
|
|
198
|
+
unless result.is_a?(WorkflowResult)
|
|
199
|
+
return "transaction block must return a WorkflowResult; got #{result.class}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
"transaction block returned a non-success WorkflowResult; " \
|
|
203
|
+
"use fail_transaction!(failure(...)) for expected failures"
|
|
204
|
+
end
|
|
66
205
|
end
|
|
67
206
|
end
|
|
68
207
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Audit
|
|
6
|
+
module ErrorMapping
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def http_status_for(error)
|
|
10
|
+
case error
|
|
11
|
+
when CommandTower::Errors::ValidationError
|
|
12
|
+
:unprocessable_entity
|
|
13
|
+
when CommandTower::Errors::ForbiddenError
|
|
14
|
+
:forbidden
|
|
15
|
+
when CommandTower::Errors::NotFoundError
|
|
16
|
+
:not_found
|
|
17
|
+
else
|
|
18
|
+
:internal_server_error
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class FilterOptionsForAdminWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
projected = CommandTower::Services::Audit::Events::FilterOptions.call(viewer_scope: :admin)
|
|
12
|
+
return result_or_failure(projected) unless projected.success?
|
|
13
|
+
|
|
14
|
+
payload = CommandTower::Serializers::Audit::Events::FilterOptionsSerializer.serialize(
|
|
15
|
+
event_names: projected.data[:event_names],
|
|
16
|
+
subject_types: projected.data[:subject_types],
|
|
17
|
+
attribution_modes: projected.data[:attribution_modes]
|
|
18
|
+
)
|
|
19
|
+
success(payload:, http_status: :ok)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def result_or_failure(result)
|
|
25
|
+
failure(
|
|
26
|
+
errors: result.errors,
|
|
27
|
+
http_status: CommandTower::Workflows::Audit::ErrorMapping.http_status_for(result.errors.first)
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class FilterOptionsForUserWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
projected = CommandTower::Services::Audit::Events::FilterOptions.call(viewer_scope: :me)
|
|
12
|
+
return result_or_failure(projected) unless projected.success?
|
|
13
|
+
|
|
14
|
+
payload = CommandTower::Serializers::Audit::Events::FilterOptionsSerializer.serialize(
|
|
15
|
+
event_names: projected.data[:event_names],
|
|
16
|
+
subject_types: projected.data[:subject_types],
|
|
17
|
+
attribution_modes: projected.data[:attribution_modes]
|
|
18
|
+
)
|
|
19
|
+
success(payload:, http_status: :ok)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def result_or_failure(result)
|
|
25
|
+
failure(
|
|
26
|
+
errors: result.errors,
|
|
27
|
+
http_status: CommandTower::Workflows::Audit::ErrorMapping.http_status_for(result.errors.first)
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class ListForAdminWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(
|
|
11
|
+
limit:,
|
|
12
|
+
offset:,
|
|
13
|
+
user:,
|
|
14
|
+
scope_value: nil,
|
|
15
|
+
actions: nil,
|
|
16
|
+
occurred_after: nil,
|
|
17
|
+
occurred_before: nil,
|
|
18
|
+
subject_types: nil,
|
|
19
|
+
affected_user_id: nil,
|
|
20
|
+
actor_user_id: nil,
|
|
21
|
+
originating_administrator_id: nil,
|
|
22
|
+
attribution_mode: nil
|
|
23
|
+
)
|
|
24
|
+
scope_result = resolve_scope(user:, scope_value:)
|
|
25
|
+
return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
26
|
+
|
|
27
|
+
listed = CommandTower::Services::Audit::Events::List.call(
|
|
28
|
+
viewer_scope: :admin,
|
|
29
|
+
limit:,
|
|
30
|
+
offset:,
|
|
31
|
+
actions:,
|
|
32
|
+
occurred_after:,
|
|
33
|
+
occurred_before:,
|
|
34
|
+
subject_types:,
|
|
35
|
+
affected_user_id:,
|
|
36
|
+
actor_user_id:,
|
|
37
|
+
originating_administrator_id:,
|
|
38
|
+
attribution_mode:,
|
|
39
|
+
principal: user,
|
|
40
|
+
scope_context: scope_result
|
|
41
|
+
)
|
|
42
|
+
return result_or_failure(listed) unless listed.success?
|
|
43
|
+
|
|
44
|
+
payload = listed.data[:events].filter_map do |event|
|
|
45
|
+
projected = CommandTower::Services::Audit::Events::Project.call(event:, viewer: :admin)
|
|
46
|
+
return result_or_failure(projected) unless projected.success?
|
|
47
|
+
|
|
48
|
+
CommandTower::Serializers::Audit::Events::EventSerializer.serialize(projected.data[:projection])
|
|
49
|
+
end
|
|
50
|
+
meta = CommandTower::Serializers::Audit::Events::PaginationMetaSerializer.serialize(listed.data[:pagination])
|
|
51
|
+
success(payload:, meta:, http_status: :ok)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def resolve_scope(user:, scope_value:)
|
|
57
|
+
CommandTower::Workflows::Admin::ScopeResolution.resolve(
|
|
58
|
+
tool_id: "audit",
|
|
59
|
+
user:,
|
|
60
|
+
scope_value:
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def result_or_failure(result)
|
|
65
|
+
failure(
|
|
66
|
+
errors: result.errors,
|
|
67
|
+
http_status: CommandTower::Workflows::Audit::ErrorMapping.http_status_for(result.errors.first)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class ListForUserWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(user:, limit:, offset:, actions: nil, occurred_after: nil, occurred_before: nil, subject_types: nil)
|
|
11
|
+
listed = CommandTower::Services::Audit::Events::List.call(
|
|
12
|
+
viewer_scope: :user,
|
|
13
|
+
affected_user_id: user.id,
|
|
14
|
+
limit:,
|
|
15
|
+
offset:,
|
|
16
|
+
actions:,
|
|
17
|
+
occurred_after:,
|
|
18
|
+
occurred_before:,
|
|
19
|
+
subject_types:
|
|
20
|
+
)
|
|
21
|
+
return result_or_failure(listed) unless listed.success?
|
|
22
|
+
|
|
23
|
+
payload = listed.data[:events].filter_map do |event|
|
|
24
|
+
projected = CommandTower::Services::Audit::Events::Project.call(event:, viewer: :user)
|
|
25
|
+
return result_or_failure(projected) unless projected.success?
|
|
26
|
+
|
|
27
|
+
CommandTower::Serializers::Audit::Events::EventSerializer.serialize(projected.data[:projection])
|
|
28
|
+
end
|
|
29
|
+
meta = CommandTower::Serializers::Audit::Events::PaginationMetaSerializer.serialize(listed.data[:pagination])
|
|
30
|
+
success(payload:, meta:, http_status: :ok)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def result_or_failure(result)
|
|
36
|
+
failure(
|
|
37
|
+
errors: result.errors,
|
|
38
|
+
http_status: CommandTower::Workflows::Audit::ErrorMapping.http_status_for(result.errors.first)
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class ShowForAdminWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(id:, user:, scope_value: nil)
|
|
11
|
+
scope_result = resolve_scope(user:, scope_value:)
|
|
12
|
+
return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
shown = CommandTower::Services::Audit::Events::Show.call(
|
|
15
|
+
viewer_scope: :admin,
|
|
16
|
+
id:,
|
|
17
|
+
principal: user,
|
|
18
|
+
scope_context: scope_result
|
|
19
|
+
)
|
|
20
|
+
return result_or_failure(shown) unless shown.success?
|
|
21
|
+
|
|
22
|
+
projected = CommandTower::Services::Audit::Events::Project.call(
|
|
23
|
+
event: shown.data[:event],
|
|
24
|
+
viewer: :admin
|
|
25
|
+
)
|
|
26
|
+
return result_or_failure(projected) unless projected.success?
|
|
27
|
+
|
|
28
|
+
payload = CommandTower::Serializers::Audit::Events::EventSerializer.serialize(
|
|
29
|
+
projected.data[:projection]
|
|
30
|
+
)
|
|
31
|
+
success(payload:, http_status: :ok)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def resolve_scope(user:, scope_value:)
|
|
37
|
+
CommandTower::Workflows::Admin::ScopeResolution.resolve(
|
|
38
|
+
tool_id: "audit",
|
|
39
|
+
user:,
|
|
40
|
+
scope_value:
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def result_or_failure(result)
|
|
45
|
+
failure(
|
|
46
|
+
errors: result.errors,
|
|
47
|
+
http_status: CommandTower::Workflows::Audit::ErrorMapping.http_status_for(result.errors.first)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class ShowForUserWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(user:, id:)
|
|
11
|
+
shown = CommandTower::Services::Audit::Events::Show.call(
|
|
12
|
+
viewer_scope: :user,
|
|
13
|
+
affected_user_id: user.id,
|
|
14
|
+
id:
|
|
15
|
+
)
|
|
16
|
+
return result_or_failure(shown) unless shown.success?
|
|
17
|
+
|
|
18
|
+
projected = CommandTower::Services::Audit::Events::Project.call(
|
|
19
|
+
event: shown.data[:event],
|
|
20
|
+
viewer: :user
|
|
21
|
+
)
|
|
22
|
+
return result_or_failure(projected) unless projected.success?
|
|
23
|
+
|
|
24
|
+
payload = CommandTower::Serializers::Audit::Events::EventSerializer.serialize(
|
|
25
|
+
projected.data[:projection]
|
|
26
|
+
)
|
|
27
|
+
success(payload:, http_status: :ok)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def result_or_failure(result)
|
|
33
|
+
failure(
|
|
34
|
+
errors: result.errors,
|
|
35
|
+
http_status: CommandTower::Workflows::Audit::ErrorMapping.http_status_for(result.errors.first)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -6,11 +6,12 @@ module CommandTower
|
|
|
6
6
|
class AuthenticateRequestWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
7
7
|
retry_strategy :none
|
|
8
8
|
|
|
9
|
-
def call(request:, response:, bypass_email_validation: false)
|
|
9
|
+
def call(request:, response:, bypass_email_validation: false, overlay_mode: :enforce)
|
|
10
10
|
request_context = CommandTower::Auth::RequestContext.from(request, response)
|
|
11
11
|
auth_result = CommandTower::Services::Auth::AuthenticateSession.call(
|
|
12
12
|
request_context: request_context,
|
|
13
|
-
bypass_email_validation: bypass_email_validation
|
|
13
|
+
bypass_email_validation: bypass_email_validation,
|
|
14
|
+
overlay_mode: overlay_mode
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
unless auth_result.success?
|
|
@@ -10,7 +10,11 @@ module CommandTower
|
|
|
10
10
|
|
|
11
11
|
def for_auth_failure(metadata)
|
|
12
12
|
effects = {}
|
|
13
|
-
|
|
13
|
+
# 412 email verification is a live session that still needs the JWT
|
|
14
|
+
# cookie so the client can send/verify the code.
|
|
15
|
+
if metadata[:cookie_authenticated] && metadata[:authentication_failed] &&
|
|
16
|
+
!metadata[:email_verification_required] &&
|
|
17
|
+
!metadata[:impersonation_session_expired]
|
|
14
18
|
effects[:clear_auth_cookie] = true
|
|
15
19
|
end
|
|
16
20
|
effects
|
|
@@ -6,13 +6,53 @@ module CommandTower
|
|
|
6
6
|
class LogoutWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
7
7
|
retry_strategy :none
|
|
8
8
|
|
|
9
|
-
def call(
|
|
9
|
+
def call(token: nil)
|
|
10
|
+
maybe_audit_session_cleared(token)
|
|
10
11
|
success(
|
|
11
12
|
payload: CommandTower::Serializers::Auth::LogoutResponseSerializer.serialize,
|
|
12
13
|
http_status: :ok,
|
|
13
14
|
response_effects: { clear_token: true }
|
|
14
15
|
)
|
|
15
16
|
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def maybe_audit_session_cleared(token)
|
|
21
|
+
return if token.to_s.strip.empty?
|
|
22
|
+
|
|
23
|
+
outcome = CommandTower::Jwt::AuthenticateUser.(token:, bypass_email_validation: true)
|
|
24
|
+
return unless outcome.success?
|
|
25
|
+
|
|
26
|
+
end_overlay_session(outcome)
|
|
27
|
+
audit(:session_cleared, affected_user: outcome.user, changes: {})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def end_overlay_session(outcome)
|
|
31
|
+
session_id = outcome.impersonation_session_id.to_s.strip
|
|
32
|
+
return if session_id.empty?
|
|
33
|
+
|
|
34
|
+
ended = CommandTower::Services::Impersonation::End.call(
|
|
35
|
+
session_id:,
|
|
36
|
+
reason: "logout",
|
|
37
|
+
actor_user_id: outcome.user.id
|
|
38
|
+
)
|
|
39
|
+
return unless ended.success? && ended.data[:ended]
|
|
40
|
+
|
|
41
|
+
session = ended.data[:session]
|
|
42
|
+
target = User.find_by(id: session.target_user_id)
|
|
43
|
+
return if target.nil?
|
|
44
|
+
|
|
45
|
+
CommandTower::Impersonation::ClearOverlayForAudit.call(actor_user_id: outcome.user.id) do
|
|
46
|
+
audit(
|
|
47
|
+
:impersonation_ended,
|
|
48
|
+
subject: target,
|
|
49
|
+
affected_user: target,
|
|
50
|
+
changes: { reason: { from: nil, to: "logout" } },
|
|
51
|
+
metadata: { impersonation_session_id: session.id },
|
|
52
|
+
attribution_mode: :admin_direct
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
16
56
|
end
|
|
17
57
|
end
|
|
18
58
|
end
|
|
@@ -15,6 +15,8 @@ module CommandTower
|
|
|
15
15
|
|
|
16
16
|
if login_result.success?
|
|
17
17
|
data = login_result.data
|
|
18
|
+
CommandTower::Execution::IdentityEnrichment.from_user(data[:user])
|
|
19
|
+
audit(:session_created, affected_user: data[:user], changes: {})
|
|
18
20
|
return success(
|
|
19
21
|
payload: CommandTower::Serializers::Auth::LoginResponseSerializer.serialize(
|
|
20
22
|
user: data[:user],
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Auth
|
|
6
|
+
module PrincipalCapabilities
|
|
7
|
+
class ShowWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(user:)
|
|
11
|
+
result = CommandTower::Services::Auth::PrincipalCapabilities::Project.call(user:)
|
|
12
|
+
unless result.success?
|
|
13
|
+
return failure(errors: result.errors, http_status: :unprocessable_entity)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
success(
|
|
17
|
+
payload: CommandTower::Serializers::Auth::PrincipalCapabilitiesSerializer.serialize(
|
|
18
|
+
result.data[:principal_capability_ids]
|
|
19
|
+
),
|
|
20
|
+
http_status: :ok
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|