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
@@ -5,6 +5,8 @@ require "set"
5
5
  require "command_tower/error"
6
6
  require "command_tower/authorization/entity"
7
7
  require "command_tower/authorization/role"
8
+ require "command_tower/authorization/effective_entity_grants"
9
+ require "command_tower/authorization/assignable_roles"
8
10
 
9
11
  module CommandTower
10
12
  module Authorization
@@ -12,6 +14,9 @@ module CommandTower
12
14
 
13
15
  class Error < CommandTower::Error; end
14
16
 
17
+ SOURCE_COMMAND_TOWER = :command_tower
18
+ SOURCE_HOST = :host
19
+
15
20
  def mapped_controllers
16
21
  @mapped_controllers ||= {}
17
22
  end
@@ -27,21 +32,29 @@ module CommandTower
27
32
  @mapped_controllers = {}
28
33
  end
29
34
 
30
- def default_defined!
35
+ def reset_graph!
36
+ CommandTower::Authorization::Role.roles_reset!
37
+ CommandTower::Authorization::Entity.entities_reset!
38
+ mapped_controllers_reset!
39
+ end
40
+
41
+ def default_defined!(validate: true)
42
+ reset_graph!
31
43
  provision_rbac_default!
32
44
  provision_rbac_user_defined!
45
+ validate_composed_graph! if validate
33
46
  end
34
47
 
35
48
  def provision_rbac_user_defined!
36
49
  path = CommandTower.config.authorization.rbac_group_path
37
50
  rbac_configuration = load_yaml(path)
38
- provision_rbac_via_yaml(rbac_configuration)
51
+ provision_rbac_via_yaml(rbac_configuration, source: SOURCE_HOST)
39
52
  end
40
53
 
41
54
  def provision_rbac_default!
42
55
  path = CommandTower::Engine.root.join("lib", "command_tower", "authorization", "default.yml")
43
56
  rbac_configuration = load_yaml(path)
44
- provision_rbac_via_yaml(rbac_configuration)
57
+ provision_rbac_via_yaml(rbac_configuration, source: SOURCE_COMMAND_TOWER)
45
58
  end
46
59
 
47
60
  def load_yaml(path)
@@ -50,27 +63,56 @@ module CommandTower
50
63
  YAML.load_file(path)
51
64
  end
52
65
 
53
- def provision_rbac_via_yaml(rbac_configuration)
66
+ def provision_rbac_via_yaml(rbac_configuration, source:)
54
67
  return if rbac_configuration.nil?
55
68
 
56
- rbac_configuration["entities"].each do |entity|
69
+ unless rbac_configuration.is_a?(Hash)
70
+ raise Error, "RBAC source must be a mapping (got #{rbac_configuration.class})"
71
+ end
72
+
73
+ entity_rows = rbac_configuration["entities"]
74
+ if !entity_rows.nil? && !entity_rows.is_a?(Array)
75
+ raise Error, "RBAC entities must be a list"
76
+ end
77
+
78
+ Array(entity_rows).each do |entity|
79
+ raise Error, "RBAC entity definition must be a mapping" unless entity.is_a?(Hash)
80
+ raise Error, "RBAC entity is missing name" if entity["name"].blank?
81
+
57
82
  CommandTower::Authorization::Entity.create_entity(
58
83
  name: entity["name"],
59
84
  controller: entity["controller"],
60
85
  only: entity["only"],
61
86
  except: entity["except"],
87
+ source: source,
62
88
  )
63
89
  end
64
90
 
65
- rbac_configuration["groups"].each do |name, metadata|
66
- entities = nil
67
- allow_everything = false
91
+ groups = rbac_configuration["groups"]
92
+ return if groups.nil?
93
+
94
+ unless groups.is_a?(Hash)
95
+ raise Error, "RBAC groups must be a mapping"
96
+ end
97
+
98
+ groups.each do |name, metadata|
99
+ raise Error, "RBAC group [#{name}] must be a mapping" unless metadata.is_a?(Hash)
100
+
68
101
  description = metadata["description"]
102
+ allow_everything = false
103
+ entities = nil
69
104
 
70
105
  if metadata["entities"] == true
71
- allow_everything = true
106
+ allow_everything = true
72
107
  else
