command_tower 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -3
- data/app/auth/command_tower/auth/auth_context.rb +10 -4
- data/app/controllers/command_tower/admin/application_controller.rb +27 -0
- data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
- data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
- data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
- data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
- data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
- data/app/controllers/command_tower/admin/users_controller.rb +46 -0
- data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
- data/app/controllers/command_tower/application_controller.rb +17 -1
- data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
- data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
- data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
- data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
- data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
- data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
- data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
- data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
- data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
- data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
- data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
- data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
- data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
- data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
- data/app/jobs/command_tower/application_job.rb +7 -0
- data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
- data/app/mailers/command_tower/application_mailer.rb +11 -1
- data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
- data/app/models/command_tower/audit/event.rb +38 -0
- data/app/models/command_tower/impersonation/session.rb +51 -0
- data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
- data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
- data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
- data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
- data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
- data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
- data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
- data/app/services/command_tower/README.md +4 -10
- data/app/services/command_tower/authorize/validate.rb +7 -4
- data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
- data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
- data/app/services/command_tower/jwt/login_create.rb +7 -4
- data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
- data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
- data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
- data/app/services/command_tower/service_base.rb +30 -43
- data/app/services/command_tower/service_logging.rb +2 -36
- data/app/services/command_tower/services/account/clear_phone.rb +8 -0
- data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
- data/app/services/command_tower/services/account/update_phone.rb +9 -1
- data/app/services/command_tower/services/admin/users.rb +377 -0
- data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
- data/app/services/command_tower/services/application_service.rb +37 -0
- data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
- data/app/services/command_tower/services/audit/events.rb +199 -0
- data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
- data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
- data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
- data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
- data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
- data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
- data/app/services/command_tower/services/impersonation/create.rb +29 -0
- data/app/services/command_tower/services/impersonation/end.rb +28 -0
- data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
- data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
- data/app/services/command_tower/services/me/change_password.rb +12 -0
- data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
- data/app/workflows/command_tower/transactional.rb +65 -0
- data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
- data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
- data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
- data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
- data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
- data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
- data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
- data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
- data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
- data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
- data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
- data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
- data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
- data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
- data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
- data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
- data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
- data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
- data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
- data/config/routes.rb +20 -0
- data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
- data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
- data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
- data/docs/admin_workspace.md +158 -0
- data/docs/api_reference.md +175 -14
- data/docs/architecture.md +5 -0
- data/docs/audit.md +195 -0
- data/docs/authentication.md +14 -0
- data/docs/authentication_authorization_guide.md +15 -11
- data/docs/authorization.md +25 -4
- data/docs/controllers.md +7 -1
- data/docs/eventing.md +179 -0
- data/docs/extending.md +32 -1
- data/docs/host_integration_guide.md +103 -12
- data/docs/initializing.md +2 -2
- data/docs/messaging_integration_guide.md +1 -1
- data/docs/models.md +4 -0
- data/docs/pagination.md +2 -2
- data/docs/principal_capabilities.md +89 -0
- data/docs/upgrades/0.10.0.md +2 -2
- data/docs/upgrades/0.11.0.md +92 -0
- data/docs/upgrades/0.11.1.md +25 -0
- data/docs/upgrades/README.md +2 -0
- data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
- data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
- data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
- data/lib/command_tower/admin_scope/resolve.rb +31 -0
- data/lib/command_tower/admin_scope/scope_context.rb +7 -0
- data/lib/command_tower/admin_scope/scope_option.rb +7 -0
- data/lib/command_tower/admin_scope.rb +20 -0
- data/lib/command_tower/admin_workspace.rb +16 -0
- data/lib/command_tower/audit/attribution.rb +90 -0
- data/lib/command_tower/audit/emit.rb +113 -0
- data/lib/command_tower/audit/masking.rb +41 -0
- data/lib/command_tower/audit/payload.rb +128 -0
- data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
- data/lib/command_tower/audit.rb +27 -0
- data/lib/command_tower/authorization/assignable_roles.rb +36 -0
- data/lib/command_tower/authorization/default.yml +99 -5
- data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
- data/lib/command_tower/authorization/entity.rb +15 -5
- data/lib/command_tower/authorization/role.rb +5 -4
- data/lib/command_tower/authorization.rb +71 -10
- data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
- data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
- data/lib/command_tower/configuration/authorization/config.rb +6 -0
- data/lib/command_tower/configuration/config.rb +20 -0
- data/lib/command_tower/configuration/impersonation/config.rb +38 -0
- data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
- data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
- data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
- data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
- data/lib/command_tower/configuration/registry/config.rb +34 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
- data/lib/command_tower/current.rb +12 -0
- data/lib/command_tower/engine.rb +19 -4
- data/lib/command_tower/events.rb +184 -0
- data/lib/command_tower/execution.rb +111 -0
- data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
- data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
- data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
- data/lib/command_tower/impersonation/establish_identity.rb +44 -0
- data/lib/command_tower/install/baseline.rb +3 -0
- data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
- data/lib/command_tower/logging/projection.rb +67 -0
- data/lib/command_tower/logging/subscriber.rb +103 -0
- data/lib/command_tower/principal_capabilities.rb +15 -0
- data/lib/command_tower/version.rb +1 -1
- data/lib/command_tower.rb +12 -0
- data/spec/factories/impersonation_session.rb +17 -0
- data/spec/factories/user.rb +16 -0
- metadata +114 -3
- data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
|
@@ -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
|
|
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
|
|
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"]
|
|
66
|
-
|
|
67
|
-
|
|
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 =
|
|
106
|
+
allow_everything = true
|
|
72
107
|
else
|
|
73
|
-
|
|
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
|