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.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -3
  3. data/app/auth/command_tower/auth/auth_context.rb +10 -4
  4. data/app/controllers/command_tower/admin/application_controller.rb +27 -0
  5. data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
  6. data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
  7. data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
  8. data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
  9. data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
  10. data/app/controllers/command_tower/admin/users_controller.rb +46 -0
  11. data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
  12. data/app/controllers/command_tower/application_controller.rb +17 -1
  13. data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
  14. data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
  15. data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
  16. data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
  17. data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
  18. data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
  19. data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
  20. data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
  21. data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
  22. data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
  23. data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
  24. data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
  25. data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
  26. data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
  27. data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
  28. data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
  29. data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
  30. data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
  31. data/app/jobs/command_tower/application_job.rb +7 -0
  32. data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
  33. data/app/mailers/command_tower/application_mailer.rb +11 -1
  34. data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
  35. data/app/models/command_tower/audit/event.rb +36 -0
  36. data/app/models/command_tower/impersonation/session.rb +51 -0
  37. data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
  38. data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
  39. data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
  40. data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
  41. data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
  42. data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
  43. data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
  44. data/app/services/command_tower/README.md +4 -10
  45. data/app/services/command_tower/authorize/validate.rb +7 -4
  46. data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
  47. data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
  48. data/app/services/command_tower/jwt/login_create.rb +7 -4
  49. data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
  50. data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
  51. data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
  52. data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
  53. data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
  54. data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
  55. data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
  56. data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
  57. data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
  58. data/app/services/command_tower/service_base.rb +30 -43
  59. data/app/services/command_tower/service_logging.rb +2 -36
  60. data/app/services/command_tower/services/account/clear_phone.rb +8 -0
  61. data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
  62. data/app/services/command_tower/services/account/update_phone.rb +9 -1
  63. data/app/services/command_tower/services/admin/users.rb +377 -0
  64. data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
  65. data/app/services/command_tower/services/application_service.rb +37 -0
  66. data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
  67. data/app/services/command_tower/services/audit/events.rb +199 -0
  68. data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
  69. data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
  70. data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
  71. data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
  72. data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
  73. data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
  74. data/app/services/command_tower/services/impersonation/create.rb +29 -0
  75. data/app/services/command_tower/services/impersonation/end.rb +28 -0
  76. data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
  77. data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
  78. data/app/services/command_tower/services/me/change_password.rb +12 -0
  79. data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
  80. data/app/workflows/command_tower/transactional.rb +65 -0
  81. data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
  82. data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
  83. data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
  84. data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
  85. data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
  86. data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
  87. data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
  88. data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
  89. data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
  90. data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
  91. data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
  92. data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
  93. data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
  94. data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
  95. data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
  96. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
  97. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
  98. data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
  99. data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
  100. data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
  101. data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
  102. data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
  103. data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
  104. data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
  105. data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
  106. data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
  107. data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
  108. data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
  109. data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
  110. data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
  111. data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
  112. data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
  113. data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
  114. data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
  115. data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
  116. data/config/routes.rb +20 -0
  117. data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
  118. data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
  119. data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
  120. data/docs/admin_workspace.md +158 -0
  121. data/docs/api_reference.md +175 -14
  122. data/docs/architecture.md +5 -0
  123. data/docs/audit.md +195 -0
  124. data/docs/authentication.md +14 -0
  125. data/docs/authentication_authorization_guide.md +15 -11
  126. data/docs/authorization.md +25 -4
  127. data/docs/controllers.md +7 -1
  128. data/docs/eventing.md +179 -0
  129. data/docs/extending.md +32 -1
  130. data/docs/host_integration_guide.md +103 -12
  131. data/docs/initializing.md +2 -2
  132. data/docs/messaging_integration_guide.md +1 -1
  133. data/docs/models.md +4 -0
  134. data/docs/pagination.md +2 -2
  135. data/docs/principal_capabilities.md +89 -0
  136. data/docs/upgrades/0.10.0.md +2 -2
  137. data/docs/upgrades/0.11.0.md +92 -0
  138. data/docs/upgrades/README.md +1 -0
  139. data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
  140. data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
  141. data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
  142. data/lib/command_tower/admin_scope/resolve.rb +31 -0
  143. data/lib/command_tower/admin_scope/scope_context.rb +7 -0
  144. data/lib/command_tower/admin_scope/scope_option.rb +7 -0
  145. data/lib/command_tower/admin_scope.rb +20 -0
  146. data/lib/command_tower/admin_workspace.rb +16 -0
  147. data/lib/command_tower/audit/attribution.rb +90 -0
  148. data/lib/command_tower/audit/emit.rb +113 -0
  149. data/lib/command_tower/audit/masking.rb +41 -0
  150. data/lib/command_tower/audit/payload.rb +128 -0
  151. data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
  152. data/lib/command_tower/audit.rb +27 -0
  153. data/lib/command_tower/authorization/assignable_roles.rb +36 -0
  154. data/lib/command_tower/authorization/default.yml +99 -5
  155. data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
  156. data/lib/command_tower/authorization/entity.rb +15 -5
  157. data/lib/command_tower/authorization/role.rb +5 -4
  158. data/lib/command_tower/authorization.rb +71 -10
  159. data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
  160. data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
  161. data/lib/command_tower/configuration/authorization/config.rb +6 -0
  162. data/lib/command_tower/configuration/config.rb +20 -0
  163. data/lib/command_tower/configuration/impersonation/config.rb +38 -0
  164. data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
  165. data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
  166. data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
  167. data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
  168. data/lib/command_tower/configuration/registry/config.rb +34 -0
  169. data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
  170. data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
  171. data/lib/command_tower/current.rb +12 -0
  172. data/lib/command_tower/engine.rb +19 -4
  173. data/lib/command_tower/events.rb +184 -0
  174. data/lib/command_tower/execution.rb +111 -0
  175. data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
  176. data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
  177. data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
  178. data/lib/command_tower/impersonation/establish_identity.rb +44 -0
  179. data/lib/command_tower/install/baseline.rb +3 -0
  180. data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
  181. data/lib/command_tower/logging/projection.rb +67 -0
  182. data/lib/command_tower/logging/subscriber.rb +103 -0
  183. data/lib/command_tower/principal_capabilities.rb +15 -0
  184. data/lib/command_tower/version.rb +1 -1
  185. data/lib/command_tower.rb +12 -0
  186. data/spec/factories/impersonation_session.rb +17 -0
  187. data/spec/factories/user.rb +16 -0
  188. metadata +112 -2
  189. 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
