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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module CommandTower
|
|
4
4
|
module Services
|
|
5
5
|
class ApplicationService < CommandTower::ServiceBase
|
|
6
|
+
include CommandTower::Transactional
|
|
7
|
+
|
|
6
8
|
on_argument_validation :fail_early
|
|
7
9
|
|
|
8
10
|
def self.inherited(subclass)
|
|
@@ -16,6 +18,41 @@ module CommandTower
|
|
|
16
18
|
ServiceResult.from_interactor_context(interactor_context)
|
|
17
19
|
end
|
|
18
20
|
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def acceptable_success_result?(result)
|
|
25
|
+
return true if result.nil?
|
|
26
|
+
return result.success? if result.is_a?(ServiceResult)
|
|
27
|
+
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def acceptable_failure_result?(result)
|
|
32
|
+
return true if result.is_a?(ServiceResult) && result.failure?
|
|
33
|
+
return true if result.is_a?(CommandTower::Errors::ApplicationError)
|
|
34
|
+
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fail_transaction_type_error_message
|
|
39
|
+
"fail_transaction! requires a failed ServiceResult or ApplicationError"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def invalid_success_result_message(result)
|
|
43
|
+
"transaction block returned a non-success ServiceResult; " \
|
|
44
|
+
"use fail_transaction!(result) for expected failures (got #{result.class})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_transaction_failure(result)
|
|
48
|
+
context.fail!(application_error: application_error_from_transaction_result(result))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def application_error_from_transaction_result(result)
|
|
52
|
+
return result if result.is_a?(CommandTower::Errors::ApplicationError)
|
|
53
|
+
|
|
54
|
+
result.errors.first || CommandTower::Errors::InternalError.new
|
|
55
|
+
end
|
|
19
56
|
end
|
|
20
57
|
end
|
|
21
58
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
# Projects safe filter-option metadata from the finalized audit registry.
|
|
8
|
+
# Not authorization — scoped catalogs only (Admin vs Me user_history).
|
|
9
|
+
class FilterOptions < CommandTower::Services::ApplicationService
|
|
10
|
+
ATTRIBUTION_MODE_LABELS = {
|
|
11
|
+
"self_service" => "Self-service",
|
|
12
|
+
"admin_direct" => "Admin action",
|
|
13
|
+
"impersonation" => "Impersonation",
|
|
14
|
+
"system" => "System"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
validate :viewer_scope, is_a: Symbol, required: true
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
unless %i[admin me].include?(viewer_scope)
|
|
21
|
+
raise CommandTower::Errors::ValidationError, "viewer_scope must be :admin or :me"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context.event_names = project_event_names
|
|
25
|
+
context.subject_types = project_subject_types
|
|
26
|
+
context.attribution_modes = project_attribution_modes
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def scoped_definitions
|
|
32
|
+
definitions = CommandTower.config.registry.audit.definitions
|
|
33
|
+
if viewer_scope == :admin
|
|
34
|
+
definitions
|
|
35
|
+
else
|
|
36
|
+
definitions.select { |_name, definition| definition.user_history == true }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def project_event_names
|
|
41
|
+
scoped_definitions.sort_by { |name, _| name }.map do |name, definition|
|
|
42
|
+
label = definition.label.to_s.strip
|
|
43
|
+
label = name if label.empty?
|
|
44
|
+
{
|
|
45
|
+
value: name,
|
|
46
|
+
label:,
|
|
47
|
+
tags: Array(definition.tags)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def project_subject_types
|
|
53
|
+
scoped_definitions.filter_map do |_name, definition|
|
|
54
|
+
token = definition.subject_type.to_s.strip
|
|
55
|
+
next if token.empty?
|
|
56
|
+
|
|
57
|
+
{ value: token, label: token }
|
|
58
|
+
end.uniq { |entry| entry[:value] }.sort_by { |entry| entry[:value] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def project_attribution_modes
|
|
62
|
+
return [] unless viewer_scope == :admin
|
|
63
|
+
|
|
64
|
+
CommandTower::Audit::Event::ATTRIBUTION_MODES.map do |mode|
|
|
65
|
+
{
|
|
66
|
+
value: mode,
|
|
67
|
+
label: ATTRIBUTION_MODE_LABELS.fetch(mode, mode.tr("_", " ").split.map(&:capitalize).join(" "))
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class List < CommandTower::Services::ApplicationService
|
|
8
|
+
VIEWER_SCOPES = %i[user admin].freeze
|
|
9
|
+
|
|
10
|
+
validate :viewer_scope, is_one: VIEWER_SCOPES, required: true
|
|
11
|
+
validate :limit, is_a: Integer, required: true, gte: 1, lte: 100
|
|
12
|
+
validate :offset, is_a: Integer, required: true, gt: -1
|
|
13
|
+
validate :affected_user_id, is_a: Integer, required: false
|
|
14
|
+
validate :actor_user_id, is_a: Integer, required: false
|
|
15
|
+
validate :originating_administrator_id, is_a: Integer, required: false
|
|
16
|
+
validate :actions, is_a: Array, required: false
|
|
17
|
+
validate :subject_types, is_a: Array, required: false
|
|
18
|
+
validate :attribution_mode, is_a: String, required: false
|
|
19
|
+
validate :occurred_after, is_a: [Time, ActiveSupport::TimeWithZone], required: false
|
|
20
|
+
validate :occurred_before, is_a: [Time, ActiveSupport::TimeWithZone], required: false
|
|
21
|
+
validate :principal, is_a: User, required: false
|
|
22
|
+
validate :scope_context, is_a: [CommandTower::AdminScope::ScopeContext, NilClass], required: false
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
if user_scope? && affected_user_id.nil?
|
|
26
|
+
return context.fail!(application_error: CommandTower::Errors::InternalError.new)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
relation = CommandTower::Audit::Event.order(occurred_at: :desc, id: :desc)
|
|
30
|
+
relation = apply_viewer_scope(relation)
|
|
31
|
+
relation = apply_admin_scope(relation)
|
|
32
|
+
relation = apply_present_filters(relation)
|
|
33
|
+
|
|
34
|
+
context.events = relation.limit(limit).offset(offset).to_a
|
|
35
|
+
context.pagination = { limit:, offset:, total_count: relation.count }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def user_scope?
|
|
41
|
+
viewer_scope == :user
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_viewer_scope(relation)
|
|
45
|
+
return relation.where(affected_user_id:, user_history: true) if user_scope?
|
|
46
|
+
|
|
47
|
+
relation
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def apply_admin_scope(relation)
|
|
51
|
+
return relation unless admin_scope?
|
|
52
|
+
|
|
53
|
+
CommandTower::AdminScope::ApplyAuditScoping.call(
|
|
54
|
+
relation:,
|
|
55
|
+
scope_context:,
|
|
56
|
+
principal: principal || scope_context_principal
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def admin_scope?
|
|
61
|
+
viewer_scope == :admin && scope_context.present?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scope_context_principal
|
|
65
|
+
principal
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def apply_present_filters(relation)
|
|
69
|
+
action_list = Array(actions).compact_blank
|
|
70
|
+
relation = relation.where(action: action_list) if action_list.any?
|
|
71
|
+
type_list = Array(subject_types).compact_blank
|
|
72
|
+
relation = relation.where(subject_type: type_list) if type_list.any?
|
|
73
|
+
relation = relation.where(occurred_at: occurred_after..) if occurred_after
|
|
74
|
+
relation = relation.where(occurred_at: ..occurred_before) if occurred_before
|
|
75
|
+
return relation if user_scope?
|
|
76
|
+
|
|
77
|
+
relation = relation.where(affected_user_id:) if affected_user_id
|
|
78
|
+
relation = relation.where(actor_user_id:) if actor_user_id
|
|
79
|
+
relation = relation.where(originating_administrator_id:) if originating_administrator_id
|
|
80
|
+
relation = relation.where(attribution_mode:) if attribution_mode.present?
|
|
81
|
+
relation
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class Show < CommandTower::Services::ApplicationService
|
|
86
|
+
VIEWER_SCOPES = %i[user admin].freeze
|
|
87
|
+
|
|
88
|
+
validate :viewer_scope, is_one: VIEWER_SCOPES, required: true
|
|
89
|
+
validate :id, is_a: Integer, required: true
|
|
90
|
+
validate :affected_user_id, is_a: Integer, required: false
|
|
91
|
+
validate :principal, is_a: User, required: false
|
|
92
|
+
validate :scope_context, is_a: [CommandTower::AdminScope::ScopeContext, NilClass], required: false
|
|
93
|
+
|
|
94
|
+
def call
|
|
95
|
+
if viewer_scope == :user && affected_user_id.nil?
|
|
96
|
+
return context.fail!(application_error: CommandTower::Errors::InternalError.new)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
relation = CommandTower::Audit::Event.where(id:)
|
|
100
|
+
if viewer_scope == :user
|
|
101
|
+
relation = relation.where(affected_user_id:, user_history: true)
|
|
102
|
+
elsif scope_context.present?
|
|
103
|
+
relation = CommandTower::AdminScope::ApplyAuditScoping.call(
|
|
104
|
+
relation:,
|
|
105
|
+
scope_context:,
|
|
106
|
+
principal: principal || scope_context_principal
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
event = relation.first
|
|
111
|
+
if event.nil?
|
|
112
|
+
return context.fail!(application_error: CommandTower::Errors::NotFoundError.new)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
context.event = event
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def scope_context_principal
|
|
121
|
+
principal
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class Project < CommandTower::Services::ApplicationService
|
|
126
|
+
validate :event, is_a: CommandTower::Audit::Event, required: true
|
|
127
|
+
validate :viewer, is_one: %i[user admin], required: true
|
|
128
|
+
|
|
129
|
+
def call
|
|
130
|
+
changes = duplicate_hash(event.change_set)
|
|
131
|
+
mask_sensitive_changes!(changes)
|
|
132
|
+
|
|
133
|
+
context.projection = {
|
|
134
|
+
id: event.id,
|
|
135
|
+
event_name: event.action,
|
|
136
|
+
event_label: registry_label_for(event.action),
|
|
137
|
+
occurred_at: event.occurred_at,
|
|
138
|
+
attribution_mode: event.attribution_mode,
|
|
139
|
+
actor_user_id: event.actor_user_id,
|
|
140
|
+
affected_user_id: event.affected_user_id,
|
|
141
|
+
subject_type: event.subject_type,
|
|
142
|
+
subject_id: event.subject_id,
|
|
143
|
+
subject_label: event.subject_label,
|
|
144
|
+
impersonation_active: event.impersonation_active,
|
|
145
|
+
originating_administrator_id: event.originating_administrator_id,
|
|
146
|
+
changes:,
|
|
147
|
+
metadata: duplicate_hash(event.metadata)
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def registry_label_for(action)
|
|
154
|
+
registry = CommandTower.config.registry.audit
|
|
155
|
+
return "" unless registry.registered?(action)
|
|
156
|
+
|
|
157
|
+
registry.fetch(action).label.to_s
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def mask_sensitive_changes!(changes)
|
|
161
|
+
sensitivity_keys.each do |key|
|
|
162
|
+
entry = changes[key] || changes[key.to_sym]
|
|
163
|
+
next unless entry.is_a?(Hash)
|
|
164
|
+
|
|
165
|
+
changes.delete(key)
|
|
166
|
+
changes.delete(key.to_sym)
|
|
167
|
+
changes[key] = {
|
|
168
|
+
"from" => CommandTower::Audit::Masking.value(field: key, raw: hash_value(entry, "from")),
|
|
169
|
+
"to" => CommandTower::Audit::Masking.value(field: key, raw: hash_value(entry, "to"))
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def sensitivity_keys
|
|
175
|
+
snapshot = Array(event.sensitive_fields).map(&:to_s)
|
|
176
|
+
snapshot | current_sensitive_fields
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def current_sensitive_fields
|
|
180
|
+
registry = CommandTower.config.registry.audit
|
|
181
|
+
return [] unless registry.registered?(event.action)
|
|
182
|
+
|
|
183
|
+
Array(registry.fetch(event.action).sensitive_fields).map(&:to_s)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def duplicate_hash(value)
|
|
187
|
+
return {} unless value.is_a?(Hash)
|
|
188
|
+
|
|
189
|
+
value.deep_dup
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def hash_value(entry, key)
|
|
193
|
+
entry[key] || entry[key.to_sym]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Auth
|
|
6
|
+
class AssignDefaultMembershipRole < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :user, is_a: User, required: true
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
role_name = CommandTower.config.authorization.default_membership_role
|
|
11
|
+
if role_name.nil?
|
|
12
|
+
context.user = user
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
unless role_present?(role_name)
|
|
17
|
+
context.fail!(
|
|
18
|
+
application_error: CommandTower::Errors::Auth::DefaultMembershipAssignmentError.new(
|
|
19
|
+
details: { role: role_name }
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
user.roles = (Array(user.roles) + [role_name.to_s]).uniq
|
|
25
|
+
unless user.save
|
|
26
|
+
context.fail!(
|
|
27
|
+
application_error: CommandTower::Errors::Auth::DefaultMembershipAssignmentError.new(
|
|
28
|
+
details: user.errors.to_hash
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context.user = user
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def role_present?(role_name)
|
|
39
|
+
role_name.to_s.strip.present? && CommandTower::Authorization::Role.roles[role_name].present?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -6,6 +6,7 @@ module CommandTower
|
|
|
6
6
|
class AuthenticateSession < CommandTower::Services::ApplicationService
|
|
7
7
|
validate :request_context, is_a: CommandTower::Auth::RequestContext, required: true
|
|
8
8
|
validate :bypass_email_validation, is_one: [true, false], default: false
|
|
9
|
+
validate :overlay_mode, is_a: [Symbol, String, NilClass], required: false
|
|
9
10
|
|
|
10
11
|
def call
|
|
11
12
|
request = request_context.request
|
|
@@ -39,7 +40,23 @@ module CommandTower
|
|
|
39
40
|
bypass_email_validation:
|
|
40
41
|
)
|
|
41
42
|
if auth_result.success?
|
|
42
|
-
|
|
43
|
+
established = CommandTower::Impersonation::EstablishIdentity.call(
|
|
44
|
+
actor: auth_result.user,
|
|
45
|
+
impersonation_session_id: auth_result.impersonation_session_id,
|
|
46
|
+
mode: resolved_overlay_mode
|
|
47
|
+
)
|
|
48
|
+
if established.expired?
|
|
49
|
+
fail_authentication!(
|
|
50
|
+
CommandTower::Errors::Auth::ImpersonationSessionExpiredError.new,
|
|
51
|
+
token_source:,
|
|
52
|
+
impersonation_session_expired: true
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context.user = established.user
|
|
58
|
+
context.actor_user = established.actor
|
|
59
|
+
context.impersonation_session_id = auth_result.impersonation_session_id
|
|
43
60
|
context.token_expires_at = auth_result.expires_at
|
|
44
61
|
context.generated_token = auth_result.generated_token if with_reset
|
|
45
62
|
context.service_metadata = observation_metadata(
|
|
@@ -53,7 +70,8 @@ module CommandTower
|
|
|
53
70
|
if auth_result.status == 412
|
|
54
71
|
fail_authentication!(
|
|
55
72
|
CommandTower::Errors::Auth::EmailVerificationRequiredError.new,
|
|
56
|
-
token_source
|
|
73
|
+
token_source:,
|
|
74
|
+
email_verification_required: true
|
|
57
75
|
)
|
|
58
76
|
return
|
|
59
77
|
end
|
|
@@ -63,24 +81,33 @@ module CommandTower
|
|
|
63
81
|
|
|
64
82
|
private
|
|
65
83
|
|
|
66
|
-
def fail_authentication!(error, token_source:)
|
|
84
|
+
def fail_authentication!(error, token_source:, email_verification_required: false, impersonation_session_expired: false)
|
|
67
85
|
context.service_metadata = observation_metadata(
|
|
68
86
|
token_source:,
|
|
69
87
|
authentication_failed: true,
|
|
70
|
-
cookie_authenticated: token_source == :cookie
|
|
88
|
+
cookie_authenticated: token_source == :cookie,
|
|
89
|
+
email_verification_required:,
|
|
90
|
+
impersonation_session_expired:
|
|
71
91
|
)
|
|
72
92
|
context.fail!(application_error: error)
|
|
73
93
|
end
|
|
74
94
|
|
|
75
|
-
def observation_metadata(token_source:, authentication_failed:, cookie_authenticated: nil)
|
|
95
|
+
def observation_metadata(token_source:, authentication_failed:, cookie_authenticated: nil, email_verification_required: false, impersonation_session_expired: false)
|
|
76
96
|
{
|
|
77
97
|
token_source:,
|
|
78
98
|
authentication_mechanism: :jwt,
|
|
79
99
|
authentication_failed:,
|
|
80
|
-
cookie_authenticated: cookie_authenticated.nil? ? token_source == :cookie : cookie_authenticated
|
|
100
|
+
cookie_authenticated: cookie_authenticated.nil? ? token_source == :cookie : cookie_authenticated,
|
|
101
|
+
email_verification_required:,
|
|
102
|
+
impersonation_session_expired:
|
|
81
103
|
}
|
|
82
104
|
end
|
|
83
105
|
|
|
106
|
+
def resolved_overlay_mode
|
|
107
|
+
mode = overlay_mode.nil? ? :enforce : overlay_mode.to_sym
|
|
108
|
+
%i[enforce capture].include?(mode) ? mode : :enforce
|
|
109
|
+
end
|
|
110
|
+
|
|
84
111
|
def csrf_error_for(message)
|
|
85
112
|
case message
|
|
86
113
|
when "csrf_mismatch"
|
|
@@ -29,9 +29,19 @@ module CommandTower
|
|
|
29
29
|
user.password = password
|
|
30
30
|
user.password_confirmation = password_confirmation
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
ActiveRecord::Base.transaction do
|
|
33
|
+
unless user.save
|
|
34
|
+
log_error("Failed to update the password for user [#{user.id}]")
|
|
35
|
+
reject!(user.errors.to_hash.transform_values { Array(_1).join(", ") })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
audit(
|
|
39
|
+
:password_changed,
|
|
40
|
+
subject: user,
|
|
41
|
+
affected_user: user,
|
|
42
|
+
changes: {},
|
|
43
|
+
metadata: { mechanism: "reset" }
|
|
44
|
+
)
|
|
35
45
|
end
|
|
36
46
|
|
|
37
47
|
log_info("Password reset completed for user [#{user.id}]")
|
|
@@ -10,7 +10,10 @@ module CommandTower
|
|
|
10
10
|
|
|
11
11
|
def call
|
|
12
12
|
user = User.where(username: identifier).or(User.where(email: identifier)).first
|
|
13
|
-
|
|
13
|
+
if user.nil?
|
|
14
|
+
audit_login_failed(affected_user: nil, outcome: "unknown_identifier")
|
|
15
|
+
invalid_credentials!
|
|
16
|
+
end
|
|
14
17
|
|
|
15
18
|
if user.authenticate(password)
|
|
16
19
|
user.successful_login += 1
|
|
@@ -20,6 +23,7 @@ module CommandTower
|
|
|
20
23
|
user.password_consecutive_fail += 1
|
|
21
24
|
user.save
|
|
22
25
|
log_warn("Valid identifier. Incorrect password. Consecutive Password failures: #{user.password_consecutive_fail}")
|
|
26
|
+
audit_login_failed(affected_user: user, outcome: "invalid_password")
|
|
23
27
|
invalid_credentials!
|
|
24
28
|
end
|
|
25
29
|
|
|
@@ -34,6 +38,10 @@ module CommandTower
|
|
|
34
38
|
context.fail!(application_error: CommandTower::Errors::Auth::InvalidCredentialsError.new)
|
|
35
39
|
end
|
|
36
40
|
|
|
41
|
+
def audit_login_failed(affected_user:, outcome:)
|
|
42
|
+
audit(:login_failed, affected_user:, changes: {}, metadata: { outcome: })
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
def token_expires_at_from_ttl
|
|
38
46
|
CommandTower.config.jwt.ttl.from_now.to_time.to_s
|
|
39
47
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Auth
|
|
6
|
+
module PrincipalCapabilities
|
|
7
|
+
# Projects possessed frontend-projectable capability ids for the effective user.
|
|
8
|
+
# Derives from composed RBAC grants ∩ curated principal_capabilities registry.
|
|
9
|
+
class Project < CommandTower::Services::ApplicationService
|
|
10
|
+
validate :user, is_a: User, required: true
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
context.principal_capability_ids = possessed_capability_ids
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def possessed_capability_ids
|
|
19
|
+
definitions = CommandTower.config.registry.principal_capabilities.definitions.values
|
|
20
|
+
ids =
|
|
21
|
+
if allow_everything?
|
|
22
|
+
definitions.map(&:id)
|
|
23
|
+
else
|
|
24
|
+
definitions.select { |definition| granted?(definition.required_entity) }.map(&:id)
|
|
25
|
+
end
|
|
26
|
+
ids.uniq.sort
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def allow_everything?
|
|
30
|
+
Array(user.roles).any? do |role_name|
|
|
31
|
+
role = CommandTower::Authorization::Role.roles[role_name]
|
|
32
|
+
role&.allow_everything
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def granted?(entity_name)
|
|
37
|
+
Array(user.roles).any? do |role_name|
|
|
38
|
+
role = CommandTower::Authorization::Role.roles[role_name]
|
|
39
|
+
next false unless role
|
|
40
|
+
|
|
41
|
+
role.allow_everything || role.entities.any? { |entity| entity.name.to_s == entity_name }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Impersonation
|
|
6
|
+
class Create < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :actor, is_a: User, required: true
|
|
8
|
+
validate :target, is_a: User, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
now = Time.current
|
|
12
|
+
idle = CommandTower.config.impersonation.idle_timeout
|
|
13
|
+
absolute = CommandTower.config.impersonation.absolute_timeout
|
|
14
|
+
|
|
15
|
+
session = CommandTower::Impersonation::Session.create!(
|
|
16
|
+
actor_user_id: actor.id,
|
|
17
|
+
target_user_id: target.id,
|
|
18
|
+
started_at: now,
|
|
19
|
+
last_activity_at: now,
|
|
20
|
+
idle_expires_at: now + idle,
|
|
21
|
+
absolute_expires_at: now + absolute
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
context.session = session
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Impersonation
|
|
6
|
+
class End < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :session_id, is_a: String, required: true
|
|
8
|
+
validate :reason, is_a: String, required: true
|
|
9
|
+
validate :actor_user_id, is_a: Integer, required: false
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
unless CommandTower::Impersonation::Session::END_REASONS.include?(reason)
|
|
13
|
+
return context.fail!(application_error: CommandTower::Errors::InternalError.new)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
now = Time.current
|
|
17
|
+
relation = CommandTower::Impersonation::Session.open.where(id: session_id)
|
|
18
|
+
relation = relation.where(actor_user_id:) if actor_user_id
|
|
19
|
+
updated = relation.update_all(ended_at: now, end_reason: reason, updated_at: now)
|
|
20
|
+
|
|
21
|
+
session = CommandTower::Impersonation::Session.find_by(id: session_id)
|
|
22
|
+
context.session = session
|
|
23
|
+
context.ended = updated.positive?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module Impersonation
|
|
6
|
+
class RecordActivity < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :session_id, is_a: String, required: true
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
now = Time.current
|
|
11
|
+
idle = CommandTower.config.impersonation.idle_timeout
|
|
12
|
+
CommandTower::Impersonation::Session.transaction do
|
|
13
|
+
session = CommandTower::Impersonation::Session.lock.find_by(id: session_id)
|
|
14
|
+
if session.nil? || !session.open? || session.expired?(at: now)
|
|
15
|
+
context.refreshed = false
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
session.update!(
|
|
20
|
+
last_activity_at: now,
|
|
21
|
+
idle_expires_at: now + idle
|
|
22
|
+
)
|
|
23
|
+
context.refreshed = true
|
|
24
|
+
context.session = session
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|