command_tower 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -3
  3. data/app/auth/command_tower/auth/auth_context.rb +10 -4
  4. data/app/controllers/command_tower/admin/application_controller.rb +27 -0
  5. data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
  6. data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
  7. data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
  8. data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
  9. data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
  10. data/app/controllers/command_tower/admin/users_controller.rb +46 -0
  11. data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
  12. data/app/controllers/command_tower/application_controller.rb +17 -1
  13. data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
  14. data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
  15. data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
  16. data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
  17. data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
  18. data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
  19. data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
  20. data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
  21. data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
  22. data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
  23. data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
  24. data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
  25. data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
  26. data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
  27. data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
  28. data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
  29. data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
  30. data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
  31. data/app/jobs/command_tower/application_job.rb +7 -0
  32. data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
  33. data/app/mailers/command_tower/application_mailer.rb +11 -1
  34. data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
  35. data/app/models/command_tower/audit/event.rb +36 -0
  36. data/app/models/command_tower/impersonation/session.rb +51 -0
  37. data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
  38. data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
  39. data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
  40. data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
  41. data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
  42. data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
  43. data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
  44. data/app/services/command_tower/README.md +4 -10
  45. data/app/services/command_tower/authorize/validate.rb +7 -4
  46. data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
  47. data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
  48. data/app/services/command_tower/jwt/login_create.rb +7 -4
  49. data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
  50. data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
  51. data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
  52. data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
  53. data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
  54. data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
  55. data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
  56. data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
  57. data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
  58. data/app/services/command_tower/service_base.rb +30 -43
  59. data/app/services/command_tower/service_logging.rb +2 -36
  60. data/app/services/command_tower/services/account/clear_phone.rb +8 -0
  61. data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
  62. data/app/services/command_tower/services/account/update_phone.rb +9 -1
  63. data/app/services/command_tower/services/admin/users.rb +377 -0
  64. data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
  65. data/app/services/command_tower/services/application_service.rb +37 -0
  66. data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
  67. data/app/services/command_tower/services/audit/events.rb +199 -0
  68. data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
  69. data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
  70. data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
  71. data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
  72. data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
  73. data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
  74. data/app/services/command_tower/services/impersonation/create.rb +29 -0
  75. data/app/services/command_tower/services/impersonation/end.rb +28 -0
  76. data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
  77. data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
  78. data/app/services/command_tower/services/me/change_password.rb +12 -0
  79. data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
  80. data/app/workflows/command_tower/transactional.rb +65 -0
  81. data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
  82. data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
  83. data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
  84. data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
  85. data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
  86. data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
  87. data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
  88. data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
  89. data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
  90. data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
  91. data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
  92. data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
  93. data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
  94. data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
  95. data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
  96. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
  97. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
  98. data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
  99. data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
  100. data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
  101. data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
  102. data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
  103. data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
  104. data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
  105. data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
  106. data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
  107. data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
  108. data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
  109. data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
  110. data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
  111. data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
  112. data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
  113. data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
  114. data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
  115. data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
  116. data/config/routes.rb +20 -0
  117. data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
  118. data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
  119. data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
  120. data/docs/admin_workspace.md +158 -0
  121. data/docs/api_reference.md +175 -14
  122. data/docs/architecture.md +5 -0
  123. data/docs/audit.md +195 -0
  124. data/docs/authentication.md +14 -0
  125. data/docs/authentication_authorization_guide.md +15 -11
  126. data/docs/authorization.md +25 -4
  127. data/docs/controllers.md +7 -1
  128. data/docs/eventing.md +179 -0
  129. data/docs/extending.md +32 -1
  130. data/docs/host_integration_guide.md +103 -12
  131. data/docs/initializing.md +2 -2
  132. data/docs/messaging_integration_guide.md +1 -1
  133. data/docs/models.md +4 -0
  134. data/docs/pagination.md +2 -2
  135. data/docs/principal_capabilities.md +89 -0
  136. data/docs/upgrades/0.10.0.md +2 -2
  137. data/docs/upgrades/0.11.0.md +92 -0
  138. data/docs/upgrades/README.md +1 -0
  139. data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
  140. data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
  141. data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
  142. data/lib/command_tower/admin_scope/resolve.rb +31 -0
  143. data/lib/command_tower/admin_scope/scope_context.rb +7 -0
  144. data/lib/command_tower/admin_scope/scope_option.rb +7 -0
  145. data/lib/command_tower/admin_scope.rb +20 -0
  146. data/lib/command_tower/admin_workspace.rb +16 -0
  147. data/lib/command_tower/audit/attribution.rb +90 -0
  148. data/lib/command_tower/audit/emit.rb +113 -0
  149. data/lib/command_tower/audit/masking.rb +41 -0
  150. data/lib/command_tower/audit/payload.rb +128 -0
  151. data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
  152. data/lib/command_tower/audit.rb +27 -0
  153. data/lib/command_tower/authorization/assignable_roles.rb +36 -0
  154. data/lib/command_tower/authorization/default.yml +99 -5
  155. data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
  156. data/lib/command_tower/authorization/entity.rb +15 -5
  157. data/lib/command_tower/authorization/role.rb +5 -4
  158. data/lib/command_tower/authorization.rb +71 -10
  159. data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
  160. data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
  161. data/lib/command_tower/configuration/authorization/config.rb +6 -0
  162. data/lib/command_tower/configuration/config.rb +20 -0
  163. data/lib/command_tower/configuration/impersonation/config.rb +38 -0
  164. data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
  165. data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
  166. data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
  167. data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
  168. data/lib/command_tower/configuration/registry/config.rb +34 -0
  169. data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
  170. data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
  171. data/lib/command_tower/current.rb +12 -0
  172. data/lib/command_tower/engine.rb +19 -4
  173. data/lib/command_tower/events.rb +184 -0
  174. data/lib/command_tower/execution.rb +111 -0
  175. data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
  176. data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
  177. data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
  178. data/lib/command_tower/impersonation/establish_identity.rb +44 -0
  179. data/lib/command_tower/install/baseline.rb +3 -0
  180. data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
  181. data/lib/command_tower/logging/projection.rb +67 -0
  182. data/lib/command_tower/logging/subscriber.rb +103 -0
  183. data/lib/command_tower/principal_capabilities.rb +15 -0
  184. data/lib/command_tower/version.rb +1 -1
  185. data/lib/command_tower.rb +12 -0
  186. data/spec/factories/impersonation_session.rb +17 -0
  187. data/spec/factories/user.rb +16 -0
  188. metadata +112 -2
  189. data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Execution
