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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 726d34845596392ec87fbeacf508323aa72fbfa73d87094e40f0747595450ae9
4
- data.tar.gz: 7f1ff986df01e27008e33867e80039a406534e57801111334e3e3c3798705e13
3
+ metadata.gz: d2becbc85a78f04d571714ca6a99a1442cc0bc9c270a322fe25d8d3881af9252
4
+ data.tar.gz: ab5432203fd213f203bdd1e5fc2af0ea81a873c5b0ee9fdd25afa357aca50988
5
5
  SHA512:
6
- metadata.gz: 065535eb260884b0982e56e94c071d2d0707133a62e033d74db8c0d3eaed6c5629b924c8a4a27c007cbb9636aee5198070ef7b3a395cde9a77b1c0bb9661a4e0
7
- data.tar.gz: dfd1337b02b1a6350a8ab5ba16316b77b219a47a383f294f42404dbb78f522f13cc0996e045a2dc9e758f4db0fd05f67bc80cfa2ebd6d9fc970e7db48508a211
6
+ metadata.gz: 8622b2e00ceb1218df245a00aad41f17e8840779e2f4b91531cdeecc6316bfa18305ce06c569e5a92582819875c87a58acb8ef396cfdcecfcc69752715e99671
7
+ data.tar.gz: 391cca33f7e186d24fa8f904be8a4f3dad6f5cb12d9615c84e54da2b0e59dc2fb4f49c8bd4279ca41083e204e8735e5864a575fc9c09e5de6694ce2afc2ace94
data/README.md CHANGED
@@ -41,10 +41,14 @@ See [Architecture](docs/architecture.md) and [ServiceBase](app/services/command_
41
41
  | Guide | Purpose |
42
42
  |-------|---------|
43
43
  | [Host integration](docs/host_integration_guide.md) | **Start here** — step-by-step new Rails host path |
44
- | [Upgrades](docs/upgrades/README.md) | Release upgrade summaries (see [0.10.0](docs/upgrades/0.10.0.md)) |
44
+ | [Upgrades](docs/upgrades/README.md) | Release upgrade summaries (see [0.11.0](docs/upgrades/0.11.0.md)) |
45
45
  | [Initializing](docs/initializing.md) | Install, configure, migrate, doctor, upgrades |
46
46
  | [Testing](docs/testing.md) | Shared FactoryBot / `CommandTower::Testing` |
47
47
  | [Architecture](docs/architecture.md) | Layer map for hosts and contributors |
48
+ | [Eventing](docs/eventing.md) | ASN grammar, lifecycle vs semantic, envelope, subscriber failure |
49
+ | [Audit](docs/audit.md) | Registered `audit(...)` + append-only `command_tower_audit_events` ledger |
50
+ | [Admin Workspace](docs/admin_workspace.md) | Tool registry + RBAC-filtered `GET /admin/workspace` manifest |
51
+ | [Principal capabilities](docs/principal_capabilities.md) | Curated FE-projectable ids via `GET /auth/principal-capabilities` |
48
52
  | [Controllers / routes](docs/controllers.md) | Engine route areas (index) |
49
53
  | [API reference](docs/api_reference.md) | Endpoint catalog (detailed contracts) |
50
54
  | [Extending](docs/extending.md) | Stable extension points / do-not-extend |
@@ -86,7 +90,7 @@ Engine HTTP coverage lives primarily under `spec/requests/`. Residual journey co
86
90
 
87
91
  ## Routes
88
92
 
89
- Engine routes cover Auth, Me (profile, inbox, preferences, phone, Pushover), and Admin messaging announcements. Index: [Controllers](docs/controllers.md). Contracts: [API reference](docs/api_reference.md).
93
+ Engine routes cover Auth, Me (profile, inbox, preferences, phone, Pushover), Admin Workspace manifest, admin audit events, and Admin messaging announcements. Index: [Controllers](docs/controllers.md). Contracts: [API reference](docs/api_reference.md).
90
94
 
91
95
  ## Models
92
96
 
@@ -109,7 +113,7 @@ Cookie mode supports HttpOnly JWT cookies, SameSite, optional double-submit CSRF
109
113
 
110
114
  Authorization establishes permission after authentication (`403` when it fails).
111
115
 
112
- Engine defaults ship `owner` and `admin` (announcements). Hosts **must** supply `rbac_groups.yml` entities for Me/Auth surfaces (fail-closed). Host controllers may use provisional `authorize_user!` after `authenticate_user!`.
116
+ Engine defaults ship `owner` (full access) plus CT-owned Me/Auth and Admin **entity** definitions. CommandTower does **not** ship an operational `admin` role. Hosts supply `rbac_groups.yml` **product roles** that grant those entity names (fail-closed), including any operational Admin bundles. Do not copy CT controller mappings. Host controllers may use provisional `authorize_user!` after `authenticate_user!`.
113
117
 
114
118
  - Quick start: [Authorization](docs/authorization.md)
115
119
  - Deep guide: [Authentication & authorization](docs/authentication_authorization_guide.md)
@@ -8,16 +8,20 @@ module CommandTower
8
8
  :token_source,
9
9
  :roles,
10
10
  :principal_type,
11
- :generated_token
11
+ :generated_token,
12
+ :actor_user,
13
+ :impersonation_session_id
12
14
  ) do
