command_tower 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -3
  3. data/app/auth/command_tower/auth/auth_context.rb +10 -4
  4. data/app/controllers/command_tower/admin/application_controller.rb +27 -0
  5. data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
  6. data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
  7. data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
  8. data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
  9. data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
  10. data/app/controllers/command_tower/admin/users_controller.rb +46 -0
  11. data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
  12. data/app/controllers/command_tower/application_controller.rb +17 -1
  13. data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
  14. data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
  15. data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
  16. data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
  17. data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
  18. data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
  19. data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
  20. data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
  21. data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
  22. data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
  23. data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
  24. data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
  25. data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
  26. data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
  27. data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
  28. data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
  29. data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
  30. data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
  31. data/app/jobs/command_tower/application_job.rb +7 -0
  32. data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
  33. data/app/mailers/command_tower/application_mailer.rb +11 -1
  34. data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
  35. data/app/models/command_tower/audit/event.rb +38 -0
  36. data/app/models/command_tower/impersonation/session.rb +51 -0
  37. data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
  38. data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
  39. data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
  40. data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
  41. data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
  42. data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
  43. data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
  44. data/app/services/command_tower/README.md +4 -10
  45. data/app/services/command_tower/authorize/validate.rb +7 -4
  46. data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
  47. data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
  48. data/app/services/command_tower/jwt/login_create.rb +7 -4
  49. data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
  50. data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
  51. data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
  52. data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
  53. data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
  54. data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
  55. data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
  56. data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
  57. data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
  58. data/app/services/command_tower/service_base.rb +30 -43
  59. data/app/services/command_tower/service_logging.rb +2 -36
  60. data/app/services/command_tower/services/account/clear_phone.rb +8 -0
  61. data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
  62. data/app/services/command_tower/services/account/update_phone.rb +9 -1
  63. data/app/services/command_tower/services/admin/users.rb +377 -0
  64. data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
  65. data/app/services/command_tower/services/application_service.rb +37 -0
  66. data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
  67. data/app/services/command_tower/services/audit/events.rb +199 -0
  68. data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
  69. data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
  70. data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
  71. data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
  72. data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
  73. data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
  74. data/app/services/command_tower/services/impersonation/create.rb +29 -0
  75. data/app/services/command_tower/services/impersonation/end.rb +28 -0
  76. data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
  77. data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
  78. data/app/services/command_tower/services/me/change_password.rb +12 -0
  79. data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
  80. data/app/workflows/command_tower/transactional.rb +65 -0
  81. data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
  82. data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
  83. data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
  84. data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
  85. data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
  86. data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
  87. data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
  88. data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
  89. data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
  90. data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
  91. data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
  92. data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
  93. data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
  94. data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
  95. data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
  96. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
  97. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
  98. data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
  99. data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
  100. data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
  101. data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
  102. data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
  103. data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
  104. data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
  105. data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
  106. data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
  107. data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
  108. data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
  109. data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
  110. data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
  111. data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
  112. data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
  113. data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
  114. data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
  115. data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
  116. data/config/routes.rb +20 -0
  117. data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
  118. data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
  119. data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
  120. data/docs/admin_workspace.md +158 -0
  121. data/docs/api_reference.md +175 -14
  122. data/docs/architecture.md +5 -0
  123. data/docs/audit.md +195 -0
  124. data/docs/authentication.md +14 -0
  125. data/docs/authentication_authorization_guide.md +15 -11
  126. data/docs/authorization.md +25 -4
  127. data/docs/controllers.md +7 -1
  128. data/docs/eventing.md +179 -0
  129. data/docs/extending.md +32 -1
  130. data/docs/host_integration_guide.md +103 -12
  131. data/docs/initializing.md +2 -2
  132. data/docs/messaging_integration_guide.md +1 -1
  133. data/docs/models.md +4 -0
  134. data/docs/pagination.md +2 -2
  135. data/docs/principal_capabilities.md +89 -0
  136. data/docs/upgrades/0.10.0.md +2 -2
  137. data/docs/upgrades/0.11.0.md +92 -0
  138. data/docs/upgrades/0.11.1.md +25 -0
  139. data/docs/upgrades/README.md +2 -0
  140. data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
  141. data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
  142. data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
  143. data/lib/command_tower/admin_scope/resolve.rb +31 -0
  144. data/lib/command_tower/admin_scope/scope_context.rb +7 -0
  145. data/lib/command_tower/admin_scope/scope_option.rb +7 -0
  146. data/lib/command_tower/admin_scope.rb +20 -0
  147. data/lib/command_tower/admin_workspace.rb +16 -0
  148. data/lib/command_tower/audit/attribution.rb +90 -0
  149. data/lib/command_tower/audit/emit.rb +113 -0
  150. data/lib/command_tower/audit/masking.rb +41 -0
  151. data/lib/command_tower/audit/payload.rb +128 -0
  152. data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
  153. data/lib/command_tower/audit.rb +27 -0
  154. data/lib/command_tower/authorization/assignable_roles.rb +36 -0
  155. data/lib/command_tower/authorization/default.yml +99 -5
  156. data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
  157. data/lib/command_tower/authorization/entity.rb +15 -5
  158. data/lib/command_tower/authorization/role.rb +5 -4
  159. data/lib/command_tower/authorization.rb +71 -10
  160. data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
  161. data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
  162. data/lib/command_tower/configuration/authorization/config.rb +6 -0
  163. data/lib/command_tower/configuration/config.rb +20 -0
  164. data/lib/command_tower/configuration/impersonation/config.rb +38 -0
  165. data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
  166. data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
  167. data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
  168. data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
  169. data/lib/command_tower/configuration/registry/config.rb +34 -0
  170. data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
  171. data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
  172. data/lib/command_tower/current.rb +12 -0
  173. data/lib/command_tower/engine.rb +19 -4
  174. data/lib/command_tower/events.rb +184 -0
  175. data/lib/command_tower/execution.rb +111 -0
  176. data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
  177. data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
  178. data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
  179. data/lib/command_tower/impersonation/establish_identity.rb +44 -0
  180. data/lib/command_tower/install/baseline.rb +3 -0
  181. data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
  182. data/lib/command_tower/logging/projection.rb +67 -0
  183. data/lib/command_tower/logging/subscriber.rb +103 -0
  184. data/lib/command_tower/principal_capabilities.rb +15 -0
  185. data/lib/command_tower/version.rb +1 -1
  186. data/lib/command_tower.rb +12 -0
  187. data/spec/factories/impersonation_session.rb +17 -0
  188. data/spec/factories/user.rb +16 -0
  189. metadata +114 -3
  190. data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "class_composer"