5
+ module JobBoundary
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ around_perform :command_tower_job_execution
10
+ end
11
+
12
+ private
13
+
14
+ def command_tower_job_execution
15
+ CommandTower.with_execution(source: :job) { yield }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,7 +2,17 @@
2
2
 
3
3
  module CommandTower
4
4
  class ApplicationMailer < ActionMailer::Base
5
- default from: "from@example.com"
5
+ FALLBACK_FROM = "from@example.com"
6
+
7
+ default from: -> { smtp_from_address }
6
8
  layout "mailer"
9
+
10
+ def self.smtp_from_address
11
+ CredentialResolution.resolve(:smtp).user_name.presence || FALLBACK_FROM
12
+ end
13
+
14
+ def smtp_from_address
15
+ self.class.smtp_from_address
16
+ end
7
17
  end
8
18
  end
@@ -8,8 +8,7 @@ module CommandTower
8
8
  raise ArgumentError, "rendered must be a RenderedPayload"
9
9
  end
10
10
 
11
- from_address = CredentialResolution.resolve(:smtp).user_name.presence
12
- from_address ||= "from@example.com"
11
+ from_address = smtp_from_address
13
12
 
14
13
  mail(
15
14
  to: rendered.recipient_address,
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Audit
5
+ class Event < CommandTower::ApplicationRecord
6
+ self.table_name = "command_tower_audit_events"
7
+
8
+ ATTRIBUTION_MODES = %w[self_service admin_direct impersonation system].freeze
9
+ SCOPE_CLASSES = {
10
+ global: "global",
11
+ host: "host",
12
+ legacy: "legacy"
13
+ }.freeze
14
+
15
+ validates :event_uuid, :action, :occurred_at, :attribution_mode, :scope_class, presence: true
16
+ validates :attribution_mode, inclusion: { in: ATTRIBUTION_MODES }
17
+ validates :scope_class, inclusion: { in: SCOPE_CLASSES.values }
18
+ validates :originating_administrator_id, presence: true, if: :impersonation_active?
19
+
20
+ after_initialize do
21
+ write_attribute(:change_set, {}) if has_attribute?(:change_set) && read_attribute(:change_set).nil?
22
+ write_attribute(:metadata, {}) if has_attribute?(:metadata) && read_attribute(:metadata).nil?
23
+ write_attribute(:sensitive_fields, []) if has_attribute?(:sensitive_fields) && read_attribute(:sensitive_fields).nil?
24
+ end
25
+
26
+ before_update :reject_mutation
27
+ before_destroy :reject_mutation
28
+
29
+ private
30
+
31
+ def reject_mutation
32
+ raise ImmutableError, "audit events are append-only"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Impersonation
5
+ class Session < CommandTower::ApplicationRecord
6
+ self.table_name = "command_tower_impersonation_sessions"
7
+
8
+ END_REASONS = %w[manual idle_timeout absolute_timeout logout revoked].freeze
9
+
10
+ belongs_to :actor, class_name: "User", foreign_key: :actor_user_id
11
+ belongs_to :target, class_name: "User", foreign_key: :target_user_id
12
+
13
+ before_create :assign_public_id
14
+
15
+ validates :actor_user_id, :target_user_id, :started_at, :last_activity_at,
16
+ :absolute_expires_at, :idle_expires_at, presence: true
17
+ validates :end_reason, inclusion: { in: END_REASONS }, allow_nil: true
18
+
19
+ scope :open, -> { where(ended_at: nil) }
20
+
21
+ def open?
22
+ ended_at.nil?
23
+ end
24
+
25
+ def idle_expired?(at: Time.current)
26
+ idle_expires_at <= at
27
+ end
28
+
29
+ def absolutely_expired?(at: Time.current)
30
+ absolute_expires_at <= at
31
+ end
32
+
33
+ def expired?(at: Time.current)
34
+ idle_expired?(at:) || absolutely_expired?(at:)
35
+ end
36
+
37
+ def expiration_reason(at: Time.current)
38
+ return "absolute_timeout" if absolutely_expired?(at:)
39
+ return "idle_timeout" if idle_expired?(at:)
40
+
41
+ nil
42
+ end
43
+
44
+ private
45
+
46
+ def assign_public_id
47
+ self.id = SecureRandom.uuid if id.blank?
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Serializers
5
+ module Admin
6
+ module Users
7
+ class UserSerializer < CommandTower::Serializers::ApplicationSerializer
8
+ def self.serialize(user)
9
+ first_name = user.first_name.to_s
10
+ last_name = user.last_name.to_s
11
+ full_name = [first_name, last_name].map(&:strip).reject(&:empty?).join(" ")
12
+ full_name = user.full_name.strip if full_name.empty? && user.respond_to?(:full_name)
13
+
14
+ {
15
+ id: user.id,
16
+ firstName: first_name,
17
+ lastName: last_name,
18
+ fullName: full_name,
19
+ username: user.username,
20
+ email: user.email,
21
+ emailValidated: user.email_validated,
22
+ phoneNumber: phone_number_for(user),
23
+ phoneNumberValidated: phone_number_validated_for(user),
24
+ roles: user.roles,
25
+ createdAt: user.created_at&.iso8601
26
+ }
27
+ end
28
+
29
+ def self.phone_number_for(user)
30
+ return nil unless user.respond_to?(:phone_number)
31
+
32
+ user.phone_number.presence
33
+ end
34
+ private_class_method :phone_number_for
35
+
36
+ def self.phone_number_validated_for(user)
37
+ return false unless user.respond_to?(:phone_number_validated)
38
+
39
+ !!user.phone_number_validated
40
+ end
41
+ private_class_method :phone_number_validated_for
42
+ end
43
+
44
+ class PaginationMetaSerializer < CommandTower::Serializers::ApplicationSerializer
45
+ def self.serialize(pagination)
46
+ {
47
+ limit: pagination.fetch(:limit),
48
+ offset: pagination.fetch(:offset),
49
+ totalCount: pagination.fetch(:total_count)
50
+ }
51
+ end
52
+ end
53
+
54
+ class AssignableRolesSerializer < CommandTower::Serializers::ApplicationSerializer
55
+ def self.serialize(roles)
56
+ {
57
+ roles: Array(roles).map do |role|
58
+ {
59
+ name: role.fetch(:name),
60
+ description: role.fetch(:description)
61
+ }
62
+ end
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Serializers
5
+ module Admin
6
+ module Workspace
7
+ class ManifestSerializer < CommandTower::Serializers::ApplicationSerializer
8
+ def self.serialize(tools)
9
+ {
10
+ tools: Array(tools).map do |tool|
11
+ payload = {
12
+ id: tool.fetch(:id),
13
+ label: tool.fetch(:label),
14
+ description: tool.fetch(:description),
15
+ route: tool.fetch(:route),
16
+ group: tool.fetch(:group),
17
+ sortOrder: tool.fetch(:sort_order),
18
+ icon: tool[:icon]
19
+ }
20
+ payload[:scope] = serialize_scope(tool[:scope]) if tool.key?(:scope)
21
+ payload[:scopeOptions] = serialize_scope_options(tool[:scope_options]) if tool.key?(:scope_options)
22
+ payload[:availability] = serialize_availability(tool[:availability]) if tool.key?(:availability)
23
+ payload
24
+ end
25
+ }
26
+ end
27
+
28
+ def self.serialize_scope(scope)
29
+ {
30
+ required: scope.fetch(:required),
31
+ parameter: scope.fetch(:parameter),
32
+ label: scope.fetch(:label)
33
+ }
34
+ end
35
+ private_class_method :serialize_scope
36
+
37
+ def self.serialize_scope_options(options)
38
+ Array(options).map do |option|
39
+ {
40
+ value: option.fetch(:value),
41
+ label: option.fetch(:label)
42
+ }
43
+ end
44
+ end
45
+ private_class_method :serialize_scope_options
46
+
47
+ def self.serialize_availability(availability)
48
+ {
49
+ enabled: availability.fetch(:enabled),
50
+ reason: availability[:reason]
51
+ }
52
+ end
53
+ private_class_method :serialize_availability
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Serializers
5
+ module Audit
6
+ module Events
7
+ class FilterOptionsSerializer < CommandTower::Serializers::ApplicationSerializer
8
+ def self.serialize(event_names:, subject_types:, attribution_modes:)
9
+ {
10
+ eventNames: Array(event_names).map { |entry| serialize_event_option(entry) },
11
+ subjectTypes: Array(subject_types).map { |entry| serialize_subject_option(entry) },
12
+ attributionModes: Array(attribution_modes).map { |entry| serialize_mode_option(entry) }
13
+ }
14
+ end
15
+
16
+ def self.serialize_event_option(entry)
17
+ {
18
+ value: entry.fetch(:value),
19
+ label: entry.fetch(:label),
20
+ tags: Array(entry.fetch(:tags))
21
+ }
22
+ end
23
+ private_class_method :serialize_event_option
24
+
25
+ def self.serialize_subject_option(entry)
26
+ {
27
+ value: entry.fetch(:value),
28
+ label: entry.fetch(:label)
29
+ }
30
+ end
31
+ private_class_method :serialize_subject_option
32
+
33
+ def self.serialize_mode_option(entry)
34
+ {
35
+ value: entry.fetch(:value),
36
+ label: entry.fetch(:label)
37
+ }
38
+ end
39
+ private_class_method :serialize_mode_option
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Serializers
5
+ module Audit
6
+ module Events
7
+ class EventSerializer < CommandTower::Serializers::ApplicationSerializer
8
+ def self.serialize(projection)
9
+ {
10
+ id: projection.fetch(:id),
11
+ eventName: projection.fetch(:event_name),
12
+ eventLabel: projection.fetch(:event_label),
13
+ occurredAt: iso8601(projection.fetch(:occurred_at)),
14
+ attributionMode: projection.fetch(:attribution_mode),
15
+ actor: { userId: projection[:actor_user_id] },
16
+ affectedUser: { userId: projection[:affected_user_id] },
17
+ subject: {
18
+ type: projection[:subject_type],
19
+ id: projection[:subject_id],
20
+ label: projection[:subject_label]
21
+ },
22
+ impersonationActive: projection.fetch(:impersonation_active),
23
+ originatingAdministratorId: projection[:originating_administrator_id],
24
+ changes: projection.fetch(:changes),
25
+ metadata: projection.fetch(:metadata)
26
+ }
27
+ end
28
+ end
29
+
30
+ class PaginationMetaSerializer < CommandTower::Serializers::ApplicationSerializer
31
+ def self.serialize(pagination)
32
+ {
33
+ limit: pagination.fetch(:limit),
34
+ offset: pagination.fetch(:offset),
35
+ totalCount: pagination.fetch(:total_count)
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Serializers
5
+ module Auth
6
+ class PrincipalCapabilitiesSerializer
7
+ def self.serialize(principal_capability_ids)
8
+ {
9
+ principalCapabilities: Array(principal_capability_ids).map(&:to_s)
10
+ }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -4,12 +4,36 @@ module CommandTower
4
4
  module Serializers
5
5
  module Auth
6
6
  class SessionResponseSerializer
7
- def self.serialize(user:, token_expires_at:)
8
- {
7
+ def self.serialize(user:, token_expires_at:, impersonation_session: nil, actor: nil)
8
+ payload = {
9
9
  user: CommandTower::Serializers::Auth::UserSerializer.serialize(user),
10
10
  tokenExpiresAt: token_expires_at
11
11
  }
12
+ return payload if impersonation_session.nil?
13
+
14
+ actor_user = actor || impersonation_session.actor
15
+ payload[:impersonation] = {
16
+ active: true,
17
+ sessionId: impersonation_session.id,
18
+ actorUserId: impersonation_session.actor_user_id,
19
+ actorDisplayName: display_name(actor_user),
20
+ targetUserId: impersonation_session.target_user_id,
21
+ idleExpiresAt: CommandTower::Serializers::ApplicationSerializer.iso8601(
22
+ impersonation_session.idle_expires_at
23
+ ),
24
+ absoluteExpiresAt: CommandTower::Serializers::ApplicationSerializer.iso8601(
25
+ impersonation_session.absolute_expires_at
26
+ )
27
+ }
28
+ payload
29
+ end
30
+
31
+ def self.display_name(user)
32
+ return nil if user.nil?
33
+
34
+ [user.first_name, user.last_name].compact_blank.join(" ").presence || user.username
12
35
  end
36
+ private_class_method :display_name
13
37
  end
14
38
  end
15
39
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Serializers
5
+ module Impersonation
6
+ class SessionSerializer < CommandTower::Serializers::ApplicationSerializer
7
+ def self.serialize(session)
8
+ {
9
+ id: session.id,
10
+ actorUserId: session.actor_user_id,
11
+ targetUserId: session.target_user_id,
12
+ idleExpiresAt: iso8601(session.idle_expires_at),
13
+ absoluteExpiresAt: iso8601(session.absolute_expires_at)
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -17,16 +17,10 @@ All services in CommandTower that use this pattern inherit `CommandTower::Servic
17
17
  ## What does ServiceBase offer
18
18
 
19
19
  ### Logging
20
- `ServiceBase` offers a convenient way to tag logs. It keeps track of:
21
- - The start of the logic call
22
- - The time it took to complete the logic
23
- - The status of the logic
24
-
25
- Additionally, it provides some convenience methods for logging
26
- - `log_debug`
27
- - `log_info`
28
- - `log_warn`
29
- - `log_error`
20
+
21
+ Lifecycle logging is an `ActiveSupport::Notifications` consumer. Services do not write Start/Finished lines to `Rails.logger`.
22
+
23
+ Operational notes from services still use `log_info` / `log_warn` / `log_error`, which publish `command_tower.log.*` events. See [Eventing](../../../docs/eventing.md).
30
24
 
31
25
  ### Argument Validation
32
26
  Argument Validation is the powerhouse behind ServiceBase
@@ -46,21 +46,24 @@ module CommandTower::Authorize
46
46
 
47
47
  def call
48
48
  unless authorization_required?
49
- log_info("controller:#{controller}; method:#{action} -- No Authorization required")
49
+ log_debug("controller:#{controller}; method:#{action} -- No Authorization required")
50
50
  return Decision.authorized(user:, authorization_required: false, msg: NOT_REQUIRED_MSG)
51
51
  end
52
52
 
53
53
  # At this point we know authorization on the route is required
54
54
  # Iterate through the users roles to find a matching role that allows authorization
55
55
  # If at least 1 of the users roles passes validation, we can allow access to the path
56
- log_info("User Roles: #{user.roles}")
56
+ log_debug("User Roles: #{user.roles}")
57
57
  authorized = user_role_objects.any? do |_role_name, role_object|
58
58
  result = role_object.authorized?(controller:, method: action, user:)
59
- log_info("Role:#{result[:role]};Authorized:[#{result[:authorized]}];Reason:[#{result[:reason]}]")
59
+ log_debug("Role:#{result[:role]};Authorized:[#{result[:authorized]}];Reason:[#{result[:reason]}]")
60
60
  result[:authorized] == true
61
61
  end
62
62
 
63
- return Decision.denied(user:, msg: UNAUTHORIZED_MSG) unless authorized
63
+ unless authorized
64
+ log_warn("controller:#{controller}; method:#{action} -- #{UNAUTHORIZED_MSG}")
65
+ return Decision.denied(user:, msg: UNAUTHORIZED_MSG)
66
+ end
64
67
 
65
68
  Decision.authorized(user:, authorization_required: true, msg: AUTHORIZED_MSG)
66
69
  end
@@ -45,12 +45,18 @@ module CommandTower::Jwt
45
45
  end
46
46
 
47
47
  generated_token = nil
48
+ impersonation_session_id = payload[:impersonation_session_id].presence
48
49
  if with_reset
49
- generated_token = LoginCreate.(user:).token
50
+ generated_token = LoginCreate.(user:, impersonation_session_id:).token
50
51
  expires_at = CommandTower.config.jwt.ttl.from_now.to_time
51
52
  end
52
53
 
53
- AuthenticationOutcome.success(user:, expires_at: expires_at.to_s, generated_token:)
54
+ AuthenticationOutcome.success(
55
+ user:,
56
+ expires_at: expires_at.to_s,
57
+ generated_token:,
58
+ impersonation_session_id:
59
+ )
54
60
  end
55
61
 
56
62
  private
@@ -4,10 +4,10 @@ module CommandTower::Jwt
4
4
  # Narrow value object describing the result of AuthenticateUser only.
5
5
  # This is deliberately not a platform result type — do not reuse it elsewhere.
6
6
  class AuthenticationOutcome
7
- attr_reader :user, :expires_at, :generated_token, :status, :msg
7
+ attr_reader :user, :expires_at, :generated_token, :status, :msg, :impersonation_session_id
8
8
 
9
- def self.success(user:, expires_at:, generated_token: nil)
10
- new(success: true, user:, expires_at:, generated_token:)
9
+ def self.success(user:, expires_at:, generated_token: nil, impersonation_session_id: nil)
10
+ new(success: true, user:, expires_at:, generated_token:, impersonation_session_id:)
11
11
  end
12
12
 
13
13
  # `user` is carried on failures so callers can distinguish a known user who
@@ -16,13 +16,14 @@ module CommandTower::Jwt
16
16
  new(success: false, msg:, status:, user:)
17
17
  end
18
18
 
19
- def initialize(success:, user: nil, expires_at: nil, generated_token: nil, status: nil, msg: nil)
19
+ def initialize(success:, user: nil, expires_at: nil, generated_token: nil, status: nil, msg: nil, impersonation_session_id: nil)
20
20
  @success = success
21
21
  @user = user
22
22
  @expires_at = expires_at
23
23
  @generated_token = generated_token
24
24
  @status = status
25
25
  @msg = msg
26
+ @impersonation_session_id = impersonation_session_id
26
27
  end
27
28
 
28
29
  def success?
@@ -4,16 +4,19 @@ module CommandTower::Jwt
4
4
  class LoginCreate
5
5
  IssuedToken = Data.define(:token)
6
6
 
7
- def self.call(user:)
8
- IssuedToken.new(token: Encode.(payload: payload_for(user)))
7
+ def self.call(user:, impersonation_session_id: nil)
8
+ IssuedToken.new(token: Encode.(payload: payload_for(user, impersonation_session_id:)))
9
9
  end
10
10
 
11
- def self.payload_for(user)
12
- {
11
+ def self.payload_for(user, impersonation_session_id: nil)
12
+ payload = {
13
13
  generated_at: Time.now.to_i,
14
14
  user_id: user.id,
15
15
  verifier_token: user.retreive_verifier_token!,
16
16
  }
17
+ token = impersonation_session_id.to_s.strip
18
+ payload[:impersonation_session_id] = token unless token.empty?
19
+ payload
17
20
  end
18
21
  private_class_method :payload_for
19
22
  end
@@ -18,7 +18,7 @@ module CommandTower
18
18
  ),
19
19
  }
20
20
 
21
- Contract::Observability::StructuredLogger.info(
21
+ Contract::Observability::Publisher.info(
22
22
  base.merge(event: "messaging.accept.started"),
23
23
  )
24
24
 
@@ -31,7 +31,7 @@ module CommandTower
31
31
  "messaging.accept.succeeded"
32
32
  end
33
33
 
34
- Contract::Observability::StructuredLogger.info(
34
+ Contract::Observability::Publisher.info(
35
35
  base.merge(
36
36
  event:,
37
37
  duration_ms: elapsed_ms(started_at),
@@ -62,7 +62,7 @@ module CommandTower
62
62
  private
63
63
 
64
64
  def log_failure(base, started_at, error, level, error_code, event)
65
- Contract::Observability::StructuredLogger.public_send(
65
+ Contract::Observability::Publisher.public_send(
66
66
  level,
67
67
  base.merge(
68
68
  event:,
@@ -8,7 +8,9 @@ module CommandTower
8
8
  module_function
9
9
 
10
10
  def resolve
11
- CommandTower::Current.request_id.presence || SecureRandom.uuid
11
+ CommandTower::Current.correlation_id.presence ||
12
+ CommandTower::Current.request_id.presence ||
13
+ SecureRandom.uuid
12
14
  end
13
15
  end
14
16
  end
@@ -14,13 +14,13 @@ module CommandTower
14
14
  messaging_operation: operation,
15
15
  }.merge(request_fields(request))
16
16
 
17
- StructuredLogger.info(
17
+ Publisher.info(
18
18
  base.merge(event: "messaging.#{operation}.started"),
19
19
  )
20
20
 
21
21
  result = yield
22
22
 
23
- StructuredLogger.info(
23
+ Publisher.info(
24
24
  base.merge(
25
25
  event: "messaging.#{operation}.succeeded",
26
26
  duration_ms: elapsed_ms(started_at),
@@ -45,7 +45,7 @@ module CommandTower
45
45
  private
46
46
 
47
47
  def log_failure(base, started_at, operation, error, level, error_code)
48
- StructuredLogger.public_send(
48
+ Publisher.public_send(
49
49
  level,
50
50
  base.merge(
51
51
  event: "messaging.#{operation}.failed",
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Messaging
5
+ module Contract
6
+ module Observability
7
+ class Publisher
8
+ FORBIDDEN_KEYS = %i[title body text metadata email password token].freeze
9
+
10
+ class << self
11
+ def info(payload = nil, **kwargs)
12
+ emit(:info, payload || kwargs)
13
+ end
14
+
15
+ def warn(payload = nil, **kwargs)
16
+ emit(:warn, payload || kwargs)
17
+ end
18
+
19
+ def error(payload = nil, **kwargs)
20
+ emit(:error, payload || kwargs)
21
+ end
22
+
23
+ def emit(level, payload)
24
+ hash = payload.respond_to?(:to_h) ? payload.to_h : {}
25
+ event = hash[:event] || hash["event"]
26
+ raise ArgumentError, "messaging event name required" if event.blank?
27
+
28
+ name = event.to_s.sub(/\Amessaging\./, "")
29
+ extra = {}
30
+ hash.each do |key, value|
31
+ next if key.to_s == "event"
32
+
33
+ extra[key.to_sym] = value
34
+ end
35
+ FORBIDDEN_KEYS.each { |key| extra.delete(key) }
36
+ extra[:log_level] = level
37
+ CommandTower::Events.publish(category: :messaging, name:, payload: extra)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end