73
- entities = CommandTower::Authorization::Entity.entities.map { |k, v| v if metadata["entities"].include?(k) }.compact
108
+ grant_names = Array(metadata["entities"])
109
+ entities = grant_names.map do |grant_name|
110
+ resolved = CommandTower::Authorization::Entity.entities[grant_name]
111
+ if resolved.nil?
112
+ raise Error, "RBAC group [#{name}] references unknown entity [#{grant_name}]"
113
+ end
114
+ resolved
115
+ end
74
116
  end
75
117
 
76
118
  CommandTower::Authorization::Role.create_role(
@@ -78,8 +120,27 @@ module CommandTower
78
120
  entities:,
79
121
  description:,
80
122
  allow_everything:,
123
+ source: source,
81
124
  )
82
125
  end
83
126
  end
127
+
128
+ def validate_composed_graph!
129
+ validate_default_membership_role!
130
+ end
131
+
132
+ def validate_default_membership_role!
133
+ role_name = CommandTower.config.authorization.default_membership_role
134
+ return if role_name.nil?
135
+
136
+ if role_name.to_s.strip.empty?
137
+ raise Error, "authorization.default_membership_role is blank; use nil to disable default assignment"
138
+ end
139
+
140
+ return if CommandTower::Authorization::Role.roles[role_name]
141
+
142
+ raise Error,
143
+ "authorization.default_membership_role [#{role_name}] is not present in the composed RBAC graph"
144
+ end
84
145
  end
85
146
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "command_tower/admin_scope"
4
+ require "command_tower/configuration/admin_scope/tool_registration"
5
+
6
+ module CommandTower
7
+ module Configuration
8
+ module AdminScope
9
+ class Config
10
+ def initialize
11
+ @registrations = {}
12
+ @finalized = false
13
+ end
14
+
15
+ def register(tool_id, &block)
16
+ raise CommandTower::AdminScope::FrozenRegistryError, "admin scope registry is frozen" if @finalized
17
+
18
+ normalized = normalize_tool_id!(tool_id)
19
+ if @registrations.key?(normalized)
20
+ raise CommandTower::AdminScope::DuplicateRegistrationError,
21
+ "admin scope registration for #{normalized} already exists"
22
+ end
23
+
24
+ registration = ToolRegistration.new
25
+ yield registration if block_given?
26
+ registration.validate!(tool_id: normalized)
27
+ @registrations[normalized] = registration
28
+ registration
29
+ end
30
+
31
+ def fetch(tool_id)
32
+ normalized = normalize_tool_id!(tool_id)
33
+ registration = @registrations[normalized]
34
+ if registration.nil?
35
+ raise CommandTower::AdminScope::UnregisteredToolError,
36
+ "admin scope registration for #{normalized} is not registered"
37
+ end
38
+
39
+ registration
40
+ end
41
+
42
+ def registered?(tool_id)
43
+ @registrations.key?(normalize_tool_id!(tool_id))
44
+ rescue CommandTower::AdminWorkspace::InvalidToolNameError
45
+ false
46
+ end
47
+
48
+ def registrations
49
+ @registrations.dup.freeze
50
+ end
51
+
52
+ def finalize!
53
+ @registrations.freeze
54
+ @finalized = true
55
+ self
56
+ end
57
+
58
+ def finalized?
59
+ @finalized
60
+ end
61
+
62
+ def validate_scoped_tools!
63
+ CommandTower.config.registry.admin_workspace.definitions.each do |tool_id, definition|
64
+ next unless definition.scope_required?
65
+
66
+ unless registered?(tool_id)
67
+ raise CommandTower::AdminScope::MissingRegistrationError,
68
+ "admin workspace tool #{tool_id} requires scope but has no admin_scope registration"
69
+ end
70
+
71
+ fetch(tool_id).validate!(tool_id:)
72
+ end
73
+ self
74
+ end
75
+
76
+ def reset_registrations!
77
+ thaw_for_test!
78
+ @registrations.clear
79
+ self
80
+ end
81
+
82
+ private
83
+
84
+ def normalize_tool_id!(tool_id)
85
+ CommandTower.config.registry.admin_workspace.fetch(tool_id)
86
+ token = tool_id.to_s.strip
87
+ unless token.match?(CommandTower::Events::SEGMENT)
88
+ raise CommandTower::AdminWorkspace::InvalidToolNameError,
89
+ "invalid admin scope tool id #{tool_id.inspect}"
90
+ end
91
+
92
+ token
93
+ end
94
+
95
+ def thaw_for_test!
96
+ return unless @finalized || @registrations.frozen?
97
+
98
+ @registrations = @registrations.dup
99
+ @finalized = false
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Configuration
5
+ module AdminScope
6
+ class ToolRegistration
7
+ REQUIRED_HOOKS = %i[options validate availability narrow_users narrow_audit affected_users_in_scope].freeze
8
+
9
+ attr_accessor :options, :validate, :availability, :narrow_users, :narrow_audit,
10
+ :affected_users_in_scope, :host_context_type
11
+
12
+ def validate!(tool_id:)
13
+ missing = REQUIRED_HOOKS.reject { |hook| callable?(public_send(hook)) }
14
+ return self if missing.empty?
15
+
16
+ raise CommandTower::AdminScope::InvalidToolRegistrationError,
17
+ "admin scope registration for #{tool_id} is missing hooks: #{missing.join(", ")}"
18
+ end
19
+
20
+ private
21
+
22
+ def callable?(value)
23
+ value.respond_to?(:call)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -18,6 +18,12 @@ module CommandTower
18
18
  allowed: String,
