command_tower 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) 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 +38 -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/0.11.1.md +25 -0
  139. data/docs/upgrades/README.md +2 -0
  140. data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
  141. data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
  142. data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
  143. data/lib/command_tower/admin_scope/resolve.rb +31 -0
  144. data/lib/command_tower/admin_scope/scope_context.rb +7 -0
  145. data/lib/command_tower/admin_scope/scope_option.rb +7 -0
  146. data/lib/command_tower/admin_scope.rb +20 -0
  147. data/lib/command_tower/admin_workspace.rb +16 -0
  148. data/lib/command_tower/audit/attribution.rb +90 -0
  149. data/lib/command_tower/audit/emit.rb +113 -0
  150. data/lib/command_tower/audit/masking.rb +41 -0
  151. data/lib/command_tower/audit/payload.rb +128 -0
  152. data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
  153. data/lib/command_tower/audit.rb +27 -0
  154. data/lib/command_tower/authorization/assignable_roles.rb +36 -0
  155. data/lib/command_tower/authorization/default.yml +99 -5
  156. data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
  157. data/lib/command_tower/authorization/entity.rb +15 -5
  158. data/lib/command_tower/authorization/role.rb +5 -4
  159. data/lib/command_tower/authorization.rb +71 -10
  160. data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
  161. data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
  162. data/lib/command_tower/configuration/authorization/config.rb +6 -0
  163. data/lib/command_tower/configuration/config.rb +20 -0
  164. data/lib/command_tower/configuration/impersonation/config.rb +38 -0
  165. data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
  166. data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
  167. data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
  168. data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
  169. data/lib/command_tower/configuration/registry/config.rb +34 -0
  170. data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
  171. data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
  172. data/lib/command_tower/current.rb +12 -0
  173. data/lib/command_tower/engine.rb +19 -4
  174. data/lib/command_tower/events.rb +184 -0
  175. data/lib/command_tower/execution.rb +111 -0
  176. data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
  177. data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
  178. data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
  179. data/lib/command_tower/impersonation/establish_identity.rb +44 -0
  180. data/lib/command_tower/install/baseline.rb +3 -0
  181. data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
  182. data/lib/command_tower/logging/projection.rb +67 -0
  183. data/lib/command_tower/logging/subscriber.rb +103 -0
  184. data/lib/command_tower/principal_capabilities.rb +15 -0
  185. data/lib/command_tower/version.rb +1 -1
  186. data/lib/command_tower.rb +12 -0
  187. data/spec/factories/impersonation_session.rb +17 -0
  188. data/spec/factories/user.rb +16 -0
  189. metadata +114 -3
  190. data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