4
+ require "command_tower/configuration/registry/audit/config"
5
+ require "command_tower/configuration/registry/admin_workspace/config"
6
+ require "command_tower/configuration/registry/principal_capabilities/config"
7
+
8
+ module CommandTower
9
+ module Configuration
10
+ module Registry
11
+ class Config
12
+ include ClassComposer::Generator
13
+
14
+ add_composer :audit,
15
+ desc: "Registered semantic audit event policy (CommandTower-owned and host-owned names)",
16
+ allowed: Audit::Config,
17
+ dynamic_default: ->(_) { Audit::Config.new },
18
+ default_shown: "Audit::Config.new"
19
+
20
+ add_composer :admin_workspace,
21
+ desc: "Registered Admin Workspace tools (CommandTower-owned and host-owned)",
22
+ allowed: AdminWorkspace::Config,
23
+ dynamic_default: ->(_) { AdminWorkspace::Config.new },
24
+ default_shown: "AdminWorkspace::Config.new"
25
+
26
+ add_composer :principal_capabilities,
27
+ desc: "Curated frontend-projectable principal capabilities (CommandTower-owned and host-owned)",
28
+ allowed: PrincipalCapabilities::Config,
29
+ dynamic_default: ->(_) { PrincipalCapabilities::Config.new },
30
+ default_shown: "PrincipalCapabilities::Config.new"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "class_composer"
4
+
5
+ module CommandTower
6
+ module Configuration
7
+ module Registry
8
+ module PrincipalCapabilities
9
+ class CapabilityDefinition
10
+ include ClassComposer::Generator
11
+
12
+ add_composer :required_entity,
13
+ desc: "RBAC entity name required for this principal capability to appear in the projection " \
14
+ "(defaults to the capability id when omitted)",
15
+ allowed: [String, Symbol],
16
+ default: ""
17
+
18
+ attr_accessor :owner, :id
19
+
20
+ def validate_definition!(name:)
21
+ self.id = name.to_s
22
+ self.owner = owner&.to_sym || :host
23
+ entity = required_entity.to_s.strip
24
+ entity = name.to_s if entity.empty?
25
+ self.required_entity = require_segment!(entity, field: "required_entity", name:)
26
+ end
27
+
28
+ private
29
+
30
+ def require_segment!(value, field:, name:)
31
+ token = value.to_s.strip
32
+ unless token.match?(CommandTower::Events::SEGMENT)
33
+ raise CommandTower::PrincipalCapabilities::InvalidCapabilityDefinitionError,
34
+ "principal capability #{name} has invalid #{field} #{value.inspect}"
35
+ end
36
+
37
+ token
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "command_tower/principal_capabilities"
4
+ require "command_tower/configuration/registry/principal_capabilities/capability_definition"
5
+
6
+ module CommandTower
7
+ module Configuration
8
+ module Registry
9
+ module PrincipalCapabilities
10
+ class Config
11
+ # Curated CT-owned frontend-projectable capabilities (1:1 entity names).
12
+ # Deliberate registration only — never an automatic Entity.entities dump.
13
+ PLATFORM_CAPABILITIES = %i[
14
+ admin_workspace
15
+ admin_audit_events
16
+ admin_messaging_announcements
17
+ admin_users
18
+ admin_users_update
19
+ admin_rbac_assignments
20
+ admin_impersonation
21
+ me_audit_events
22
+ ].freeze
23
+
24
+ def initialize
25
+ @definitions = {}
26
+ @finalized = false
27
+ seed_platform_capabilities!
28
+ end
29
+
30
+ def capability(name, owner: :host)
31
+ raise CommandTower::PrincipalCapabilities::FrozenRegistryError,
32
+ "principal capabilities registry is frozen" if @finalized
33
+
34
+ normalized = normalize_name!(name)
35
+ existing = @definitions[normalized]
36
+ if existing
37
+ if existing.owner == :command_tower && owner == :host
38
+ raise CommandTower::PrincipalCapabilities::HostOverrideError,
39
+ "host cannot redefine CommandTower-owned principal capability #{normalized}"
40
+ end
41
+
42
+ raise CommandTower::PrincipalCapabilities::DuplicateCapabilityError,
43
+ "principal capability #{normalized} is already registered"
44
+ end
45
+
46
+ definition = CapabilityDefinition.new
47
+ yield definition if block_given?
48
+ definition.owner = owner
49
+ definition.validate_definition!(name: normalized)
50
+ @definitions[normalized] = definition
51
+ definition
52
+ end
53
+
54
+ def fetch(name)
55
+ normalized = normalize_name!(name)
56
+ definition = @definitions[normalized]
57
+ if definition.nil?
58
+ raise CommandTower::PrincipalCapabilities::UnregisteredCapabilityError,
59
+ "principal capability #{normalized} is not registered"
60
+ end
61
+
62
+ definition
63
+ end
64
+
65
+ def registered?(name)
66
+ @definitions.key?(normalize_name!(name))
67
+ rescue CommandTower::PrincipalCapabilities::InvalidCapabilityNameError
68
+ false
69
+ end
70
+
71
+ def definitions
72
+ @definitions.dup.freeze
73
+ end
74
+
75
+ def finalize!
76
+ @definitions.each_value do |definition|
77
+ next unless definition.respond_to?(:class_composer_freeze_objects!)
78
+
79
+ definition.class_composer_freeze_objects!(behavior: :raise, children: true)
80
+ end
81
+ @definitions.freeze
82
+ @finalized = true
83
+ self
84
+ end
85
+
86
+ def finalized?
87
+ @finalized
88
+ end
89
+
90
+ def validate_required_entities!(entities)
91
+ missing = @definitions.filter_map do |name, definition|
92
+ next if entities.key?(definition.required_entity)
93
+
94
+ "#{name} requires #{definition.required_entity}"
95
+ end
96
+ return self if missing.empty?
97
+
98
+ raise CommandTower::PrincipalCapabilities::MissingRequiredEntityError,
99
+ "principal capabilities reference unknown RBAC entities: #{missing.join(", ")}"
100
+ end
101
+
102
+ def reset_host_definitions!
103
+ thaw_for_test!
104
+ @definitions.delete_if { |_name, definition| definition.owner == :host }
105
+ self
106
+ end
107
+
108
+ private
109
+
110
+ def seed_platform_capabilities!
111
+ PLATFORM_CAPABILITIES.each do |name|
112
+ capability(name, owner: :command_tower)
113
+ end
114
+ end
115
+
116
+ def thaw_for_test!
117
+ return unless @finalized || @definitions.frozen?
118
+
119
+ @definitions = @definitions.dup
120
+ @finalized = false
121
+ end
122
+
123
+ def normalize_name!(name)
124
+ token = name.to_s.strip
125
+ unless token.match?(CommandTower::Events::SEGMENT)
126
+ raise CommandTower::PrincipalCapabilities::InvalidCapabilityNameError,
127
+ "invalid principal capability name #{name.inspect}"
128
+ end
129
+
130
+ token
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -2,6 +2,18 @@
2
2
 
