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.
- 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 +38 -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/0.11.1.md +25 -0
- data/docs/upgrades/README.md +2 -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 +114 -3
- 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)
|
data/docs/upgrades/0.10.0.md
CHANGED
|
@@ -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 |
|
|
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.
|
|
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
|
data/docs/upgrades/README.md
CHANGED
|
@@ -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,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
|