- begin
10
- new.call(**args)
11
- rescue StandardError => e
12
- log_unknown_exception(e)
13
- WorkflowResult.failure(
14
- errors: [CommandTower::Errors::InternalError.new(cause: e)],
15
- http_status: :internal_server_error
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
- # Job transport entry: same WorkflowResult as `.call`, but raises when the
21
- # workflow marks the failure for ActiveJob propagation (`meta[:propagate_to_job]`).
22
- # Controllers continue to use `.call` and render envelopes from WorkflowResult.
23
- def call_from_job(**args)
24
- result = call(**args)
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 retry_strategy(strategy)
36
- @retry_strategy = strategy
37
- end
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
- def validate_retry_strategy!
40
- return if @retry_strategy
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
- raise "#{name} must declare retry_strategy (:none, :sidekiq, or :scheduled_cadence)"
43
- end
145
+ if job.nil?
146
+ raise DelayedContinuationRequiresJob,
147
+ "#{name} deferred continuation requires call_from_job(job:)"
148
+ end
44
149
 
45
- def log_unknown_exception(exception)
46
- Rails.logger.error(
47
- "[#{name}] Unknown workflow exception: #{exception.class}: #{exception.message}\n" \
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
- if metadata[:cookie_authenticated] && metadata[:authentication_failed]
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(input: nil, request_context: nil)
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