3
3
  module CommandTower
4
4
  class Current < ActiveSupport::CurrentAttributes
5
+ attribute :execution_uuid
6
+ attribute :correlation_id
5
7
  attribute :request_id
8
+ attribute :causation_id
9
+ attribute :source
10
+ attribute :user_id
11
+ attribute :originating_administrator_id
12
+ attribute :effective_user_id
13
+ attribute :impersonation_active, default: false
14
+ attribute :impersonation_session_id
15
+ attribute :impersonation_activity_recorded, default: false
16
+ attribute :remote_ip
17
+ attribute :user_agent
6
18
  end
7
19
  end
@@ -7,6 +7,8 @@ module CommandTower
7
7
  class Engine < ::Rails::Engine
8
8
  isolate_namespace CommandTower
9
9
 
10
+ paths.add "app/shared_sequences", eager_load: true
11
+
10
12
  # Run after Rails loads the initializes and environment files
11
13
  # Ensures User has already set their desired config before we lock this down
12
14
  config.after_initialize do
@@ -22,6 +24,11 @@ module CommandTower
22
24
  CommandTower::CredentialResolution::SmtpActionMailerBridge.apply!
23
25
 
24
26
  unless Rails.env.test?
27
+ CommandTower.config.impersonation.validate!
28
+ CommandTower.config.registry.audit.finalize!
29
+ CommandTower.config.registry.admin_workspace.finalize!
30
+ CommandTower.config.registry.principal_capabilities.finalize!
31
+ CommandTower.config.admin_scope.finalize!
25
32
  # Now that we can confirm all variables are defined, freeze all objects an their children
