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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Impersonation
|
|
6
|
+
class TerminateOpenSessions < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :actor_user_id, is_a: Integer, required: true
|
|
8
|
+
validate :reason, is_a: String, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
unless CommandTower::Impersonation::Session::END_REASONS.include?(reason)
|
|
12
|
+
return context.fail!(application_error: CommandTower::Errors::InternalError.new)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
ids = CommandTower::Impersonation::Session.open.where(actor_user_id:).pluck(:id)
|
|
16
|
+
ended_count = 0
|
|
17
|
+
ids.each do |session_id|
|
|
18
|
+
ended = CommandTower::Services::Impersonation::End.call(
|
|
19
|
+
session_id:,
|
|
20
|
+
reason:,
|
|
21
|
+
actor_user_id:
|
|
22
|
+
)
|
|
23
|
+
next unless ended.success? && ended.data[:ended]
|
|
24
|
+
|
|
25
|
+
ended_count += 1
|
|
26
|
+
emit_ended(ended.data[:session], reason:)
|
|
27
|
+
end
|
|
28
|
+
context.ended_count = ended_count
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def emit_ended(session, reason:)
|
|
34
|
+
target = User.find_by(id: session.target_user_id)
|
|
35
|
+
return if target.nil?
|
|
36
|
+
|
|
37
|
+
CommandTower::Impersonation::ClearOverlayForAudit.call(actor_user_id:) do
|
|
38
|
+
audit(
|
|
39
|
+
:impersonation_ended,
|
|
40
|
+
subject: target,
|
|
41
|
+
affected_user: target,
|
|
42
|
+
changes: { reason: { from: nil, to: reason } },
|
|
43
|
+
metadata: { impersonation_session_id: session.id },
|
|
44
|
+
attribution_mode: :admin_direct
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -42,11 +42,23 @@ module CommandTower
|
|
|
42
42
|
|
|
43
43
|
begin
|
|
44
44
|
user.reset_verifier_token!
|
|
45
|
+
CommandTower::Services::Impersonation::TerminateOpenSessions.call(
|
|
46
|
+
actor_user_id: user.id,
|
|
47
|
+
reason: "revoked"
|
|
48
|
+
)
|
|
45
49
|
rescue StandardError => e
|
|
46
50
|
log_error("Failed to rotate session verifier for user [#{user.id}]: #{e.class}")
|
|
47
51
|
infrastructure_failure = true
|
|
48
52
|
raise ActiveRecord::Rollback
|
|
49
53
|
end
|
|
54
|
+
|
|
55
|
+
audit(
|
|
56
|
+
:password_changed,
|
|
57
|
+
subject: user,
|
|
58
|
+
affected_user: user,
|
|
59
|
+
changes: {},
|
|
60
|
+
metadata: { mechanism: "authenticated_change" }
|
|
61
|
+
)
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
if persistence_errors
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module SharedSequences
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class ResolveScopedUser
|
|
8
|
+
Result = Data.define(:user)
|
|
9
|
+
|
|
10
|
+
def self.call(id:, principal:, scope_value: nil)
|
|
11
|
+
new.call(id:, principal:, scope_value:)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(id:, principal:, scope_value: nil)
|
|
15
|
+
scope_result = CommandTower::Workflows::Admin::ScopeResolution.resolve(
|
|
16
|
+
tool_id: "users",
|
|
17
|
+
user: principal,
|
|
18
|
+
scope_value:
|
|
19
|
+
)
|
|
20
|
+
return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
21
|
+
|
|
22
|
+
shown = CommandTower::Services::Admin::Users::Show.call(
|
|
23
|
+
id:,
|
|
24
|
+
principal:,
|
|
25
|
+
scope_context: scope_result
|
|
26
|
+
)
|
|
27
|
+
unless shown.success?
|
|
28
|
+
return CommandTower::Workflows::WorkflowResult.failure(
|
|
29
|
+
errors: shown.errors,
|
|
30
|
+
http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
|
|
31
|
+
shown.errors.first
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Result.new(user: shown.data[:user])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
# Generic ActiveRecord atomic boundary shared by ApplicationWorkflow and ApplicationService.
|
|
5
|
+
# Layer-specific result contracts are supplied by includers.
|
|
6
|
+
module Transactional
|
|
7
|
+
class TransactionFailure < StandardError
|
|
8
|
+
attr_reader :result
|
|
9
|
+
|
|
10
|
+
def initialize(result)
|
|
11
|
+
@result = result
|
|
12
|
+
super("transaction failed")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class InvalidTransactionResult < StandardError; end
|
|
17
|
+
|
|
18
|
+
def transaction
|
|
19
|
+
result = nil
|
|
20
|
+
ActiveRecord::Base.transaction do
|
|
21
|
+
result = yield
|
|
22
|
+
validate_transaction_success!(result)
|
|
23
|
+
end
|
|
24
|
+
result
|
|
25
|
+
rescue TransactionFailure => e
|
|
26
|
+
handle_transaction_failure(e.result)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def fail_transaction!(result)
|
|
30
|
+
unless acceptable_failure_result?(result)
|
|
31
|
+
raise InvalidTransactionResult, fail_transaction_type_error_message
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
raise TransactionFailure.new(result)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def validate_transaction_success!(result)
|
|
40
|
+
return if acceptable_success_result?(result)
|
|
41
|
+
|
|
42
|
+
raise InvalidTransactionResult, invalid_success_result_message(result)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def acceptable_success_result?(_result)
|
|
46
|
+
raise NotImplementedError, "#{self.class} must implement #acceptable_success_result?"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def acceptable_failure_result?(_result)
|
|
50
|
+
raise NotImplementedError, "#{self.class} must implement #acceptable_failure_result?"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fail_transaction_type_error_message
|
|
54
|
+
"fail_transaction! requires a failed result"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def invalid_success_result_message(result)
|
|
58
|
+
"transaction block returned a non-success result; got #{result.class}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_transaction_failure(result)
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -30,6 +30,17 @@ module CommandTower
|
|
|
30
30
|
)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
audit(
|
|
34
|
+
:announcement_produced,
|
|
35
|
+
changes: {},
|
|
36
|
+
metadata: {
|
|
37
|
+
campaign_identity: input.campaign_identity,
|
|
38
|
+
notification_type_key: input.notification_type_key,
|
|
39
|
+
recipient_count: user_ids.size
|
|
40
|
+
},
|
|
41
|
+
attribution_mode: :admin_direct
|
|
42
|
+
)
|
|
43
|
+
|
|
33
44
|
success(
|
|
34
45
|
payload: CommandTower::Serializers::Admin::Messaging::AnnouncementResponseSerializer.serialize(
|
|
35
46
|
result.data
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module ScopeResolution
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def resolve(tool_id:, user:, scope_value:)
|
|
10
|
+
CommandTower::AdminScope::Resolve.call(tool_id:, principal: user, scope_value:)
|
|
11
|
+
rescue CommandTower::Errors::ForbiddenError => error
|
|
12
|
+
CommandTower::Workflows::WorkflowResult.failure(errors: [error], http_status: :forbidden)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
module ErrorMapping
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def http_status_for(error)
|
|
11
|
+
case error
|
|
12
|
+
when CommandTower::Errors::ValidationError
|
|
13
|
+
:unprocessable_entity
|
|
14
|
+
when CommandTower::Errors::ForbiddenError
|
|
15
|
+
:forbidden
|
|
16
|
+
when CommandTower::Errors::NotFoundError
|
|
17
|
+
:not_found
|
|
18
|
+
else
|
|
19
|
+
:internal_server_error
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
module IdentityMutation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def resolve_target(id:, principal:, scope_value:)
|
|
11
|
+
CommandTower::SharedSequences::Admin::Users::ResolveScopedUser.call(
|
|
12
|
+
id:,
|
|
13
|
+
principal:,
|
|
14
|
+
scope_value:
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def map_service_failure(result)
|
|
19
|
+
CommandTower::Workflows::WorkflowResult.failure(
|
|
20
|
+
errors: result.errors,
|
|
21
|
+
http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
|
|
22
|
+
result.errors.first
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class ListAssignableRolesWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
listed = CommandTower::Services::Admin::Users::ListAssignableRoles.call
|
|
12
|
+
unless listed.success?
|
|
13
|
+
return failure(
|
|
14
|
+
errors: listed.errors,
|
|
15
|
+
http_status: ErrorMapping.http_status_for(listed.errors.first)
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
success(
|
|
20
|
+
payload: CommandTower::Serializers::Admin::Users::AssignableRolesSerializer.serialize(
|
|
21
|
+
listed.data[:roles]
|
|
22
|
+
),
|
|
23
|
+
http_status: :ok
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class ListWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(limit:, offset:, search: nil, user:, scope_value: nil)
|
|
11
|
+
scope_result = resolve_scope(user:, scope_value:)
|
|
12
|
+
return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
listed = CommandTower::Services::Admin::Users::List.call(
|
|
15
|
+
limit:,
|
|
16
|
+
offset:,
|
|
17
|
+
search:,
|
|
18
|
+
principal: user,
|
|
19
|
+
scope_context: scope_result
|
|
20
|
+
)
|
|
21
|
+
return result_or_failure(listed) unless listed.success?
|
|
22
|
+
|
|
23
|
+
payload = listed.data[:users].map do |user|
|
|
24
|
+
CommandTower::Serializers::Admin::Users::UserSerializer.serialize(user)
|
|
25
|
+
end
|
|
26
|
+
meta = CommandTower::Serializers::Admin::Users::PaginationMetaSerializer.serialize(
|
|
27
|
+
listed.data[:pagination]
|
|
28
|
+
)
|
|
29
|
+
success(payload:, meta:, http_status: :ok)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def resolve_scope(user:, scope_value:)
|
|
35
|
+
CommandTower::Workflows::Admin::ScopeResolution.resolve(
|
|
36
|
+
tool_id: "users",
|
|
37
|
+
user:,
|
|
38
|
+
scope_value:
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def result_or_failure(result)
|
|
43
|
+
failure(
|
|
44
|
+
errors: result.errors,
|
|
45
|
+
http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
|
|
46
|
+
result.errors.first
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class SetEmailValidatedWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(id:, user:, email_validated:, scope_value: nil)
|
|
11
|
+
resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
|
|
12
|
+
return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
target = resolved.user
|
|
15
|
+
previous = target.email_validated
|
|
16
|
+
|
|
17
|
+
transaction do
|
|
18
|
+
result = CommandTower::Services::Admin::Users::SetEmailValidated.call(
|
|
19
|
+
user: target,
|
|
20
|
+
email_validated:
|
|
21
|
+
)
|
|
22
|
+
fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
|
|
23
|
+
|
|
24
|
+
updated = result.data[:user]
|
|
25
|
+
if result.data[:changed]
|
|
26
|
+
audit(
|
|
27
|
+
:admin_user_email_validation_changed,
|
|
28
|
+
subject: updated,
|
|
29
|
+
affected_user: updated,
|
|
30
|
+
changes: { email_validated: { from: previous, to: updated.email_validated } },
|
|
31
|
+
attribution_mode: :admin_direct
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
success(
|
|
36
|
+
payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
|
|
37
|
+
http_status: :ok
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class ShowWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(id:, user:, scope_value: nil)
|
|
11
|
+
scope_result = resolve_scope(user:, scope_value:)
|
|
12
|
+
return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
shown = CommandTower::Services::Admin::Users::Show.call(
|
|
15
|
+
id:,
|
|
16
|
+
principal: user,
|
|
17
|
+
scope_context: scope_result
|
|
18
|
+
)
|
|
19
|
+
return result_or_failure(shown) unless shown.success?
|
|
20
|
+
|
|
21
|
+
payload = CommandTower::Serializers::Admin::Users::UserSerializer.serialize(
|
|
22
|
+
shown.data[:user]
|
|
23
|
+
)
|
|
24
|
+
success(payload:, http_status: :ok)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def resolve_scope(user:, scope_value:)
|
|
30
|
+
CommandTower::Workflows::Admin::ScopeResolution.resolve(
|
|
31
|
+
tool_id: "users",
|
|
32
|
+
user:,
|
|
33
|
+
scope_value:
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def result_or_failure(result)
|
|
38
|
+
failure(
|
|
39
|
+
errors: result.errors,
|
|
40
|
+
http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
|
|
41
|
+
result.errors.first
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class UpdateEmailWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(id:, user:, email:, scope_value: nil)
|
|
11
|
+
resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
|
|
12
|
+
return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
target = resolved.user
|
|
15
|
+
|
|
16
|
+
transaction do
|
|
17
|
+
result = CommandTower::Services::Admin::Users::UpdateEmail.call(
|
|
18
|
+
user: target,
|
|
19
|
+
email:
|
|
20
|
+
)
|
|
21
|
+
fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
|
|
22
|
+
|
|
23
|
+
updated = result.data[:user]
|
|
24
|
+
if result.data[:changed]
|
|
25
|
+
audit(
|
|
26
|
+
:admin_user_email_changed,
|
|
27
|
+
subject: updated,
|
|
28
|
+
affected_user: updated,
|
|
29
|
+
changes: {},
|
|
30
|
+
metadata: { email_changed: true },
|
|
31
|
+
attribution_mode: :admin_direct
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
success(
|
|
36
|
+
payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
|
|
37
|
+
http_status: :ok
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class UpdateNameWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(id:, user:, first_name:, last_name:, scope_value: nil)
|
|
11
|
+
resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
|
|
12
|
+
return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
target = resolved.user
|
|
15
|
+
previous = { first_name: target.first_name, last_name: target.last_name }
|
|
16
|
+
|
|
17
|
+
transaction do
|
|
18
|
+
result = CommandTower::Services::Admin::Users::UpdateName.call(
|
|
19
|
+
user: target,
|
|
20
|
+
first_name:,
|
|
21
|
+
last_name:
|
|
22
|
+
)
|
|
23
|
+
fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
|
|
24
|
+
|
|
25
|
+
updated = result.data[:user]
|
|
26
|
+
if result.data[:changed]
|
|
27
|
+
audit(
|
|
28
|
+
:admin_user_name_changed,
|
|
29
|
+
subject: updated,
|
|
30
|
+
affected_user: updated,
|
|
31
|
+
changes: {
|
|
32
|
+
first_name: { from: previous[:first_name], to: updated.first_name },
|
|
33
|
+
last_name: { from: previous[:last_name], to: updated.last_name }
|
|
34
|
+
},
|
|
35
|
+
attribution_mode: :admin_direct
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
success(
|
|
40
|
+
payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
|
|
41
|
+
http_status: :ok
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class UpdateRolesWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(id:, user:, roles:, scope_value: nil)
|
|
11
|
+
resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
|
|
12
|
+
return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
target = resolved.user
|
|
15
|
+
policy = CommandTower::Services::Admin::Users::RoleAssignmentPolicy.call(
|
|
16
|
+
actor: user,
|
|
17
|
+
target:,
|
|
18
|
+
desired_roles: roles
|
|
19
|
+
)
|
|
20
|
+
return IdentityMutation.map_service_failure(policy) unless policy.success?
|
|
21
|
+
|
|
22
|
+
transaction do
|
|
23
|
+
result = CommandTower::Services::Admin::Users::ReplaceUserRoles.call(
|
|
24
|
+
user: target,
|
|
25
|
+
desired_roles: policy.data[:desired_roles]
|
|
26
|
+
)
|
|
27
|
+
fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
|
|
28
|
+
|
|
29
|
+
updated = result.data[:user]
|
|
30
|
+
if result.data[:changed]
|
|
31
|
+
Array(result.data[:assigned_roles]).each do |role_name|
|
|
32
|
+
audit(
|
|
33
|
+
:role_assigned,
|
|
34
|
+
subject: updated,
|
|
35
|
+
affected_user: updated,
|
|
36
|
+
changes: { role: { from: nil, to: role_name.to_s } },
|
|
37
|
+
attribution_mode: :admin_direct
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
Array(result.data[:revoked_roles]).each do |role_name|
|
|
41
|
+
audit(
|
|
42
|
+
:role_revoked,
|
|
43
|
+
subject: updated,
|
|
44
|
+
affected_user: updated,
|
|
45
|
+
changes: { role: { from: role_name.to_s, to: nil } },
|
|
46
|
+
attribution_mode: :admin_direct
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
success(
|
|
52
|
+
payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
|
|
53
|
+
http_status: :ok
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class UpdateUsernameWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(id:, user:, username:, scope_value: nil)
|
|
11
|
+
resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
|
|
12
|
+
return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
13
|
+
|
|
14
|
+
target = resolved.user
|
|
15
|
+
previous_username = target.username
|
|
16
|
+
|
|
17
|
+
transaction do
|
|
18
|
+
result = CommandTower::Services::Admin::Users::UpdateUsername.call(
|
|
19
|
+
user: target,
|
|
20
|
+
username:
|
|
21
|
+
)
|
|
22
|
+
fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
|
|
23
|
+
|
|
24
|
+
updated = result.data[:user]
|
|
25
|
+
if result.data[:changed]
|
|
26
|
+
audit(
|
|
27
|
+
:admin_user_username_changed,
|
|
28
|
+
subject: updated,
|
|
29
|
+
affected_user: updated,
|
|
30
|
+
changes: { username: { from: previous_username, to: updated.username } },
|
|
31
|
+
attribution_mode: :admin_direct
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
success(
|
|
36
|
+
payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
|
|
37
|
+
http_status: :ok
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Admin
|
|
6
|
+
module Workspace
|
|
7
|
+
class ManifestWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(user:)
|
|
11
|
+
result = CommandTower::Services::Admin::Workspace::Manifest.call(user:)
|
|
12
|
+
unless result.success?
|
|
13
|
+
return failure(errors: result.errors, http_status: :unprocessable_entity)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
success(
|
|
17
|
+
payload: CommandTower::Serializers::Admin::Workspace::ManifestSerializer.serialize(result.data[:tools]),
|
|
18
|
+
http_status: :ok
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|