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.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -3
  3. data/app/auth/command_tower/auth/auth_context.rb +10 -4
  4. data/app/controllers/command_tower/admin/application_controller.rb +27 -0
  5. data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
  6. data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
  7. data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
  8. data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
  9. data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
  10. data/app/controllers/command_tower/admin/users_controller.rb +46 -0
  11. data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
  12. data/app/controllers/command_tower/application_controller.rb +17 -1
  13. data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
  14. data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
  15. data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
  16. data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
  17. data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
  18. data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
  19. data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
  20. data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
  21. data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
  22. data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
  23. data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
  24. data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
  25. data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
  26. data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
  27. data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
  28. data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
  29. data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
  30. data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
  31. data/app/jobs/command_tower/application_job.rb +7 -0
  32. data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
  33. data/app/mailers/command_tower/application_mailer.rb +11 -1
  34. data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
  35. data/app/models/command_tower/audit/event.rb +36 -0
  36. data/app/models/command_tower/impersonation/session.rb +51 -0
  37. data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
  38. data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
  39. data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
  40. data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
  41. data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
  42. data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
  43. data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
  44. data/app/services/command_tower/README.md +4 -10
  45. data/app/services/command_tower/authorize/validate.rb +7 -4
  46. data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
  47. data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
  48. data/app/services/command_tower/jwt/login_create.rb +7 -4
  49. data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
  50. data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
  51. data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
  52. data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
  53. data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
  54. data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
  55. data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
  56. data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
  57. data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
  58. data/app/services/command_tower/service_base.rb +30 -43
  59. data/app/services/command_tower/service_logging.rb +2 -36
  60. data/app/services/command_tower/services/account/clear_phone.rb +8 -0
  61. data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
  62. data/app/services/command_tower/services/account/update_phone.rb +9 -1
  63. data/app/services/command_tower/services/admin/users.rb +377 -0
  64. data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
  65. data/app/services/command_tower/services/application_service.rb +37 -0
  66. data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
  67. data/app/services/command_tower/services/audit/events.rb +199 -0
  68. data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
  69. data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
  70. data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
  71. data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
  72. data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
  73. data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
  74. data/app/services/command_tower/services/impersonation/create.rb +29 -0
  75. data/app/services/command_tower/services/impersonation/end.rb +28 -0
  76. data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
  77. data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
  78. data/app/services/command_tower/services/me/change_password.rb +12 -0
  79. data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
  80. data/app/workflows/command_tower/transactional.rb +65 -0
  81. data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
  82. data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
  83. data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
  84. data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
  85. data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
  86. data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
  87. data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
  88. data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
  89. data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
  90. data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
  91. data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
  92. data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
  93. data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
  94. data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
  95. data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
  96. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
  97. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
  98. data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
  99. data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
  100. data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
  101. data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
  102. data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
  103. data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
  104. data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
  105. data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
  106. data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
  107. data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
  108. data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
  109. data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
  110. data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
  111. data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
  112. data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
  113. data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
  114. data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
  115. data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
  116. data/config/routes.rb +20 -0
  117. data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
  118. data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
  119. data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
  120. data/docs/admin_workspace.md +158 -0
  121. data/docs/api_reference.md +175 -14
  122. data/docs/architecture.md +5 -0
  123. data/docs/audit.md +195 -0
  124. data/docs/authentication.md +14 -0
  125. data/docs/authentication_authorization_guide.md +15 -11
  126. data/docs/authorization.md +25 -4
  127. data/docs/controllers.md +7 -1
  128. data/docs/eventing.md +179 -0
  129. data/docs/extending.md +32 -1
  130. data/docs/host_integration_guide.md +103 -12
  131. data/docs/initializing.md +2 -2
  132. data/docs/messaging_integration_guide.md +1 -1
  133. data/docs/models.md +4 -0
  134. data/docs/pagination.md +2 -2
  135. data/docs/principal_capabilities.md +89 -0
  136. data/docs/upgrades/0.10.0.md +2 -2
  137. data/docs/upgrades/0.11.0.md +92 -0
  138. data/docs/upgrades/README.md +1 -0
  139. data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
  140. data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
  141. data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
  142. data/lib/command_tower/admin_scope/resolve.rb +31 -0
  143. data/lib/command_tower/admin_scope/scope_context.rb +7 -0
  144. data/lib/command_tower/admin_scope/scope_option.rb +7 -0
  145. data/lib/command_tower/admin_scope.rb +20 -0
  146. data/lib/command_tower/admin_workspace.rb +16 -0
  147. data/lib/command_tower/audit/attribution.rb +90 -0
  148. data/lib/command_tower/audit/emit.rb +113 -0
  149. data/lib/command_tower/audit/masking.rb +41 -0
  150. data/lib/command_tower/audit/payload.rb +128 -0
  151. data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
  152. data/lib/command_tower/audit.rb +27 -0
  153. data/lib/command_tower/authorization/assignable_roles.rb +36 -0
  154. data/lib/command_tower/authorization/default.yml +99 -5
  155. data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
  156. data/lib/command_tower/authorization/entity.rb +15 -5
  157. data/lib/command_tower/authorization/role.rb +5 -4
  158. data/lib/command_tower/authorization.rb +71 -10
  159. data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
  160. data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
  161. data/lib/command_tower/configuration/authorization/config.rb +6 -0
  162. data/lib/command_tower/configuration/config.rb +20 -0
  163. data/lib/command_tower/configuration/impersonation/config.rb +38 -0
  164. data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
  165. data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
  166. data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
  167. data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
  168. data/lib/command_tower/configuration/registry/config.rb +34 -0
  169. data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
  170. data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
  171. data/lib/command_tower/current.rb +12 -0
  172. data/lib/command_tower/engine.rb +19 -4
  173. data/lib/command_tower/events.rb +184 -0
  174. data/lib/command_tower/execution.rb +111 -0
  175. data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
  176. data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
  177. data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
  178. data/lib/command_tower/impersonation/establish_identity.rb +44 -0
  179. data/lib/command_tower/install/baseline.rb +3 -0
  180. data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
  181. data/lib/command_tower/logging/projection.rb +67 -0
  182. data/lib/command_tower/logging/subscriber.rb +103 -0
  183. data/lib/command_tower/principal_capabilities.rb +15 -0
  184. data/lib/command_tower/version.rb +1 -1
  185. data/lib/command_tower.rb +12 -0
  186. data/spec/factories/impersonation_session.rb +17 -0
  187. data/spec/factories/user.rb +16 -0
  188. metadata +112 -2
  189. data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Services