26
33
  CommandTower.config.class_composer_freeze_objects!(behavior: :raise, children: true)
27
34
  end
@@ -46,11 +53,19 @@ module CommandTower
46
53
  # Add all RBAC based role definitions prior to fork/loading
47
54
  # Load once use forever
48
55
  config.to_prepare do
49
- CommandTower::Authorization::Role.roles_reset!
50
- CommandTower::Authorization::Entity.entities_reset!
51
- CommandTower::Authorization.mapped_controllers_reset!
56
+ skip_composition_validation = defined?(Rake) && (Rake.application.top_level_tasks.any? { |task|
57
+ task =~ /db:|install:migrations|command_tower:install\z|command_tower:doctor/
58
+ } rescue nil)
52
59
 
53
- CommandTower::Authorization.default_defined!
60
+ CommandTower::Authorization.default_defined!(validate: !skip_composition_validation)
61
+ unless skip_composition_validation
62
+ entities = CommandTower::Authorization::Entity.entities
63
+ CommandTower.config.registry.admin_workspace.validate_required_entities!(entities)
64
+ CommandTower.config.registry.principal_capabilities.validate_required_entities!(entities)
65
+ CommandTower.config.admin_scope.validate_scoped_tools!
66
+ end
67
+ CommandTower::Logging::Subscriber.attach!
68
+ CommandTower::Audit::Persistence::Subscriber.attach! unless skip_composition_validation
54
69
  end