19
19
  dynamic_default: ->(_) { Rails.root.join("config","rbac_groups.yml").to_s },
20
20
  default_shown: "Rails.root.join(\"config\",\"rbac_groups.yml\")"
21
+
22
+ add_composer :default_membership_role,
23
+ desc: "Optional role name assigned atomically when a user is registered. " \
24
+ "nil disables assignment. Must exist in the composed RBAC graph at boot.",
25
+ allowed: [String, NilClass],
26
+ default: nil
21
27
  end
22
28
  end
23
29
  end
@@ -3,12 +3,14 @@
3
3
  require "singleton"
4
4
  require "class_composer"
5
5
  require "command_tower/configuration/admin/config"
6
+ require "command_tower/configuration/admin_scope/config"
6
7
  require "command_tower/configuration/application/config"
7
8
  require "command_tower/configuration/authorization/config"
8
9
  require "command_tower/configuration/base"
9
10
  require "command_tower/configuration/credentials/config"
10
11
  require "command_tower/configuration/email/config"
11
12
  require "command_tower/configuration/identity/config"
13
+ require "command_tower/configuration/impersonation/config"
12
14
  require "command_tower/configuration/jwt/config"
13
15
  require "command_tower/configuration/jwt/cookie/config"
14
16
  require "command_tower/configuration/login/config"
@@ -16,6 +18,7 @@ require "command_tower/configuration/messaging/config"
16
18
  require "command_tower/configuration/otp/config"
17
19
  require "command_tower/configuration/pagination/config"
18
20
  require "command_tower/configuration/password_recovery_session/config"
21
+ require "command_tower/configuration/registry/config"
19
22
  require "command_tower/configuration/signup_session/config"
20
23
  require "command_tower/configuration/user/config"
21
24
  require "command_tower/configuration/username/config"
@@ -78,6 +81,11 @@ module CommandTower
78
81
  allowed: Configuration::Identity::Config,
79
82
  default: Configuration::Identity::Config.new
80
83
 
84
+ add_composer :impersonation,
85
+ desc: "Impersonation session idle and absolute timeouts",
86
+ allowed: Configuration::Impersonation::Config,
87
+ default: Configuration::Impersonation::Config.new
88
+
81
89
  add_composer :messaging,
82
90
  desc: "Messaging delivery configuration (notification SMS, etc.). Separate from Identity OTP.",
83
91
  allowed: Configuration::Messaging::Config,
@@ -88,11 +96,23 @@ module CommandTower
88
96
  allowed: Configuration::Admin::Config,
89
97
  default: Configuration::Admin::Config.new
90
98
 
99
+ add_composer :admin_scope,
100
+ desc: "Host hooks for optional Admin resource scoping (options, validation, narrowing)",
101
+ allowed: Configuration::AdminScope::Config,
102
+ dynamic_default: ->(_) { Configuration::AdminScope::Config.new },
103
+ default_shown: "Configuration::AdminScope::Config.new"
104
+
91
105
  add_composer :pagination,
92
106
  desc: "Pagination configuration for the app",
93
107
  allowed: Configuration::Pagination::Config,
94
108
  default: Configuration::Pagination::Config.new
95
109
 