5
+ module Impersonation
6
+ class TerminateOpenSessions < CommandTower::Services::ApplicationService
7
+ validate :actor_user_id, is_a: Integer, required: true
8
+ validate :reason, is_a: String, required: true
9
+
10
+ def call
11
+ unless CommandTower::Impersonation::Session::END_REASONS.include?(reason)
12
+ return context.fail!(application_error: CommandTower::Errors::InternalError.new)
13
+ end
14
+
15
+ ids = CommandTower::Impersonation::Session.open.where(actor_user_id:).pluck(:id)
16
+ ended_count = 0
17
+ ids.each do |session_id|
18
+ ended = CommandTower::Services::Impersonation::End.call(
19
+ session_id:,
20
+ reason:,
21
+ actor_user_id:
22
+ )
23
+ next unless ended.success? && ended.data[:ended]
24
+
25
+ ended_count += 1
26
+ emit_ended(ended.data[:session], reason:)
27
+ end
28
+ context.ended_count = ended_count
29
+ end
30
+
31
+ private
32
+
33
+ def emit_ended(session, reason:)
34
+ target = User.find_by(id: session.target_user_id)
35
+ return if target.nil?
36
+
37
+ CommandTower::Impersonation::ClearOverlayForAudit.call(actor_user_id:) do
38
+ audit(
39
+ :impersonation_ended,
40
+ subject: target,
41
+ affected_user: target,
42
+ changes: { reason: { from: nil, to: reason } },
43
+ metadata: { impersonation_session_id: session.id },
44
+ attribution_mode: :admin_direct
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -42,11 +42,23 @@ module CommandTower
42
42
 
43
43
  begin
44
44
  user.reset_verifier_token!
45
+ CommandTower::Services::Impersonation::TerminateOpenSessions.call(
46
+ actor_user_id: user.id,
47
+ reason: "revoked"
48
+ )
45
49
  rescue StandardError => e
46
50
  log_error("Failed to rotate session verifier for user [#{user.id}]: #{e.class}")
47
51
  infrastructure_failure = true
48
52
  raise ActiveRecord::Rollback
49
53
  end
54
+
55
+ audit(
56
+ :password_changed,
57
+ subject: user,
58
+ affected_user: user,
59
+ changes: {},
60
+ metadata: { mechanism: "authenticated_change" }
61
+ )
50
62
  end
51
63
 
52
64
  if persistence_errors
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module SharedSequences
5
+ module Admin
6
+ module Users
7
+ class ResolveScopedUser
8
+ Result = Data.define(:user)
9
+
10
+ def self.call(id:, principal:, scope_value: nil)
11
+ new.call(id:, principal:, scope_value:)
12
+ end
13
+
14
+ def call(id:, principal:, scope_value: nil)
15
+ scope_result = CommandTower::Workflows::Admin::ScopeResolution.resolve(
16
+ tool_id: "users",
17
+ user: principal,
18
+ scope_value:
19
+ )
20
+ return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
21
+
22
+ shown = CommandTower::Services::Admin::Users::Show.call(
23
+ id:,
24
+ principal:,
25
+ scope_context: scope_result
26
+ )
27
+ unless shown.success?
28
+ return CommandTower::Workflows::WorkflowResult.failure(
29
+ errors: shown.errors,
30
+ http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
31
+ shown.errors.first
32
+ )
33
+ )
34
+ end
35
+
36
+ Result.new(user: shown.data[:user])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ # Generic ActiveRecord atomic boundary shared by ApplicationWorkflow and ApplicationService.
5
+ # Layer-specific result contracts are supplied by includers.
6
+ module Transactional
7
+ class TransactionFailure < StandardError
8
+ attr_reader :result
9
+
10
+ def initialize(result)
11
+ @result = result
12
+ super("transaction failed")
13
+ end
14
+ end
15
+
16
+ class InvalidTransactionResult < StandardError; end
17
+
18
+ def transaction
19
+ result = nil
20
+ ActiveRecord::Base.transaction do
21
+ result = yield
22
+ validate_transaction_success!(result)
23
+ end
24
+ result
25
+ rescue TransactionFailure => e
26
+ handle_transaction_failure(e.result)
27
+ end
28
+
29
+ def fail_transaction!(result)
30
+ unless acceptable_failure_result?(result)
31
+ raise InvalidTransactionResult, fail_transaction_type_error_message
32
+ end
33
+
34
+ raise TransactionFailure.new(result)
35
+ end
36
+
37
+ private
38
+
39
+ def validate_transaction_success!(result)
40
+ return if acceptable_success_result?(result)
41
+
42
+ raise InvalidTransactionResult, invalid_success_result_message(result)
43
+ end
44
+
45
+ def acceptable_success_result?(_result)
46
+ raise NotImplementedError, "#{self.class} must implement #acceptable_success_result?"
47
+ end
48
+
49
+ def acceptable_failure_result?(_result)
50
+ raise NotImplementedError, "#{self.class} must implement #acceptable_failure_result?"
51
+ end
52
+
53
+ def fail_transaction_type_error_message
54
+ "fail_transaction! requires a failed result"
55
+ end
56
+
57
+ def invalid_success_result_message(result)
58
+ "transaction block returned a non-success result; got #{result.class}"
59
+ end
60
+
61
+ def handle_transaction_failure(result)
62
+ result
63
+ end
64
+ end
65
+ end
@@ -30,6 +30,17 @@ module CommandTower
30
30
  )