13
- def initialize(user:, token_expires_at:, token_source:, roles:, principal_type:, generated_token: nil)
15
+ def initialize(user:, token_expires_at:, token_source:, roles:, principal_type:, generated_token: nil, actor_user: nil, impersonation_session_id: nil)
14
16
  super(
15
17
  user:,
16
18
  token_expires_at:,
17
19
  token_source:,
18
20
  roles:,
19
21
  principal_type:,
20
- generated_token:
22
+ generated_token:,
23
+ actor_user: actor_user || user,
24
+ impersonation_session_id:
21
25
  )
22
26
  end
23
27
 
@@ -28,7 +32,9 @@ module CommandTower
28
32
  token_source: metadata[:token_source],
29
33
  roles: data[:user].roles,
30
34
  principal_type: :user,
31
- generated_token: data[:generated_token]
35
+ generated_token: data[:generated_token],
36
+ actor_user: data[:actor_user] || data[:user],
37
+ impersonation_session_id: data[:impersonation_session_id]
32
38
  )
33
39
  end
34
40
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Admin
5
+ class ApplicationController < CommandTower::ApplicationController
6
+ include CommandTower::Auth::AuthenticationBoundary
7
+ include CommandTower::Auth::AuthorizationBoundary
8
+
9
+ before_action :authenticate_request!
10
+ before_action :authorize_request!
11
+ before_action :reject_admin_operations_during_impersonation!
12
+
13
+ private
14
+
15
+ def reject_admin_operations_during_impersonation!
16
+ return unless CommandTower::Current.impersonation_active
17
+
18
+ render_application_result(
19
+ CommandTower::Workflows::WorkflowResult.failure(
20
+ errors: [CommandTower::Errors::Auth::AdminUnavailableDuringImpersonationError.new],
21
+ http_status: 418
22
+ )
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Admin
5
+ module Audit
6
+ class EventsController < CommandTower::Admin::ApplicationController
7
+ def index
8
+ deserialized = CommandTower::Deserializers::Audit::Events::AdminListDeserializer.call(params)
9
+ return render_deserializer_errors unless deserialized.success?
10
+
11
+ render_application_result(
12
+ CommandTower::Workflows::Audit::Events::ListForAdminWorkflow.call(
13
+ limit: deserialized.input.limit,
14
+ offset: deserialized.input.offset,
15
+ actions: deserialized.input.actions,
16
+ occurred_after: deserialized.input.occurred_after,
17
+ occurred_before: deserialized.input.occurred_before,
18
+ subject_types: deserialized.input.subject_types,
19
+ affected_user_id: deserialized.input.affected_user_id,
20
+ actor_user_id: deserialized.input.actor_user_id,
21
+ originating_administrator_id: deserialized.input.originating_administrator_id,
22
+ attribution_mode: deserialized.input.attribution_mode,
23
+ user: current_user,
24
+ scope_value: deserialized.input.scope_value
25
+ )
26
+ )
27
+ end
28
+
29
+ def show
30
+ deserialized = CommandTower::Deserializers::Audit::Events::ShowDeserializer.call(params)
31
+ return render_deserializer_errors unless deserialized.success?
32
+
33
+ render_application_result(
34
+ CommandTower::Workflows::Audit::Events::ShowForAdminWorkflow.call(
35
+ id: deserialized.input.id,
36
+ user: current_user,
37
+ scope_value: deserialized.input.scope_value
38
+ )
39
+ )
40
+ end
41
+
42
+ def filter_options
43
+ render_application_result(
44
+ CommandTower::Workflows::Audit::Events::FilterOptionsForAdminWorkflow.call
45
+ )
46
+ end
47
+
48
+ private
49
+
50
+ def render_deserializer_errors
51
+ render_application_result(
52
+ CommandTower::Workflows::WorkflowResult.failure(
53
+ errors: [CommandTower::Errors::ValidationError.new(details: { base: "Invalid request parameters" })],
54
+ http_status: :unprocessable_entity
55
+ )
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -3,14 +3,7 @@
3
3
  module CommandTower
4
4
  module Admin
5
5
  module Messaging
6
- class AnnouncementsController < CommandTower::ApplicationController
7
- include CommandTower::Auth::AuthenticationBoundary
8
- include CommandTower::Auth::AuthorizationBoundary
9
- include CommandTower::Api::ApplicationResponseRenderer
10
-
11
- before_action :authenticate_request!
12
- before_action :authorize_request!
13
-
6
+ class AnnouncementsController < CommandTower::Admin::ApplicationController
14
7
  def create
15
8
  deserialized = CommandTower::Deserializers::Admin::Messaging::CreateAnnouncementDeserializer.call(params)
16
9
  unless deserialized.success?
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Admin
5
+ module Users
6
+ class IdentitiesController < CommandTower::Admin::ApplicationController
7
+ def update_name
8
+ deserialized = CommandTower::Deserializers::Admin::Users::UpdateNameDeserializer.call(params)
9
+ return render_deserializer_errors unless deserialized.success?
10
+
11
+ render_application_result(
12
+ CommandTower::Workflows::Admin::Users::UpdateNameWorkflow.call(
13
+ id: deserialized.input.id,
14
+ user: current_user,
15
+ first_name: deserialized.input.first_name,
16
+ last_name: deserialized.input.last_name,
17
+ scope_value: deserialized.input.scope_value
18
+ )
19
+ )
20
+ end
21
+
22
+ def update_username
23
+ deserialized = CommandTower::Deserializers::Admin::Users::UpdateUsernameDeserializer.call(params)
24
+ return render_deserializer_errors unless deserialized.success?
25
+
26
+ render_application_result(
27
+ CommandTower::Workflows::Admin::Users::UpdateUsernameWorkflow.call(
28
+ id: deserialized.input.id,
29
+ user: current_user,
30
+ username: deserialized.input.username,
31
+ scope_value: deserialized.input.scope_value
32
+ )
33
+ )
34
+ end
35
+
36
+ def update_email
37
+ deserialized = CommandTower::Deserializers::Admin::Users::UpdateEmailDeserializer.call(params)
38
+ return render_deserializer_errors unless deserialized.success?
39
+
40
+ render_application_result(
41
+ CommandTower::Workflows::Admin::Users::UpdateEmailWorkflow.call(
42
+ id: deserialized.input.id,
43
+ user: current_user,
44
+ email: deserialized.input.email,
45
+ scope_value: deserialized.input.scope_value
46
+ )
47
+ )
48
+ end
49
+
50
+ def update_email_validation
51
+ deserialized = CommandTower::Deserializers::Admin::Users::UpdateEmailValidationDeserializer.call(params)
52
+ return render_deserializer_errors unless deserialized.success?
53
+
54
+ render_application_result(
55
+ CommandTower::Workflows::Admin::Users::SetEmailValidatedWorkflow.call(
56
+ id: deserialized.input.id,
57
+ user: current_user,
58
+ email_validated: deserialized.input.email_validated,
59
+ scope_value: deserialized.input.scope_value
60
+ )
61
+ )
62
+ end
63
+
64
+ private
65
+
66
+ def render_deserializer_errors
67
+ render_application_result(
68
+ CommandTower::Workflows::WorkflowResult.failure(
69
+ errors: [CommandTower::Errors::ValidationError.new(details: { base: "Invalid request parameters" })],
70
+ http_status: :unprocessable_entity
71
+ )
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Admin
5
+ module Users
6
+ class ImpersonationSessionsController < CommandTower::Admin::ApplicationController
7
+ def create
8
+ deserialized = CommandTower::Deserializers::Admin::Users::ShowDeserializer.call(params)
9
+ return render_deserializer_errors unless deserialized.success?
10
+
11
+ render_application_result(
12
+ CommandTower::Workflows::Impersonation::StartWorkflow.call(
13
+ id: deserialized.input.id,
14
+ actor: current_user,
15
+ scope_value: deserialized.input.scope_value
16
+ )
17
+ )
18
+ end
19
+
20
+ private
21
+
22
+ def render_deserializer_errors
23
+ render_application_result(
24
+ CommandTower::Workflows::WorkflowResult.failure(
25
+ errors: [CommandTower::Errors::ValidationError.new(details: { base: "Invalid request parameters" })],
26
+ http_status: :unprocessable_entity
27
+ )
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Admin
5
+ module Users
6
+ class RolesController < CommandTower::Admin::ApplicationController
7
+ def index
8
+ render_application_result(
9
+ CommandTower::Workflows::Admin::Users::ListAssignableRolesWorkflow.call
10
+ )
11
+ end
12
+
13
+ def update
14
+ deserialized = CommandTower::Deserializers::Admin::Users::UpdateRolesDeserializer.call(params)
15
+ return render_deserializer_errors unless deserialized.success?
16
+
17
+ render_application_result(
18
+ CommandTower::Workflows::Admin::Users::UpdateRolesWorkflow.call(
19
+ id: deserialized.input.id,
20
+ user: current_user,
21
+ roles: deserialized.input.roles,
22
+ scope_value: deserialized.input.scope_value
23
+ )
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def render_deserializer_errors
30
+ render_application_result(
31
+ CommandTower::Workflows::WorkflowResult.failure(
32
+ errors: [CommandTower::Errors::ValidationError.new(details: { base: "Invalid request parameters" })],
33
+ http_status: :unprocessable_entity
34
+ )
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Admin
5
+ class UsersController < CommandTower::Admin::ApplicationController
6
+ def index
7
+ deserialized = CommandTower::Deserializers::Admin::Users::ListDeserializer.call(params)
8
+ return render_deserializer_errors unless deserialized.success?
9
+
10
+ render_application_result(
11
+ CommandTower::Workflows::Admin::Users::ListWorkflow.call(
12
+ limit: deserialized.input.limit,
13
+ offset: deserialized.input.offset,
14
+ search: deserialized.input.search,
15
+ user: current_user,
16
+ scope_value: deserialized.input.scope_value
17
+ )
18
+ )
19
+ end
20
+
21
+ def show
22
+ deserialized = CommandTower::Deserializers::Admin::Users::ShowDeserializer.call(params)
23
+ return render_deserializer_errors unless deserialized.success?
24
+
25
+ render_application_result(
26
+ CommandTower::Workflows::Admin::Users::ShowWorkflow.call(
27
+ id: deserialized.input.id,
28
+ user: current_user,
29
+ scope_value: deserialized.input.scope_value
30
+ )
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ def render_deserializer_errors
37
+ render_application_result(
38
+ CommandTower::Workflows::WorkflowResult.failure(
39
+ errors: [CommandTower::Errors::ValidationError.new(details: { base: "Invalid request parameters" })],
40
+ http_status: :unprocessable_entity
41
+ )
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Admin
5
+ class WorkspaceController < CommandTower::Admin::ApplicationController
6
+ skip_before_action :reject_admin_operations_during_impersonation!, only: :show
7
+
8
+ def show
9
+ render_application_result(
10
+ CommandTower::Workflows::Admin::Workspace::ManifestWorkflow.call(user: current_user)
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module CommandTower
4
4
  class ApplicationController < ActionController::API
5
+ include CommandTower::Execution::HttpBoundary
6
+
5
7
  AUTHENTICATION_HEADER = CommandTower::Jwt::AuthorizationHelper::AUTHENTICATION_HEADER
6
8
  AUTHENTICATION_EXPIRE_HEADER = CommandTower::Jwt::AuthorizationHelper::AUTHENTICATION_EXPIRE_HEADER
7
9
  AUTHENTICATION_WITH_RESET = CommandTower::Jwt::AuthorizationHelper::AUTHENTICATION_WITH_RESET
@@ -46,7 +48,21 @@ module CommandTower
46
48
  with_reset = CommandTower::Jwt::AuthorizationHelper.get_with_reset_flag(request)
47
49
  result = CommandTower::Jwt::AuthenticateUser.(token:, bypass_email_validation:, with_reset:)
48
50
  if result.success?
49
- @current_user = result.user
51
+ established = CommandTower::Impersonation::EstablishIdentity.call(
52
+ actor: result.user,
53
+ impersonation_session_id: result.impersonation_session_id
54
+ )
55
+ if established.expired?
56
+ status = 401
57
+ schema = CommandTower::Schema::Error::Base.new(
58
+ status:,
59
+ message: CommandTower::Errors::Auth::ImpersonationSessionExpiredError.new.message
60
+ )
61
+ render(json: schema.to_h, status:)
62
+ return false
63
+ end
64
+
65
+ @current_user = established.user
50
66
  if with_reset
51
67
  # Use helper to set both headers and cookie (if enabled)
52
68
  CommandTower::Jwt::AuthorizationHelper.set_token(
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Auth
5
+ class ImpersonationSessionController < CommandTower::ApplicationController
6
+ include CommandTower::Auth::AuthenticationBoundary
7
+ include CommandTower::Api::ApplicationResponseRenderer
8
+
9
+ before_action :authenticate_stop_request!
10
+
11
+ def destroy
12
+ result = CommandTower::Workflows::Impersonation::StopWorkflow.call(
13
+ actor: current_auth_context.actor_user,
14
+ impersonation_session_id: current_auth_context.impersonation_session_id,
15
+ token_expires_at: current_auth_context.token_expires_at
16
+ )
17
+ render_application_result(result)
18
+ end
19
+
20
+ private
21
+
22
+ def authenticate_stop_request!
23
+ authenticate_request!(overlay_mode: :capture)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -6,7 +6,9 @@ module CommandTower
6
6
  include CommandTower::Api::ApplicationResponseRenderer
7
7
 
8
8
  def create
9
- result = CommandTower::Workflows::Auth::LogoutWorkflow.call
9
+ token_data = CommandTower::Jwt::AuthorizationHelper.extract_token(request)
10
+ token = token_data[:error] ? nil : token_data[:token]
11
+ result = CommandTower::Workflows::Auth::LogoutWorkflow.call(token:)
10
12
  render_application_result(result)
11
13
  end
12
14
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Auth
5
+ class PrincipalCapabilitiesController < CommandTower::ApplicationController
6
+ include CommandTower::Auth::AuthenticationBoundary
7
+ include CommandTower::Auth::AuthorizationBoundary
8
+
9
+ before_action :authenticate_request!
10
+ before_action :authorize_request!
11
+
12
+ def show
13
+ render_application_result(
14
+ CommandTower::Workflows::Auth::PrincipalCapabilities::ShowWorkflow.call(user: current_user)
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Me
5
+ class AuditEventsController < CommandTower::ApplicationController
6
+ include CommandTower::Auth::AuthenticationBoundary
7
+ include CommandTower::Auth::AuthorizationBoundary
8
+
9
+ before_action :authenticate_request!
10
+ before_action :authorize_request!
11
+
12
+ def index
13
+ deserialized = CommandTower::Deserializers::Audit::Events::UserListDeserializer.call(params)
14
+ return render_deserializer_errors unless deserialized.success?
15
+
16
+ render_application_result(
17
+ CommandTower::Workflows::Audit::Events::ListForUserWorkflow.call(
18
+ user: current_user,
19
+ limit: deserialized.input.limit,
20
+ offset: deserialized.input.offset,
21
+ actions: deserialized.input.actions,
22
+ occurred_after: deserialized.input.occurred_after,
23
+ occurred_before: deserialized.input.occurred_before,
24
+ subject_types: deserialized.input.subject_types
25
+ )
26
+ )
27
+ end
28
+
29
+ def show
30
+ deserialized = CommandTower::Deserializers::Audit::Events::ShowDeserializer.call(params)
31
+ return render_deserializer_errors unless deserialized.success?
32
+
33
+ render_application_result(
34
+ CommandTower::Workflows::Audit::Events::ShowForUserWorkflow.call(
35
+ user: current_user,
36
+ id: deserialized.input.id
37
+ )
38
+ )
39
+ end
40
+
41
+ def filter_options
42
+ render_application_result(
43
+ CommandTower::Workflows::Audit::Events::FilterOptionsForUserWorkflow.call
44
+ )
45
+ end
46
+
47
+ private
48
+
49
+ def render_deserializer_errors
50
+ render_application_result(
51
+ CommandTower::Workflows::WorkflowResult.failure(
52
+ errors: [CommandTower::Errors::ValidationError.new(details: { base: "Invalid request parameters" })],
53
+ http_status: :unprocessable_entity
54
+ )
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
@@ -14,6 +14,11 @@ module CommandTower
14
14
  data: result.payload,
15
15
  meta: result.meta
16
16
  )
17
+ elsif result.deferred?
18
+ CommandTower::Serializers::Application::EnvelopeSerializer.success(
19
+ data: result.payload,
20
+ meta: result.meta.merge(reason: result.reason, retry_after: result.retry_after)
21
+ )
17
22
  else
18
23
  serialized_errors = result.errors.map do |error|
19
24
  CommandTower::Serializers::Application::ErrorEntrySerializer.serialize(error)
@@ -13,11 +13,12 @@ module CommandTower
13
13
 
14
14
  private
15
15
 
16
- def authenticate_request!(bypass_email_validation: false)
16
+ def authenticate_request!(bypass_email_validation: false, overlay_mode: :enforce)
17
17
  result = CommandTower::Workflows::Auth::AuthenticateRequestWorkflow.call(
18
18
  request: request,
19
19
  response: response,
20
- bypass_email_validation: bypass_email_validation
20
+ bypass_email_validation: bypass_email_validation,
21
+ overlay_mode: overlay_mode
21
22
  )
22
23
 
23
24
  unless result.success?
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Execution
5
+ module HttpBoundary
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ around_action :command_tower_http_execution
10
+ end
11
+
12
+ private
13
+
14
+ def command_tower_http_execution
15
+ request_id = request.request_id.presence || request.uuid
16
+ CommandTower.with_execution(
17
+ source: :http,
18
+ request_id:,
19
+ correlation_id: request_id,
20
+ remote_ip: request.remote_ip,
21
+ user_agent: request.user_agent
22
+ ) do
23
+ yield
24
+ record_impersonation_activity_if_needed
25
+ end
26
+ end
27
+
28
+ def record_impersonation_activity_if_needed
29
+ return unless CommandTower::Current.impersonation_active
30
+ return unless CommandTower::Current.impersonation_activity_recorded
31
+ return unless response.status.to_i.between?(200, 299)
32
+
33
+ session_id = CommandTower::Current.impersonation_session_id
34
+ return if session_id.blank?
35
+
36
+ result = CommandTower::Services::Impersonation::RecordActivity.call(session_id:)
37
+ return unless result.success?
38
+ return unless result.data[:refreshed]
39
+
40
+ merge_impersonation_clock_meta!(result.data[:session])
41
+ end
42
+
43
+ def merge_impersonation_clock_meta!(session)
44
+ return if session.nil?
45
+
46
+ raw = response.body
47
+ return if raw.blank?
48
+
49
+ parsed = JSON.parse(raw)
50
+ return unless parsed.is_a?(Hash)
51
+
52
+ parsed["meta"] ||= {}
53
+ parsed["meta"]["impersonation"] = {
54
+ "idleExpiresAt" => CommandTower::Serializers::ApplicationSerializer.iso8601(session.idle_expires_at),
55
+ "absoluteExpiresAt" => CommandTower::Serializers::ApplicationSerializer.iso8601(session.absolute_expires_at)
56
+ }
57
+ json = parsed.to_json
58
+ response.body = json
59
+ response.headers["Content-Length"] = json.bytesize.to_s
60
+ rescue JSON::ParserError
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end