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
@@ -15,31 +15,64 @@ module CommandTower
15
15
  )
16
16
  end
17
17
 
18
- register_result = CommandTower::Services::Auth::Register.call(
19
- first_name: input.first_name,
20
- last_name: input.last_name,
21
- username: input.username,
22
- email: input.email,
23
- password: input.password,
24
- password_confirmation: input.password_confirmation
25
- )
18
+ user = nil
19
+ result = transaction do
20
+ register_result = CommandTower::Services::Auth::Register.call(
21
+ first_name: input.first_name,
22
+ last_name: input.last_name,
23
+ username: input.username,
24
+ email: input.email,
25
+ password: input.password,
26
+ password_confirmation: input.password_confirmation
27
+ )
28
+ unless register_result.success?
29
+ fail_transaction!(
30
+ failure(
31
+ errors: register_result.errors,
32
+ http_status: SignupErrorStatus.http_status_for(register_result.errors.first)
33
+ )
34
+ )
35
+ end
26
36
 
27
- unless register_result.success?
28
- return failure(
29
- errors: register_result.errors,
30
- http_status: SignupErrorStatus.http_status_for(register_result.errors.first)
37
+ user = register_result.data[:user]
38
+ audit(
39
+ :user_registered,
40
+ subject: user,
41
+ affected_user: user,
42
+ changes: {}
31
43
  )
32
- end
33
44
 
34
- user = register_result.data[:user]
35
- emit_welcome(user)
45
+ roles_before = Array(user.roles)
46
+ assign_result = CommandTower::Services::Auth::AssignDefaultMembershipRole.call(user: user)
47
+ unless assign_result.success?
48
+ fail_transaction!(
49
+ failure(
50
+ errors: assign_result.errors,
51
+ http_status: :unprocessable_entity
52
+ )
53
+ )
54
+ end
36
55
 
37
- success(
38
- payload: CommandTower::Serializers::Auth::RegisterResponseSerializer.serialize(
39
- user: user
40
- ),
41
- http_status: :created
42
- )
56
+ user = assign_result.data[:user]
57
+ assigned_roles = Array(user.roles) - roles_before
58
+ if assigned_roles.any?
59
+ audit(
60
+ :role_assigned,
61
+ subject: user,
62
+ affected_user: user,
63
+ changes: { role: { from: nil, to: assigned_roles.first.to_s } }
64
+ )
65
+ end
66
+ success(
67
+ payload: CommandTower::Serializers::Auth::RegisterResponseSerializer.serialize(
68
+ user: user
69
+ ),
70
+ http_status: :created
71
+ )
72
+ end
73
+
74
+ emit_welcome(user) if result.success? && user.present?
75
+ result
43
76
  end
44
77
 
45
78
  private
@@ -69,13 +102,12 @@ module CommandTower
69
102
 
70
103
  def log_welcome_failure(user, error)
71
104
  payload = {
72
- event: "messaging.welcome_produce_failed",
73
105
  user_id: user.id,
74
106
  error_class: error.class.name,
107
+ log_level: :error
75
108
  }
76
109
  payload[:error_code] = error.code if error.respond_to?(:code)
77
- payload[:error_message] = error.message if error.respond_to?(:message)
78
- Rails.logger.error(payload.to_json)
110
+ publish_event(category: :messaging, name: :welcome_produce_failed, payload:)
79
111
  end
80
112
  end
81
113
  end
@@ -20,10 +20,14 @@ module CommandTower
20
20
  response_effects[:ensure_csrf_cookie] = { rotate: csrf_rotate_on_reset? }
21
21
  end
22
22
 
23
+ impersonation_session = impersonation_session_for(auth_context)
24
+
23
25
  success(
24
26
  payload: CommandTower::Serializers::Auth::SessionResponseSerializer.serialize(
25
27
  user: current_user,
26
- token_expires_at: auth_context.token_expires_at
28
+ token_expires_at: auth_context.token_expires_at,
29
+ impersonation_session:,
30
+ actor: auth_context.actor_user
27
31
  ),
28
32
  http_status: :ok,
29
33
  response_effects: response_effects
@@ -37,6 +41,16 @@ module CommandTower
37
41
 
38
42
  CommandTower.config.jwt.cookie.csrf.rotate_on_reset
39
43
  end
44
+
45
+ def impersonation_session_for(auth_context)
46
+ return unless CommandTower::Current.impersonation_active
47
+
48
+ session_id = auth_context.impersonation_session_id.presence ||
49
+ CommandTower::Current.impersonation_session_id
50
+ return if session_id.blank?
51
+
52
+ CommandTower::Impersonation::Session.find_by(id: session_id)
53
+ end
40
54
  end
41
55
  end
42
56
  end
@@ -18,6 +18,7 @@ module CommandTower
18
18
  CommandTower::Errors::ForbiddenError
19
19
  :forbidden
20
20
  when CommandTower::Errors::Auth::InvalidCredentialsError,
21
+ CommandTower::Errors::Auth::ImpersonationSessionExpiredError,
21
22
  CommandTower::Errors::UnauthorizedError
22
23
  :unauthorized
23
24
  else
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Impersonation
6
+ module ErrorMapping
7
+ module_function
8
+
9
+ def http_status_for(error)
10
+ case error
11
+ when CommandTower::Errors::Auth::SelfImpersonationError,
12
+ CommandTower::Errors::Auth::ImpersonationSessionMissingError,
13
+ CommandTower::Errors::ValidationError
14
+ :unprocessable_entity
15
+ when CommandTower::Errors::Auth::NestedImpersonationError,
16
+ CommandTower::Errors::ForbiddenError
17
+ :forbidden
18
+ when CommandTower::Errors::NotFoundError
19
+ :not_found
20
+ else
21
+ :internal_server_error
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Impersonation
6
+ class StartWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
+ retry_strategy :none
8
+
9
+ def call(id:, actor:, scope_value: nil)
10
+ if CommandTower::Current.impersonation_active
11
+ return failure(
12
+ errors: [CommandTower::Errors::Auth::NestedImpersonationError.new],
13
+ http_status: :forbidden
14
+ )
15
+ end
16
+
17
+ if actor.id == id
18
+ return failure(
19
+ errors: [CommandTower::Errors::Auth::SelfImpersonationError.new],
20
+ http_status: :unprocessable_entity
21
+ )
22
+ end
23
+
24
+ scope_result = CommandTower::Workflows::Admin::ScopeResolution.resolve(
25
+ tool_id: "users",
26
+ user: actor,
27
+ scope_value:
28
+ )
29
+ return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
30
+
31
+ shown = CommandTower::Services::Admin::Users::Show.call(
32
+ id:,
33
+ principal: actor,
34
+ scope_context: scope_result
35
+ )
36
+ unless shown.success?
37
+ return failure(
38
+ errors: shown.errors,
39
+ http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(shown.errors.first)
40
+ )
41
+ end
42
+
43
+ target = shown.data[:user]
44
+ created = CommandTower::Services::Impersonation::Create.call(actor:, target:)
45
+ unless created.success?
46
+ return failure(
47
+ errors: created.errors,
48
+ http_status: :internal_server_error
49
+ )
50
+ end
51
+
52
+ session = created.data[:session]
53
+ audit_started(session:, target:, scope_context: scope_result)
54
+
55
+ token = CommandTower::Jwt::LoginCreate.call(
56
+ user: actor,
57
+ impersonation_session_id: session.id
58
+ ).token
59
+ expires_at = CommandTower.config.jwt.ttl.from_now.to_time.to_s
60
+
61
+ success(
62
+ payload: CommandTower::Serializers::Impersonation::SessionSerializer.serialize(session),
63
+ http_status: :created,
64
+ response_effects: {
65
+ set_token: { token:, expires_at: }
66
+ }
67
+ )
68
+ end
69
+
70
+ private
71
+
72
+ def audit_started(session:, target:, scope_context:)
73
+ scope_class, host_context = audit_scope_for(scope_context)
74
+ audit(
75
+ :impersonation_started,
76
+ subject: target,
77
+ affected_user: target,
78
+ metadata: { impersonation_session_id: session.id },
79
+ attribution_mode: :admin_direct,
80
+ scope_class:,
81
+ host_context:
82
+ )
83
+ end
84
+
85
+ def audit_scope_for(scope_context)
86
+ return [:global, nil] if scope_context.nil?
87
+ return [:global, nil] unless CommandTower.config.admin_scope.registered?("users")
88
+
89
+ type = CommandTower.config.admin_scope.fetch("users").host_context_type
90
+ return [:global, nil] if type.to_s.strip.empty?
91
+
92
+ [:host, { type:, identifier: scope_context.scope_value }]
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Impersonation
6
+ class StopWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
+ retry_strategy :none
8
+
9
+ def call(actor:, impersonation_session_id:, token_expires_at: nil)
10
+ session_id = impersonation_session_id.to_s.strip
11
+ if session_id.empty?
12
+ return failure(
13
+ errors: [CommandTower::Errors::Auth::ImpersonationSessionMissingError.new],
14
+ http_status: :unprocessable_entity
15
+ )
16
+ end
17
+
18
+ ended = CommandTower::Services::Impersonation::End.call(
19
+ session_id:,
20
+ reason: "manual",
21
+ actor_user_id: actor.id
22
+ )
23
+ unless ended.success?
24
+ return failure(
25
+ errors: ended.errors,
26
+ http_status: CommandTower::Workflows::Impersonation::ErrorMapping.http_status_for(ended.errors.first)
27
+ )
28
+ end
29
+
30
+ session = ended.data[:session]
31
+ if session.nil? || session.actor_user_id != actor.id
32
+ return failure(
33
+ errors: [CommandTower::Errors::Auth::ImpersonationSessionMissingError.new],
34
+ http_status: :unprocessable_entity
35
+ )
36
+ end
37
+
38
+ if ended.data[:ended]
39
+ target = User.find_by(id: session.target_user_id)
40
+ if target
41
+ audit(
42
+ :impersonation_ended,
43
+ subject: target,
44
+ affected_user: target,
45
+ changes: { reason: { from: nil, to: "manual" } },
46
+ metadata: { impersonation_session_id: session.id },
47
+ attribution_mode: CommandTower::Current.impersonation_active ? nil : :admin_direct
48
+ )
49
+ end
50
+ end
51
+
52
+ token = CommandTower::Jwt::LoginCreate.call(user: actor).token
53
+ expires_at = token_expires_at.presence || CommandTower.config.jwt.ttl.from_now.to_time.to_s
54
+
55
+ success(
56
+ payload: { message: "impersonation_ended" },
57
+ http_status: :ok,
58
+ response_effects: {
59
+ set_token: { token:, expires_at: }
60
+ }
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -5,6 +5,7 @@ module CommandTower
5
5
  module Me
6
6
  class UpdateNameWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
7
  retry_strategy :none
8
+ impersonation_activity!
8
9
 
9
10
  def call(current_user:, first_name:, last_name:, auth_context: nil)
10
11
  update_result = CommandTower::Services::Me::UpdateName.call(
@@ -5,6 +5,7 @@ module CommandTower
5
5
  module Profile
6
6
  class ShowWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
7
  retry_strategy :none
8
+ impersonation_activity!
8
9
 
9
10
  def call(current_user:, auth_context: nil)
10
11
  success(
@@ -3,31 +3,55 @@
3
3
  module CommandTower
4
4
  module Workflows
5
5
  class WorkflowResult
6
- attr_reader :payload, :errors, :http_status, :response_effects, :meta
6
+ STATUSES = %i[success deferred failure].freeze
7
+
8
+ attr_reader :payload, :errors, :http_status, :response_effects, :meta, :reason, :retry_after
9
+
10
+ def initialize(status:, payload:, errors:, http_status:, response_effects: nil, meta: {},
11
+ reason: nil, retry_after: nil)
12
+ @status = status.to_sym
13
+ raise ArgumentError, "invalid WorkflowResult status #{@status}" unless STATUSES.include?(@status)
7
14
 
8
- def initialize(success:, payload:, errors:, http_status:, response_effects: nil, meta: {})
9
- @success = success
10
15
  @payload = payload
11
16
  @errors = errors
12
17
  @http_status = http_status
13
18
  @response_effects = response_effects
14
19
  @meta = meta
20
+ @reason = reason
21
+ @retry_after = retry_after
15
22
  end
16
23
 
17
24
  def success?
18
- @success
25
+ @status == :success
26
+ end
27
+
28
+ def deferred?
29
+ @status == :deferred
19
30
  end
20
31
 
21
32
  def failure?
22
- !success?
33
+ @status == :failure
23
34
  end
24
35
 
25
36
  def self.success(payload:, http_status: :ok, response_effects: nil, meta: {})
26
- new(success: true, payload:, errors: [], http_status:, response_effects:, meta:)
37
+ new(status: :success, payload:, errors: [], http_status:, response_effects:, meta:)
27
38
  end
28
39
 
29
40
  def self.failure(errors:, http_status:, response_effects: nil, meta: {})
30
- new(success: false, payload: nil, errors:, http_status:, response_effects:, meta:)
41
+ new(status: :failure, payload: nil, errors:, http_status:, response_effects:, meta:)
42
+ end
43
+
44
+ def self.deferred(reason:, retry_after:, payload: {}, http_status: :accepted, response_effects: nil, meta: {})
45
+ new(
46
+ status: :deferred,
47
+ payload:,
48
+ errors: [],
49
+ http_status:,
50
+ response_effects:,
51
+ meta:,
52
+ reason: reason.to_sym,
53
+ retry_after: Integer(retry_after)
54
+ )
31
55
  end
32
56
  end
33
57
  end
data/config/routes.rb CHANGED
@@ -11,6 +11,8 @@ CommandTower::Engine.routes.draw do
11
11
  namespace :auth do
12
12
  post "logout", to: "logout#create"
13
13
  get "session", to: "session#show"
14
+ get "principal-capabilities", to: "principal_capabilities#show"
15
+ delete "impersonation-session", to: "impersonation_session#destroy"
14
16
 
15
17
  constraints(->(_req) { CommandTower.config.login.plain_text.enable? }) do
16
18
  scope path: "plain-text", module: "plain_text" do
@@ -74,6 +76,10 @@ CommandTower::Engine.routes.draw do
74
76
  end
75
77
  end
76
78
 
79
+ get "audit-events/filter-options", to: "audit_events#filter_options"
80
+ get "audit-events", to: "audit_events#index"
81
+ get "audit-events/:id", to: "audit_events#show"
82
+
77
83
  resource :preferences, only: [:show], controller: "preferences"
78
84
  patch "preferences/:notification_type_key", to: "preferences#update"
79
85
 
@@ -96,6 +102,20 @@ CommandTower::Engine.routes.draw do
96
102
  end
97
103
 
98
104
  namespace :admin do
105
+ get "audit-events/filter-options", to: "audit/events#filter_options"
106
+ get "audit-events", to: "audit/events#index"
107
+ get "audit-events/:id", to: "audit/events#show"
108
+ get "users", to: "users#index"
109
+ get "users/assignable-roles", to: "users/roles#index"
110
+ get "users/:id", to: "users#show"
111
+ patch "users/:id/name", to: "users/identities#update_name"
112
+ patch "users/:id/username", to: "users/identities#update_username"
113
+ patch "users/:id/email", to: "users/identities#update_email"
114
+ patch "users/:id/email-validation", to: "users/identities#update_email_validation"
115
+ patch "users/:id/roles", to: "users/roles#update"
116
+ post "users/:id/impersonation-sessions", to: "users/impersonation_sessions#create"
117
+ get "workspace", to: "workspace#show"
118
+
99
119
  namespace :messaging do
100
120
  post "announcements", to: "announcements#create"
101
121
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCommandTowerAuditEvents < ActiveRecord::Migration[7.2]
4
+ def change
5
+ create_table :command_tower_audit_events, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci" do |t|
6
+ t.string :event_uuid, null: false
7
+ t.string :action, null: false
8
+ t.datetime :occurred_at, null: false
9
+
10
+ t.string :execution_uuid
11
+ t.string :correlation_id
12
+ t.string :request_id
13
+ t.string :causation_id
14
+ t.string :source
15
+
16
+ t.bigint :actor_user_id
17
+ t.bigint :affected_user_id
18
+ t.bigint :effective_user_id
19
+ t.bigint :originating_administrator_id
20
+ t.boolean :impersonation_active, null: false, default: false
21
+ t.string :attribution_mode, null: false
22
+
23
+ t.string :subject_type
24
+ t.bigint :subject_id
25
+ t.string :subject_label
26
+
27
+ t.json :change_set, null: false
28
+ t.json :metadata, null: false
29
+
30
+ t.boolean :user_history, null: false
31
+ t.json :sensitive_fields, null: false
32
+ t.string :retention, null: false
33
+
34
+ t.timestamps
35
+ end
36
+
37
+ add_index :command_tower_audit_events, :event_uuid, unique: true, name: "index_ct_audit_events_on_event_uuid"
38
+ add_index :command_tower_audit_events, %i[affected_user_id occurred_at], name: "index_ct_audit_events_on_affected_occurred"
39
+ add_index :command_tower_audit_events, %i[actor_user_id occurred_at], name: "index_ct_audit_events_on_actor_occurred"
40
+ add_index :command_tower_audit_events, %i[action occurred_at], name: "index_ct_audit_events_on_action_occurred"
41
+ add_index :command_tower_audit_events, :execution_uuid, name: "index_ct_audit_events_on_execution_uuid"
42
+ add_index :command_tower_audit_events, :correlation_id, name: "index_ct_audit_events_on_correlation_id"
43
+ add_index :command_tower_audit_events, %i[originating_administrator_id occurred_at], name: "index_ct_audit_events_on_originating_admin_occurred"
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddScopeColumnsToCommandTowerAuditEvents < ActiveRecord::Migration[7.2]
4
+ def up
5
+ change_table :command_tower_audit_events, bulk: true do |t|
6
+ t.string :scope_class, null: false, default: "legacy"
7
+ t.string :host_context_type
8
+ t.string :host_context_identifier
9
+ end
10
+
11
+ execute <<~SQL.squish
12
+ UPDATE command_tower_audit_events
13
+ SET scope_class = 'legacy'
14
+ WHERE scope_class IS NULL OR scope_class = 'legacy'
15
+ SQL
16
+
17
+ add_index :command_tower_audit_events,
18
+ %i[host_context_type host_context_identifier occurred_at],
19
+ name: "index_ct_audit_events_on_host_context_occurred"
20
+ add_index :command_tower_audit_events,
21
+ %i[scope_class occurred_at],
22
+ name: "index_ct_audit_events_on_scope_class_occurred"
23
+ end
24
+
25
+ def down
26
+ change_table :command_tower_audit_events, bulk: true do |t|
27
+ t.remove_index name: "index_ct_audit_events_on_host_context_occurred"
28
+ t.remove_index name: "index_ct_audit_events_on_scope_class_occurred"
29
+ t.remove :scope_class
30
+ t.remove :host_context_type
31
+ t.remove :host_context_identifier
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCommandTowerImpersonationSessions < ActiveRecord::Migration[7.2]
4
+ def change
5
+ create_table :command_tower_impersonation_sessions, id: { type: :string, limit: 36 } do |t|
6
+ t.bigint :actor_user_id, null: false
7
+ t.bigint :target_user_id, null: false
8
+ t.datetime :started_at, null: false
9
+ t.datetime :last_activity_at, null: false
10
+ t.datetime :absolute_expires_at, null: false
11
+ t.datetime :idle_expires_at, null: false
12
+ t.datetime :ended_at
13
+ t.string :end_reason, limit: 32
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :command_tower_impersonation_sessions, :actor_user_id
18
+ add_index :command_tower_impersonation_sessions, :target_user_id
19
+ add_index :command_tower_impersonation_sessions,
20
+ %i[actor_user_id ended_at],
21
+ name: "index_ct_impersonation_sessions_on_actor_ended"
22
+
23
+ add_foreign_key :command_tower_impersonation_sessions, :users, column: :actor_user_id
24
+ add_foreign_key :command_tower_impersonation_sessions, :users, column: :target_user_id
25
+ end
26
+ end