31
31
  end
32
32
 
33
+ audit(
34
+ :announcement_produced,
35
+ changes: {},
36
+ metadata: {
37
+ campaign_identity: input.campaign_identity,
38
+ notification_type_key: input.notification_type_key,
39
+ recipient_count: user_ids.size
40
+ },
41
+ attribution_mode: :admin_direct
42
+ )
43
+
33
44
  success(
34
45
  payload: CommandTower::Serializers::Admin::Messaging::AnnouncementResponseSerializer.serialize(
35
46
  result.data
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module ScopeResolution
7
+ module_function
8
+
9
+ def resolve(tool_id:, user:, scope_value:)
10
+ CommandTower::AdminScope::Resolve.call(tool_id:, principal: user, scope_value:)
11
+ rescue CommandTower::Errors::ForbiddenError => error
12
+ CommandTower::Workflows::WorkflowResult.failure(errors: [error], http_status: :forbidden)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ module ErrorMapping
8
+ module_function
9
+
10
+ def http_status_for(error)
11
+ case error
12
+ when CommandTower::Errors::ValidationError
13
+ :unprocessable_entity
14
+ when CommandTower::Errors::ForbiddenError
15
+ :forbidden
16
+ when CommandTower::Errors::NotFoundError
17
+ :not_found
18
+ else
19
+ :internal_server_error
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ module IdentityMutation
8
+ module_function
9
+
10
+ def resolve_target(id:, principal:, scope_value:)
11
+ CommandTower::SharedSequences::Admin::Users::ResolveScopedUser.call(
12
+ id:,
13
+ principal:,
14
+ scope_value:
15
+ )
16
+ end
17
+
18
+ def map_service_failure(result)
19
+ CommandTower::Workflows::WorkflowResult.failure(
20
+ errors: result.errors,
21
+ http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
22
+ result.errors.first
23
+ )
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class ListAssignableRolesWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call
11
+ listed = CommandTower::Services::Admin::Users::ListAssignableRoles.call
12
+ unless listed.success?
13
+ return failure(
14
+ errors: listed.errors,
15
+ http_status: ErrorMapping.http_status_for(listed.errors.first)
16
+ )
17
+ end
18
+
19
+ success(
20
+ payload: CommandTower::Serializers::Admin::Users::AssignableRolesSerializer.serialize(
21
+ listed.data[:roles]
22
+ ),
23
+ http_status: :ok
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class ListWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(limit:, offset:, search: nil, user:, scope_value: nil)
11
+ scope_result = resolve_scope(user:, scope_value:)
12
+ return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
13
+
14
+ listed = CommandTower::Services::Admin::Users::List.call(
15
+ limit:,
16
+ offset:,
17
+ search:,
18
+ principal: user,
19
+ scope_context: scope_result
20
+ )
21
+ return result_or_failure(listed) unless listed.success?
22
+
23
+ payload = listed.data[:users].map do |user|
24
+ CommandTower::Serializers::Admin::Users::UserSerializer.serialize(user)
25
+ end
26
+ meta = CommandTower::Serializers::Admin::Users::PaginationMetaSerializer.serialize(
27
+ listed.data[:pagination]
28
+ )
29
+ success(payload:, meta:, http_status: :ok)
30
+ end
31
+
32
+ private
33
+
34
+ def resolve_scope(user:, scope_value:)
35
+ CommandTower::Workflows::Admin::ScopeResolution.resolve(
36
+ tool_id: "users",
37
+ user:,
38
+ scope_value:
39
+ )
40
+ end
41
+
42
+ def result_or_failure(result)
43
+ failure(
44
+ errors: result.errors,
45
+ http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
46
+ result.errors.first
47
+ )
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class SetEmailValidatedWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(id:, user:, email_validated:, scope_value: nil)
11
+ resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
12
+ return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
13
+
14
+ target = resolved.user
15
+ previous = target.email_validated
16
+
17
+ transaction do
18
+ result = CommandTower::Services::Admin::Users::SetEmailValidated.call(
19
+ user: target,
20
+ email_validated:
21
+ )
22
+ fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
23
+
24
+ updated = result.data[:user]
25
+ if result.data[:changed]
26
+ audit(
27
+ :admin_user_email_validation_changed,
28
+ subject: updated,
29
+ affected_user: updated,
30
+ changes: { email_validated: { from: previous, to: updated.email_validated } },
31
+ attribution_mode: :admin_direct
32
+ )
33
+ end
34
+
35
+ success(
36
+ payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
37
+ http_status: :ok
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class ShowWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(id:, user:, scope_value: nil)
11
+ scope_result = resolve_scope(user:, scope_value:)
12
+ return scope_result if scope_result.is_a?(CommandTower::Workflows::WorkflowResult)
13
+
14
+ shown = CommandTower::Services::Admin::Users::Show.call(
15
+ id:,
16
+ principal: user,
17
+ scope_context: scope_result
18
+ )
19
+ return result_or_failure(shown) unless shown.success?
20
+
21
+ payload = CommandTower::Serializers::Admin::Users::UserSerializer.serialize(
22
+ shown.data[:user]
23
+ )
24
+ success(payload:, http_status: :ok)
25
+ end
26
+
27
+ private
28
+
29
+ def resolve_scope(user:, scope_value:)
30
+ CommandTower::Workflows::Admin::ScopeResolution.resolve(
31
+ tool_id: "users",
32
+ user:,
33
+ scope_value:
34
+ )
35
+ end
36
+
37
+ def result_or_failure(result)
38
+ failure(
39
+ errors: result.errors,
40
+ http_status: CommandTower::Workflows::Admin::Users::ErrorMapping.http_status_for(
41
+ result.errors.first
42
+ )
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class UpdateEmailWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(id:, user:, email:, scope_value: nil)
11
+ resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
12
+ return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
13
+
14
+ target = resolved.user
15
+
16
+ transaction do
17
+ result = CommandTower::Services::Admin::Users::UpdateEmail.call(
18
+ user: target,
19
+ email:
20
+ )
21
+ fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
22
+
23
+ updated = result.data[:user]
24
+ if result.data[:changed]
25
+ audit(
26
+ :admin_user_email_changed,
27
+ subject: updated,
28
+ affected_user: updated,
29
+ changes: {},
30
+ metadata: { email_changed: true },
31
+ attribution_mode: :admin_direct
32
+ )
33
+ end
34
+
35
+ success(
36
+ payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
37
+ http_status: :ok
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class UpdateNameWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(id:, user:, first_name:, last_name:, scope_value: nil)
11
+ resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
12
+ return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
13
+
14
+ target = resolved.user
15
+ previous = { first_name: target.first_name, last_name: target.last_name }
16
+
17
+ transaction do
18
+ result = CommandTower::Services::Admin::Users::UpdateName.call(
19
+ user: target,
20
+ first_name:,
21
+ last_name:
22
+ )
23
+ fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
24
+
25
+ updated = result.data[:user]
26
+ if result.data[:changed]
27
+ audit(
28
+ :admin_user_name_changed,
29
+ subject: updated,
30
+ affected_user: updated,
31
+ changes: {
32
+ first_name: { from: previous[:first_name], to: updated.first_name },
33
+ last_name: { from: previous[:last_name], to: updated.last_name }
34
+ },
35
+ attribution_mode: :admin_direct
36
+ )
37
+ end
38
+
39
+ success(
40
+ payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
41
+ http_status: :ok
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class UpdateRolesWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(id:, user:, roles:, scope_value: nil)
11
+ resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
12
+ return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
13
+
14
+ target = resolved.user
15
+ policy = CommandTower::Services::Admin::Users::RoleAssignmentPolicy.call(
16
+ actor: user,
17
+ target:,
18
+ desired_roles: roles
19
+ )
20
+ return IdentityMutation.map_service_failure(policy) unless policy.success?
21
+
22
+ transaction do
23
+ result = CommandTower::Services::Admin::Users::ReplaceUserRoles.call(
24
+ user: target,
25
+ desired_roles: policy.data[:desired_roles]
26
+ )
27
+ fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
28
+
29
+ updated = result.data[:user]
30
+ if result.data[:changed]
31
+ Array(result.data[:assigned_roles]).each do |role_name|
32
+ audit(
33
+ :role_assigned,
34
+ subject: updated,
35
+ affected_user: updated,
36
+ changes: { role: { from: nil, to: role_name.to_s } },
37
+ attribution_mode: :admin_direct
38
+ )
39
+ end
40
+ Array(result.data[:revoked_roles]).each do |role_name|
41
+ audit(
42
+ :role_revoked,
43
+ subject: updated,
44
+ affected_user: updated,
45
+ changes: { role: { from: role_name.to_s, to: nil } },
46
+ attribution_mode: :admin_direct
47
+ )
48
+ end
49
+ end
50
+
51
+ success(
52
+ payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
53
+ http_status: :ok
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Users
7
+ class UpdateUsernameWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(id:, user:, username:, scope_value: nil)
11
+ resolved = IdentityMutation.resolve_target(id:, principal: user, scope_value:)
12
+ return resolved if resolved.is_a?(CommandTower::Workflows::WorkflowResult)
13
+
14
+ target = resolved.user
15
+ previous_username = target.username
16
+
17
+ transaction do
18
+ result = CommandTower::Services::Admin::Users::UpdateUsername.call(
19
+ user: target,
20
+ username:
21
+ )
22
+ fail_transaction!(IdentityMutation.map_service_failure(result)) unless result.success?
23
+
24
+ updated = result.data[:user]
25
+ if result.data[:changed]
26
+ audit(
27
+ :admin_user_username_changed,
28
+ subject: updated,
29
+ affected_user: updated,
30
+ changes: { username: { from: previous_username, to: updated.username } },
31
+ attribution_mode: :admin_direct
32
+ )
33
+ end
34
+
35
+ success(
36
+ payload: CommandTower::Serializers::Admin::Users::UserSerializer.serialize(updated),
37
+ http_status: :ok
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Admin
6
+ module Workspace
7
+ class ManifestWorkflow < CommandTower::Workflows::ApplicationWorkflow
8
+ retry_strategy :none
9
+
10
+ def call(user:)
11
+ result = CommandTower::Services::Admin::Workspace::Manifest.call(user:)
12
+ unless result.success?
13
+ return failure(errors: result.errors, http_status: :unprocessable_entity)
14
+ end
15
+
16
+ success(
17
+ payload: CommandTower::Serializers::Admin::Workspace::ManifestSerializer.serialize(result.data[:tools]),
18
+ http_status: :ok
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end