110
+ add_composer :registry,
111
+ desc: "Host and platform registrations expressed as configuration (not a plugin API)",
112
+ allowed: Configuration::Registry::Config,
113
+ dynamic_default: ->(_) { Configuration::Registry::Config.new },
114
+ default_shown: "Configuration::Registry::Config.new"
115
+
96
116
  add_composer :signup_session,
97
117
  desc: "Signup session token claims, TTL, and signup rate limit ceilings",
98
118
  allowed: Configuration::SignupSession::Config,
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "class_composer"
4
+ require "command_tower/configuration/base"
5
+
6
+ module CommandTower
7
+ module Configuration
8
+ module Impersonation
9
+ class Config < ::CommandTower::Configuration::Base
10
+ include ClassComposer::Generator
11
+
12
+ add_composer :idle_timeout,
13
+ desc: "Idle lifetime of an impersonation session. Successful qualifying workflow execution may extend this clock. HTTP activity alone does not.",
14
+ allowed: ActiveSupport::Duration,
15
+ default: 10.minutes
16
+
17
+ add_composer :absolute_timeout,
18
+ desc: "Absolute maximum lifetime of an impersonation session measured from start. Activity never extends this clock.",
19
+ allowed: ActiveSupport::Duration,
20
+ default: 1.hour
21
+
22
+ def validate!
23
+ if idle_timeout.nil? || idle_timeout <= 0
24
+ raise ArgumentError, "config.impersonation.idle_timeout must be a positive duration"
25
+ end
26
+ if absolute_timeout.nil? || absolute_timeout <= 0
27
+ raise ArgumentError, "config.impersonation.absolute_timeout must be a positive duration"
28
+ end
29
+ if idle_timeout >= absolute_timeout
30
+ raise ArgumentError, "config.impersonation.idle_timeout must be less than absolute_timeout"
31
+ end
32
+
33
+ self
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "command_tower/admin_workspace"
4
+ require "command_tower/configuration/registry/admin_workspace/tool_definition"
5
+
6
+ module CommandTower
7
+ module Configuration
8
+ module Registry
9
+ module AdminWorkspace
10
+ class Config
11
+ PLATFORM_TOOLS = {
12
+ users: {
13
+ label: "Users",
14
+ description: "Find and inspect platform user accounts.",
15
+ route: "/admin/users",
16
+ group: "operations",
17
+ sort_order: 50,
18
+ required_entity: "admin_users",
19
+ icon: "users"
20
+ },
21
+ audit: {
22
+ label: "Audit",
23
+ description: "Browse account and administrative audit history.",
24
+ route: "/admin/audit",
25
+ group: "operations",
26
+ sort_order: 100,
27
+ required_entity: "admin_audit_events",
28
+ icon: "history"
29
+ },
30
+ messaging: {
31
+ label: "Messaging",
32
+ description: "Manage platform announcements and administrative messaging.",
33
+ route: "/admin/messaging",
34
+ group: "messaging",
35
+ sort_order: 200,
36
+ required_entity: "admin_messaging_announcements",
37
+ icon: "megaphone"
38
+ }
39
+ }.freeze
40
+
41
+ def initialize
42
+ @definitions = {}
43
+ @finalized = false
44
+ seed_platform_tools!
45
+ end
46
+
47
+ def tool(name, owner: :host)
48
+ raise CommandTower::AdminWorkspace::FrozenRegistryError, "admin workspace registry is frozen" if @finalized
49
+
50
+ normalized = normalize_name!(name)
51
+ existing = @definitions[normalized]
52
+ if existing
53
+ if existing.owner == :command_tower && owner == :host
54
+ raise CommandTower::AdminWorkspace::HostOverrideError,
55
+ "host cannot redefine CommandTower-owned admin workspace tool #{normalized}"
56
+ end
57
+
58
+ raise CommandTower::AdminWorkspace::DuplicateToolError,
59
+ "admin workspace tool #{normalized} is already registered"
60
+ end
61
+
62
+ definition = ToolDefinition.new
63
+ yield definition if block_given?
64
+ definition.owner = owner
65
+ definition.validate_definition!(name: normalized)
66
+ assert_unique_route!(definition)
67
+ @definitions[normalized] = definition
68
+ definition
69
+ end
70
+
71
+ def configure_tool(name)
72
+ raise CommandTower::AdminWorkspace::FrozenRegistryError, "admin workspace registry is frozen" if @finalized
73
+
74
+ normalized = normalize_name!(name)
75
+ definition = fetch(normalized)
76
+ if definition.owner != :command_tower
77
+ raise CommandTower::AdminWorkspace::HostOverrideError,
78
+ "configure_tool is only for CommandTower-owned admin workspace tools"
79
+ end
80
+
81
+ yield definition
82
+ definition.validate_definition!(name: normalized)
83
+ definition
84
+ end
85
+
86
+ def fetch(name)
87
+ normalized = normalize_name!(name)
88
+ definition = @definitions[normalized]
89
+ if definition.nil?
90
+ raise CommandTower::AdminWorkspace::UnregisteredToolError,
91
+ "admin workspace tool #{normalized} is not registered"
92
+ end
93
+
94
+ definition
95
+ end
96
+
97
+ def registered?(name)
98
+ @definitions.key?(normalize_name!(name))
99
+ rescue CommandTower::AdminWorkspace::InvalidToolNameError
100
+ false
101
+ end
102
+
103
+ def definitions
104
+ @definitions.dup.freeze
105
+ end
106
+
107
+ def finalize!
108
+ @definitions.each_value do |definition|
109
+ next unless definition.respond_to?(:class_composer_freeze_objects!)
110
+
111
+ definition.class_composer_freeze_objects!(behavior: :raise, children: true)
112
+ end
113
+ @definitions.freeze
114
+ @finalized = true
115
+ self
116
+ end
117
+
118
+ def finalized?
119
+ @finalized
120
+ end
121
+
122
+ def validate_required_entities!(entities)
123
+ missing = @definitions.filter_map do |name, definition|
124
+ next if entities.key?(definition.required_entity)
125
+
126
+ "#{name} requires #{definition.required_entity}"
127
+ end
128
+ return self if missing.empty?
129
+
130
+ raise CommandTower::AdminWorkspace::MissingRequiredEntityError,
131
+ "admin workspace tools reference unknown RBAC entities: #{missing.join(", ")}"
132
+ end
133
+
134
+ def reset_host_definitions!
135
+ thaw_for_test!
136
+ @definitions.delete_if { |_name, definition| definition.owner == :host }
137
+ restore_platform_tools!
138
+ self
139
+ end
140
+
141
+ # Spec cleanup: re-seed unfrozen platform tools (scope_required defaults false).
142
+ # finalize! freezes ToolDefinition composers; thaw alone does not unfreeze them.
143
+ def reset_platform_tool_scope_config!
144
+ thaw_for_test!
145
+ restore_platform_tools!
146
+ self
147
+ end
148
+
149
+ private
150
+
151
+ def seed_platform_tools!
152
+ PLATFORM_TOOLS.each do |name, attrs|
153
+ tool(name, owner: :command_tower) do |definition|
154
+ definition.label = attrs[:label]
155
+ definition.description = attrs[:description]
156
+ definition.route = attrs[:route]
157
+ definition.group = attrs[:group]
158
+ definition.sort_order = attrs[:sort_order]
159
+ definition.required_entity = attrs[:required_entity]
160
+ definition.icon = attrs[:icon]
161
+ end
162
+ end
163
+ end
164
+
165
+ def restore_platform_tools!
166
+ PLATFORM_TOOLS.each_key { |name| @definitions.delete(name.to_s) }
167
+ seed_platform_tools!
168
+ end
169
+
170
+ def thaw_for_test!
171
+ return unless @finalized || @definitions.frozen?
172
+
173
+ @definitions = @definitions.dup
174
+ @finalized = false
175
+ end
176
+
177
+ def assert_unique_route!(definition)
178
+ conflict = @definitions.values.find { |other| other.route == definition.route }
179
+ return unless conflict
180
+
181
+ raise CommandTower::AdminWorkspace::DuplicateRouteError,
182
+ "admin workspace tool #{definition.id} reuses route #{definition.route} " \
183
+ "(already registered by #{conflict.id})"
184
+ end
185
+
186
+ def normalize_name!(name)
187
+ token = name.to_s.strip
188
+ unless token.match?(CommandTower::Events::SEGMENT)
189
+ raise CommandTower::AdminWorkspace::InvalidToolNameError,
190
+ "invalid admin workspace tool name #{name.inspect}"
191
+ end
192
+
193
+ token
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end