55
70
  end
56
71
  end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Events
5
+ PREFIX = "command_tower"
6
+ SEGMENT = /\A[a-z][a-z0-9_]*\z/
7
+ SNAPSHOT_KEYS = %i[
8
+ execution_uuid
9
+ correlation_id
10
+ request_id
11
+ causation_id
12
+ source
13
+ user_id
14
+ originating_administrator_id
15
+ effective_user_id
16
+ impersonation_active
17
+ ].freeze
18
+ SCALAR_TYPES = [NilClass, TrueClass, FalseClass, Integer, Float, String, Symbol].freeze
19
+
20
+ WORKFLOW_STARTED = "command_tower.lifecycle.workflow.started"
21
+ WORKFLOW_COMPLETED = "command_tower.lifecycle.workflow.completed"
22
+ SERVICE_STARTED = "command_tower.lifecycle.service.started"
23
+ SERVICE_COMPLETED = "command_tower.lifecycle.service.completed"
24
+
25
+ module_function
26
+
27
+ def instrument_name(category:, name:)
28
+ category_segment = normalize_segment(category, field: "category")
29
+ name_segments = name.to_s.split(".")
30
+ raise ArgumentError, "name must contain at least one segment" if name_segments.empty?
31
+
32
+ normalized_name = name_segments.map { |segment| normalize_segment(segment, field: "name") }
33
+ "#{PREFIX}.#{category_segment}.#{normalized_name.join(".")}"
34
+ end
35
+
36
+ def snapshot
37
+ SNAPSHOT_KEYS.each_with_object({}) do |key, memo|
38
+ memo[key] = CommandTower::Current.public_send(key)
39
+ end.freeze
40
+ end
41
+
42
+ def publish(category:, name:, payload: {}, subject: nil, layer: nil)
43
+ event_payload = snapshot.merge(event_uuid: SecureRandom.uuid)
44
+ event_payload[:subject] = subject if subject
45
+ event_payload[:layer] = layer if layer
46
+ event_payload.merge!(sanitize_payload(payload))
47
+ ActiveSupport::Notifications.instrument(instrument_name(category:, name:), event_payload.freeze)
48
+ end
49
+
50
+ def publish_audit(name:, envelope:)
51
+ raise CommandTower::Audit::InvalidPayloadError, "audit envelope must be a Hash" unless envelope.is_a?(Hash)
52
+
53
+ CommandTower::Audit::Payload.validate!(envelope[:changes] || {}, path: "changes")
54
+ CommandTower::Audit::Payload.validate!(envelope[:metadata] || {}, path: "metadata")
55
+ CommandTower::Audit::Payload.enforce_size!(envelope)
56
+
57
+ event_payload = snapshot.merge(
58
+ event_uuid: SecureRandom.uuid,
59
+ action: name.to_s,
60
+ occurred_at: envelope[:occurred_at] || Time.now.utc.iso8601(6)
61
+ )
62
+ %i[
63
+ actor_user_id
64
+ affected_user_id
65
+ effective_user_id
66
+ originating_administrator_id
67
+ impersonation_active
68
+ attribution_mode
69
+ subject_type
70
+ subject_id
71
+ subject_label
72
+ user_id
73
+ scope_class
74
+ host_context_type
75
+ host_context_identifier
76
+ changes
77
+ metadata
78
+ ].each do |key|
79
+ event_payload[key] = envelope[key] if envelope.key?(key)
80
+ end
81
+
82
+ ActiveSupport::Notifications.instrument(instrument_name(category: :audit, name:), event_payload.freeze)
83
+ end
84
+
85
+ def publish_lifecycle(layer:, phase:, subject:, outcome: nil, duration_ms: nil, error_class: nil, error_codes: nil, log_level: nil, log_lifecycle: false)
86
+ extra = {}
87
+ extra[:outcome] = outcome if phase == :completed
88
+ extra[:duration_ms] = duration_ms if phase == :completed && !duration_ms.nil?
89
+ extra[:error_class] = error_class if error_class
90
+ extra[:error_codes] = error_codes if error_codes
91
+ extra[:log_level] = log_level if log_level
92
+ extra[:log_lifecycle] = log_lifecycle ? true : false
93
+ publish(
94
+ category: :lifecycle,
95
+ name: "#{layer}.#{phase}",
96
+ payload: extra,
97
+ subject:,
98
+ layer:
99
+ )
100
+ end
101
+
102
+ def around_execution(layer:, subject:, log_lifecycle: false)
103
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
+ publish_lifecycle(layer:, phase: :started, subject:, log_lifecycle:)
105
+ record = { result: nil, unexpected: nil, error_codes: nil, log_level: nil }
106
+ begin
107
+ yield record
108
+ ensure
109
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
110
+ publish_lifecycle(
111
+ layer:,
112
+ phase: :completed,
113
+ subject:,
114
+ outcome: outcome_for(layer:, record:),
115
+ duration_ms:,
116
+ error_class: record[:unexpected]&.class&.name,
117
+ error_codes: error_codes_for(record),
118
+ log_level: log_level_for(record),
119
+ log_lifecycle:
120
+ )
121
+ end
122
+ record[:result]
123
+ end
124
+
125
+ def outcome_for(layer:, record:)
126
+ return :error if record[:unexpected]
127
+
128
+ result = record[:result]
129
+ return result if layer == :service && %i[success failure].include?(result)
130
+ return :success if result.respond_to?(:success?) && result.success?
131
+ return :deferred if result.respond_to?(:deferred?) && result.deferred?
132
+ return :failure if result.respond_to?(:failure?) && result.failure?
133
+
134
+ :error
135
+ end
136
+
137
+ def error_codes_for(record)
138
+ return record[:error_codes] if record[:error_codes]
139
+ return nil unless record[:result].respond_to?(:errors)
140
+
141
+ codes = Array(record[:result].errors).filter_map { |error| error.code if error.respond_to?(:code) }
142
+ codes.empty? ? nil : codes
143
+ end
144
+
145
+ def log_level_for(record)
146
+ return :error if record[:unexpected]
147
+ return record[:log_level] if record[:log_level]
148
+ return nil unless record[:result].respond_to?(:errors)
149
+
150
+ Array(record[:result].errors).filter_map { |error| error.log_level if error.respond_to?(:log_level) }.first
151
+ end
152
+
153
+ def sanitize_payload(payload)
154
+ raise ArgumentError, "payload must be a Hash" unless payload.nil? || payload.is_a?(Hash)
155
+
156
+ (payload || {}).each_with_object({}) do |(key, value), memo|
157
+ next unless scalar_key?(key)
158
+ next unless scalar_value?(value)
159
+
160
+ memo[key.to_sym] = value
161
+ end
162
+ end
163
+
164
+ def normalize_segment(value, field:)
165
+ segment = value.to_s
166
+ unless segment.match?(SEGMENT)
167
+ raise ArgumentError, "invalid #{field} segment #{value.inspect}"
168
+ end
169
+
170
+ segment
171
+ end
172
+
173
+ def scalar_key?(key)
174
+ key.is_a?(String) || key.is_a?(Symbol)
175
+ end
176
+
177
+ def scalar_value?(value)
178
+ return true if SCALAR_TYPES.any? { |type| value.is_a?(type) }
179
+ return false unless value.is_a?(Array)
180
+
181
+ value.all? { |item| SCALAR_TYPES.any? { |type| item.is_a?(type) } }
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Execution
5
+ SOURCES = %i[http job rake console].freeze
6
+
7
+ module ContextAccess
8
+ def execution_context
9
+ CommandTower::Current
10
+ end
11
+
12
+ def publish_event(category:, name:, payload: {})
13
+ CommandTower::Events.publish(category:, name:, payload:, subject: self.class.name)
14
+ end
15
+
16
+ def audit(name, subject: nil, affected_user: nil, changes: {}, metadata: {}, attribution_mode: nil, subject_label: nil, scope_class: :global, host_context: nil)
17
+ CommandTower::Audit::Emit.call(
18
+ name:,
19
+ subject:,
20
+ affected_user:,
21
+ changes:,
22
+ metadata:,
23
+ attribution_mode:,
24
+ subject_label:,
25
+ scope_class:,
26
+ host_context:
27
+ )
28
+ end
29
+
30
+ def log_debug(msg)
31
+ publish_event(category: :log, name: :debug, payload: { message: msg.to_s })
32
+ end
33
+
34
+ def log_info(msg)
35
+ publish_event(category: :log, name: :info, payload: { message: msg.to_s })
36
+ end
37
+
38
+ def log_warn(msg)
39
+ publish_event(category: :log, name: :warn, payload: { message: msg.to_s })
40
+ end
41
+
42
+ def log_error(msg)
43
+ publish_event(category: :log, name: :error, payload: { message: msg.to_s })
44
+ end
45
+ end
46
+
47
+ module IdentityEnrichment
48
+ module_function
49
+
50
+ def from_user(user)
51
+ return if user.nil?
52
+
53
+ CommandTower::Current.user_id = user.id
54
+ CommandTower::Current.effective_user_id = user.id
55
+ CommandTower::Current.originating_administrator_id = nil
56
+ CommandTower::Current.impersonation_active = false
57
+ CommandTower::Current.impersonation_session_id = nil
58
+ end
59
+
60
+ def from_impersonation_session(session, actor:, target:)
61
+ CommandTower::Current.user_id = target.id
62
+ CommandTower::Current.effective_user_id = target.id
63
+ CommandTower::Current.originating_administrator_id = actor.id
64
+ CommandTower::Current.impersonation_active = true
65
+ CommandTower::Current.impersonation_session_id = session.id
66
+ end
67
+ end
68
+ end
69
+
70
+ def self.with_execution(source:, **attrs, &block)
71
+ raise ArgumentError, "block required" unless block
72
+
73
+ source = source.to_sym
74
+ unless Execution::SOURCES.include?(source)
75
+ raise ArgumentError, "invalid execution source #{source.inspect} (allowed: #{Execution::SOURCES.join(", ")})"
76
+ end
77
+
78
+ return yield if Current.execution_uuid.present?
79
+
80
+ execution_uuid = attrs[:execution_uuid].presence || SecureRandom.uuid
81
+ assigned = {
82
+ execution_uuid:,
83
+ correlation_id: attrs[:correlation_id].presence || execution_uuid,
84
+ source:,
85
+ request_id: attrs[:request_id],
86
+ causation_id: attrs[:causation_id],
87
+ user_id: attrs[:user_id],
88
+ originating_administrator_id: attrs[:originating_administrator_id],
89
+ effective_user_id: attrs[:effective_user_id],
90
+ impersonation_active: attrs.key?(:impersonation_active) ? attrs[:impersonation_active] : false,
91
+ impersonation_session_id: attrs[:impersonation_session_id],
92
+ impersonation_activity_recorded: attrs.key?(:impersonation_activity_recorded) ? attrs[:impersonation_activity_recorded] : false,
93
+ remote_ip: attrs[:remote_ip],
94
+ user_agent: attrs[:user_agent]
95
+ }
96
+
97
+ run = -> { Current.set(assigned, &block) }
98
+
99
+ if execution_wrapper_idle?
100
+ Rails.application.executor.wrap { run.call }
101
+ else
102
+ run.call
103
+ end
104
+ end
105
+
106
+ def self.execution_wrapper_idle?
107
+ return true unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
108
+
109
+ !Rails.application.executor.active?
110
+ end
111
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Impersonation
5
+ module ActivityDeclaration
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def impersonation_activity!
12
+ @impersonation_activity = true
13
+ end
14
+
15
+ def impersonation_activity?
16
+ return @impersonation_activity unless @impersonation_activity.nil?
17
+
18
+ superclass.respond_to?(:impersonation_activity?) ? superclass.impersonation_activity? : false
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Impersonation
5
+ Overlay = Data.define(:actor, :target, :session, :status)
6
+
7
+ module ApplyOverlay
8
+ module_function
9
+
10
+ def call(actor:, session_id:, mode: :enforce)
11
+ token = session_id.to_s.strip
12
+ return Overlay.new(actor:, target: actor, session: nil, status: :none) if token.empty?
13
+
14
+ session = CommandTower::Impersonation::Session.find_by(id: token)
15
+ if session.nil? || session.actor_user_id != actor.id
16
+ return reject_or_none(actor:, session: nil, mode:)
17
+ end
18
+
19
+ unless session.open?
20
+ return reject_or_none(actor:, session:, mode:)
21
+ end
22
+
23
+ if session.expired?
24
+ return expire_open_session(actor:, session:, mode:)
25
+ end
26
+
27
+ target = User.find_by(id: session.target_user_id)
28
+ if target.nil?
29
+ ended = end_session(session, reason: "revoked", actor:)
30
+ emit_ended(ended, reason: "revoked") if ended
31
+ return reject_or_none(actor:, session: session.reload, mode:)
32
+ end
33
+
34
+ Overlay.new(actor:, target:, session:, status: :active)
35
+ end
36
+
37
+ def reject_or_none(actor:, session:, mode:)
38
+ if mode.to_sym == :enforce
39
+ return Overlay.new(actor:, target: actor, session:, status: :expired)
40
+ end
41
+
42
+ Overlay.new(actor:, target: actor, session:, status: :none)
43
+ end
44
+ private_class_method :reject_or_none
45
+
46
+ def expire_open_session(actor:, session:, mode:)
47
+ reason = session.expiration_reason
48
+ if mode.to_sym == :enforce
49
+ ended = end_session(session, reason:, actor:)
50
+ emit_ended(ended, reason:) if ended
51
+ return Overlay.new(actor:, target: actor, session: session.reload, status: :expired)
52
+ end
53
+
54
+ Overlay.new(actor:, target: actor, session:, status: :stale)
55
+ end
56
+ private_class_method :expire_open_session
57
+
58
+ def end_session(session, reason:, actor:)
59
+ result = CommandTower::Services::Impersonation::End.call(
60
+ session_id: session.id,
61
+ reason:,
62
+ actor_user_id: actor.id
63
+ )
64
+ return unless result.success? && result.data[:ended]
65
+
66
+ result.data[:session]
67
+ end
68
+ private_class_method :end_session
69
+
70
+ def emit_ended(session, reason:)
71
+ target = User.find_by(id: session.target_user_id)
72
+ return if target.nil?
73
+
74
+ CommandTower::Impersonation::ClearOverlayForAudit.call(actor_user_id: session.actor_user_id) do
75
+ CommandTower::Audit::Emit.call(
76
+ name: :impersonation_ended,
77
+ subject: target,
78
+ affected_user: target,
79
+ changes: { reason: { from: nil, to: reason } },
80
+ metadata: { impersonation_session_id: session.id },
81
+ attribution_mode: :admin_direct
82
+ )
83
+ end
84
+ end
85
+ private_class_method :emit_ended
86
+ end
87
+ end
88
+ end