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.
- 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 +36 -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/README.md +1 -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 +112 -2
- data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Admin
|
|
6
|
+
module ScopeParameter
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def extract(params, tool_id:)
|
|
10
|
+
definition = CommandTower.config.registry.admin_workspace.fetch(tool_id)
|
|
11
|
+
return nil unless definition.scope_required?
|
|
12
|
+
|
|
13
|
+
keys = parameter_keys(definition.scope_parameter)
|
|
14
|
+
raw = fetch_raw(params, keys)
|
|
15
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
16
|
+
|
|
17
|
+
raw.to_s.strip
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch_raw(params, keys)
|
|
21
|
+
hash =
|
|
22
|
+
if params.respond_to?(:to_unsafe_h)
|
|
23
|
+
params.to_unsafe_h
|
|
24
|
+
elsif params.respond_to?(:to_h)
|
|
25
|
+
params.to_h
|
|
26
|
+
else
|
|
27
|
+
params
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
keys.each do |key|
|
|
31
|
+
string_key = key.to_s
|
|
32
|
+
symbol_key = key.to_sym
|
|
33
|
+
return hash[string_key] if hash.key?(string_key)
|
|
34
|
+
return hash[symbol_key] if hash.key?(symbol_key)
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
private_class_method :fetch_raw
|
|
39
|
+
|
|
40
|
+
def parameter_keys(parameter)
|
|
41
|
+
snake = parameter.to_s
|
|
42
|
+
camel = snake.camelize(:lower)
|
|
43
|
+
[snake.to_sym, snake, camel.to_sym, camel]
|
|
44
|
+
end
|
|
45
|
+
private_class_method :parameter_keys
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
module IdentityId
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def coerce(raw)
|
|
11
|
+
return raw if raw.is_a?(Integer) && raw.positive?
|
|
12
|
+
return if raw.nil?
|
|
13
|
+
|
|
14
|
+
text = raw.to_s.strip
|
|
15
|
+
return if text.empty? || !/\A\d+\z/.match?(text)
|
|
16
|
+
|
|
17
|
+
Integer(text, 10).then { |value| value.positive? ? value : nil }
|
|
18
|
+
rescue ArgumentError
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class ListDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
24
|
+
Input = Data.define(:limit, :offset, :search, :scope_value)
|
|
25
|
+
DEFAULT_LIMIT = 50
|
|
26
|
+
MAX_LIMIT = 100
|
|
27
|
+
MAX_SEARCH_LENGTH = 100
|
|
28
|
+
TOOL_ID = "users"
|
|
29
|
+
|
|
30
|
+
def call(params)
|
|
31
|
+
limit = pagination_integer(
|
|
32
|
+
fetch_param(params, :limit).value,
|
|
33
|
+
default: DEFAULT_LIMIT,
|
|
34
|
+
minimum: 1,
|
|
35
|
+
maximum: MAX_LIMIT
|
|
36
|
+
)
|
|
37
|
+
return failure(errors: { message: "invalid_limit" }) if limit.nil?
|
|
38
|
+
|
|
39
|
+
offset = pagination_integer(fetch_param(params, :offset).value, default: 0, minimum: 0)
|
|
40
|
+
return failure(errors: { message: "invalid_offset" }) if offset.nil?
|
|
41
|
+
|
|
42
|
+
search = optional_search(fetch_param(params, :search).value)
|
|
43
|
+
return search if deserializer_result?(search)
|
|
44
|
+
|
|
45
|
+
scope_value = CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: TOOL_ID)
|
|
46
|
+
|
|
47
|
+
success(Input.new(limit:, offset:, search:, scope_value:))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def pagination_integer(raw, default:, minimum:, maximum: nil)
|
|
53
|
+
return default if raw.nil? || (raw.is_a?(String) && raw.strip.empty?)
|
|
54
|
+
|
|
55
|
+
value = raw.is_a?(Integer) ? raw : (Integer(raw, 10) if raw.is_a?(String) && /\A-?\d+\z/.match?(raw.strip))
|
|
56
|
+
return if value.nil? || value < minimum || (maximum && value > maximum)
|
|
57
|
+
|
|
58
|
+
value
|
|
59
|
+
rescue ArgumentError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns nil, a trimmed String, or a DeserializerResult failure.
|
|
64
|
+
def optional_search(raw)
|
|
65
|
+
return nil if raw.nil?
|
|
66
|
+
|
|
67
|
+
text = raw.to_s.strip
|
|
68
|
+
return nil if text.empty?
|
|
69
|
+
return failure(errors: { message: "invalid_search" }) if text.length > MAX_SEARCH_LENGTH
|
|
70
|
+
|
|
71
|
+
text
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class ShowDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
76
|
+
Input = Data.define(:id, :scope_value)
|
|
77
|
+
TOOL_ID = "users"
|
|
78
|
+
|
|
79
|
+
def call(params)
|
|
80
|
+
id = IdentityId.coerce(fetch_param(params, :id).value)
|
|
81
|
+
return failure(errors: { message: "invalid_id" }) if id.nil?
|
|
82
|
+
|
|
83
|
+
scope_value = CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: TOOL_ID)
|
|
84
|
+
|
|
85
|
+
success(Input.new(id:, scope_value:))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class UpdateNameDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
90
|
+
Input = Data.define(:id, :first_name, :last_name, :scope_value)
|
|
91
|
+
|
|
92
|
+
def call(params)
|
|
93
|
+
id = IdentityId.coerce(fetch_param(params, :id).value)
|
|
94
|
+
return failure(errors: { message: "invalid_id" }) if id.nil?
|
|
95
|
+
|
|
96
|
+
first_name = fetch_param(params, :first_name, :firstName).value.to_s.strip
|
|
97
|
+
last_name = fetch_param(params, :last_name, :lastName).value.to_s.strip
|
|
98
|
+
if first_name.blank? || last_name.blank?
|
|
99
|
+
return failure(errors: { message: "missing_required_fields" })
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
success(
|
|
103
|
+
Input.new(
|
|
104
|
+
id:,
|
|
105
|
+
first_name:,
|
|
106
|
+
last_name:,
|
|
107
|
+
scope_value: CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: ShowDeserializer::TOOL_ID)
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class UpdateUsernameDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
114
|
+
Input = Data.define(:id, :username, :scope_value)
|
|
115
|
+
|
|
116
|
+
def call(params)
|
|
117
|
+
id = IdentityId.coerce(fetch_param(params, :id).value)
|
|
118
|
+
return failure(errors: { message: "invalid_id" }) if id.nil?
|
|
119
|
+
|
|
120
|
+
username = fetch_param(params, :username).value.to_s.strip
|
|
121
|
+
return failure(errors: { message: "missing_required_fields" }) if username.blank?
|
|
122
|
+
|
|
123
|
+
success(
|
|
124
|
+
Input.new(
|
|
125
|
+
id:,
|
|
126
|
+
username:,
|
|
127
|
+
scope_value: CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: ShowDeserializer::TOOL_ID)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class UpdateEmailDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
134
|
+
Input = Data.define(:id, :email, :scope_value)
|
|
135
|
+
|
|
136
|
+
def call(params)
|
|
137
|
+
id = IdentityId.coerce(fetch_param(params, :id).value)
|
|
138
|
+
return failure(errors: { message: "invalid_id" }) if id.nil?
|
|
139
|
+
|
|
140
|
+
email = fetch_param(params, :email).value.to_s.strip
|
|
141
|
+
return failure(errors: { message: "missing_required_fields" }) if email.blank?
|
|
142
|
+
|
|
143
|
+
success(
|
|
144
|
+
Input.new(
|
|
145
|
+
id:,
|
|
146
|
+
email:,
|
|
147
|
+
scope_value: CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: ShowDeserializer::TOOL_ID)
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class UpdateEmailValidationDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
154
|
+
Input = Data.define(:id, :email_validated, :scope_value)
|
|
155
|
+
|
|
156
|
+
def call(params)
|
|
157
|
+
id = IdentityId.coerce(fetch_param(params, :id).value)
|
|
158
|
+
return failure(errors: { message: "invalid_id" }) if id.nil?
|
|
159
|
+
|
|
160
|
+
email_validated = coerce_boolean(fetch_param(params, :emailValidated, :email_validated).value)
|
|
161
|
+
return failure(errors: { message: "invalid_email_validated" }) if email_validated.nil?
|
|
162
|
+
|
|
163
|
+
success(
|
|
164
|
+
Input.new(
|
|
165
|
+
id:,
|
|
166
|
+
email_validated:,
|
|
167
|
+
scope_value: CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: ShowDeserializer::TOOL_ID)
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def coerce_boolean(raw)
|
|
175
|
+
case raw
|
|
176
|
+
when true, false
|
|
177
|
+
raw
|
|
178
|
+
when "true", "1", 1
|
|
179
|
+
true
|
|
180
|
+
when "false", "0", 0
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class UpdateRolesDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
187
|
+
Input = Data.define(:id, :roles, :scope_value)
|
|
188
|
+
|
|
189
|
+
def call(params)
|
|
190
|
+
id = IdentityId.coerce(fetch_param(params, :id).value)
|
|
191
|
+
return failure(errors: { message: "invalid_id" }) if id.nil?
|
|
192
|
+
|
|
193
|
+
raw = fetch_param(params, :roles).value
|
|
194
|
+
unless raw.is_a?(Array)
|
|
195
|
+
return failure(errors: { message: "invalid_roles" })
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
roles = raw.map { |value| value.to_s.strip }
|
|
199
|
+
if roles.any?(&:blank?)
|
|
200
|
+
return failure(errors: { message: "invalid_roles" })
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
success(
|
|
204
|
+
Input.new(
|
|
205
|
+
id:,
|
|
206
|
+
roles:,
|
|
207
|
+
scope_value: CommandTower::Deserializers::Admin::ScopeParameter.extract(
|
|
208
|
+
params,
|
|
209
|
+
tool_id: ShowDeserializer::TOOL_ID
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class UserListDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
8
|
+
Input = Data.define(:limit, :offset, :actions, :occurred_after, :occurred_before, :subject_types)
|
|
9
|
+
DEFAULT_LIMIT = 50
|
|
10
|
+
MAX_LIMIT = 100
|
|
11
|
+
MAX_MULTI = 50
|
|
12
|
+
EVENT_NAME = /\A[a-z][a-z0-9_]*\z/
|
|
13
|
+
SUBJECT_TYPE = /\A[A-Z][A-Za-z0-9_:]*\z/
|
|
14
|
+
|
|
15
|
+
def call(params)
|
|
16
|
+
limit = pagination_integer(fetch_param(params, :limit).value, default: DEFAULT_LIMIT, minimum: 1, maximum: MAX_LIMIT)
|
|
17
|
+
return failure(errors: { message: "invalid_limit" }) if limit.nil?
|
|
18
|
+
|
|
19
|
+
offset = pagination_integer(fetch_param(params, :offset).value, default: 0, minimum: 0)
|
|
20
|
+
return failure(errors: { message: "invalid_offset" }) if offset.nil?
|
|
21
|
+
|
|
22
|
+
actions = optional_event_names(params)
|
|
23
|
+
return actions if deserializer_result?(actions)
|
|
24
|
+
|
|
25
|
+
occurred_after = optional_time(fetch_param(params, :occurredAfter, :occurred_after).value, field: "occurredAfter")
|
|
26
|
+
return occurred_after if deserializer_result?(occurred_after)
|
|
27
|
+
|
|
28
|
+
occurred_before = optional_time(fetch_param(params, :occurredBefore, :occurred_before).value, field: "occurredBefore")
|
|
29
|
+
return occurred_before if deserializer_result?(occurred_before)
|
|
30
|
+
|
|
31
|
+
subject_types = optional_subject_types(params)
|
|
32
|
+
return subject_types if deserializer_result?(subject_types)
|
|
33
|
+
|
|
34
|
+
success(Input.new(limit:, offset:, actions:, occurred_after:, occurred_before:, subject_types:))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def pagination_integer(raw, default:, minimum:, maximum: nil)
|
|
40
|
+
return default if raw.nil? || (raw.is_a?(String) && raw.strip.empty?)
|
|
41
|
+
|
|
42
|
+
value = raw.is_a?(Integer) ? raw : (Integer(raw, 10) if raw.is_a?(String) && /\A-?\d+\z/.match?(raw.strip))
|
|
43
|
+
return if value.nil? || value < minimum || (maximum && value > maximum)
|
|
44
|
+
|
|
45
|
+
value
|
|
46
|
+
rescue ArgumentError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Prefer eventNames[] / event_names[]; singular eventName remains a one-element alias.
|
|
51
|
+
def optional_event_names(params)
|
|
52
|
+
multi = fetch_param(params, :eventNames, :event_names).value
|
|
53
|
+
singular = fetch_param(params, :eventName, :event_name).value
|
|
54
|
+
normalize_token_list(
|
|
55
|
+
multi,
|
|
56
|
+
singular,
|
|
57
|
+
regex: EVENT_NAME,
|
|
58
|
+
invalid_message: "invalid_eventName",
|
|
59
|
+
too_many_message: "invalid_eventNames"
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def optional_subject_types(params)
|
|
64
|
+
multi = fetch_param(params, :subjectTypes, :subject_types).value
|
|
65
|
+
singular = fetch_param(params, :subjectType, :subject_type).value
|
|
66
|
+
normalize_token_list(
|
|
67
|
+
multi,
|
|
68
|
+
singular,
|
|
69
|
+
regex: SUBJECT_TYPE,
|
|
70
|
+
invalid_message: "invalid_subjectType",
|
|
71
|
+
too_many_message: "invalid_subjectTypes"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def normalize_token_list(multi, singular, regex:, invalid_message:, too_many_message:)
|
|
76
|
+
raw_values =
|
|
77
|
+
if multi.nil? || (multi.is_a?(String) && multi.strip.empty?)
|
|
78
|
+
singular.nil? || (singular.is_a?(String) && singular.strip.empty?) ? [] : [singular]
|
|
79
|
+
elsif multi.is_a?(Array)
|
|
80
|
+
multi
|
|
81
|
+
elsif multi.is_a?(String)
|
|
82
|
+
[multi]
|
|
83
|
+
else
|
|
84
|
+
return failure(errors: { message: too_many_message })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
tokens = []
|
|
88
|
+
raw_values.each do |raw|
|
|
89
|
+
next if raw.nil? || (raw.is_a?(String) && raw.strip.empty?)
|
|
90
|
+
return failure(errors: { message: invalid_message }) unless raw.is_a?(String) && regex.match?(raw.strip)
|
|
91
|
+
|
|
92
|
+
tokens << raw.strip
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
tokens = tokens.uniq
|
|
96
|
+
return failure(errors: { message: too_many_message }) if tokens.length > MAX_MULTI
|
|
97
|
+
|
|
98
|
+
tokens
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def optional_time(raw, field:)
|
|
102
|
+
return if raw.nil? || (raw.is_a?(String) && raw.strip.empty?)
|
|
103
|
+
return failure(errors: { message: "invalid_#{field}" }) unless raw.is_a?(String)
|
|
104
|
+
|
|
105
|
+
Time.iso8601(raw.strip)
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
failure(errors: { message: "invalid_#{field}" })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def optional_positive_integer(raw, field:)
|
|
111
|
+
return if raw.nil? || (raw.is_a?(String) && raw.strip.empty?)
|
|
112
|
+
|
|
113
|
+
value = raw.is_a?(Integer) ? raw : (Integer(raw, 10) if raw.is_a?(String) && /\A\d+\z/.match?(raw.strip))
|
|
114
|
+
return failure(errors: { message: "invalid_#{field}" }) unless value&.positive?
|
|
115
|
+
|
|
116
|
+
value
|
|
117
|
+
rescue ArgumentError
|
|
118
|
+
failure(errors: { message: "invalid_#{field}" })
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class AdminListDeserializer < UserListDeserializer
|
|
123
|
+
Input = Data.define(
|
|
124
|
+
:limit,
|
|
125
|
+
:offset,
|
|
126
|
+
:actions,
|
|
127
|
+
:occurred_after,
|
|
128
|
+
:occurred_before,
|
|
129
|
+
:subject_types,
|
|
130
|
+
:affected_user_id,
|
|
131
|
+
:actor_user_id,
|
|
132
|
+
:originating_administrator_id,
|
|
133
|
+
:attribution_mode,
|
|
134
|
+
:scope_value
|
|
135
|
+
)
|
|
136
|
+
TOOL_ID = "audit"
|
|
137
|
+
|
|
138
|
+
def call(params)
|
|
139
|
+
base = super
|
|
140
|
+
return base unless base.success?
|
|
141
|
+
|
|
142
|
+
affected_user_id = optional_positive_integer(
|
|
143
|
+
fetch_param(params, :affectedUserId, :affected_user_id).value,
|
|
144
|
+
field: "affectedUserId"
|
|
145
|
+
)
|
|
146
|
+
return affected_user_id if deserializer_result?(affected_user_id)
|
|
147
|
+
|
|
148
|
+
actor_user_id = optional_positive_integer(
|
|
149
|
+
fetch_param(params, :actorUserId, :actor_user_id).value,
|
|
150
|
+
field: "actorUserId"
|
|
151
|
+
)
|
|
152
|
+
return actor_user_id if deserializer_result?(actor_user_id)
|
|
153
|
+
|
|
154
|
+
originating_administrator_id = optional_positive_integer(
|
|
155
|
+
fetch_param(params, :originatingAdministratorId, :originating_administrator_id).value,
|
|
156
|
+
field: "originatingAdministratorId"
|
|
157
|
+
)
|
|
158
|
+
return originating_administrator_id if deserializer_result?(originating_administrator_id)
|
|
159
|
+
|
|
160
|
+
attribution_mode = optional_attribution_mode(fetch_param(params, :attributionMode, :attribution_mode).value)
|
|
161
|
+
return attribution_mode if deserializer_result?(attribution_mode)
|
|
162
|
+
|
|
163
|
+
scope_value = CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: TOOL_ID)
|
|
164
|
+
|
|
165
|
+
success(
|
|
166
|
+
Input.new(
|
|
167
|
+
limit: base.input.limit,
|
|
168
|
+
offset: base.input.offset,
|
|
169
|
+
actions: base.input.actions,
|
|
170
|
+
occurred_after: base.input.occurred_after,
|
|
171
|
+
occurred_before: base.input.occurred_before,
|
|
172
|
+
subject_types: base.input.subject_types,
|
|
173
|
+
affected_user_id:,
|
|
174
|
+
actor_user_id:,
|
|
175
|
+
originating_administrator_id:,
|
|
176
|
+
attribution_mode:,
|
|
177
|
+
scope_value:
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def optional_attribution_mode(raw)
|
|
185
|
+
return if raw.nil? || (raw.is_a?(String) && raw.strip.empty?)
|
|
186
|
+
unless raw.is_a?(String) && CommandTower::Audit::Event::ATTRIBUTION_MODES.include?(raw.strip)
|
|
187
|
+
return failure(errors: { message: "invalid_attributionMode" })
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
raw.strip
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class ShowDeserializer < CommandTower::Deserializers::ApplicationDeserializer
|
|
195
|
+
Input = Data.define(:id, :scope_value)
|
|
196
|
+
TOOL_ID = "audit"
|
|
197
|
+
|
|
198
|
+
def call(params)
|
|
199
|
+
id = optional_positive_integer(fetch_param(params, :id).value, field: "id")
|
|
200
|
+
return id if deserializer_result?(id)
|
|
201
|
+
return failure(errors: { message: "invalid_id" }) if id.nil?
|
|
202
|
+
|
|
203
|
+
scope_value = CommandTower::Deserializers::Admin::ScopeParameter.extract(params, tool_id: TOOL_ID)
|
|
204
|
+
|
|
205
|
+
success(Input.new(id:, scope_value:))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
def optional_positive_integer(raw, field:)
|
|
211
|
+
return if raw.nil? || (raw.is_a?(String) && raw.strip.empty?)
|
|
212
|
+
|
|
213
|
+
value = raw.is_a?(Integer) ? raw : (Integer(raw, 10) if raw.is_a?(String) && /\A\d+\z/.match?(raw.strip))
|
|
214
|
+
return failure(errors: { message: "invalid_#{field}" }) unless value&.positive?
|
|
215
|
+
|
|
216
|
+
value
|
|
217
|
+
rescue ArgumentError
|
|
218
|
+
failure(errors: { message: "invalid_#{field}" })
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -22,6 +22,10 @@ module CommandTower
|
|
|
22
22
|
BooleanType.instance
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def float
|
|
26
|
+
FloatType.instance
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def array(element_type)
|
|
26
30
|
ArrayType.new(normalize(element_type))
|
|
27
31
|
end
|
|
@@ -127,6 +131,29 @@ module CommandTower
|
|
|
127
131
|
end
|
|
128
132
|
end
|
|
129
133
|
|
|
134
|
+
class FloatType < Type
|
|
135
|
+
include Singleton
|
|
136
|
+
|
|
137
|
+
def call(value, path:)
|
|
138
|
+
return value if value.is_a?(Float)
|
|
139
|
+
return value.to_f if value.is_a?(Integer)
|
|
140
|
+
|
|
141
|
+
if value.is_a?(String)
|
|
142
|
+
begin
|
|
143
|
+
return Float(value)
|
|
144
|
+
rescue ArgumentError
|
|
145
|
+
reject!(path, value, "float", "not a numeric string")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
reject!(path, value, "float", "expected Float")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def expected_label
|
|
153
|
+
"float"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
130
157
|
class ResultInstanceType < Type
|
|
131
158
|
def initialize(klass)
|
|
132
159
|
@klass = klass
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
module Auth
|
|
6
|
+
class AdminUnavailableDuringImpersonationError < CommandTower::Errors::ApplicationError
|
|
7
|
+
def code
|
|
8
|
+
"admin_unavailable_during_impersonation"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"Admin tools are unavailable while impersonating a user."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
module Auth
|
|
6
|
+
class DefaultMembershipAssignmentError < CommandTower::Errors::ApplicationError
|
|
7
|
+
def code
|
|
8
|
+
"default_membership_assignment_failed"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"Default membership role could not be assigned"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
module Auth
|
|
6
|
+
class ImpersonationSessionExpiredError < CommandTower::Errors::UnauthorizedError
|
|
7
|
+
def code
|
|
8
|
+
"impersonation_session_expired"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"Impersonation session is no longer valid"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
module Auth
|
|
6
|
+
class ImpersonationSessionMissingError < CommandTower::Errors::ValidationError
|
|
7
|
+
def initialize
|
|
8
|
+
super(details: { impersonation: "not_active" })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def code
|
|
12
|
+
"impersonation_session_missing"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def message
|
|
16
|
+
"Impersonation is not active"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
module Auth
|
|
6
|
+
class NestedImpersonationError < CommandTower::Errors::ForbiddenError
|
|
7
|
+
def code
|
|
8
|
+
"nested_impersonation_forbidden"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"Nested impersonation is forbidden"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
module Auth
|
|
6
|
+
class SelfImpersonationError < CommandTower::Errors::ValidationError
|
|
7
|
+
def initialize
|
|
8
|
+
super(details: { target: "cannot_impersonate_self" })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def code
|
|
12
|
+
"self_impersonation_forbidden"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def message
|
|
16
|
+
"Cannot impersonate yourself"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Errors
|
|
5
|
+
class ContinuationExhaustedError < ApplicationError
|
|
6
|
+
def code
|
|
7
|
+
"continuation_exhausted"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def message
|
|
11
|
+
attempt = details.is_a?(Hash) ? details[:attempt] : nil
|
|
12
|
+
max_attempts = details.is_a?(Hash) ? details[:max_attempts] : nil
|
|
13
|
+
if attempt && max_attempts
|
|
14
|
+
"delayed continuation exhausted after attempt #{attempt} of #{max_attempts}"
|
|
15
|
+
else
|
|
16
|
+
"delayed continuation exhausted"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def log_level
|
|
21
|
+
:error
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CommandTower
|
|
2
4
|
class ApplicationJob < ActiveJob::Base
|
|
5
|
+
include CommandTower::Execution::JobBoundary
|
|
6
|
+
|
|
7
|
+
def execute_workflow(workflow_class, **args)
|
|
8
|
+
workflow_class.call_from_job(job: self, **args)
|
|
9
|
+
end
|
|
3
10
|
end
|
|
4
11
|
end
|