command_tower 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -3
- data/app/auth/command_tower/auth/auth_context.rb +10 -4
- data/app/controllers/command_tower/admin/application_controller.rb +27 -0
- data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
- data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
- data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
- data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
- data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
- data/app/controllers/command_tower/admin/users_controller.rb +46 -0
- data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
- data/app/controllers/command_tower/application_controller.rb +17 -1
- data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
- data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
- data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
- data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
- data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
- data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
- data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
- data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
- data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
- data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
- data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
- data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
- data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
- data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
- data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
- data/app/jobs/command_tower/application_job.rb +7 -0
- data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
- data/app/mailers/command_tower/application_mailer.rb +11 -1
- data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
- data/app/models/command_tower/audit/event.rb +38 -0
- data/app/models/command_tower/impersonation/session.rb +51 -0
- data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
- data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
- data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
- data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
- data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
- data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
- data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
- data/app/services/command_tower/README.md +4 -10
- data/app/services/command_tower/authorize/validate.rb +7 -4
- data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
- data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
- data/app/services/command_tower/jwt/login_create.rb +7 -4
- data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
- data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
- data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
- data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
- data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
- data/app/services/command_tower/service_base.rb +30 -43
- data/app/services/command_tower/service_logging.rb +2 -36
- data/app/services/command_tower/services/account/clear_phone.rb +8 -0
- data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
- data/app/services/command_tower/services/account/update_phone.rb +9 -1
- data/app/services/command_tower/services/admin/users.rb +377 -0
- data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
- data/app/services/command_tower/services/application_service.rb +37 -0
- data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
- data/app/services/command_tower/services/audit/events.rb +199 -0
- data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
- data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
- data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
- data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
- data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
- data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
- data/app/services/command_tower/services/impersonation/create.rb +29 -0
- data/app/services/command_tower/services/impersonation/end.rb +28 -0
- data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
- data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
- data/app/services/command_tower/services/me/change_password.rb +12 -0
- data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
- data/app/workflows/command_tower/transactional.rb +65 -0
- data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
- data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
- data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
- data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
- data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
- data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
- data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
- data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
- data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
- data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
- data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
- data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
- data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
- data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
- data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
- data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
- data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
- data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
- data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
- data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
- data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
- data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
- data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
- data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
- data/config/routes.rb +20 -0
- data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
- data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
- data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
- data/docs/admin_workspace.md +158 -0
- data/docs/api_reference.md +175 -14
- data/docs/architecture.md +5 -0
- data/docs/audit.md +195 -0
- data/docs/authentication.md +14 -0
- data/docs/authentication_authorization_guide.md +15 -11
- data/docs/authorization.md +25 -4
- data/docs/controllers.md +7 -1
- data/docs/eventing.md +179 -0
- data/docs/extending.md +32 -1
- data/docs/host_integration_guide.md +103 -12
- data/docs/initializing.md +2 -2
- data/docs/messaging_integration_guide.md +1 -1
- data/docs/models.md +4 -0
- data/docs/pagination.md +2 -2
- data/docs/principal_capabilities.md +89 -0
- data/docs/upgrades/0.10.0.md +2 -2
- data/docs/upgrades/0.11.0.md +92 -0
- data/docs/upgrades/0.11.1.md +25 -0
- data/docs/upgrades/README.md +2 -0
- data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
- data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
- data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
- data/lib/command_tower/admin_scope/resolve.rb +31 -0
- data/lib/command_tower/admin_scope/scope_context.rb +7 -0
- data/lib/command_tower/admin_scope/scope_option.rb +7 -0
- data/lib/command_tower/admin_scope.rb +20 -0
- data/lib/command_tower/admin_workspace.rb +16 -0
- data/lib/command_tower/audit/attribution.rb +90 -0
- data/lib/command_tower/audit/emit.rb +113 -0
- data/lib/command_tower/audit/masking.rb +41 -0
- data/lib/command_tower/audit/payload.rb +128 -0
- data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
- data/lib/command_tower/audit.rb +27 -0
- data/lib/command_tower/authorization/assignable_roles.rb +36 -0
- data/lib/command_tower/authorization/default.yml +99 -5
- data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
- data/lib/command_tower/authorization/entity.rb +15 -5
- data/lib/command_tower/authorization/role.rb +5 -4
- data/lib/command_tower/authorization.rb +71 -10
- data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
- data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
- data/lib/command_tower/configuration/authorization/config.rb +6 -0
- data/lib/command_tower/configuration/config.rb +20 -0
- data/lib/command_tower/configuration/impersonation/config.rb +38 -0
- data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
- data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
- data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
- data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
- data/lib/command_tower/configuration/registry/config.rb +34 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
- data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
- data/lib/command_tower/current.rb +12 -0
- data/lib/command_tower/engine.rb +19 -4
- data/lib/command_tower/events.rb +184 -0
- data/lib/command_tower/execution.rb +111 -0
- data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
- data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
- data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
- data/lib/command_tower/impersonation/establish_identity.rb +44 -0
- data/lib/command_tower/install/baseline.rb +3 -0
- data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
- data/lib/command_tower/logging/projection.rb +67 -0
- data/lib/command_tower/logging/subscriber.rb +103 -0
- data/lib/command_tower/principal_capabilities.rb +15 -0
- data/lib/command_tower/version.rb +1 -1
- data/lib/command_tower.rb +12 -0
- data/spec/factories/impersonation_session.rb +17 -0
- data/spec/factories/user.rb +16 -0
- metadata +114 -3
- data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
|
@@ -0,0 +1,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,38 @@
|
|
|
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
|
+
# Force JSON attribute types so writes are JSON.generate'd on every adapter.
|
|
21
|
+
# MySQL 8 already maps t.json → Type::Json (serialize is rejected). MariaDB
|
|
22
|
+
# often stores t.json as LONGTEXT + json_valid CHECK and would otherwise
|
|
23
|
+
# persist Hash#inspect — failing the CHECK (HTTP 500 on login_failed audit).
|
|
24
|
+
attribute :metadata, :json, default: -> { {} }
|
|
25
|
+
attribute :change_set, :json, default: -> { {} }
|
|
26
|
+
attribute :sensitive_fields, :json, default: -> { [] }
|
|
27
|
+
|
|
28
|
+
before_update :reject_mutation
|
|
29
|
+
before_destroy :reject_mutation
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def reject_mutation
|
|
34
|
+
raise ImmutableError, "audit events are append-only"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
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
|