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
|
@@ -15,31 +15,64 @@ module CommandTower
|
|
|
15
15
|
)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
user = nil
|
|
19
|
+
result = transaction do
|
|
20
|
+
register_result = CommandTower::Services::Auth::Register.call(
|
|
21
|
+
first_name: input.first_name,
|
|
22
|
+
last_name: input.last_name,
|
|
23
|
+
username: input.username,
|
|
24
|
+
email: input.email,
|
|
25
|
+
password: input.password,
|
|
26
|
+
password_confirmation: input.password_confirmation
|
|
27
|
+
)
|
|
28
|
+
unless register_result.success?
|
|
29
|
+
fail_transaction!(
|
|
30
|
+
failure(
|
|
31
|
+
errors: register_result.errors,
|
|
32
|
+
http_status: SignupErrorStatus.http_status_for(register_result.errors.first)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
user = register_result.data[:user]
|
|
38
|
+
audit(
|
|
39
|
+
:user_registered,
|
|
40
|
+
subject: user,
|
|
41
|
+
affected_user: user,
|
|
42
|
+
changes: {}
|
|
31
43
|
)
|
|
32
|
-
end
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
roles_before = Array(user.roles)
|
|
46
|
+
assign_result = CommandTower::Services::Auth::AssignDefaultMembershipRole.call(user: user)
|
|
47
|
+
unless assign_result.success?
|
|
48
|
+
fail_transaction!(
|
|
49
|
+
failure(
|
|
50
|
+
errors: assign_result.errors,
|
|
51
|
+
http_status: :unprocessable_entity
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
user = assign_result.data[:user]
|
|
57
|
+
assigned_roles = Array(user.roles) - roles_before
|
|
58
|
+
if assigned_roles.any?
|
|
59
|
+
audit(
|
|
60
|
+
:role_assigned,
|
|
61
|
+
subject: user,
|
|
62
|
+
affected_user: user,
|
|
63
|
+
changes: { role: { from: nil, to: assigned_roles.first.to_s } }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
success(
|
|
67
|
+
payload: CommandTower::Serializers::Auth::RegisterResponseSerializer.serialize(
|
|
68
|
+
user: user
|
|
69
|
+
),
|
|
70
|
+
http_status: :created
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
emit_welcome(user) if result.success? && user.present?
|
|
75
|
+
result
|
|
43
76
|
end
|
|
44
77
|
|
|
45
78
|
private
|
|
@@ -69,13 +102,12 @@ module CommandTower
|
|
|
69
102
|
|
|
70
103
|
def log_welcome_failure(user, error)
|
|
71
104
|
payload = {
|
|
72
|
-
event: "messaging.welcome_produce_failed",
|
|
73
105
|
user_id: user.id,
|
|
74
106
|
error_class: error.class.name,
|
|
107
|
+
log_level: :error
|
|
75
108
|
}
|
|
76
109
|
payload[:error_code] = error.code if error.respond_to?(:code)
|
|
77
|
-
|
|
78
|
-
Rails.logger.error(payload.to_json)
|
|
110
|
+
publish_event(category: :messaging, name: :welcome_produce_failed, payload:)
|
|
79
111
|
end
|
|
80
112
|
end
|
|
81
113
|
end
|
|
@@ -20,10 +20,14 @@ module CommandTower
|
|
|
20
20
|
response_effects[:ensure_csrf_cookie] = { rotate: csrf_rotate_on_reset? }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
impersonation_session = impersonation_session_for(auth_context)
|
|
24
|
+
|
|
23
25
|
success(
|
|
24
26
|
payload: CommandTower::Serializers::Auth::SessionResponseSerializer.serialize(
|
|
25
27
|
user: current_user,
|
|
26
|
-
token_expires_at: auth_context.token_expires_at
|
|
28
|
+
token_expires_at: auth_context.token_expires_at,
|
|
29
|
+
impersonation_session:,
|
|
30
|
+
actor: auth_context.actor_user
|
|
27
31
|
),
|
|
28
32
|
http_status: :ok,
|
|
29
33
|
response_effects: response_effects
|
|
@@ -37,6 +41,16 @@ module CommandTower
|
|
|
37
41
|
|
|
38
42
|
CommandTower.config.jwt.cookie.csrf.rotate_on_reset
|
|
39
43
|
end
|
|
44
|
+
|
|
45
|
+
def impersonation_session_for(auth_context)
|
|
46
|
+
return unless CommandTower::Current.impersonation_active
|
|
47
|
+
|
|
48
|
+
session_id = auth_context.impersonation_session_id.presence ||
|
|
49
|
+
CommandTower::Current.impersonation_session_id
|
|
50
|
+
return if session_id.blank?
|
|
51
|
+
|
|
52
|
+
CommandTower::Impersonation::Session.find_by(id: session_id)
|
|
53
|
+
end
|
|
40
54
|
end
|
|
41
55
|
end
|
|
42
56
|
end
|
|
@@ -18,6 +18,7 @@ module CommandTower
|
|
|
18
18
|
CommandTower::Errors::ForbiddenError
|
|
19
19
|
:forbidden
|
|
20
20
|
when CommandTower::Errors::Auth::InvalidCredentialsError,
|
|
21
|
+
CommandTower::Errors::Auth::ImpersonationSessionExpiredError,
|
|
21
22
|
CommandTower::Errors::UnauthorizedError
|
|
22
23
|
:unauthorized
|
|
23
24
|
else
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Impersonation
|
|
6
|
+
module ErrorMapping
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def http_status_for(error)
|
|
10
|
+
case error
|
|
11
|
+
when CommandTower::Errors::Auth::SelfImpersonationError,
|
|
12
|
+
CommandTower::Errors::Auth::ImpersonationSessionMissingError,
|
|
13
|
+
CommandTower::Errors::ValidationError
|
|
14
|
+
:unprocessable_entity
|
|
15
|
+
when CommandTower::Errors::Auth::NestedImpersonationError,
|
|
16
|
+
CommandTower::Errors::ForbiddenError
|
|
17
|
+
:forbidden
|
|
18
|
+
when CommandTower::Errors::NotFoundError
|
|
19
|
+
:not_found
|
|
20
|
+
else
|
|
21
|
+
:internal_server_error
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Impersonation
|
|
6
|
+
class StartWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
7
|
+
retry_strategy :none
|
|
8
|
+
|
|
9
|
+
def call(id:, actor:, scope_value: nil)
|
|
10
|
+
if CommandTower::Current.impersonation_active
|
|
11
|
+
return failure(
|
|
12
|
+
errors: [CommandTower::Errors::Auth::NestedImpersonationError.new],
|
|
13
|
+
http_status: :forbidden
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if actor.id == id
|
|
18
|
+
return failure(
|
|
19
|
+
errors: [CommandTower::Errors::Auth::SelfImpersonationError.new],
|
|
20
|
+
http_status: :unprocessable_entity
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
scope_result = CommandTower::Workflows::Admin::ScopeResolution.resolve(
|
|
25
|
+
tool_id: "users",
|
|
26
|
+
user: actor,
|
|
27
|
+
scope_value:
|
|
28
|
+
)
|
|
29
|
+
return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
|
|
30
|
+
|
|
31
|
+
shown = CommandTower::Services::Admin::Users::Show.call(
|
|
32
|
+
id:,
|
|
33
|
+
principal: actor,
|
|
34
|
+
scope_context: scope_result
|
|
35
|
+
)
|
|
36
|
+
unless shown.success?
|
|
37
|
+
return failure(
|
|
38
|
+
errors: shown.errors,
|
|
39
|
+
http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(shown.errors.first)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
target = shown.data[:user]
|
|
44
|
+
created = CommandTower::Services::Impersonation::Create.call(actor:, target:)
|
|
45
|
+
unless created.success?
|
|
46
|
+
return failure(
|
|
47
|
+
errors: created.errors,
|
|
48
|
+
http_status: :internal_server_error
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
session = created.data[:session]
|
|
53
|
+
audit_started(session:, target:, scope_context: scope_result)
|
|
54
|
+
|
|
55
|
+
token = CommandTower::Jwt::LoginCreate.call(
|
|
56
|
+
user: actor,
|
|
57
|
+
impersonation_session_id: session.id
|
|
58
|
+
).token
|
|
59
|
+
expires_at = CommandTower.config.jwt.ttl.from_now.to_time.to_s
|
|
60
|
+
|
|
61
|
+
success(
|
|
62
|
+
payload: CommandTower::Serializers::Impersonation::SessionSerializer.serialize(session),
|
|
63
|
+
http_status: :created,
|
|
64
|
+
response_effects: {
|
|
65
|
+
set_token: { token:, expires_at: }
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def audit_started(session:, target:, scope_context:)
|
|
73
|
+
scope_class, host_context = audit_scope_for(scope_context)
|
|
74
|
+
audit(
|
|
75
|
+
:impersonation_started,
|
|
76
|
+
subject: target,
|
|
77
|
+
affected_user: target,
|
|
78
|
+
metadata: { impersonation_session_id: session.id },
|
|
79
|
+
attribution_mode: :admin_direct,
|
|
80
|
+
scope_class:,
|
|
81
|
+
host_context:
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def audit_scope_for(scope_context)
|
|
86
|
+
return [:global, nil] if scope_context.nil?
|
|
87
|
+
return [:global, nil] unless CommandTower.config.admin_scope.registered?("users")
|
|
88
|
+
|
|
89
|
+
type = CommandTower.config.admin_scope.fetch("users").host_context_type
|
|
90
|
+
return [:global, nil] if type.to_s.strip.empty?
|
|
91
|
+
|
|
92
|
+
[:host, { type:, identifier: scope_context.scope_value }]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Workflows
|
|
5
|
+
module Impersonation
|
|
6
|
+
class StopWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
7
|
+
retry_strategy :none
|
|
8
|
+
|
|
9
|
+
def call(actor:, impersonation_session_id:, token_expires_at: nil)
|
|
10
|
+
session_id = impersonation_session_id.to_s.strip
|
|
11
|
+
if session_id.empty?
|
|
12
|
+
return failure(
|
|
13
|
+
errors: [CommandTower::Errors::Auth::ImpersonationSessionMissingError.new],
|
|
14
|
+
http_status: :unprocessable_entity
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ended = CommandTower::Services::Impersonation::End.call(
|
|
19
|
+
session_id:,
|
|
20
|
+
reason: "manual",
|
|
21
|
+
actor_user_id: actor.id
|
|
22
|
+
)
|
|
23
|
+
unless ended.success?
|
|
24
|
+
return failure(
|
|
25
|
+
errors: ended.errors,
|
|
26
|
+
http_status: CommandTower::Workflows::Impersonation::ErrorMapping.http_status_for(ended.errors.first)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
session = ended.data[:session]
|
|
31
|
+
if session.nil? || session.actor_user_id != actor.id
|
|
32
|
+
return failure(
|
|
33
|
+
errors: [CommandTower::Errors::Auth::ImpersonationSessionMissingError.new],
|
|
34
|
+
http_status: :unprocessable_entity
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if ended.data[:ended]
|
|
39
|
+
target = User.find_by(id: session.target_user_id)
|
|
40
|
+
if target
|
|
41
|
+
audit(
|
|
42
|
+
:impersonation_ended,
|
|
43
|
+
subject: target,
|
|
44
|
+
affected_user: target,
|
|
45
|
+
changes: { reason: { from: nil, to: "manual" } },
|
|
46
|
+
metadata: { impersonation_session_id: session.id },
|
|
47
|
+
attribution_mode: CommandTower::Current.impersonation_active ? nil : :admin_direct
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
token = CommandTower::Jwt::LoginCreate.call(user: actor).token
|
|
53
|
+
expires_at = token_expires_at.presence || CommandTower.config.jwt.ttl.from_now.to_time.to_s
|
|
54
|
+
|
|
55
|
+
success(
|
|
56
|
+
payload: { message: "impersonation_ended" },
|
|
57
|
+
http_status: :ok,
|
|
58
|
+
response_effects: {
|
|
59
|
+
set_token: { token:, expires_at: }
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -5,6 +5,7 @@ module CommandTower
|
|
|
5
5
|
module Me
|
|
6
6
|
class UpdateNameWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
7
7
|
retry_strategy :none
|
|
8
|
+
impersonation_activity!
|
|
8
9
|
|
|
9
10
|
def call(current_user:, first_name:, last_name:, auth_context: nil)
|
|
10
11
|
update_result = CommandTower::Services::Me::UpdateName.call(
|
|
@@ -3,31 +3,55 @@
|
|
|
3
3
|
module CommandTower
|
|
4
4
|
module Workflows
|
|
5
5
|
class WorkflowResult
|
|
6
|
-
|
|
6
|
+
STATUSES = %i[success deferred failure].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :payload, :errors, :http_status, :response_effects, :meta, :reason, :retry_after
|
|
9
|
+
|
|
10
|
+
def initialize(status:, payload:, errors:, http_status:, response_effects: nil, meta: {},
|
|
11
|
+
reason: nil, retry_after: nil)
|
|
12
|
+
@status = status.to_sym
|
|
13
|
+
raise ArgumentError, "invalid WorkflowResult status #{@status}" unless STATUSES.include?(@status)
|
|
7
14
|
|
|
8
|
-
def initialize(success:, payload:, errors:, http_status:, response_effects: nil, meta: {})
|
|
9
|
-
@success = success
|
|
10
15
|
@payload = payload
|
|
11
16
|
@errors = errors
|
|
12
17
|
@http_status = http_status
|
|
13
18
|
@response_effects = response_effects
|
|
14
19
|
@meta = meta
|
|
20
|
+
@reason = reason
|
|
21
|
+
@retry_after = retry_after
|
|
15
22
|
end
|
|
16
23
|
|
|
17
24
|
def success?
|
|
18
|
-
@success
|
|
25
|
+
@status == :success
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deferred?
|
|
29
|
+
@status == :deferred
|
|
19
30
|
end
|
|
20
31
|
|
|
21
32
|
def failure?
|
|
22
|
-
|
|
33
|
+
@status == :failure
|
|
23
34
|
end
|
|
24
35
|
|
|
25
36
|
def self.success(payload:, http_status: :ok, response_effects: nil, meta: {})
|
|
26
|
-
new(
|
|
37
|
+
new(status: :success, payload:, errors: [], http_status:, response_effects:, meta:)
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
def self.failure(errors:, http_status:, response_effects: nil, meta: {})
|
|
30
|
-
new(
|
|
41
|
+
new(status: :failure, payload: nil, errors:, http_status:, response_effects:, meta:)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.deferred(reason:, retry_after:, payload: {}, http_status: :accepted, response_effects: nil, meta: {})
|
|
45
|
+
new(
|
|
46
|
+
status: :deferred,
|
|
47
|
+
payload:,
|
|
48
|
+
errors: [],
|
|
49
|
+
http_status:,
|
|
50
|
+
response_effects:,
|
|
51
|
+
meta:,
|
|
52
|
+
reason: reason.to_sym,
|
|
53
|
+
retry_after: Integer(retry_after)
|
|
54
|
+
)
|
|
31
55
|
end
|
|
32
56
|
end
|
|
33
57
|
end
|
data/config/routes.rb
CHANGED
|
@@ -11,6 +11,8 @@ CommandTower::Engine.routes.draw do
|
|
|
11
11
|
namespace :auth do
|
|
12
12
|
post "logout", to: "logout#create"
|
|
13
13
|
get "session", to: "session#show"
|
|
14
|
+
get "principal-capabilities", to: "principal_capabilities#show"
|
|
15
|
+
delete "impersonation-session", to: "impersonation_session#destroy"
|
|
14
16
|
|
|
15
17
|
constraints(->(_req) { CommandTower.config.login.plain_text.enable? }) do
|
|
16
18
|
scope path: "plain-text", module: "plain_text" do
|
|
@@ -74,6 +76,10 @@ CommandTower::Engine.routes.draw do
|
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
get "audit-events/filter-options", to: "audit_events#filter_options"
|
|
80
|
+
get "audit-events", to: "audit_events#index"
|
|
81
|
+
get "audit-events/:id", to: "audit_events#show"
|
|
82
|
+
|
|
77
83
|
resource :preferences, only: [:show], controller: "preferences"
|
|
78
84
|
patch "preferences/:notification_type_key", to: "preferences#update"
|
|
79
85
|
|
|
@@ -96,6 +102,20 @@ CommandTower::Engine.routes.draw do
|
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
namespace :admin do
|
|
105
|
+
get "audit-events/filter-options", to: "audit/events#filter_options"
|
|
106
|
+
get "audit-events", to: "audit/events#index"
|
|
107
|
+
get "audit-events/:id", to: "audit/events#show"
|
|
108
|
+
get "users", to: "users#index"
|
|
109
|
+
get "users/assignable-roles", to: "users/roles#index"
|
|
110
|
+
get "users/:id", to: "users#show"
|
|
111
|
+
patch "users/:id/name", to: "users/identities#update_name"
|
|
112
|
+
patch "users/:id/username", to: "users/identities#update_username"
|
|
113
|
+
patch "users/:id/email", to: "users/identities#update_email"
|
|
114
|
+
patch "users/:id/email-validation", to: "users/identities#update_email_validation"
|
|
115
|
+
patch "users/:id/roles", to: "users/roles#update"
|
|
116
|
+
post "users/:id/impersonation-sessions", to: "users/impersonation_sessions#create"
|
|
117
|
+
get "workspace", to: "workspace#show"
|
|
118
|
+
|
|
99
119
|
namespace :messaging do
|
|
100
120
|
post "announcements", to: "announcements#create"
|
|
101
121
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateCommandTowerAuditEvents < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
create_table :command_tower_audit_events do |t|
|
|
6
|
+
t.string :event_uuid, null: false
|
|
7
|
+
t.string :action, null: false
|
|
8
|
+
t.datetime :occurred_at, null: false
|
|
9
|
+
|
|
10
|
+
t.string :execution_uuid
|
|
11
|
+
t.string :correlation_id
|
|
12
|
+
t.string :request_id
|
|
13
|
+
t.string :causation_id
|
|
14
|
+
t.string :source
|
|
15
|
+
|
|
16
|
+
t.bigint :actor_user_id
|
|
17
|
+
t.bigint :affected_user_id
|
|
18
|
+
t.bigint :effective_user_id
|
|
19
|
+
t.bigint :originating_administrator_id
|
|
20
|
+
t.boolean :impersonation_active, null: false, default: false
|
|
21
|
+
t.string :attribution_mode, null: false
|
|
22
|
+
|
|
23
|
+
t.string :subject_type
|
|
24
|
+
t.bigint :subject_id
|
|
25
|
+
t.string :subject_label
|
|
26
|
+
|
|
27
|
+
t.json :change_set, null: false
|
|
28
|
+
t.json :metadata, null: false
|
|
29
|
+
|
|
30
|
+
t.boolean :user_history, null: false
|
|
31
|
+
t.json :sensitive_fields, null: false
|
|
32
|
+
t.string :retention, null: false
|
|
33
|
+
|
|
34
|
+
t.timestamps
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
add_index :command_tower_audit_events, :event_uuid, unique: true, name: "index_ct_audit_events_on_event_uuid"
|
|
38
|
+
add_index :command_tower_audit_events, %i[affected_user_id occurred_at], name: "index_ct_audit_events_on_affected_occurred"
|
|
39
|
+
add_index :command_tower_audit_events, %i[actor_user_id occurred_at], name: "index_ct_audit_events_on_actor_occurred"
|
|
40
|
+
add_index :command_tower_audit_events, %i[action occurred_at], name: "index_ct_audit_events_on_action_occurred"
|
|
41
|
+
add_index :command_tower_audit_events, :execution_uuid, name: "index_ct_audit_events_on_execution_uuid"
|
|
42
|
+
add_index :command_tower_audit_events, :correlation_id, name: "index_ct_audit_events_on_correlation_id"
|
|
43
|
+
add_index :command_tower_audit_events, %i[originating_administrator_id occurred_at], name: "index_ct_audit_events_on_originating_admin_occurred"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddScopeColumnsToCommandTowerAuditEvents < ActiveRecord::Migration[7.2]
|
|
4
|
+
def up
|
|
5
|
+
change_table :command_tower_audit_events, bulk: true do |t|
|
|
6
|
+
t.string :scope_class, null: false, default: "legacy"
|
|
7
|
+
t.string :host_context_type
|
|
8
|
+
t.string :host_context_identifier
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
execute <<~SQL.squish
|
|
12
|
+
UPDATE command_tower_audit_events
|
|
13
|
+
SET scope_class = 'legacy'
|
|
14
|
+
WHERE scope_class IS NULL OR scope_class = 'legacy'
|
|
15
|
+
SQL
|
|
16
|
+
|
|
17
|
+
add_index :command_tower_audit_events,
|
|
18
|
+
%i[host_context_type host_context_identifier occurred_at],
|
|
19
|
+
name: "index_ct_audit_events_on_host_context_occurred"
|
|
20
|
+
add_index :command_tower_audit_events,
|
|
21
|
+
%i[scope_class occurred_at],
|
|
22
|
+
name: "index_ct_audit_events_on_scope_class_occurred"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def down
|
|
26
|
+
change_table :command_tower_audit_events, bulk: true do |t|
|
|
27
|
+
t.remove_index name: "index_ct_audit_events_on_host_context_occurred"
|
|
28
|
+
t.remove_index name: "index_ct_audit_events_on_scope_class_occurred"
|
|
29
|
+
t.remove :scope_class
|
|
30
|
+
t.remove :host_context_type
|
|
31
|
+
t.remove :host_context_identifier
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateCommandTowerImpersonationSessions < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
create_table :command_tower_impersonation_sessions, id: { type: :string, limit: 36 } do |t|
|
|
6
|
+
t.bigint :actor_user_id, null: false
|
|
7
|
+
t.bigint :target_user_id, null: false
|
|
8
|
+
t.datetime :started_at, null: false
|
|
9
|
+
t.datetime :last_activity_at, null: false
|
|
10
|
+
t.datetime :absolute_expires_at, null: false
|
|
11
|
+
t.datetime :idle_expires_at, null: false
|
|
12
|
+
t.datetime :ended_at
|
|
13
|
+
t.string :end_reason, limit: 32
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :command_tower_impersonation_sessions, :actor_user_id
|
|
18
|
+
add_index :command_tower_impersonation_sessions, :target_user_id
|
|
19
|
+
add_index :command_tower_impersonation_sessions,
|
|
20
|
+
%i[actor_user_id ended_at],
|
|
21
|
+
name: "index_ct_impersonation_sessions_on_actor_ended"
|
|
22
|
+
|
|
23
|
+
add_foreign_key :command_tower_impersonation_sessions, :users, column: :actor_user_id
|
|
24
|
+
add_foreign_key :command_tower_impersonation_sessions, :users, column: :target_user_id
|
|
25
|
+
end
|
|
26
|
+
end
|