command_tower 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -3
- data/app/auth/command_tower/auth/auth_context.rb +10 -4
- data/app/controllers/command_tower/admin/application_controller.rb +27 -0
- data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
- data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
- data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
- data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
- data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
- data/app/controllers/command_tower/admin/users_controller.rb +46 -0
- data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
- data/app/controllers/command_tower/application_controller.rb +17 -1
- data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
- data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
- data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
- data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
- data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
- data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
- data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
- data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
- data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
- data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
- data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
- data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
- data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
- data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
- data/app/jobs/command_tower/application_job.rb +7 -0
- data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
- data/app/mailers/command_tower/application_mailer.rb +11 -1
- data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
- data/app/models/command_tower/audit/event.rb +36 -0
- data/app/models/command_tower/impersonation/session.rb +51 -0
- data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
- data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
- data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
- data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
- data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
- data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
- data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
- data/app/services/command_tower/README.md +4 -10
- data/app/services/command_tower/authorize/validate.rb +7 -4
- data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
- data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
- data/app/services/command_tower/jwt/login_create.rb +7 -4
- data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
- data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
- data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
- data/app/services/command_tower/service_base.rb +30 -43
- data/app/services/command_tower/service_logging.rb +2 -36
- data/app/services/command_tower/services/account/clear_phone.rb +8 -0
- data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
- data/app/services/command_tower/services/account/update_phone.rb +9 -1
- data/app/services/command_tower/services/admin/users.rb +377 -0
- data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
- data/app/services/command_tower/services/application_service.rb +37 -0
- data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
- data/app/services/command_tower/services/audit/events.rb +199 -0
- data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
- data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
- data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
- data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
- data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
- data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
- data/app/services/command_tower/services/impersonation/create.rb +29 -0
- data/app/services/command_tower/services/impersonation/end.rb +28 -0
- data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
- data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
- data/app/services/command_tower/services/me/change_password.rb +12 -0
- data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
- data/app/workflows/command_tower/transactional.rb +65 -0
- data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
- data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
- data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
- data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
- data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
- data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
- data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
- data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
- data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
- data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
- data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
- data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
- data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
- data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
- data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
- data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
- data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
- data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
- data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
- data/config/routes.rb +20 -0
- data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
- data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
- data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
- data/docs/admin_workspace.md +158 -0
- data/docs/api_reference.md +175 -14
- data/docs/architecture.md +5 -0
- data/docs/audit.md +195 -0
- data/docs/authentication.md +14 -0
- data/docs/authentication_authorization_guide.md +15 -11
- data/docs/authorization.md +25 -4
- data/docs/controllers.md +7 -1
- data/docs/eventing.md +179 -0
- data/docs/extending.md +32 -1
- data/docs/host_integration_guide.md +103 -12
- data/docs/initializing.md +2 -2
- data/docs/messaging_integration_guide.md +1 -1
- data/docs/models.md +4 -0
- data/docs/pagination.md +2 -2
- data/docs/principal_capabilities.md +89 -0
- data/docs/upgrades/0.10.0.md +2 -2
- data/docs/upgrades/0.11.0.md +92 -0
- data/docs/upgrades/README.md +1 -0
- data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
- data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
- data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
- data/lib/command_tower/admin_scope/resolve.rb +31 -0
- data/lib/command_tower/admin_scope/scope_context.rb +7 -0
- data/lib/command_tower/admin_scope/scope_option.rb +7 -0
- data/lib/command_tower/admin_scope.rb +20 -0
- data/lib/command_tower/admin_workspace.rb +16 -0
- data/lib/command_tower/audit/attribution.rb +90 -0
- data/lib/command_tower/audit/emit.rb +113 -0
- data/lib/command_tower/audit/masking.rb +41 -0
- data/lib/command_tower/audit/payload.rb +128 -0
- data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
- data/lib/command_tower/audit.rb +27 -0
- data/lib/command_tower/authorization/assignable_roles.rb +36 -0
- data/lib/command_tower/authorization/default.yml +99 -5
- data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
- data/lib/command_tower/authorization/entity.rb +15 -5
- data/lib/command_tower/authorization/role.rb +5 -4
- data/lib/command_tower/authorization.rb +71 -10
- data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
- data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
- data/lib/command_tower/configuration/authorization/config.rb +6 -0
- data/lib/command_tower/configuration/config.rb +20 -0
- data/lib/command_tower/configuration/impersonation/config.rb +38 -0
- data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
- data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
- data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
- data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
- data/lib/command_tower/configuration/registry/config.rb +34 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
- data/lib/command_tower/current.rb +12 -0
- data/lib/command_tower/engine.rb +19 -4
- data/lib/command_tower/events.rb +184 -0
- data/lib/command_tower/execution.rb +111 -0
- data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
- data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
- data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
- data/lib/command_tower/impersonation/establish_identity.rb +44 -0
- data/lib/command_tower/install/baseline.rb +3 -0
- data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
- data/lib/command_tower/logging/projection.rb +67 -0
- data/lib/command_tower/logging/subscriber.rb +103 -0
- data/lib/command_tower/principal_capabilities.rb +15 -0
- data/lib/command_tower/version.rb +1 -1
- data/lib/command_tower.rb +12 -0
- data/spec/factories/impersonation_session.rb +17 -0
- data/spec/factories/user.rb +16 -0
- metadata +112 -2
- data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Execution
|
|
5
|
+
module JobBoundary
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
around_perform :command_tower_job_execution
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def command_tower_job_execution
|
|
15
|
+
CommandTower.with_execution(source: :job) { yield }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module CommandTower
|
|
4
4
|
class ApplicationMailer < ActionMailer::Base
|
|
5
|
-
|
|
5
|
+
FALLBACK_FROM = "from@example.com"
|
|
6
|
+
|
|
7
|
+
default from: -> { smtp_from_address }
|
|
6
8
|
layout "mailer"
|
|
9
|
+
|
|
10
|
+
def self.smtp_from_address
|
|
11
|
+
CredentialResolution.resolve(:smtp).user_name.presence || FALLBACK_FROM
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def smtp_from_address
|
|
15
|
+
self.class.smtp_from_address
|
|
16
|
+
end
|
|
7
17
|
end
|
|
8
18
|
end
|
|
@@ -8,8 +8,7 @@ module CommandTower
|
|
|
8
8
|
raise ArgumentError, "rendered must be a RenderedPayload"
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
from_address =
|
|
12
|
-
from_address ||= "from@example.com"
|
|
11
|
+
from_address = smtp_from_address
|
|
13
12
|
|
|
14
13
|
mail(
|
|
15
14
|
to: rendered.recipient_address,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Audit
|
|
5
|
+
class Event < CommandTower::ApplicationRecord
|
|
6
|
+
self.table_name = "command_tower_audit_events"
|
|
7
|
+
|
|
8
|
+
ATTRIBUTION_MODES = %w[self_service admin_direct impersonation system].freeze
|
|
9
|
+
SCOPE_CLASSES = {
|
|
10
|
+
global: "global",
|
|
11
|
+
host: "host",
|
|
12
|
+
legacy: "legacy"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
validates :event_uuid, :action, :occurred_at, :attribution_mode, :scope_class, presence: true
|
|
16
|
+
validates :attribution_mode, inclusion: { in: ATTRIBUTION_MODES }
|
|
17
|
+
validates :scope_class, inclusion: { in: SCOPE_CLASSES.values }
|
|
18
|
+
validates :originating_administrator_id, presence: true, if: :impersonation_active?
|
|
19
|
+
|
|
20
|
+
after_initialize do
|
|
21
|
+
write_attribute(:change_set, {}) if has_attribute?(:change_set) && read_attribute(:change_set).nil?
|
|
22
|
+
write_attribute(:metadata, {}) if has_attribute?(:metadata) && read_attribute(:metadata).nil?
|
|
23
|
+
write_attribute(:sensitive_fields, []) if has_attribute?(:sensitive_fields) && read_attribute(:sensitive_fields).nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
before_update :reject_mutation
|
|
27
|
+
before_destroy :reject_mutation
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def reject_mutation
|
|
32
|
+
raise ImmutableError, "audit events are append-only"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Impersonation
|
|
5
|
+
class Session < CommandTower::ApplicationRecord
|
|
6
|
+
self.table_name = "command_tower_impersonation_sessions"
|
|
7
|
+
|
|
8
|
+
END_REASONS = %w[manual idle_timeout absolute_timeout logout revoked].freeze
|
|
9
|
+
|
|
10
|
+
belongs_to :actor, class_name: "User", foreign_key: :actor_user_id
|
|
11
|
+
belongs_to :target, class_name: "User", foreign_key: :target_user_id
|
|
12
|
+
|
|
13
|
+
before_create :assign_public_id
|
|
14
|
+
|
|
15
|
+
validates :actor_user_id, :target_user_id, :started_at, :last_activity_at,
|
|
16
|
+
:absolute_expires_at, :idle_expires_at, presence: true
|
|
17
|
+
validates :end_reason, inclusion: { in: END_REASONS }, allow_nil: true
|
|
18
|
+
|
|
19
|
+
scope :open, -> { where(ended_at: nil) }
|
|
20
|
+
|
|
21
|
+
def open?
|
|
22
|
+
ended_at.nil?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def idle_expired?(at: Time.current)
|
|
26
|
+
idle_expires_at <= at
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def absolutely_expired?(at: Time.current)
|
|
30
|
+
absolute_expires_at <= at
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def expired?(at: Time.current)
|
|
34
|
+
idle_expired?(at:) || absolutely_expired?(at:)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def expiration_reason(at: Time.current)
|
|
38
|
+
return "absolute_timeout" if absolutely_expired?(at:)
|
|
39
|
+
return "idle_timeout" if idle_expired?(at:)
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def assign_public_id
|
|
47
|
+
self.id = SecureRandom.uuid if id.blank?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Serializers
|
|
5
|
+
module Admin
|
|
6
|
+
module Users
|
|
7
|
+
class UserSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
8
|
+
def self.serialize(user)
|
|
9
|
+
first_name = user.first_name.to_s
|
|
10
|
+
last_name = user.last_name.to_s
|
|
11
|
+
full_name = [first_name, last_name].map(&:strip).reject(&:empty?).join(" ")
|
|
12
|
+
full_name = user.full_name.strip if full_name.empty? && user.respond_to?(:full_name)
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
id: user.id,
|
|
16
|
+
firstName: first_name,
|
|
17
|
+
lastName: last_name,
|
|
18
|
+
fullName: full_name,
|
|
19
|
+
username: user.username,
|
|
20
|
+
email: user.email,
|
|
21
|
+
emailValidated: user.email_validated,
|
|
22
|
+
phoneNumber: phone_number_for(user),
|
|
23
|
+
phoneNumberValidated: phone_number_validated_for(user),
|
|
24
|
+
roles: user.roles,
|
|
25
|
+
createdAt: user.created_at&.iso8601
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.phone_number_for(user)
|
|
30
|
+
return nil unless user.respond_to?(:phone_number)
|
|
31
|
+
|
|
32
|
+
user.phone_number.presence
|
|
33
|
+
end
|
|
34
|
+
private_class_method :phone_number_for
|
|
35
|
+
|
|
36
|
+
def self.phone_number_validated_for(user)
|
|
37
|
+
return false unless user.respond_to?(:phone_number_validated)
|
|
38
|
+
|
|
39
|
+
!!user.phone_number_validated
|
|
40
|
+
end
|
|
41
|
+
private_class_method :phone_number_validated_for
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class PaginationMetaSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
45
|
+
def self.serialize(pagination)
|
|
46
|
+
{
|
|
47
|
+
limit: pagination.fetch(:limit),
|
|
48
|
+
offset: pagination.fetch(:offset),
|
|
49
|
+
totalCount: pagination.fetch(:total_count)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class AssignableRolesSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
55
|
+
def self.serialize(roles)
|
|
56
|
+
{
|
|
57
|
+
roles: Array(roles).map do |role|
|
|
58
|
+
{
|
|
59
|
+
name: role.fetch(:name),
|
|
60
|
+
description: role.fetch(:description)
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Serializers
|
|
5
|
+
module Admin
|
|
6
|
+
module Workspace
|
|
7
|
+
class ManifestSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
8
|
+
def self.serialize(tools)
|
|
9
|
+
{
|
|
10
|
+
tools: Array(tools).map do |tool|
|
|
11
|
+
payload = {
|
|
12
|
+
id: tool.fetch(:id),
|
|
13
|
+
label: tool.fetch(:label),
|
|
14
|
+
description: tool.fetch(:description),
|
|
15
|
+
route: tool.fetch(:route),
|
|
16
|
+
group: tool.fetch(:group),
|
|
17
|
+
sortOrder: tool.fetch(:sort_order),
|
|
18
|
+
icon: tool[:icon]
|
|
19
|
+
}
|
|
20
|
+
payload[:scope] = serialize_scope(tool[:scope]) if tool.key?(:scope)
|
|
21
|
+
payload[:scopeOptions] = serialize_scope_options(tool[:scope_options]) if tool.key?(:scope_options)
|
|
22
|
+
payload[:availability] = serialize_availability(tool[:availability]) if tool.key?(:availability)
|
|
23
|
+
payload
|
|
24
|
+
end
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.serialize_scope(scope)
|
|
29
|
+
{
|
|
30
|
+
required: scope.fetch(:required),
|
|
31
|
+
parameter: scope.fetch(:parameter),
|
|
32
|
+
label: scope.fetch(:label)
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
private_class_method :serialize_scope
|
|
36
|
+
|
|
37
|
+
def self.serialize_scope_options(options)
|
|
38
|
+
Array(options).map do |option|
|
|
39
|
+
{
|
|
40
|
+
value: option.fetch(:value),
|
|
41
|
+
label: option.fetch(:label)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
private_class_method :serialize_scope_options
|
|
46
|
+
|
|
47
|
+
def self.serialize_availability(availability)
|
|
48
|
+
{
|
|
49
|
+
enabled: availability.fetch(:enabled),
|
|
50
|
+
reason: availability[:reason]
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
private_class_method :serialize_availability
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Serializers
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class FilterOptionsSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
8
|
+
def self.serialize(event_names:, subject_types:, attribution_modes:)
|
|
9
|
+
{
|
|
10
|
+
eventNames: Array(event_names).map { |entry| serialize_event_option(entry) },
|
|
11
|
+
subjectTypes: Array(subject_types).map { |entry| serialize_subject_option(entry) },
|
|
12
|
+
attributionModes: Array(attribution_modes).map { |entry| serialize_mode_option(entry) }
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.serialize_event_option(entry)
|
|
17
|
+
{
|
|
18
|
+
value: entry.fetch(:value),
|
|
19
|
+
label: entry.fetch(:label),
|
|
20
|
+
tags: Array(entry.fetch(:tags))
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
private_class_method :serialize_event_option
|
|
24
|
+
|
|
25
|
+
def self.serialize_subject_option(entry)
|
|
26
|
+
{
|
|
27
|
+
value: entry.fetch(:value),
|
|
28
|
+
label: entry.fetch(:label)
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
private_class_method :serialize_subject_option
|
|
32
|
+
|
|
33
|
+
def self.serialize_mode_option(entry)
|
|
34
|
+
{
|
|
35
|
+
value: entry.fetch(:value),
|
|
36
|
+
label: entry.fetch(:label)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
private_class_method :serialize_mode_option
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Serializers
|
|
5
|
+
module Audit
|
|
6
|
+
module Events
|
|
7
|
+
class EventSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
8
|
+
def self.serialize(projection)
|
|
9
|
+
{
|
|
10
|
+
id: projection.fetch(:id),
|
|
11
|
+
eventName: projection.fetch(:event_name),
|
|
12
|
+
eventLabel: projection.fetch(:event_label),
|
|
13
|
+
occurredAt: iso8601(projection.fetch(:occurred_at)),
|
|
14
|
+
attributionMode: projection.fetch(:attribution_mode),
|
|
15
|
+
actor: { userId: projection[:actor_user_id] },
|
|
16
|
+
affectedUser: { userId: projection[:affected_user_id] },
|
|
17
|
+
subject: {
|
|
18
|
+
type: projection[:subject_type],
|
|
19
|
+
id: projection[:subject_id],
|
|
20
|
+
label: projection[:subject_label]
|
|
21
|
+
},
|
|
22
|
+
impersonationActive: projection.fetch(:impersonation_active),
|
|
23
|
+
originatingAdministratorId: projection[:originating_administrator_id],
|
|
24
|
+
changes: projection.fetch(:changes),
|
|
25
|
+
metadata: projection.fetch(:metadata)
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class PaginationMetaSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
31
|
+
def self.serialize(pagination)
|
|
32
|
+
{
|
|
33
|
+
limit: pagination.fetch(:limit),
|
|
34
|
+
offset: pagination.fetch(:offset),
|
|
35
|
+
totalCount: pagination.fetch(:total_count)
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Serializers
|
|
5
|
+
module Auth
|
|
6
|
+
class PrincipalCapabilitiesSerializer
|
|
7
|
+
def self.serialize(principal_capability_ids)
|
|
8
|
+
{
|
|
9
|
+
principalCapabilities: Array(principal_capability_ids).map(&:to_s)
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -4,12 +4,36 @@ module CommandTower
|
|
|
4
4
|
module Serializers
|
|
5
5
|
module Auth
|
|
6
6
|
class SessionResponseSerializer
|
|
7
|
-
def self.serialize(user:, token_expires_at:)
|
|
8
|
-
{
|
|
7
|
+
def self.serialize(user:, token_expires_at:, impersonation_session: nil, actor: nil)
|
|
8
|
+
payload = {
|
|
9
9
|
user: CommandTower::Serializers::Auth::UserSerializer.serialize(user),
|
|
10
10
|
tokenExpiresAt: token_expires_at
|
|
11
11
|
}
|
|
12
|
+
return payload if impersonation_session.nil?
|
|
13
|
+
|
|
14
|
+
actor_user = actor || impersonation_session.actor
|
|
15
|
+
payload[:impersonation] = {
|
|
16
|
+
active: true,
|
|
17
|
+
sessionId: impersonation_session.id,
|
|
18
|
+
actorUserId: impersonation_session.actor_user_id,
|
|
19
|
+
actorDisplayName: display_name(actor_user),
|
|
20
|
+
targetUserId: impersonation_session.target_user_id,
|
|
21
|
+
idleExpiresAt: CommandTower::Serializers::ApplicationSerializer.iso8601(
|
|
22
|
+
impersonation_session.idle_expires_at
|
|
23
|
+
),
|
|
24
|
+
absoluteExpiresAt: CommandTower::Serializers::ApplicationSerializer.iso8601(
|
|
25
|
+
impersonation_session.absolute_expires_at
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
payload
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.display_name(user)
|
|
32
|
+
return nil if user.nil?
|
|
33
|
+
|
|
34
|
+
[user.first_name, user.last_name].compact_blank.join(" ").presence || user.username
|
|
12
35
|
end
|
|
36
|
+
private_class_method :display_name
|
|
13
37
|
end
|
|
14
38
|
end
|
|
15
39
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Serializers
|
|
5
|
+
module Impersonation
|
|
6
|
+
class SessionSerializer < CommandTower::Serializers::ApplicationSerializer
|
|
7
|
+
def self.serialize(session)
|
|
8
|
+
{
|
|
9
|
+
id: session.id,
|
|
10
|
+
actorUserId: session.actor_user_id,
|
|
11
|
+
targetUserId: session.target_user_id,
|
|
12
|
+
idleExpiresAt: iso8601(session.idle_expires_at),
|
|
13
|
+
absoluteExpiresAt: iso8601(session.absolute_expires_at)
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -17,16 +17,10 @@ All services in CommandTower that use this pattern inherit `CommandTower::Servic
|
|
|
17
17
|
## What does ServiceBase offer
|
|
18
18
|
|
|
19
19
|
### Logging
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Additionally, it provides some convenience methods for logging
|
|
26
|
-
- `log_debug`
|
|
27
|
-
- `log_info`
|
|
28
|
-
- `log_warn`
|
|
29
|
-
- `log_error`
|
|
20
|
+
|
|
21
|
+
Lifecycle logging is an `ActiveSupport::Notifications` consumer. Services do not write Start/Finished lines to `Rails.logger`.
|
|
22
|
+
|
|
23
|
+
Operational notes from services still use `log_info` / `log_warn` / `log_error`, which publish `command_tower.log.*` events. See [Eventing](../../../docs/eventing.md).
|
|
30
24
|
|
|
31
25
|
### Argument Validation
|
|
32
26
|
Argument Validation is the powerhouse behind ServiceBase
|
|
@@ -46,21 +46,24 @@ module CommandTower::Authorize
|
|
|
46
46
|
|
|
47
47
|
def call
|
|
48
48
|
unless authorization_required?
|
|
49
|
-
|
|
49
|
+
log_debug("controller:#{controller}; method:#{action} -- No Authorization required")
|
|
50
50
|
return Decision.authorized(user:, authorization_required: false, msg: NOT_REQUIRED_MSG)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# At this point we know authorization on the route is required
|
|
54
54
|
# Iterate through the users roles to find a matching role that allows authorization
|
|
55
55
|
# If at least 1 of the users roles passes validation, we can allow access to the path
|
|
56
|
-
|
|
56
|
+
log_debug("User Roles: #{user.roles}")
|
|
57
57
|
authorized = user_role_objects.any? do |_role_name, role_object|
|
|
58
58
|
result = role_object.authorized?(controller:, method: action, user:)
|
|
59
|
-
|
|
59
|
+
log_debug("Role:#{result[:role]};Authorized:[#{result[:authorized]}];Reason:[#{result[:reason]}]")
|
|
60
60
|
result[:authorized] == true
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
unless authorized
|
|
64
|
+
log_warn("controller:#{controller}; method:#{action} -- #{UNAUTHORIZED_MSG}")
|
|
65
|
+
return Decision.denied(user:, msg: UNAUTHORIZED_MSG)
|
|
66
|
+
end
|
|
64
67
|
|
|
65
68
|
Decision.authorized(user:, authorization_required: true, msg: AUTHORIZED_MSG)
|
|
66
69
|
end
|
|
@@ -45,12 +45,18 @@ module CommandTower::Jwt
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
generated_token = nil
|
|
48
|
+
impersonation_session_id = payload[:impersonation_session_id].presence
|
|
48
49
|
if with_reset
|
|
49
|
-
generated_token = LoginCreate.(user:).token
|
|
50
|
+
generated_token = LoginCreate.(user:, impersonation_session_id:).token
|
|
50
51
|
expires_at = CommandTower.config.jwt.ttl.from_now.to_time
|
|
51
52
|
end
|
|
52
53
|
|
|
53
|
-
AuthenticationOutcome.success(
|
|
54
|
+
AuthenticationOutcome.success(
|
|
55
|
+
user:,
|
|
56
|
+
expires_at: expires_at.to_s,
|
|
57
|
+
generated_token:,
|
|
58
|
+
impersonation_session_id:
|
|
59
|
+
)
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
private
|
|
@@ -4,10 +4,10 @@ module CommandTower::Jwt
|
|
|
4
4
|
# Narrow value object describing the result of AuthenticateUser only.
|
|
5
5
|
# This is deliberately not a platform result type — do not reuse it elsewhere.
|
|
6
6
|
class AuthenticationOutcome
|
|
7
|
-
attr_reader :user, :expires_at, :generated_token, :status, :msg
|
|
7
|
+
attr_reader :user, :expires_at, :generated_token, :status, :msg, :impersonation_session_id
|
|
8
8
|
|
|
9
|
-
def self.success(user:, expires_at:, generated_token: nil)
|
|
10
|
-
new(success: true, user:, expires_at:, generated_token:)
|
|
9
|
+
def self.success(user:, expires_at:, generated_token: nil, impersonation_session_id: nil)
|
|
10
|
+
new(success: true, user:, expires_at:, generated_token:, impersonation_session_id:)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# `user` is carried on failures so callers can distinguish a known user who
|
|
@@ -16,13 +16,14 @@ module CommandTower::Jwt
|
|
|
16
16
|
new(success: false, msg:, status:, user:)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def initialize(success:, user: nil, expires_at: nil, generated_token: nil, status: nil, msg: nil)
|
|
19
|
+
def initialize(success:, user: nil, expires_at: nil, generated_token: nil, status: nil, msg: nil, impersonation_session_id: nil)
|
|
20
20
|
@success = success
|
|
21
21
|
@user = user
|
|
22
22
|
@expires_at = expires_at
|
|
23
23
|
@generated_token = generated_token
|
|
24
24
|
@status = status
|
|
25
25
|
@msg = msg
|
|
26
|
+
@impersonation_session_id = impersonation_session_id
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def success?
|
|
@@ -4,16 +4,19 @@ module CommandTower::Jwt
|
|
|
4
4
|
class LoginCreate
|
|
5
5
|
IssuedToken = Data.define(:token)
|
|
6
6
|
|
|
7
|
-
def self.call(user:)
|
|
8
|
-
IssuedToken.new(token: Encode.(payload: payload_for(user)))
|
|
7
|
+
def self.call(user:, impersonation_session_id: nil)
|
|
8
|
+
IssuedToken.new(token: Encode.(payload: payload_for(user, impersonation_session_id:)))
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def self.payload_for(user)
|
|
12
|
-
{
|
|
11
|
+
def self.payload_for(user, impersonation_session_id: nil)
|
|
12
|
+
payload = {
|
|
13
13
|
generated_at: Time.now.to_i,
|
|
14
14
|
user_id: user.id,
|
|
15
15
|
verifier_token: user.retreive_verifier_token!,
|
|
16
16
|
}
|
|
17
|
+
token = impersonation_session_id.to_s.strip
|
|
18
|
+
payload[:impersonation_session_id] = token unless token.empty?
|
|
19
|
+
payload
|
|
17
20
|
end
|
|
18
21
|
private_class_method :payload_for
|
|
19
22
|
end
|
|
@@ -18,7 +18,7 @@ module CommandTower
|
|
|
18
18
|
),
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
Contract::Observability::
|
|
21
|
+
Contract::Observability::Publisher.info(
|
|
22
22
|
base.merge(event: "messaging.accept.started"),
|
|
23
23
|
)
|
|
24
24
|
|
|
@@ -31,7 +31,7 @@ module CommandTower
|
|
|
31
31
|
"messaging.accept.succeeded"
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
Contract::Observability::
|
|
34
|
+
Contract::Observability::Publisher.info(
|
|
35
35
|
base.merge(
|
|
36
36
|
event:,
|
|
37
37
|
duration_ms: elapsed_ms(started_at),
|
|
@@ -62,7 +62,7 @@ module CommandTower
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
64
|
def log_failure(base, started_at, error, level, error_code, event)
|
|
65
|
-
Contract::Observability::
|
|
65
|
+
Contract::Observability::Publisher.public_send(
|
|
66
66
|
level,
|
|
67
67
|
base.merge(
|
|
68
68
|
event:,
|
|
@@ -8,7 +8,9 @@ module CommandTower
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def resolve
|
|
11
|
-
CommandTower::Current.
|
|
11
|
+
CommandTower::Current.correlation_id.presence ||
|
|
12
|
+
CommandTower::Current.request_id.presence ||
|
|
13
|
+
SecureRandom.uuid
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -14,13 +14,13 @@ module CommandTower
|
|
|
14
14
|
messaging_operation: operation,
|
|
15
15
|
}.merge(request_fields(request))
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Publisher.info(
|
|
18
18
|
base.merge(event: "messaging.#{operation}.started"),
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
result = yield
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Publisher.info(
|
|
24
24
|
base.merge(
|
|
25
25
|
event: "messaging.#{operation}.succeeded",
|
|
26
26
|
duration_ms: elapsed_ms(started_at),
|
|
@@ -45,7 +45,7 @@ module CommandTower
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
47
|
def log_failure(base, started_at, operation, error, level, error_code)
|
|
48
|
-
|
|
48
|
+
Publisher.public_send(
|
|
49
49
|
level,
|
|
50
50
|
base.merge(
|
|
51
51
|
event: "messaging.#{operation}.failed",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Messaging
|
|
5
|
+
module Contract
|
|
6
|
+
module Observability
|
|
7
|
+
class Publisher
|
|
8
|
+
FORBIDDEN_KEYS = %i[title body text metadata email password token].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def info(payload = nil, **kwargs)
|
|
12
|
+
emit(:info, payload || kwargs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def warn(payload = nil, **kwargs)
|
|
16
|
+
emit(:warn, payload || kwargs)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def error(payload = nil, **kwargs)
|
|
20
|
+
emit(:error, payload || kwargs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def emit(level, payload)
|
|
24
|
+
hash = payload.respond_to?(:to_h) ? payload.to_h : {}
|
|
25
|
+
event = hash[:event] || hash["event"]
|
|
26
|
+
raise ArgumentError, "messaging event name required" if event.blank?
|
|
27
|
+
|
|
28
|
+
name = event.to_s.sub(/\Amessaging\./, "")
|
|
29
|
+
extra = {}
|
|
30
|
+
hash.each do |key, value|
|
|
31
|
+
next if key.to_s == "event"
|
|
32
|
+
|
|
33
|
+
extra[key.to_sym] = value
|
|
34
|
+
end
|
|
35
|
+
FORBIDDEN_KEYS.each { |key| extra.delete(key) }
|
|
36
|
+
extra[:log_level] = level
|
|
37
|
+
CommandTower::Events.publish(category: :messaging, name:, payload: extra)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|