@@ -0,0 +1,89 @@
1
+ # Principal capabilities
2
+
3
+ Authenticated **frontend-projectable** capability ids for the current principal. Projection is `effective RBAC entity grants ∩ curated registry` — never role/group names, and never an auto-dump of `Entity.entities`.
4
+
5
+ ```text
6
+ RBAC groups → entity grants → curated projectable registry
7
+ → possessed principalCapabilities → shared FE gating (4.6.2+)
8
+ ```
9
+
10
+ CommandTower owns which CT entities are frontend-projectable. Hosts own who receives those entities through RBAC group composition. Hosts register **host-owned** projectables only; they do **not** re-register or redefine CT capability ids.
11
+
12
+ ## Register (configuration)
13
+
14
+ ```ruby
15
+ CommandTower.configure do |config|
16
+ # Default 1:1 — id and required_entity share the same name
17
+ config.registry.principal_capabilities.capability :admin_workspace
18
+
19
+ # Host additive (example) — override required_entity when names differ
20
+ config.registry.principal_capabilities.capability :manage_wagers do |capability|
21
+ capability.required_entity = :pickem_admin_wagers
22
+ end
23
+ end
24
+ ```
25
+
26
+ | Field | Rules |
27
+ |-------|--------|
28
+ | id | DSL name; public contract |
29
+ | required_entity | RBAC entity name; defaults to id when omitted; must exist after RBAC composition |
30
+ | owner | `:command_tower` (seeded) or `:host` (host registration) |
31
+
32
+ Lookup: `CommandTower.config.registry.principal_capabilities.fetch(:admin_workspace)`.
33
+
34
+ ## Seeded CommandTower catalog (4.6.1 + 4.7.1 + 6.3)
35
+
36
+ | id / required_entity | Why projectable |
37
+ |----------------------|-----------------|
38
+ | `admin_workspace` | Gate Admin Workspace affordances without probing `/admin/workspace` |
39
+ | `admin_users` | Gate Admin Users Collection/detail presentation |
40
+ | `admin_users_update` | Gate Admin Users identity mutations (name, username, email, email validation) |
41
+ | `admin_rbac_assignments` | Gate Admin Users role assignment (assignable catalog + PATCH roles) |
42
+ | `admin_audit_events` | Gate Audit Admin affordances and deep links outside the workspace manifest |
43
+ | `admin_messaging_announcements` | Gate Messaging Admin affordances outside the manifest |
44
+ | `admin_impersonation` | Gate Impersonate affordances (5.6 composition). Possession does not start a session by itself. |
45
+ | `me_audit_events` | Gate Account **Activity** / self-audit Explorer presentation (4.7.1). Hosts own who receives the entity. |
46
+
47
+ Me/Auth route gates (`session`, `me`, …) and unimplemented Admin entities are **not** seeded. Adding a new CT RBAC entity later requires an explicit slice decision to also register a principal capability.
48
+
49
+ ## Boot validation
50
+
51
+ 1. Host `CommandTower.configure` may register additive capabilities (mutable).
52
+ 2. `after_initialize` — `principal_capabilities.finalize!` with audit / admin_workspace, then freeze (skipped in test).
53
+ 3. `to_prepare` — after RBAC, `validate_required_entities!(Entity.entities)`.
54
+
55
+ ## Runtime HTTP
56
+
57
+ `GET /auth/principal-capabilities` (engine-relative; hosts mounting at `/api` use `GET /api/auth/principal-capabilities`).
58
+
59
+ - Authn + authz: entity `principal_capabilities` (`PrincipalCapabilitiesController#show`). Grant to host `member` (and operators that need the call) like other Me/Auth surfaces.
60
+ - Controller → `Workflows::Auth::PrincipalCapabilities::ShowWorkflow` (`retry_strategy :none`) → `Services::Auth::PrincipalCapabilities::Project` → `PrincipalCapabilitiesSerializer`.
61
+ - Service: if any role has `allow_everything`, return **all** registered projectable ids; else include definitions whose `required_entity` is granted. Unique + sorted. No role-name checks.
62
+ - Envelope `data`: `{ principalCapabilities: string[] }` — possessed ids only.
63
+
64
+ | Persona (typical host composition) | Example `principalCapabilities` |
65
+ |------------------------------------|---------------------------------|
66
+ | guest | **401** |
67
+ | email verification required | **412** |
68
+ | `member` (host grants `me_audit_events`) | `me_audit_events` |
69
+ | `member` (host withholds `me_audit_events`) | `[]` |
70
+ | `audit_operator` | `admin_audit_events`, `admin_workspace` |
71
+ | `messaging_operator` | `admin_messaging_announcements`, `admin_workspace` |
72
+ | host `admin` / `operations_admin` | CT Admin seeds (not necessarily `me_audit_events`) |
73
+ | `owner` | all **registered** ids (CT + host additive) |
74
+
75
+ ## Naming collisions (different concepts)
76
+
77
+ | Name | Meaning |
78
+ |------|---------|
79
+ | `principalCapabilities` / this registry | Runtime possessed, frontend-projectable RBAC-backed facts |
80
+ | `/me` `capabilities` | Account self-service flags — **not** this projection |
81
+ | HostConfig / build-time `capabilities` | Compile-time product modules — **not** this projection |
82
+ | RBAC group / role names | Host privilege composition — **never** the FE gating contract |
83
+
84
+ ## Related
85
+
86
+ - [API reference](api_reference.md#get-authprincipal-capabilities)
87
+ - [Authorization](authorization.md)
88
+ - [Admin Workspace](admin_workspace.md) — tool manifest only; not a UI permission probe
89
+ - [Host integration](host_integration_guide.md)
@@ -14,7 +14,7 @@ This document summarizes **all changes from `main` through the `0.10.0` release*
14
14
  | Current-user path | Use `GET /me` (and `GET /profile`); not `GET /user` |
15
15
  | Login path | `POST /auth/plain-text/login` when `login.plain_text.enable?` (route absent → **404** if gate off) |
16
16
  | Password change | `PATCH /me/password` (rotates verifier; does not re-issue JWT) |
17
- | Host RBAC required | Without host `rbac_groups.yml` entity mappings, Me/Auth calls **403** (fail-closed) |
17
+ | Host RBAC required | Host `rbac_groups.yml` must grant CT-owned Me/Auth entity **names** to product roles; do not copy CT controller mappings. Conflicts fail at boot. |
18
18
  | Feature gates | Login, email verify, password reset, availability routes are config-gated |
19
19
  | Schema ownership (**AD-SCH-01**) | CT authors migrations; hosts install via `command_tower:install` / `install:migrations` then `db:migrate` |
20
20
  | Admin HTTP | Only `POST /admin/messaging/announcements` (no SchemaHelper user admin) |
@@ -40,7 +40,7 @@ This document summarizes **all changes from `main` through the `0.10.0` release*
40
40
 
41
41
  1. Bump gem to `0.10.0` and run `bin/rails command_tower:install:migrations` (or `SKIP_CONFIGURE=1 bin/rails command_tower:install`) then `bin/rails db:migrate`.
42
42
  2. Set JWT + signup/recovery session secrets; run `bin/rails command_tower:doctor`.
43
- 3. Copy/adapt [`rails_app/config/rbac_groups.yml`](../../rails_app/config/rbac_groups.yml) into the host; do not redefine engine `owner` / `admin` groups.
43
+ 3. Add host `rbac_groups.yml` with a product role (typically `member`) that **grants** CT-owned entity names; do not redefine engine `owner` or copy CT controller mappings. Operational Admin roles are host-owned.
44
44
  4. Ensure users who need Me surfaces have the host `member` role (or equivalent).
45
45
  5. Enable feature gates you rely on (`login.plain_text.enable`, password reset, email verify, availability).
46
46
  6. Point clients at modern paths (`/me`, `/auth/plain-text/login`, envelope parsing).
@@ -0,0 +1,92 @@
1
+ # Upgrade: CommandTower 0.11.0
2
+
3
+ **From:** `0.10.0`
4
+ **To:** `0.11.0`
5
+
6
+ This document summarizes **host-visible changes from `0.10.0` through the `0.11.0` release**. It is the durable upgrade / change SST for hosts already on the Auth/Me/Messaging platform. For a sequenced blank-host path, use [Host integration](../host_integration_guide.md).
7
+
8
+ ## Breaking / host-impact
9
+
10
+ | Change | Host impact |
11
+ |--------|-------------|
12
+ | New migrations | Install and migrate audit events (+ scope columns) and impersonation sessions via `command_tower:install:migrations` then `db:migrate` |
13
+ | New RBAC entities | Hosts must **explicitly grant** new CT-owned entity names; they are **not** auto-added to a generic CT admin role |
14
+ | `GET /auth/principal-capabilities` | Frontend gating should use possessed projectable ids from this endpoint — **not** role names and **not** `GET /admin/workspace` as a permission probe |
15
+ | Admin HTTP during impersonation | Admin routes return **418** while an impersonation session is active |
16
+ | Admin least privilege | Operational Admin privilege is host-owned role composition; CT ships entities only |
17
+ | Optional Admin resource scoping | Hosts that need scoped Users/Audit register `config.admin_scope` and may set `scope_required` on workspace tools |
18
+
19
+ ## Platform capabilities in 0.11.0
20
+
21
+ (In addition to everything in [0.10.0](0.10.0.md).)
22
+
23
+ - **Execution context + eventing:** workflow/service lifecycle on `ActiveSupport::Notifications`; structured logging is a subscriber (not a second bus)
24
+ - **Audit ledger:** append-only `command_tower_audit_events`; registered `audit(...)` on workflows/services; Me + Admin query HTTP + filter-options
25
+ - **Admin Workspace:** tool registry + RBAC-filtered `GET /admin/workspace` manifest; optional host `admin_scope`
26
+ - **Principal capabilities:** curated FE-projectable ids via `GET /auth/principal-capabilities` (effective grants ∩ registry)
27
+ - **Impersonation:** start via Admin Users, end via `DELETE /auth/impersonation-session`; overlay identity; idle/absolute timeouts
28
+ - **Admin Users:** list/show, identity update, assignable roles, RBAC assignment — host-granted entities only
29
+
30
+ ### New engine HTTP (engine-relative)
31
+
32
+ | Method | Path |
33
+ |--------|------|
34
+ | GET | `/auth/principal-capabilities` |
35
+ | DELETE | `/auth/impersonation-session` |
36
+ | GET | `/me/audit-events`, `/me/audit-events/:id`, `/me/audit-events/filter-options` |
37
+ | GET | `/admin/workspace` |
38
+ | GET | `/admin/audit-events`, `/admin/audit-events/:id`, `/admin/audit-events/filter-options` |
39
+ | GET | `/admin/users`, `/admin/users/:id`, `/admin/users/assignable-roles` |
40
+ | PATCH | `/admin/users/:id/name`, `/username`, `/email`, `/email-validation`, `/roles` |
41
+ | POST | `/admin/users/:id/impersonation-sessions` |
42
+
43
+ ### New RBAC entities (host grants required)
44
+
45
+ | Entity | Typical grant |
46
+ |--------|----------------|
47
+ | `principal_capabilities` | `member` (and operators that need the call) |
48
+ | `me_audit_events` | `member` when Account Activity is in product |
49
+ | `admin_workspace` | host operator roles |
50
+ | `admin_audit_events` | audit operators |
51
+ | `admin_users` | users operators |
52
+ | `admin_users_update` | identity-update operators |
53
+ | `admin_rbac_assignments` | RBAC assignment operators |
54
+ | `admin_impersonation` | impersonation operators |
55
+
56
+ See dummy composition in `rails_app/config/rbac_groups.yml`.
57
+
58
+ ### Migrations
59
+
60
+ - `20260816000001_create_command_tower_audit_events.rb`
61
+ - `20260817000001_add_scope_columns_to_command_tower_audit_events.rb`
62
+ - `20260817000003_create_command_tower_impersonation_sessions.rb`
63
+
64
+ ## Documentation
65
+
66
+ - [Eventing](../eventing.md)
67
+ - [Audit](../audit.md)
68
+ - [Admin Workspace](../admin_workspace.md)
69
+ - [Principal capabilities](../principal_capabilities.md)
70
+ - Host start-here: [Host integration](../host_integration_guide.md)
71
+ - Canonical HTTP catalog: [API reference](../api_reference.md)
72
+
73
+ ## Upgrade checklist (existing 0.10.0 hosts)
74
+
75
+ 1. Bump gem to `0.11.0` and run `bin/rails command_tower:install:migrations` then `bin/rails db:migrate`.
76
+ 2. Run `bin/rails command_tower:doctor`.
77
+ 3. Grant `principal_capabilities` (and `me_audit_events` if Account Activity is in product) on host `member` / equivalent.
78
+ 4. Explicitly grant Admin entities to **host-owned** operator roles (`admin_workspace`, `admin_users`, `admin_users_update`, `admin_rbac_assignments`, `admin_audit_events`, `admin_impersonation`) — do not assume a platform `admin` role.
79
+ 5. Point Admin FE gating at `GET /auth/principal-capabilities`, not `GET /admin/workspace`.
80
+ 6. Optional: register host Admin Workspace tools, `admin_scope`, host audit events, and impersonation timeouts.
81
+ 7. Smoke: login → `GET /auth/principal-capabilities` **200**; operator with grants → `GET /admin/workspace` **200**; without grants → **403**.
82
+ 8. Smoke: start impersonation → Admin routes **418**; `DELETE /auth/impersonation-session` restores actor.
83
+
84
+ New hosts: follow [Host integration](../host_integration_guide.md) end-to-end (includes 0.10.0 foundations plus this release).
85
+
86
+ ## Related
87
+
88
+ - [Initializing](../initializing.md)
89
+ - [Extending](../extending.md)
90
+ - [Authentication & authorization guide](../authentication_authorization_guide.md)
91
+ - [README](../../README.md)
92
+ - Prior release: [0.10.0](0.10.0.md)
@@ -0,0 +1,25 @@
1
+ # Upgrade: CommandTower 0.11.1
2
+
3
+ **From:** `0.11.0`
4
+ **To:** `0.11.1`
5
+
6
+ Patch release for MariaDB-safe audit persistence and install migrations.
7
+
8
+ ## Host-visible changes
9
+
10
+ | Change | Host impact |
11
+ |--------|-------------|
12
+ | `CommandTower::Audit::Event` JSON coding | `metadata`, `change_set`, and `sensitive_fields` declare `attribute :…, :json`. Forces JSON encoding on MySQL 8 and MariaDB (where `t.json` is often LONGTEXT + `json_valid` CHECK). Fixes HTTP 500 `ActiveRecord::CheckViolation` on failed-login audit writes. |
13
+ | Audit events migration collation | Engine migration no longer hardcodes MySQL 8–only `utf8mb4_0900_ai_ci`. Fresh installs / `command_tower:install:migrations` copies inherit the database default (e.g. `utf8mb4_unicode_ci` on MariaDB 10.11). |
14
+
15
+ ## Host actions
16
+
17
+ 1. Bump gem to `0.11.1` and deploy.
18
+ 2. **No new migration** is required if `command_tower_audit_events` already exists.
19
+ 3. Already-installed host copies of the audit migration that still contain `utf8mb4_0900_ai_ci` only matter for **new** databases; runtime login fix does not depend on re-running that migration.
20
+ 4. Smoke: unknown-user `POST /auth/plain-text/login` should return auth failure (e.g. `invalid_credentials`), **not** `internal_error` / 500.
21
+
22
+ ## Not in this release
23
+
24
+ - Jumbotron / host `t.json` columns outside CommandTower
25
+ - Altering charset/collation on already-created production audit tables
@@ -4,6 +4,8 @@ Host-facing upgrade / change summaries for CommandTower releases.
4
4
 
5
5
  | Version | Summary |
6
6
  |---------|---------|
7
+ | [0.11.1](0.11.1.md) | Audit Event `attribute :json` (MariaDB); audit migration without `utf8mb4_0900_ai_ci` |
8
+ | [0.11.0](0.11.0.md) | Execution context, audit ledger, Admin Workspace, principal capabilities, impersonation, Admin Users |
7
9
  | [0.10.0](0.10.0.md) | Modern Auth/Me/Messaging platform; SchemaHelper removal; host RBAC required |
8
10
 
9
11
  New hosts should start with [Host integration](../host_integration_guide.md).
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminScope
5
+ module ApplyAuditScoping
6
+ module_function
7
+
8
+ def call(relation:, scope_context:, principal:)
9
+ return relation if scope_context.nil?
10
+
11
+ registration = CommandTower.config.admin_scope.fetch(scope_context.tool_id)
12
+ global_event_names = global_visible_event_names
13
+ affected_user_ids = registration.affected_users_in_scope.call(
14
+ scope_value: scope_context.scope_value,
15
+ principal:,
16
+ tool_id: scope_context.tool_id
17
+ )
18
+
19
+ host_relation = registration.narrow_audit.call(
20
+ relation: relation.where(scope_class: CommandTower::Audit::Event::SCOPE_CLASSES[:host]),
21
+ scope_value: scope_context.scope_value,
22
+ principal:,
23
+ tool_id: scope_context.tool_id
24
+ )
25
+
26
+ global_relation = relation.where(
27
+ scope_class: CommandTower::Audit::Event::SCOPE_CLASSES[:global],
28
+ action: global_event_names,
29
+ affected_user_id: affected_user_ids
30
+ )
31
+
32
+ relation.where(id: host_relation.select(:id))
33
+ .or(relation.where(id: global_relation.select(:id)))
34
+ .where.not(scope_class: CommandTower::Audit::Event::SCOPE_CLASSES[:legacy])
35
+ end
36
+
37
+ def global_visible_event_names
38
+ CommandTower.config.registry.audit.definitions.filter_map do |name, definition|
39
+ name if definition.global_visible_in_host_scope?
40
+ end
41
+ end
42
+ private_class_method :global_visible_event_names
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminScope
5
+ module ApplyUsersNarrowing
6
+ module_function
7
+
8
+ def call(relation:, scope_context:, principal:)
9
+ return relation if scope_context.nil?
10
+
11
+ registration = CommandTower.config.admin_scope.fetch(scope_context.tool_id)
12
+ registration.narrow_users.call(
13
+ relation:,
14
+ scope_value: scope_context.scope_value,
15
+ principal:,
16
+ tool_id: scope_context.tool_id
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminScope
5
+ module ManifestProjection
6
+ IMPERSONATION_DISABLED_REASON = "Admin tools are unavailable while impersonating a user."
7
+
8
+ module_function
9
+
10
+ def call(definition:, principal:)
11
+ scope_payload = scope_payload_for(definition)
12
+ unscoped_unregistered = scope_payload.nil? && !admin_scope_registered?(definition.id)
13
+
14
+ availability = availability_for(definition, principal:)
15
+ scope_options = scope_options_for(definition, principal:)
16
+
17
+ payload = {
18
+ scope: scope_payload,
19
+ availability:,
20
+ scope_options:
21
+ }.compact
22
+
23
+ if CommandTower::Current.impersonation_active
24
+ payload[:availability] = {
25
+ enabled: false,
26
+ reason: IMPERSONATION_DISABLED_REASON
27
+ }
28
+ return payload
29
+ end
30
+
31
+ return {} if unscoped_unregistered
32
+
33
+ payload
34
+ end
35
+
36
+ def scope_payload_for(definition)
37
+ return unless definition.scope_required?
38
+
39
+ {
40
+ required: true,
41
+ parameter: camelize_parameter(definition.scope_parameter),
42
+ label: definition.scope_label
43
+ }
44
+ end
45
+ private_class_method :scope_payload_for
46
+
47
+ def availability_for(definition, principal:)
48
+ return { enabled: true, reason: nil } unless admin_scope_registered?(definition.id)
49
+
50
+ registration = CommandTower.config.admin_scope.fetch(definition.id)
51
+ normalize_availability(registration.availability.call(principal:))
52
+ end
53
+ private_class_method :availability_for
54
+
55
+ def scope_options_for(definition, principal:)
56
+ return unless definition.scope_required? && admin_scope_registered?(definition.id)
57
+
58
+ registration = CommandTower.config.admin_scope.fetch(definition.id)
59
+ Array(registration.options.call(principal:)).map do |option|
60
+ normalize_scope_option(option)
61
+ end
62
+ end
63
+ private_class_method :scope_options_for
64
+
65
+ def admin_scope_registered?(tool_id)
66
+ CommandTower.config.admin_scope.registered?(tool_id)
67
+ end
68
+ private_class_method :admin_scope_registered?
69
+
70
+ def normalize_availability(value)
71
+ hash = value.is_a?(Hash) ? value : {}
72
+ enabled = hash.fetch(:enabled, hash["enabled"])
73
+ reason = hash.fetch(:reason, hash["reason"])
74
+ {
75
+ enabled: enabled != false,
76
+ reason: reason.nil? ? nil : reason.to_s
77
+ }
78
+ end
79
+ private_class_method :normalize_availability
80
+
81
+ def normalize_scope_option(option)
82
+ case option
83
+ when AdminScope::ScopeOption
84
+ { value: option.value, label: option.label }
85
+ when Hash
86
+ {
87
+ value: option.fetch(:value, option["value"]).to_s,
88
+ label: option.fetch(:label, option["label"]).to_s
89
+ }
90
+ else
91
+ raise ArgumentError, "scope option must be ScopeOption or Hash, got #{option.class}"
92
+ end
93
+ end
94
+ private_class_method :normalize_scope_option
95
+
96
+ def camelize_parameter(parameter)
97
+ parameter.to_s.camelize(:lower)
98
+ end
99
+ private_class_method :camelize_parameter
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminScope
5
+ module Resolve
6
+ module_function
7
+
8
+ def call(tool_id:, principal:, scope_value:)
9
+ definition = CommandTower.config.registry.admin_workspace.fetch(tool_id)
10
+ return nil unless definition.scope_required?
11
+
12
+ token = scope_value.to_s.strip
13
+ if token.empty?
14
+ raise CommandTower::Errors::ForbiddenError
15
+ end
16
+
17
+ registration = CommandTower.config.admin_scope.fetch(tool_id)
18
+ valid = registration.validate.call(value: token, principal:)
19
+ unless valid
20
+ raise CommandTower::Errors::ForbiddenError
21
+ end
22
+
23
+ ScopeContext.new(
24
+ tool_id: definition.id,
25
+ scope_value: token,
26
+ scope_parameter: definition.scope_parameter
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminScope
5
+ ScopeContext = Data.define(:tool_id, :scope_value, :scope_parameter)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminScope
5
+ ScopeOption = Data.define(:value, :label)
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminScope
5
+ class Error < CommandTower::Error; end
6
+
7
+ class UnregisteredToolError < Error; end
8
+ class DuplicateRegistrationError < Error; end
9
+ class InvalidToolRegistrationError < Error; end
10
+ class MissingRegistrationError < Error; end
11
+ class FrozenRegistryError < Error; end
12
+ end
13
+ end
14
+
15
+ require "command_tower/admin_scope/scope_context"
16
+ require "command_tower/admin_scope/scope_option"
17
+ require "command_tower/admin_scope/resolve"
18
+ require "command_tower/admin_scope/apply_users_narrowing"
19
+ require "command_tower/admin_scope/apply_audit_scoping"
20
+ require "command_tower/admin_scope/manifest_projection"
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module AdminWorkspace
5
+ class Error < CommandTower::Error; end
6
+
7
+ class UnregisteredToolError < Error; end
8
+ class DuplicateToolError < Error; end
9
+ class HostOverrideError < Error; end
10
+ class InvalidToolNameError < Error; end
11
+ class InvalidToolDefinitionError < Error; end
12
+ class DuplicateRouteError < Error; end
13
+ class MissingRequiredEntityError < Error; end
14
+ class FrozenRegistryError < Error; end
15
+ end
16
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Audit
5
+ module Attribution
6
+ MODES = %i[self_service admin_direct impersonation system].freeze
7
+
8
+ module_function
9
+
10
+ def resolve(affected_user_id:, attribution_mode:)
11
+ requested = normalize_mode(attribution_mode)
12
+ impersonating = CommandTower::Current.impersonation_active == true
13
+
14
+ if impersonating
15
+ if requested && requested != :impersonation
16
+ raise InvalidAttributionError,
17
+ "attribution_mode #{requested} conflicts with active impersonation"
18
+ end
19
+
20
+ originating = CommandTower::Current.originating_administrator_id
21
+ if originating.nil?
22
+ raise InvalidAttributionError, "impersonation requires originating_administrator_id"
23
+ end
24
+
25
+ return envelope(
26
+ mode: :impersonation,
27
+ actor_user_id: originating,
28
+ affected_user_id:
29
+ )
30
+ end
31
+
32
+ if requested == :impersonation
33
+ raise InvalidAttributionError, "attribution_mode impersonation requires impersonation_active"
34
+ end
35
+
36
+ if requested == :system || (requested.nil? && CommandTower::Current.user_id.nil?)
37
+ return envelope(mode: :system, actor_user_id: nil, affected_user_id:)
38
+ end
39
+
40
+ if requested == :admin_direct
41
+ if CommandTower::Current.user_id.nil?
42
+ raise InvalidAttributionError, "admin_direct requires a Current.user_id actor"
43
+ end
44
+
45
+ return envelope(mode: :admin_direct, actor_user_id: CommandTower::Current.user_id, affected_user_id:)
46
+ end
47
+
48
+ actor_id = CommandTower::Current.user_id
49
+ if actor_id.nil?
50
+ raise InvalidAttributionError, "self_service requires a Current.user_id actor"
51
+ end
52
+
53
+ if requested.nil? && !affected_user_id.nil? && actor_id != affected_user_id
54
+ raise InvalidAttributionError,
55
+ "attribution_mode must be explicit when actor and affected user differ"
56
+ end
57
+
58
+ if requested == :self_service && !affected_user_id.nil? && actor_id != affected_user_id
59
+ raise InvalidAttributionError,
60
+ "self_service requires Current.user_id to match affected_user_id"
61
+ end
62
+
63
+ envelope(mode: :self_service, actor_user_id: actor_id, affected_user_id:)
64
+ end
65
+
66
+ def normalize_mode(value)
67
+ return nil if value.nil?
68
+
69
+ mode = value.to_sym
70
+ unless MODES.include?(mode)
71
+ raise InvalidAttributionError, "invalid attribution_mode #{value.inspect}"
72
+ end
73
+
74
+ mode
75
+ end
76
+
77
+ def envelope(mode:, actor_user_id:, affected_user_id:)
78
+ {
79
+ attribution_mode: mode,
80
+ actor_user_id:,
81
+ affected_user_id:,
82
+ effective_user_id: CommandTower::Current.effective_user_id || CommandTower::Current.user_id,
83
+ originating_administrator_id: CommandTower::Current.originating_administrator_id,
84
+ impersonation_active: CommandTower::Current.impersonation_active == true,
85
+ user_id: CommandTower::Current.user_id
86
+ }
87
+ end
88
+ end
89
+ end
90
+ end