command_tower 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -3
- data/app/auth/command_tower/auth/auth_context.rb +10 -4
- data/app/controllers/command_tower/admin/application_controller.rb +27 -0
- data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
- data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
- data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
- data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
- data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
- data/app/controllers/command_tower/admin/users_controller.rb +46 -0
- data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
- data/app/controllers/command_tower/application_controller.rb +17 -1
- data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
- data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
- data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
- data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
- data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
- data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
- data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
- data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
- data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
- data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
- data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
- data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
- data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
- data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
- data/app/jobs/command_tower/application_job.rb +7 -0
- data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
- data/app/mailers/command_tower/application_mailer.rb +11 -1
- data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
- data/app/models/command_tower/audit/event.rb +36 -0
- data/app/models/command_tower/impersonation/session.rb +51 -0
- data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
- data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
- data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
- data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
- data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
- data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
- data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
- data/app/services/command_tower/README.md +4 -10
- data/app/services/command_tower/authorize/validate.rb +7 -4
- data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
- data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
- data/app/services/command_tower/jwt/login_create.rb +7 -4
- data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
- data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
- data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
- data/app/services/command_tower/service_base.rb +30 -43
- data/app/services/command_tower/service_logging.rb +2 -36
- data/app/services/command_tower/services/account/clear_phone.rb +8 -0
- data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
- data/app/services/command_tower/services/account/update_phone.rb +9 -1
- data/app/services/command_tower/services/admin/users.rb +377 -0
- data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
- data/app/services/command_tower/services/application_service.rb +37 -0
- data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
- data/app/services/command_tower/services/audit/events.rb +199 -0
- data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
- data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
- data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
- data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
- data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
- data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
- data/app/services/command_tower/services/impersonation/create.rb +29 -0
- data/app/services/command_tower/services/impersonation/end.rb +28 -0
- data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
- data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
- data/app/services/command_tower/services/me/change_password.rb +12 -0
- data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
- data/app/workflows/command_tower/transactional.rb +65 -0
- data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
- data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
- data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
- data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
- data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
- data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
- data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
- data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
- data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
- data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
- data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
- data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
- data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
- data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
- data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
- data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
- data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
- data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
- data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
- data/config/routes.rb +20 -0
- data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
- data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
- data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
- data/docs/admin_workspace.md +158 -0
- data/docs/api_reference.md +175 -14
- data/docs/architecture.md +5 -0
- data/docs/audit.md +195 -0
- data/docs/authentication.md +14 -0
- data/docs/authentication_authorization_guide.md +15 -11
- data/docs/authorization.md +25 -4
- data/docs/controllers.md +7 -1
- data/docs/eventing.md +179 -0
- data/docs/extending.md +32 -1
- data/docs/host_integration_guide.md +103 -12
- data/docs/initializing.md +2 -2
- data/docs/messaging_integration_guide.md +1 -1
- data/docs/models.md +4 -0
- data/docs/pagination.md +2 -2
- data/docs/principal_capabilities.md +89 -0
- data/docs/upgrades/0.10.0.md +2 -2
- data/docs/upgrades/0.11.0.md +92 -0
- data/docs/upgrades/README.md +1 -0
- data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
- data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
- data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
- data/lib/command_tower/admin_scope/resolve.rb +31 -0
- data/lib/command_tower/admin_scope/scope_context.rb +7 -0
- data/lib/command_tower/admin_scope/scope_option.rb +7 -0
- data/lib/command_tower/admin_scope.rb +20 -0
- data/lib/command_tower/admin_workspace.rb +16 -0
- data/lib/command_tower/audit/attribution.rb +90 -0
- data/lib/command_tower/audit/emit.rb +113 -0
- data/lib/command_tower/audit/masking.rb +41 -0
- data/lib/command_tower/audit/payload.rb +128 -0
- data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
- data/lib/command_tower/audit.rb +27 -0
- data/lib/command_tower/authorization/assignable_roles.rb +36 -0
- data/lib/command_tower/authorization/default.yml +99 -5
- data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
- data/lib/command_tower/authorization/entity.rb +15 -5
- data/lib/command_tower/authorization/role.rb +5 -4
- data/lib/command_tower/authorization.rb +71 -10
- data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
- data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
- data/lib/command_tower/configuration/authorization/config.rb +6 -0
- data/lib/command_tower/configuration/config.rb +20 -0
- data/lib/command_tower/configuration/impersonation/config.rb +38 -0
- data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
- data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
- data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
- data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
- data/lib/command_tower/configuration/registry/config.rb +34 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
- data/lib/command_tower/current.rb +12 -0
- data/lib/command_tower/engine.rb +19 -4
- data/lib/command_tower/events.rb +184 -0
- data/lib/command_tower/execution.rb +111 -0
- data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
- data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
- data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
- data/lib/command_tower/impersonation/establish_identity.rb +44 -0
- data/lib/command_tower/install/baseline.rb +3 -0
- data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
- data/lib/command_tower/logging/projection.rb +67 -0
- data/lib/command_tower/logging/subscriber.rb +103 -0
- data/lib/command_tower/principal_capabilities.rb +15 -0
- data/lib/command_tower/version.rb +1 -1
- data/lib/command_tower.rb +12 -0
- data/spec/factories/impersonation_session.rb +17 -0
- data/spec/factories/user.rb +16 -0
- metadata +112 -2
- data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
|
@@ -61,6 +61,7 @@ Paths are engine-relative. Full request/response shapes: [api_reference.md](api_
|
|
|
61
61
|
| Session | `GET /auth/session` |
|
|
62
62
|
| Logout | `POST /auth/logout` |
|
|
63
63
|
| Current account | `GET /me`, `GET /profile` |
|
|
64
|
+
| Principal capabilities | `GET /auth/principal-capabilities` |
|
|
64
65
|
| Name | `PATCH /me/name` |
|
|
65
66
|
| Password change | `PATCH /me/password` |
|
|
66
67
|
| Identity policy | `GET /auth/identity-policy` |
|
|
@@ -92,25 +93,24 @@ Validate/reset use the emailed reset token in the body (public). See [password_r
|
|
|
92
93
|
|
|
93
94
|
`lib/command_tower/authorization/default.yml`:
|
|
94
95
|
|
|
95
|
-
- **`owner`** — `entities: true` (full access)
|
|
96
|
-
- **`admin`** — entity `admin_messaging_announcements` on `Admin::Messaging::AnnouncementsController#create`
|
|
96
|
+
- **`owner`** — `entities: true` (full access). Explicit top-level authority; distinct from host operational Admin roles.
|
|
97
97
|
|
|
98
|
-
There
|
|
98
|
+
There is **no** CommandTower operational `admin` role. Admin capabilities are CT-owned **entities** (`admin_workspace`, `admin_users`, `admin_impersonation`, `admin_audit_events`, `admin_messaging_announcements`, …) that hosts grant to their own roles. Impersonation start is `admin_impersonation`; it is not included in dummy `admin` / `operations_admin`. Nested impersonation is forbidden at HTTP **418** while overlaying (workflow nested 403 if reached). Stop is a session primitive (`DELETE /auth/impersonation-session`), not an Admin Users mutation. Other Admin resource endpoints return **418** `admin_unavailable_during_impersonation` during overlay except `GET /admin/workspace` (tools disabled).
|
|
99
99
|
|
|
100
100
|
### Host RBAC file (required for Me/Auth)
|
|
101
101
|
|
|
102
|
-
`AuthorizeRequest` fails closed when
|
|
102
|
+
`AuthorizeRequest` fails closed when the caller’s roles do not grant the CT-owned entity for the action. CommandTower ships those entity definitions. Hosts supply `config/rbac_groups.yml` with **product roles** that grant entity **names** (default path already `config/rbac_groups.yml`):
|
|
103
103
|
|
|
104
104
|
```ruby
|
|
105
105
|
CommandTower.configure do |c|
|
|
106
106
|
c.authorization.rbac_group_path = Rails.root.join("config/rbac_groups.yml")
|
|
107
|
+
c.authorization.default_membership_role = "member"
|
|
107
108
|
end
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
Dummy host example: `rails_app/config/rbac_groups.yml` —
|
|
111
|
-
|
|
112
|
-
Admin announcements: assign users the `admin` role (or another role that includes `admin_messaging_announcements`).
|
|
111
|
+
Dummy host example: `rails_app/config/rbac_groups.yml` — **`member`** grants session, me, profile, inbox, preferences, phone, pushover, email verification; operator roles grant selected Admin entities; a host-owned **`admin`** may deliberately grant a broad Admin bundle. Do not redefine `owner` or copy CT controller/entity blocks. Hosts may define an `admin` role as host policy.
|
|
113
112
|
|
|
113
|
+
Admin announcements: assign users a host role that includes `admin_messaging_announcements` (for example a host `admin` or `messaging_operator`).
|
|
114
114
|
### Host controller recipe
|
|
115
115
|
|
|
116
116
|
```ruby
|
|
@@ -124,17 +124,21 @@ class Host::ThingsController < ApplicationController
|
|
|
124
124
|
end
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
Map host controllers to RBAC entities the
|
|
127
|
+
Map host controllers to **host-owned** RBAC entities in the host YAML. Grant CT-owned entity names to product roles; do not copy engine controller mappings.
|
|
128
128
|
|
|
129
129
|
## Engine admin HTTP
|
|
130
130
|
|
|
131
|
-
Only:
|
|
132
|
-
|
|
133
131
|
```http
|
|
132
|
+
GET /admin/workspace
|
|
133
|
+
GET /admin/users
|
|
134
|
+
GET /admin/users/:id
|
|
135
|
+
POST /admin/users/:id/impersonation-sessions
|
|
136
|
+
GET /admin/audit-events
|
|
134
137
|
POST /admin/messaging/announcements
|
|
138
|
+
DELETE /auth/impersonation-session
|
|
135
139
|
```
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
Impersonation is a session overlay, not a User mutation and not an Admin Workspace tool. There is no role-assign or attribute-modify admin surface.
|
|
138
142
|
|
|
139
143
|
## Email verification gate
|
|
140
144
|
|
data/docs/authorization.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Authorization establishes **permission** after authentication. Failed authorization returns `403`.
|
|
4
4
|
|
|
5
|
-
Host `rbac_groups.yml` is a **required integration step** (Step 4) — see [Host integration](host_integration_guide.md#step-4--host-rbac-required). Without
|
|
5
|
+
Host `rbac_groups.yml` is a **required integration step** (Step 4) — see [Host integration](host_integration_guide.md#step-4--host-rbac-required). CommandTower ships CT-owned entity definitions. Hosts grant those names to product roles; they must not copy CT controller mappings. Without a host role that grants Me/Auth entities, authenticated calls return **403**.
|
|
6
6
|
|
|
7
7
|
## Quick usage (host provisional)
|
|
8
8
|
|
|
@@ -14,20 +14,41 @@ before_action :authorize_user!
|
|
|
14
14
|
|
|
15
15
|
Engine controllers use `authorize_request!` instead (AuthorizationBoundary) and return the application envelope on failure.
|
|
16
16
|
|
|
17
|
+
## Capability ownership vs role composition
|
|
18
|
+
|
|
19
|
+
**Platform owns capabilities.** CommandTower defines RBAC entities (Me/Auth surfaces and Admin capabilities such as `admin_workspace`, `admin_users`, `admin_impersonation`, `admin_audit_events`, `admin_messaging_announcements`).
|
|
20
|
+
|
|
21
|
+
**Hosts own privilege bundles.** Operational/admin roles are host-defined groups that grant selected CT entities (and optional host entities). Installing CommandTower does **not** automatically grant operational Admin access.
|
|
22
|
+
|
|
17
23
|
## Roles and host mapping
|
|
18
24
|
|
|
19
25
|
Engine defaults in `lib/command_tower/authorization/default.yml`:
|
|
20
26
|
|
|
21
|
-
- `owner` — full access (`entities: true`)
|
|
22
|
-
-
|
|
27
|
+
- `owner` — explicit full access (`entities: true`). Distinct from host-defined operational Admin roles.
|
|
28
|
+
- CT-owned **entities** for Me, session, profile, inbox, preferences, phone, Pushover, email verification, admin announcements, admin audit events, Admin Users, impersonation start, and the Admin Workspace manifest
|
|
29
|
+
- **No** CommandTower operational `admin` role. New Admin entities require explicit host grants and must not accumulate into a platform Admin bundle.
|
|
30
|
+
|
|
31
|
+
Hosts grant `admin_impersonation` explicitly. Dummy `admin` / `operations_admin` do **not** include it; `impersonation_operator` does. `owner` keeps `entities: true`.
|
|
32
|
+
|
|
33
|
+
Stop / return-to-self (`DELETE /auth/impersonation-session`) is a session primitive: it authenticates the administrator JWT and does not authorize `admin_impersonation` on the effective (target) user.
|
|
34
|
+
|
|
35
|
+
While overlaying, Admin resource endpoints other than `GET /admin/workspace` return **418** `admin_unavailable_during_impersonation`. Workspace remains allowed so the operator can see disabled tiles. Impersonation start consumes Admin Resource Scoping as the sole target-visibility contract.
|
|
36
|
+
|
|
37
|
+
Hosts define product roles (for example `member`, `audit_operator`, or a deliberate host-owned `admin`) that **grant entity names**. See dummy host `rails_app/config/rbac_groups.yml`. Do not redefine `owner` or CT **entity** identifiers. A host may define a broad `admin` role as **host policy**; that is allowed. Composition is additive; conflicts fail at boot.
|
|
38
|
+
|
|
39
|
+
Optional: `config.authorization.default_membership_role = "member"` assigns that composed role atomically on register.
|
|
40
|
+
|
|
41
|
+
## Principal capabilities (FE projection)
|
|
23
42
|
|
|
24
|
-
|
|
43
|
+
`GET /auth/principal-capabilities` returns possessed **frontend-projectable** ids from effective entity grants ∩ `config.registry.principal_capabilities`. Groups compose privilege; they are never the FE gating contract. See [Principal capabilities](principal_capabilities.md).
|
|
25
44
|
|
|
26
45
|
## Where to go next
|
|
27
46
|
|
|
28
47
|
| Need | Guide |
|
|
29
48
|
|------|-------|
|
|
30
49
|
| Full RBAC entities, roles, failure shapes | [Authentication & authorization guide](authentication_authorization_guide.md) |
|
|
50
|
+
| Frontend-projectable capability ids | [Principal capabilities](principal_capabilities.md) |
|
|
51
|
+
| Admin Workspace least privilege | [Admin Workspace](admin_workspace.md) |
|
|
31
52
|
| Authentication | [Authentication](authentication.md) |
|
|
32
53
|
| Install / config | [Initializing](initializing.md) |
|
|
33
54
|
|
data/docs/controllers.md
CHANGED
|
@@ -8,11 +8,15 @@ This page is an **index** of route areas. Detailed request/response contracts li
|
|
|
8
8
|
|
|
9
9
|
| Area | Prefix (engine-relative) | Purpose |
|
|
10
10
|
|------|--------------------------|---------|
|
|
11
|
-
| Auth session | `/auth/*` | Login, logout, session, register, signup-session, identity policy, availability |
|
|
11
|
+
| Auth session | `/auth/*` | Login, logout, session, register, signup-session, identity policy, principal-capabilities, availability |
|
|
12
12
|
| Email verification | `/auth/email-verification/*` | Send / verify email codes |
|
|
13
13
|
| Password recovery | `/auth/password-recovery-session`, `/auth/password-reset/*` | Forgot / reset password |
|
|
14
14
|
| Me / profile | `/me`, `/profile`, `/me/name`, `/me/password` | Account reads and updates |
|
|
15
15
|
| Inbox | `/me/inbox*` | User inbox consume (list, open, archive, bulk ops) |
|
|
16
|
+
| Audit events | `/me/audit-events`, `/admin/audit-events` | User and admin audit history reads (masked) |
|
|
17
|
+
| Admin Users | `/admin/users` | Read-only Admin user list/show |
|
|
18
|
+
| Impersonation | `/admin/users/:id/impersonation-sessions`, `/auth/impersonation-session` | Start overlay; stop / return-to-self |
|
|
19
|
+
| Admin Workspace | `/admin/workspace` | RBAC-filtered admin tool manifest |
|
|
16
20
|
| Preferences | `/me/preferences*` | Notification preferences |
|
|
17
21
|
| Phone | `/me/phone*` | Phone endpoint + verification |
|
|
18
22
|
| Pushover | `/me/pushover*` | Pushover endpoint lifecycle + verification |
|
|
@@ -27,6 +31,8 @@ Engine controllers are transport adapters: authenticate/authorize as required, d
|
|
|
27
31
|
## Related
|
|
28
32
|
|
|
29
33
|
- [API reference](api_reference.md) — endpoint catalog
|
|
34
|
+
- [Principal capabilities](principal_capabilities.md) — FE-projectable possession projection
|
|
35
|
+
- [Admin Workspace](admin_workspace.md) — registry and manifest
|
|
30
36
|
- [Messaging](messaging_integration_guide.md) — messaging surfaces summary
|
|
31
37
|
- [Extending](extending.md) — host extension boundaries
|
|
32
38
|
- [Initializing](initializing.md) — mount and configure
|
data/docs/eventing.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Eventing
|
|
2
|
+
|
|
3
|
+
CommandTower publishes internal events on **one** Rails-native network: `ActiveSupport::Notifications`. Hosts and platform code do not invent a second bus.
|
|
4
|
+
|
|
5
|
+
Logging is a **consumer** of that network. CommandTower supplies structured Hash fields to `Rails.logger`. The host Rails logger/formatter/tagged/broadcast configuration owns the final representation. CommandTower does not JSON-encode log lines.
|
|
6
|
+
|
|
7
|
+
**Event emission is exhaustive; log materialization is selective.** Lifecycle started/completed pairs always publish. **Workflow** completed success/deferred logs to Rails `info` by default (`ApplicationWorkflow` declares `log_lifecycle!`). **Service** completed success stays quiet unless the service class opts in. `started` never materializes. Envelope `log_lifecycle` means “success may be materialized,” not “emit only when true.” Subscribers must not `const_get(subject)`.
|
|
8
|
+
|
|
9
|
+
Audit persistence is a **raising** subscriber on `command_tower.audit.*` (not on lifecycle, and not the logging subscriber). **Audit authoring** (`audit(...)`, registered policy, structured emission) is documented in [Audit](audit.md).
|
|
10
|
+
|
|
11
|
+
## Grammar
|
|
12
|
+
|
|
13
|
+
Instrument names are the subscription keys:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
command_tower.<category>.<name...>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `command_tower` is the platform prefix. Hosts do **not** prepend it themselves.
|
|
20
|
+
- `category` is one token: `\A[a-z][a-z0-9_]*\z`. Categories are **extensible**, not a closed enum.
|
|
21
|
+
- `name` is one or more tokens of the same shape, joined by `.`.
|
|
22
|
+
|
|
23
|
+
Do **not** publish a generic `command_tower.event` name and dispatch on `payload[:type]`.
|
|
24
|
+
|
|
25
|
+
Do **not** use `ActiveSupport::Subscriber#attach_to` as the taxonomy driver. That convention is reversed `event.namespace` (for example `sql.active_record`). CommandTower names are `command_tower.<category>.<name...>`. Subscribe with the full string or a prefix regexp.
|
|
26
|
+
|
|
27
|
+
### Lifecycle (automatic)
|
|
28
|
+
|
|
29
|
+
| Instrument name |
|
|
30
|
+
|-----------------|
|
|
31
|
+
| `command_tower.lifecycle.workflow.started` |
|
|
32
|
+
| `command_tower.lifecycle.workflow.completed` |
|
|
33
|
+
| `command_tower.lifecycle.service.started` |
|
|
34
|
+
| `command_tower.lifecycle.service.completed` |
|
|
35
|
+
|
|
36
|
+
Every `ApplicationWorkflow` `.call` / `call_from_job` invocation and every `ServiceBase` Interactor invocation emits one started/completed pair. Nested workflow→service shares `execution_uuid` and emits four events (two pairs).
|
|
37
|
+
|
|
38
|
+
### Semantic (deliberate)
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
command_tower.<category>.<name...>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Example: `command_tower.audit.wager_transition` via **registered** `audit(:wager_transition, ...)` — see [Audit](audit.md). Generic scalar `publish_event` remains available for non-audit categories; nested audit `changes` require `audit(...)` / `Events.publish_audit`.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
publish_event(category: :messaging, name: :welcome_produce_failed, payload: { code: "x" })
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`publish_event` is on `CommandTower::Execution::ContextAccess` (workflows and services inherit it). It delegates to `CommandTower::Events.publish`. Do not include a separate Eventing module.
|
|
51
|
+
|
|
52
|
+
## Subscribe
|
|
53
|
+
|
|
54
|
+
Rails 8.1 Fanout matches the **full instrument string**.
|
|
55
|
+
|
|
56
|
+
Exact:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
ActiveSupport::Notifications.subscribe("command_tower.lifecycle.workflow.completed") { |*args| }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Broad (lifecycle category):
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
ActiveSupport::Notifications.subscribe(/\Acommand_tower\.lifecycle(?:\.|\z)/) { |*args| }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Envelope
|
|
69
|
+
|
|
70
|
+
Payload is a frozen Hash of scalars (and frozen arrays of scalars). Never `CommandTower::Current`, User, request, AuthContext, JWT, cookies, or exception objects.
|
|
71
|
+
|
|
72
|
+
| Field | When |
|
|
73
|
+
|-------|------|
|
|
74
|
+
| `event_uuid` | every event (new UUID at publish; **not** `execution_uuid`) |
|
|
75
|
+
| `subject` | class name string |
|
|
76
|
+
| `layer` | `:workflow` / `:service` (lifecycle only) |
|
|
77
|
+
| `execution_uuid`, `correlation_id`, `request_id`, `causation_id`, `source` | Execution Context snapshot |
|
|
78
|
+
| `user_id`, `effective_user_id`, `originating_administrator_id`, `impersonation_active` | snapshot |
|
|
79
|
+
| `outcome` | completed only: `:success` / `:deferred` / `:failure` / `:error` |
|
|
80
|
+
| `duration_ms` | completed only (monotonic clock from matching started) |
|
|
81
|
+
| `error_class`, `error_codes` | completed failure/error; codes only, no messages/PII |
|
|
82
|
+
| caller keys | semantic publish only; unsafe objects are dropped |
|
|
83
|
+
|
|
84
|
+
`event_uuid` ≠ `execution_uuid`. There is no parent/child event graph. Nested work shares `execution_uuid` only.
|
|
85
|
+
|
|
86
|
+
The snapshot is taken at publish time and is frozen against later `Current` mutation.
|
|
87
|
+
|
|
88
|
+
## Publisher
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
CommandTower::Events.publish(category:, name:, payload: {}, subject: nil, layer: nil)
|
|
92
|
+
CommandTower::Events.snapshot
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`publish` builds the instrument name, merges snapshot + `event_uuid` + sanitized caller payload, then calls `ActiveSupport::Notifications.instrument` **without a block**. Business work is **not** yielded to `instrument`.
|
|
96
|
+
|
|
97
|
+
## Subscriber failure (Rails-native)
|
|
98
|
+
|
|
99
|
+
CommandTower uses ASN as a **synchronous** event network. `Events.publish` does **not** wrap `instrument` in `rescue StandardError`.
|
|
100
|
+
|
|
101
|
+
> CommandTower preserves Rails-native synchronous `ActiveSupport::Notifications` publication semantics. Subscriber failure policy belongs to the subscriber/capability consuming the event. Best-effort consumers must protect their own failure boundary. Correctness-sensitive subscriber semantics are defined by the owning capability.
|
|
102
|
+
|
|
103
|
+
The generic publisher does **not** impose either policy.
|
|
104
|
+
|
|
105
|
+
| Consumer | Policy owner |
|
|
106
|
+
|----------|----------------|
|
|
107
|
+
| Logging subscriber | **This slice** — best-effort; rescues materialization failures |
|
|
108
|
+
| Metrics subscriber | future (likely self-protecting) |
|
|
109
|
+
| Audit subscriber | later phase (may be correctness-sensitive) |
|
|
110
|
+
| Other semantic subscribers | owning capability |
|
|
111
|
+
|
|
112
|
+
The logging subscriber (`CommandTower::Logging::Subscriber` < `ActiveSupport::LogSubscriber`) subscribes with prefix regexes (not `attach_to`) to:
|
|
113
|
+
|
|
114
|
+
- `command_tower.lifecycle.*`
|
|
115
|
+
- `command_tower.log.*`
|
|
116
|
+
- `command_tower.messaging.*`
|
|
117
|
+
|
|
118
|
+
It does **not** subscribe to `audit` or `metric`. It calls `Rails.logger` with a Hash. Host formatters decide text vs JSON vs tagged output.
|
|
119
|
+
|
|
120
|
+
### Lifecycle Rails logs
|
|
121
|
+
|
|
122
|
+
`ApplicationWorkflow` declares `log_lifecycle!`, so workflow **completed** success/deferred materializes at `info` by default. Services stay quiet unless they opt in. Use `disable_lifecycle_logging!` on a workflow subclass to suppress its completed success line.
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class NoisyService < ApplicationService
|
|
126
|
+
log_lifecycle!
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class QuietWorkflow < ApplicationWorkflow
|
|
130
|
+
disable_lifecycle_logging!
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`lifecycle_loggable?` walks superclasses when unset. `log_lifecycle:` is passed into `Events.around_execution` as a **boolean scalar**. `started` is never materialized.
|
|
135
|
+
|
|
136
|
+
### Severity and materialization
|
|
137
|
+
|
|
138
|
+
| Event | Materialize? | Level when materialized |
|
|
139
|
+
|-------|----------------|-------------------------|
|
|
140
|
+
| lifecycle started | **no** (even with opt-in — completed has outcome/duration) | — |
|
|
141
|
+
| lifecycle completed success / deferred | only if `payload[:log_lifecycle]` | `info` |
|
|
142
|
+
| lifecycle completed failure | **always** | payload `log_level` or `:info` |
|
|
143
|
+
| lifecycle completed error | **always** | `error` |
|
|
144
|
+
| `command_tower.log.<level>` | **always** | that level |
|
|
145
|
+
| `command_tower.messaging.*` | unchanged (3.3) | payload `log_level` or `:info` |
|
|
146
|
+
|
|
147
|
+
Authorization (`Authorize::Validate`) success diagnostics (“No Authorization required”, “User Roles”, per-role Authorized/Reason) publish `command_tower.log.debug`. Denial publishes `command_tower.log.warn`.
|
|
148
|
+
|
|
149
|
+
### Events are comprehensive. Logs are projections.
|
|
150
|
+
|
|
151
|
+
ASN payloads remain full Execution Context snapshots (`event_uuid`, nils, `layer`, `log_lifecycle`, duplicate identity fields). The logging subscriber **projects** a new Hash for `Rails.logger`. It does not mutate the canonical payload and does not define the event contract.
|
|
152
|
+
|
|
153
|
+
**Core log fields** (when present): `event`, `subject`, `execution_uuid`, `correlation_id`, lifecycle `outcome`, `duration_ms`.
|
|
154
|
+
|
|
155
|
+
**Conditional:** `user_id` when present; `request_id` when present and different from `correlation_id`; `source` when not `:http`; `causation_id` / `originating_administrator_id` when present; `effective_user_id` when different from `user_id`; `impersonation_active` only when `true`; `error_class` / `error_codes` when present.
|
|
156
|
+
|
|
157
|
+
**Omitted from ordinary logs:** nils; `event_uuid`; `layer`; `log_lifecycle`; payload `log_level` (Rails severity already carries level); default `impersonation_active: false`; HTTP `source`.
|
|
158
|
+
|
|
159
|
+
Semantic events (`command_tower.messaging.*`, `command_tower.log.*`) keep the same common context plus remaining safe scalar fields (`message`, `channel`, `provider`, `attempt`, …). Host formatters still own JSON / tags / LGTM representation; CommandTower does not promise a Rails JSON schema.
|
|
160
|
+
|
|
161
|
+
`ApplicationError#log_level` is copied onto completed failure events as a scalar.
|
|
162
|
+
|
|
163
|
+
`log_debug` / `log_info` / `log_warn` / `log_error` publish `command_tower.log.*`. Messaging OperationLoggers publish `command_tower.messaging.*`. Delivery LogAdapters remain a **transport** (`Rails.logger.info` of a send), not observation.
|
|
164
|
+
|
|
165
|
+
Direct `Rails.logger` remains allowed for JWT decode/authenticate, boot/lib/controllers, and LogAdapters. Workflows/services must not write lifecycle observation directly.
|
|
166
|
+
|
|
167
|
+
A raising **logging** subscriber does not fail the business action. A raising **non-logging** test/audit subscriber still surfaces through `Events.publish`.
|
|
168
|
+
|
|
169
|
+
Emission and consumption remain separate: logger silence / level skips **materialization**, never ASN publication.
|
|
170
|
+
|
|
171
|
+
Architecture specs guard these seams: one `CommandTower::Current`, kernel-owned lifecycle, `Events.publish` as the ASN publisher, curated log projection, and an explicit `Rails.logger` allowlist in JWT/LogAdapter infrastructure.
|
|
172
|
+
|
|
173
|
+
## Kernel behavior (when subscribers do not raise)
|
|
174
|
+
|
|
175
|
+
- `ApplicationWorkflow.call`: unexpected `StandardError` still becomes InternalError **after** completed `outcome: :error`. `validate_retry_strategy!` stays outside instrumentation.
|
|
176
|
+
- `call_from_job` / `invoke_for_job`: still **re-raise** after completed `outcome: :error`.
|
|
177
|
+
- Services: Interactor success → `:success`; `Interactor::Failure` → `:failure` then re-raise; unexpected exception → `:error` then re-raise. Services have no deferred outcome.
|
|
178
|
+
|
|
179
|
+
Do not wrap instance `#call` in addition to class entry (duplicate pairs). Do not also wrap `ApplicationService.call` (the Interactor `around` is the service pair).
|
data/docs/extending.md
CHANGED
|
@@ -17,7 +17,7 @@ Layer map: [architecture.md](architecture.md). Install/configure/migrate/doctor:
|
|
|
17
17
|
| `Communications::Produce` / `ProduceMany` | Sending messaging |
|
|
18
18
|
| `FactoryBot.modify` | Extend shared factories |
|
|
19
19
|
| Model reopen | Product associations / behavior |
|
|
20
|
-
| Initializers | Configuration |
|
|
20
|
+
| Initializers | Configuration, including `config.registry.audit.event`, `config.registry.admin_workspace.tool`, and `config.registry.principal_capabilities.capability` |
|
|
21
21
|
| Notification catalogs / channel policy | Host-owned messaging customization |
|
|
22
22
|
|
|
23
23
|
## Internal platform — do not extend
|
|
@@ -97,8 +97,39 @@ Do not create one Messaging workflow per message type. Do not call the messaging
|
|
|
97
97
|
|
|
98
98
|
No DoubleFloor Me (or other product) source is required to use the platform.
|
|
99
99
|
|
|
100
|
+
## Execution Context
|
|
101
|
+
|
|
102
|
+
Greenfield hosts inherit CommandTower-owned execution-boundary bases so HTTP and jobs establish ambient context automatically:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class ApplicationController < CommandTower::ApplicationController
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class ApplicationJob < CommandTower::ApplicationJob
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`CommandTower::Current` is the single `CurrentAttributes` bag (`execution_uuid`, `correlation_id`, `request_id`, `source`, identity scalars). Workflows and services **read** it (`execution_context`); they do not mint a new execution per call.
|
|
113
|
+
|
|
114
|
+
`CommandTower::Auth::RequestContext` remains the HTTP request/response JWT transport handle. It is not Execution Context.
|
|
115
|
+
|
|
116
|
+
Rake/console (and other non-HTTP/job entry points) wrap work with:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
CommandTower.with_execution(source: :rake) do
|
|
120
|
+
# ...
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Nested `with_execution` shares the outer context. Do not wrap `db:migrate` or doctor.
|
|
125
|
+
|
|
126
|
+
If a host **cannot** change `ApplicationController` / `ApplicationJob` superclasses, include `CommandTower::Execution::HttpBoundary` / `CommandTower::Execution::JobBoundary` as a compatibility escape hatch. Do not include those modules into every `ActionController` or `ActiveJob::Base`.
|
|
127
|
+
|
|
128
|
+
Automatic workflow/service lifecycle notifications are emitted by CommandTower kernels. Logging is a subscriber: emission is exhaustive; materialization is selective; log records are curated projections. The host owns format. See [Eventing](eventing.md).
|
|
129
|
+
|
|
100
130
|
## Related
|
|
101
131
|
|
|
132
|
+
- [Eventing](eventing.md)
|
|
102
133
|
- [Architecture](architecture.md)
|
|
103
134
|
- [Controllers](controllers.md)
|
|
104
135
|
- [Sensitive changes](sensitive_routes.md)
|
|
@@ -37,6 +37,24 @@ Re-run `bin/rails command_tower:doctor`. Details: [Initializing — Configuratio
|
|
|
37
37
|
|
|
38
38
|
Dummy-host reference: [`rails_app/config/initializers/command_tower.rb`](../rails_app/config/initializers/command_tower.rb).
|
|
39
39
|
|
|
40
|
+
### Email / SMTP
|
|
41
|
+
|
|
42
|
+
CommandTower owns ActionMailer delivery for engine mailers (email verification, password reset, messaging channel mail).
|
|
43
|
+
|
|
44
|
+
| Environment | Delivery |
|
|
45
|
+
|-------------|----------|
|
|
46
|
+
| `test` | `:test` (in-memory `ActionMailer::Base.deliveries`; no external SMTP) |
|
|
47
|
+
| development / production | `:smtp` unless the host explicitly sets `config.email.delivery_method` |
|
|
48
|
+
|
|
49
|
+
Non-secret knobs live on `config.email.*` (defaults: `smtp.gmail.com`, port `587`, `plain`, STARTTLS auto). Secrets come from Credential Resolution:
|
|
50
|
+
|
|
51
|
+
- `config.credentials.smtp.user_name` / `password`, or
|
|
52
|
+
- ENV `GMAIL_USER_NAME` / `GMAIL_PASSWORD`
|
|
53
|
+
|
|
54
|
+
`SmtpActionMailerBridge` merges resolved credentials into `action_mailer.smtp_settings` at the end of `CommandTower.configure`. Missing credentials fail at send when `raise_delivery_errors` is true. `From` uses the resolved SMTP username when present.
|
|
55
|
+
|
|
56
|
+
Doctor does **not** probe SMTP connectivity.
|
|
57
|
+
|
|
40
58
|
## Step 3 — Confirm mount path
|
|
41
59
|
|
|
42
60
|
Ensure routes include something like:
|
|
@@ -47,35 +65,108 @@ mount CommandTower::Engine => "/" # or "/api"
|
|
|
47
65
|
|
|
48
66
|
Engine paths below are **relative to that mount**. Route area index: [Controllers](controllers.md).
|
|
49
67
|
|
|
68
|
+
## Step 3b — Inherit execution-boundary bases
|
|
69
|
+
|
|
70
|
+
Greenfield hosts should inherit CommandTower bases so Execution Context is established automatically:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class ApplicationController < CommandTower::ApplicationController
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class ApplicationJob < CommandTower::ApplicationJob
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Unauthenticated host endpoints (for example health checks) still receive HTTP `execution_uuid` / `correlation_id`. Successful authentication enriches the **same** context with `user_id` / `effective_user_id`.
|
|
81
|
+
|
|
82
|
+
Workflows and services consume `CommandTower::Current` (or `execution_context`); they do not establish a new execution. For Rake/console, use `CommandTower.with_execution(source: :rake) { ... }`.
|
|
83
|
+
|
|
84
|
+
`Auth::RequestContext` is JWT request/response transport, not Execution Context. Details: [Extending — Execution Context](extending.md#execution-context).
|
|
85
|
+
|
|
86
|
+
Lifecycle and semantic events: [Eventing](eventing.md).
|
|
87
|
+
|
|
88
|
+
If superclass inheritance is technically blocked, include `CommandTower::Execution::HttpBoundary` / `JobBoundary` on the host bases. That is an escape hatch, not the preferred contract.
|
|
89
|
+
|
|
50
90
|
## Step 4 — Host RBAC (required)
|
|
51
91
|
|
|
52
|
-
AuthorizeRequest **fails closed**.
|
|
92
|
+
AuthorizeRequest **fails closed**. CommandTower ships **CT-owned** entity definitions (Me, session, inbox, Admin Workspace capabilities, …) and the platform full-access role (`owner`) in `lib/command_tower/authorization/default.yml`. CommandTower does **not** ship an operational `admin` role.
|
|
93
|
+
|
|
94
|
+
The host YAML (default `config/rbac_groups.yml`) is a **second source**. Composition is additive. Hosts:
|
|
95
|
+
|
|
96
|
+
1. Define product roles (typically `member`).
|
|
97
|
+
2. **Grant names** of already-defined CT entities to those roles.
|
|
98
|
+
3. Optionally define **host-owned** entities for **host** controllers.
|
|
99
|
+
4. Deliberately compose operational Admin roles (least privilege or a broad host-owned `admin`).
|
|
53
100
|
|
|
54
|
-
|
|
101
|
+
Do **not** copy CommandTower controller/entity blocks into the host file. Do **not** redefine `owner` or CT entity names. A host **may** define an `admin` group as host policy. Conflicts fail at boot (no last-write-wins).
|
|
55
102
|
|
|
56
|
-
|
|
57
|
-
2. Keep a host group such as `member` with entities for session, me, profile, inbox, preferences, phone, pushover, email verification.
|
|
58
|
-
3. Do **not** redefine engine groups that already exist in `default.yml` (`owner`, `admin`).
|
|
59
|
-
4. Point config at the file if needed:
|
|
103
|
+
Dummy host grants-only example: [`rails_app/config/rbac_groups.yml`](../rails_app/config/rbac_groups.yml).
|
|
60
104
|
|
|
61
105
|
```ruby
|
|
62
106
|
CommandTower.configure do |c|
|
|
63
107
|
c.authorization.rbac_group_path = Rails.root.join("config/rbac_groups.yml")
|
|
108
|
+
c.authorization.default_membership_role = "member" # optional; nil disables
|
|
64
109
|
end
|
|
65
110
|
```
|
|
66
111
|
|
|
67
|
-
|
|
112
|
+
`default_membership_role` is validated against the **composed** graph at boot. Unknown names fail configuration finalization.
|
|
68
113
|
|
|
69
114
|
More: [Authorization](authorization.md), [Authentication & authorization guide](authentication_authorization_guide.md).
|
|
70
115
|
|
|
116
|
+
Product audit names (later) register additively:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
CommandTower.configure do |c|
|
|
120
|
+
c.registry.audit.event :wager_placed do |event|
|
|
121
|
+
event.allowed_changes = %i[status]
|
|
122
|
+
event.user_history = true
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Do **not** redefine CommandTower-owned audit names. Audit rows persist in CommandTower after `command_tower:install:migrations` and `db:migrate`. See [Audit](audit.md).
|
|
128
|
+
|
|
129
|
+
Admin Workspace tools register additively (navigation metadata, not a dispatcher):
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
CommandTower.configure do |c|
|
|
133
|
+
c.registry.admin_workspace.tool :host_example do |tool|
|
|
134
|
+
tool.label = "Example"
|
|
135
|
+
tool.description = "Short launcher explanation of what this tool does."
|
|
136
|
+
tool.route = "/admin/example"
|
|
137
|
+
tool.group = :product
|
|
138
|
+
tool.sort_order = 300
|
|
139
|
+
tool.required_entity = :host_example_entity
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Do **not** redefine CommandTower-owned tool ids (`users`, `audit`, `messaging`). Hosts own copy for host tools (`description` soft ≤100 / hard ≤160). Manifest: [Admin Workspace](admin_workspace.md).
|
|
145
|
+
|
|
146
|
+
Principal capabilities register additively (frontend-projectable ids, not a dump of all entities):
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
CommandTower.configure do |c|
|
|
150
|
+
c.registry.principal_capabilities.capability :host_example do |capability|
|
|
151
|
+
capability.required_entity = :host_example_entity
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Do **not** redefine CommandTower-owned capability ids (`admin_workspace`, `admin_users`, `admin_impersonation`, `admin_audit_events`, `admin_messaging_announcements`). Projection: [Principal capabilities](principal_capabilities.md). Grant `admin_impersonation` only to roles that should start impersonation of visible users. Dummy `admin` does not include it.
|
|
157
|
+
|
|
71
158
|
## Step 5 — Roles on users
|
|
72
159
|
|
|
73
160
|
RBAC YAML defines what a role **may** do. Users still need that role assigned.
|
|
74
161
|
|
|
75
|
-
- Specs and the dummy pattern use role name **`member`** for Me/Auth surfaces.
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
|
|
162
|
+
- Specs and the dummy pattern use role name **`member`** for Me/Auth surfaces (including entity `principal_capabilities`).
|
|
163
|
+
- When `authorization.default_membership_role` is set (for example `"member"`), register assigns that role **in the same transaction** as user create. Failure rolls back the user.
|
|
164
|
+
- When it is `nil`, register does not attach roles. Hosts may assign via product logic, ops (`command_tower:users:create`), or other host workflows.
|
|
165
|
+
- Installing CommandTower does **not** grant operational Admin access. Hosts must deliberately grant Admin entities.
|
|
166
|
+
- Admin announcements need a host role that includes `admin_messaging_announcements`.
|
|
167
|
+
- Admin Workspace manifest needs a host role that includes `admin_workspace`. Tool visibility still depends on each tool's `required_entity`.
|
|
168
|
+
- `GET /auth/principal-capabilities` needs entity `principal_capabilities` on the caller’s roles (grant on `member` like session/me). Possessed Admin projectables still depend on the Admin entity grants above.
|
|
169
|
+
- A host may define a broad `admin` role that grants many Admin entities — that is host policy, not a CommandTower default.
|
|
79
170
|
## Step 6 — Enable feature gates you need
|
|
80
171
|
|
|
81
172
|
When a gate is off, the route is **not drawn** → **404**.
|
|
@@ -101,7 +192,7 @@ Engine HTTP success/error bodies use the application envelope `{ data, meta, err
|
|
|
101
192
|
|
|
102
193
|
With gates and RBAC in place (paths relative to your mount):
|
|
103
194
|
|
|
104
|
-
1. `POST /auth/register` (always drawn) —
|
|
195
|
+
1. `POST /auth/register` (always drawn) — with `default_membership_role` configured, the user is created **and** assigned that role in one transaction.
|
|
105
196
|
2. `POST /auth/plain-text/login` (if enabled) — receive `data.token`.
|
|
106
197
|
3. `GET /me` with Bearer token — expect **200** and envelope `data` (account payload).
|
|
107
198
|
|
data/docs/initializing.md
CHANGED
|
@@ -21,8 +21,8 @@ bin/rails command_tower:doctor
|
|
|
21
21
|
|
|
22
22
|
Continue with [Host integration](host_integration_guide.md):
|
|
23
23
|
|
|
24
|
-
1. Host `rbac_groups.yml` (required — otherwise authenticated Me/Auth calls **403**)
|
|
25
|
-
2.
|
|
24
|
+
1. Host `rbac_groups.yml` product role that **grants** CT-owned Me/Auth entity names (required — otherwise authenticated Me/Auth calls **403**)
|
|
25
|
+
2. Set `authorization.default_membership_role` (for example `"member"`) or assign roles some other supported way
|
|
26
26
|
3. Enable feature gates you need (login, reset, availability, …)
|
|
27
27
|
4. Smoke-check `GET /me` with a Bearer token
|
|
28
28
|
5. Wire messaging catalog/adapters when you emit or use phone/Pushover
|
|
@@ -74,7 +74,7 @@ Pagination detail: [pagination.md](pagination.md). Full catalog: [api_reference.
|
|
|
74
74
|
- Notification catalog content (registered into CommandTower notification types)
|
|
75
75
|
- `platform_enabled_channels` / channel policy injection
|
|
76
76
|
- Messaging adapter credentials (email / SMS / Pushover) via initializer or ENV
|
|
77
|
-
- Host
|
|
77
|
+
- Host product roles that grant CT-owned Me inbox/preferences/phone/pushover entities (and `admin` if used)
|
|
78
78
|
- Product-specific operational tooling (announce rake tasks, welcome copy)
|
|
79
79
|
|
|
80
80
|
## Related
|
data/docs/models.md
CHANGED
|
@@ -44,6 +44,10 @@ end
|
|
|
44
44
|
|
|
45
45
|
Engine-owned messaging persistence (communications, endpoints, preferences, and related records) ships with CommandTower. Prefer platform services/workflows for mutations; do not dual-author schema in the host ([Initializing](initializing.md)).
|
|
46
46
|
|
|
47
|
+
## Audit ledger
|
|
48
|
+
|
|
49
|
+
`CommandTower::Audit::Event` is the append-only platform ledger (`command_tower_audit_events`). Only `CommandTower::Audit::Persistence::Subscriber` writes rows. There are no foreign keys to users. Envelope `changes` persist as `change_set`. See [Audit](audit.md).
|
|
50
|
+
|
|
47
51
|
## Related
|
|
48
52
|
|
|
49
53
|
- [Architecture](architecture.md)
|
data/docs/pagination.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Pagination
|
|
2
2
|
|
|
3
|
-
CommandTower exposes
|
|
3
|
+
CommandTower exposes live HTTP list contracts that return pagination metadata: **Me Inbox**, **audit event lists** (`GET /me/audit-events`, `GET /admin/audit-events`), and **Admin Users** (`GET /admin/users`). These lists reuse the Inbox `limit` / `offset` / `totalCount` contract.
|
|
4
4
|
|
|
5
5
|
Back to [README](../README.md).
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@ CommandTower.configure do |c|
|
|
|
38
38
|
end
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
There is no
|
|
41
|
+
There is no `pagination=true` / `page` / `cursor` query API on current engine HTTP surfaces. Admin audit list uses the same limit/offset contract as Inbox.
|
|
42
42
|
|
|
43
43
|
## Related
|
|
44
44
|
|