This commit is contained in:
@@ -44,6 +44,7 @@ defmodule WorkloadService.Aggregates.Task do
|
||||
|
||||
alias unquote(commands_module).CreateTask
|
||||
alias unquote(commands_module).SubmitResponse
|
||||
alias unquote(commands_module).RequestApproval
|
||||
alias unquote(commands_module).ApproveSubmission
|
||||
alias unquote(commands_module).CompleteTask
|
||||
|
||||
@@ -86,6 +87,18 @@ defmodule WorkloadService.Aggregates.Task do
|
||||
end
|
||||
end
|
||||
|
||||
@impl Aggregate
|
||||
def execute(%__MODULE__{status: "draft"}, %RequestApproval{} = cmd) do
|
||||
%WorkloadService.Events.ApprovalRequested{
|
||||
id: cmd.id
|
||||
}
|
||||
end
|
||||
|
||||
@impl Aggregate
|
||||
def execute(%__MODULE__{status: status}, %RequestApproval{}) do
|
||||
{:error, {:invalid_state, "cannot request approval in state: #{status}"}}
|
||||
end
|
||||
|
||||
@impl Aggregate
|
||||
def execute(%__MODULE__{id: id, status: "draft"}, %ApproveSubmission{}) do
|
||||
%WorkloadService.Events.SubmissionApproved{
|
||||
@@ -141,6 +154,14 @@ defmodule WorkloadService.Aggregates.Task do
|
||||
}
|
||||
end
|
||||
|
||||
@impl Aggregate
|
||||
def apply(%__MODULE__{} = agg, %WorkloadService.Events.ApprovalRequested{}) do
|
||||
%{
|
||||
agg
|
||||
| status: "approval_requested"
|
||||
}
|
||||
end
|
||||
|
||||
@impl Aggregate
|
||||
def apply(%__MODULE__{} = agg, %WorkloadService.Events.TaskCompleted{}) do
|
||||
%{
|
||||
|
||||
@@ -15,6 +15,11 @@ defmodule WorkloadService.Application do
|
||||
WorkloadServiceWeb.Telemetry,
|
||||
{DNSCluster, query: Application.get_env(:workload_service, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: WorkloadService.PubSub},
|
||||
{Oidcc.ProviderConfiguration.Worker,
|
||||
%{
|
||||
issuer: Application.get_env(:workload_service, :zitadel)[:issuer],
|
||||
name: WorkloadService.ZitadelProvider
|
||||
}},
|
||||
WorkloadServiceWeb.Endpoint
|
||||
]
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule WorkloadService.Router do
|
||||
[
|
||||
WorkloadService.Commands.QuoteTask.CreateTask,
|
||||
WorkloadService.Commands.QuoteTask.SubmitResponse,
|
||||
WorkloadService.Commands.QuoteTask.RequestApproval,
|
||||
WorkloadService.Commands.QuoteTask.ApproveSubmission,
|
||||
WorkloadService.Commands.QuoteTask.CompleteTask
|
||||
],
|
||||
@@ -16,6 +17,7 @@ defmodule WorkloadService.Router do
|
||||
[
|
||||
WorkloadService.Commands.SolicitationTask.CreateTask,
|
||||
WorkloadService.Commands.SolicitationTask.SubmitResponse,
|
||||
WorkloadService.Commands.SolicitationTask.RequestApproval,
|
||||
WorkloadService.Commands.SolicitationTask.ApproveSubmission,
|
||||
WorkloadService.Commands.SolicitationTask.CompleteTask
|
||||
],
|
||||
|
||||
@@ -8,11 +8,11 @@ defmodule WorkloadService.Commands.QuoteTask do
|
||||
Command to create a new quote task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
application_id: WorkloadService.Aggregates.ApplicationId.t(),
|
||||
task_info: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
application_id: WorkloadService.Aggregates.ApplicationId.t(),
|
||||
task_info: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :application_id, :task_info, :attachments]
|
||||
@@ -23,10 +23,10 @@ defmodule WorkloadService.Commands.QuoteTask do
|
||||
Command to submit response for a quote task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
submission: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
submission: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :submission, :attachments]
|
||||
@@ -37,8 +37,21 @@ defmodule WorkloadService.Commands.QuoteTask do
|
||||
Command to approve submission for a quote task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t()
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t()
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id]
|
||||
end
|
||||
|
||||
defmodule RequestApproval do
|
||||
@moduledoc """
|
||||
Command to request approval for a quote task.
|
||||
Moves task from 'draft' to 'approval_requested'.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t()
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id]
|
||||
@@ -49,9 +62,9 @@ defmodule WorkloadService.Commands.QuoteTask do
|
||||
Command to complete a quote task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
completed_by: String.t()
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
completed_by: String.t()
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :completed_by]
|
||||
|
||||
@@ -8,11 +8,11 @@ defmodule WorkloadService.Commands.SolicitationTask do
|
||||
Command to create a new solicitation task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
application_id: WorkloadService.Aggregates.ApplicationId.t(),
|
||||
task_info: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
application_id: WorkloadService.Aggregates.ApplicationId.t(),
|
||||
task_info: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :application_id, :task_info, :attachments]
|
||||
@@ -23,10 +23,10 @@ defmodule WorkloadService.Commands.SolicitationTask do
|
||||
Command to submit response for a solicitation task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
submission: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
submission: map(),
|
||||
attachments: [String.t()]
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :submission, :attachments]
|
||||
@@ -37,8 +37,21 @@ defmodule WorkloadService.Commands.SolicitationTask do
|
||||
Command to approve submission for a solicitation task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t()
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t()
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id]
|
||||
end
|
||||
|
||||
defmodule RequestApproval do
|
||||
@moduledoc """
|
||||
Command to request approval for a solicitation task.
|
||||
Moves task from 'draft' to 'approval_requested'.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t()
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id]
|
||||
@@ -49,11 +62,11 @@ defmodule WorkloadService.Commands.SolicitationTask do
|
||||
Command to complete a solicitation task.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
completed_by: String.t()
|
||||
}
|
||||
id: WorkloadService.Aggregates.TaskId.t(),
|
||||
completed_by: String.t()
|
||||
}
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :completed_by]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,6 +54,16 @@ defmodule WorkloadService.Events.SubmissionApproved do
|
||||
defstruct [:id]
|
||||
end
|
||||
|
||||
defmodule WorkloadService.Events.ApprovalRequested do
|
||||
@moduledoc """
|
||||
Emitted when a user requests approval for their submission.
|
||||
This transitions the task from 'draft' to 'approval_requested'.
|
||||
"""
|
||||
use WorkloadService.Events
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id]
|
||||
end
|
||||
|
||||
defmodule WorkloadService.Events.TaskCompleted do
|
||||
@moduledoc """
|
||||
Emitted when task is completed and sent to policy-service.
|
||||
@@ -62,4 +72,4 @@ defmodule WorkloadService.Events.TaskCompleted do
|
||||
use WorkloadService.Events
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :completed_by]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,6 +45,16 @@ defmodule WorkloadService.Projectors.TaskProjector do
|
||||
end)
|
||||
end)
|
||||
|
||||
project(%Events.ApprovalRequested{} = e, _meta, fn multi ->
|
||||
multi
|
||||
|> Ecto.Multi.run(:fetch, fn repo, _ ->
|
||||
{:ok, repo.get(Task, to_string(e.id))}
|
||||
end)
|
||||
|> Ecto.Multi.update(:task, fn %{fetch: task} ->
|
||||
Ecto.Changeset.change(task, %{status: "approval_requested"})
|
||||
end)
|
||||
end)
|
||||
|
||||
project(%Events.TaskCompleted{} = e, _meta, fn multi ->
|
||||
multi
|
||||
|> Ecto.Multi.run(:fetch, fn repo, _ ->
|
||||
|
||||
@@ -18,8 +18,8 @@ defmodule WorkloadService.Workload.Queries do
|
||||
|> Flop.validate_and_run(params, for: Task)
|
||||
end
|
||||
|
||||
def get_task_by_id(id) do
|
||||
case Repo.get(Task, id) do
|
||||
def get_task_by_id(org_id, id) do
|
||||
case Repo.get_by(Task, id: id, org_id: org_id) do
|
||||
nil -> {:error, :not_found}
|
||||
task -> {:ok, task}
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
alias WorkloadServiceWeb.QueryHelpers
|
||||
|
||||
tags(["Tasks"])
|
||||
security([%{"bearerAuth" => []}])
|
||||
|
||||
operation(:list,
|
||||
summary: "List tasks",
|
||||
@@ -24,7 +25,9 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
)
|
||||
|
||||
def list(conn, params) do
|
||||
case Queries.list_tasks(params) do
|
||||
org_id = conn.private[WorkloadServiceWeb.Plugs.ExtractOrganizationId]
|
||||
|
||||
case Queries.list_tasks_by_org(org_id, params) do
|
||||
{:ok, {tasks, meta}} ->
|
||||
conn
|
||||
|> put_status(:ok)
|
||||
@@ -50,7 +53,9 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
)
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
case Queries.get_task_by_id(id) do
|
||||
org_id = conn.private[WorkloadServiceWeb.Plugs.ExtractOrganizationId]
|
||||
|
||||
case Queries.get_task_by_id(org_id, id) do
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "task not found"})
|
||||
|
||||
@@ -73,22 +78,23 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
)
|
||||
|
||||
def submit(conn, %{"id" => id} = params) do
|
||||
org_id = conn.private[WorkloadServiceWeb.Plugs.ExtractOrganizationId]
|
||||
task_type = get_task_type(id)
|
||||
|
||||
case task_type do
|
||||
"quote" ->
|
||||
handle_quote_submit(conn, id, params)
|
||||
handle_quote_submit(conn, id, params, org_id)
|
||||
|
||||
"solicitation" ->
|
||||
handle_solicitation_submit(conn, id, params)
|
||||
handle_solicitation_submit(conn, id, params, org_id)
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "invalid task type"})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_quote_submit(conn, id, params) do
|
||||
case Queries.get_task_by_id(id) do
|
||||
defp handle_quote_submit(conn, id, params, org_id) do
|
||||
case Queries.get_task_by_id(org_id, id) do
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "task not found"})
|
||||
|
||||
@@ -107,15 +113,15 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
attachments: params["document_urls"] || []
|
||||
}
|
||||
|
||||
dispatch_and_respond(conn, id, command)
|
||||
dispatch_and_respond(conn, id, command, org_id)
|
||||
|
||||
{:ok, _task} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "invalid state for submit"})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_solicitation_submit(conn, id, params) do
|
||||
case Queries.get_task_by_id(id) do
|
||||
defp handle_solicitation_submit(conn, id, params, org_id) do
|
||||
case Queries.get_task_by_id(org_id, id) do
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "task not found"})
|
||||
|
||||
@@ -133,7 +139,7 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
attachments: params["document_urls"] || []
|
||||
}
|
||||
|
||||
dispatch_and_respond(conn, id, command)
|
||||
dispatch_and_respond(conn, id, command, org_id)
|
||||
|
||||
{:ok, _task} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "invalid state for submit"})
|
||||
@@ -153,29 +159,35 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
)
|
||||
|
||||
def approve(conn, %{"id" => id}) do
|
||||
org_id = conn.private[WorkloadServiceWeb.Plugs.ExtractOrganizationId]
|
||||
task_type = get_task_type(id)
|
||||
|
||||
case task_type do
|
||||
"quote" ->
|
||||
handle_approve(conn, id, WorkloadService.Commands.QuoteTask.ApproveSubmission)
|
||||
handle_approve(conn, id, WorkloadService.Commands.QuoteTask.ApproveSubmission, org_id)
|
||||
|
||||
"solicitation" ->
|
||||
handle_approve(conn, id, WorkloadService.Commands.SolicitationTask.ApproveSubmission)
|
||||
handle_approve(
|
||||
conn,
|
||||
id,
|
||||
WorkloadService.Commands.SolicitationTask.ApproveSubmission,
|
||||
org_id
|
||||
)
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "invalid task type"})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_approve(conn, id, command_module) do
|
||||
case Queries.get_task_by_id(id) do
|
||||
defp handle_approve(conn, id, command_module, org_id) do
|
||||
case Queries.get_task_by_id(org_id, id) do
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "task not found"})
|
||||
|
||||
{:ok, %{status: "draft"} = _task} ->
|
||||
task_id = TaskId.parse!(id)
|
||||
command = struct(command_module, id: task_id)
|
||||
dispatch_and_respond(conn, id, command)
|
||||
dispatch_and_respond(conn, id, command, org_id)
|
||||
|
||||
{:ok, _task} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "invalid state for approve"})
|
||||
@@ -195,20 +207,37 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
]
|
||||
)
|
||||
|
||||
def complete(conn, %{"id" => id} = params) do
|
||||
operation(:request_approval,
|
||||
summary: "Request approval for task",
|
||||
parameters: [
|
||||
id: [in: :path, type: :string, required: true]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Task approved", "application/json", S.TaskDetailResponse},
|
||||
not_found: {"Not found", "application/json", S.ErrorResponse},
|
||||
unprocessable_entity: {"Error", "application/json", S.ErrorResponse}
|
||||
]
|
||||
)
|
||||
|
||||
def request_approval(conn, %{"id" => id}) do
|
||||
org_id = conn.private[WorkloadServiceWeb.Plugs.ExtractOrganizationId]
|
||||
task_type = get_task_type(id)
|
||||
completed_by = params["completed_by"] || "system"
|
||||
|
||||
case task_type do
|
||||
"quote" ->
|
||||
handle_complete(conn, id, completed_by, WorkloadService.Commands.QuoteTask.CompleteTask)
|
||||
|
||||
"solicitation" ->
|
||||
handle_complete(
|
||||
handle_request_approval(
|
||||
conn,
|
||||
id,
|
||||
completed_by,
|
||||
WorkloadService.Commands.SolicitationTask.CompleteTask
|
||||
WorkloadService.Commands.QuoteTask.RequestApproval,
|
||||
org_id
|
||||
)
|
||||
|
||||
"solicitation" ->
|
||||
handle_request_approval(
|
||||
conn,
|
||||
id,
|
||||
WorkloadService.Commands.SolicitationTask.RequestApproval,
|
||||
org_id
|
||||
)
|
||||
|
||||
_ ->
|
||||
@@ -216,25 +245,54 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_complete(conn, id, completed_by, command_module) do
|
||||
case Queries.get_task_by_id(id) do
|
||||
def complete(conn, %{"id" => id} = params) do
|
||||
org_id = conn.private[WorkloadServiceWeb.Plugs.ExtractOrganizationId]
|
||||
task_type = get_task_type(id)
|
||||
completed_by = params["completed_by"] || "system"
|
||||
|
||||
case task_type do
|
||||
"quote" ->
|
||||
handle_complete(
|
||||
conn,
|
||||
id,
|
||||
completed_by,
|
||||
WorkloadService.Commands.QuoteTask.CompleteTask,
|
||||
org_id
|
||||
)
|
||||
|
||||
"solicitation" ->
|
||||
handle_complete(
|
||||
conn,
|
||||
id,
|
||||
completed_by,
|
||||
WorkloadService.Commands.SolicitationTask.CompleteTask,
|
||||
org_id
|
||||
)
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "invalid task type"})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_complete(conn, id, completed_by, command_module, org_id) do
|
||||
case Queries.get_task_by_id(org_id, id) do
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "task not found"})
|
||||
|
||||
{:ok, %{status: "approved"} = _task} ->
|
||||
task_id = TaskId.parse!(id)
|
||||
command = struct(command_module, id: task_id, completed_by: completed_by)
|
||||
dispatch_and_respond(conn, id, command)
|
||||
dispatch_and_respond(conn, id, command, org_id)
|
||||
|
||||
{:ok, _task} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "invalid state for complete"})
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch_and_respond(conn, id, command) do
|
||||
defp dispatch_and_respond(conn, id, command, org_id) do
|
||||
case CommandedApp.dispatch(command) do
|
||||
:ok ->
|
||||
{:ok, task} = Queries.get_task_by_id(id)
|
||||
{:ok, task} = Queries.get_task_by_id(org_id, id)
|
||||
conn |> put_status(:ok) |> json(%{data: task_detail(task)})
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -242,6 +300,23 @@ defmodule WorkloadServiceWeb.TaskController do
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_request_approval(conn, id, command_module, org_id) do
|
||||
case Queries.get_task_by_id(org_id, id) do
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "task not found"})
|
||||
|
||||
{:ok, %{status: "draft"} = _task} ->
|
||||
task_id = TaskId.parse!(id)
|
||||
command = struct(command_module, id: task_id)
|
||||
dispatch_and_respond(conn, id, command, org_id)
|
||||
|
||||
{:ok, _task} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{error: "invalid state for request_approval"})
|
||||
end
|
||||
end
|
||||
|
||||
defp get_task_type(id) do
|
||||
case WorkloadService.Aggregates.TaskId.parse(id) do
|
||||
{:ok, %{type: type}} -> type
|
||||
|
||||
81
lib/workload_service_web/plugs/authorize_roles.ex
Normal file
81
lib/workload_service_web/plugs/authorize_roles.ex
Normal file
@@ -0,0 +1,81 @@
|
||||
defmodule WorkloadServiceWeb.Plugs.AuthorizeRoles do
|
||||
@moduledoc """
|
||||
Authorize request based on Zitadel role permissions.
|
||||
|
||||
After token introspection, checks if the user holds any of the
|
||||
`required_permissions` roles for the organization identified by
|
||||
`X-Organization-Id` header.
|
||||
|
||||
The Zitadel roles claim structure is:
|
||||
%{"urn:zitadel:iam:org:project:<project_id>:roles": {
|
||||
"<role>": {
|
||||
"<org_id>": "<org_domain>"
|
||||
},
|
||||
"<role>": {
|
||||
"<org_id>": "<org_domain>"
|
||||
}
|
||||
}}
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
@impl Plug
|
||||
def init(opts),
|
||||
do:
|
||||
opts
|
||||
|> Keyword.validate!([
|
||||
:roles_claim
|
||||
])
|
||||
|
||||
@impl Plug
|
||||
def call(conn, opts) do
|
||||
if authorized?(
|
||||
conn,
|
||||
Keyword.get(opts, :roles_claim),
|
||||
Keyword.get(opts, :required_permissions)
|
||||
) do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> halt()
|
||||
|> send_resp(
|
||||
:forbidden,
|
||||
%{error: "Forbidden", reason: "Missing required role"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp authorized?(conn, roles_claim, required_permissions) do
|
||||
org_id = conn.private[WorkloadServiceWeb.Plugs.ExtractOrganizationId]
|
||||
|
||||
with true <- org_id_given?(org_id),
|
||||
roles_map <- get_roles_map(conn, roles_claim),
|
||||
true <- has_any_role?(roles_map, org_id, required_permissions) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp org_id_given?(org_id), do: not is_nil(org_id)
|
||||
|
||||
defp get_roles_map(conn, roles_claim) do
|
||||
case conn.private[Oidcc.Plug.IntrospectToken] do
|
||||
%Oidcc.TokenIntrospection{extra: extra} ->
|
||||
Map.get(extra, roles_claim, %{})
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
defp has_any_role?(roles_map, org_id, required_permissions) do
|
||||
Enum.any?(required_permissions, fn role ->
|
||||
role_orgs = Map.get(roles_map, role, %{})
|
||||
Map.has_key?(role_orgs, org_id)
|
||||
end)
|
||||
end
|
||||
end
|
||||
22
lib/workload_service_web/plugs/extract_organization_id.ex
Normal file
22
lib/workload_service_web/plugs/extract_organization_id.ex
Normal file
@@ -0,0 +1,22 @@
|
||||
defmodule WorkloadServiceWeb.Plugs.ExtractOrganizationId do
|
||||
@moduledoc """
|
||||
Extract `X-Organization-Id` request header.
|
||||
|
||||
Stores the organization identifier in conn.private[__MODULE__] for downstream authorization checks.
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn, only: [get_req_header: 2, put_private: 3]
|
||||
|
||||
@impl Plug
|
||||
def init(_opts), do: %{}
|
||||
|
||||
@impl Plug
|
||||
def call(conn, _opts) do
|
||||
case get_req_header(conn, "x-organization-id") do
|
||||
[org_id | _rest] -> put_private(conn, __MODULE__, org_id)
|
||||
[] -> put_private(conn, __MODULE__, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
27
lib/workload_service_web/plugs/require_organization_id.ex
Normal file
27
lib/workload_service_web/plugs/require_organization_id.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
defmodule WorkloadServiceWeb.Plugs.RequireOrganizationId do
|
||||
@moduledoc """
|
||||
Ensure `X-Organization-Id` header is provided.
|
||||
|
||||
This plug must be used after `WorkloadServiceWeb.Plugs.ExtractOrganizationId`.
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn, only: [get_req_header: 2, halt: 1, send_resp: 3]
|
||||
|
||||
@impl Plug
|
||||
def init(_opts), do: %{}
|
||||
|
||||
@impl Plug
|
||||
def call(conn, _opts) do
|
||||
case get_req_header(conn, "x-organization-id") do
|
||||
[] ->
|
||||
conn
|
||||
|> halt()
|
||||
|> send_resp(:bad_request, "The organization id is required")
|
||||
|
||||
[_org_id] ->
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,29 +5,94 @@ defmodule WorkloadServiceWeb.Router do
|
||||
alias WorkloadServiceWeb.HealthController
|
||||
|
||||
pipeline :api do
|
||||
plug OpenApiSpex.Plug.PutApiSpec, module: WorkloadServiceWeb.ApiSpec
|
||||
plug(OpenApiSpex.Plug.PutApiSpec, module: WorkloadServiceWeb.ApiSpec)
|
||||
end
|
||||
|
||||
get "/health", HealthController, :health
|
||||
get "/health/ready", HealthController, :ready
|
||||
pipeline :auth do
|
||||
plug(Oidcc.Plug.ExtractAuthorization)
|
||||
plug(Oidcc.Plug.RequireAuthorization)
|
||||
|
||||
plug(WorkloadServiceWeb.Plugs.RequireOrganizationId)
|
||||
plug(WorkloadServiceWeb.Plugs.ExtractOrganizationId)
|
||||
|
||||
plug(:introspect)
|
||||
end
|
||||
|
||||
pipeline(:read, do: plug(:authorize_roles, required_permissions: ["task:read"]))
|
||||
pipeline(:submit, do: plug(:authorize_roles, required_permissions: ["task:submit"]))
|
||||
|
||||
pipeline(:request_approval,
|
||||
do: plug(:authorize_roles, required_permissions: ["task:request_approval"])
|
||||
)
|
||||
|
||||
pipeline(:approve, do: plug(:authorize_roles, required_permissions: ["task:approve"]))
|
||||
pipeline(:complete, do: plug(:authorize_roles, required_permissions: ["task:complete"]))
|
||||
|
||||
get("/health", HealthController, :health)
|
||||
get("/health/ready", HealthController, :ready)
|
||||
|
||||
scope "/api" do
|
||||
pipe_through [:api]
|
||||
pipe_through([:api])
|
||||
|
||||
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
|
||||
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
|
||||
|
||||
scope "/v1" do
|
||||
get "/tasks", TaskController, :list
|
||||
get "/tasks/:id", TaskController, :show
|
||||
post "/tasks/:id/submit", TaskController, :submit
|
||||
post "/tasks/:id/approve", TaskController, :approve
|
||||
post "/tasks/:id/complete", TaskController, :complete
|
||||
pipe_through([:auth])
|
||||
|
||||
scope "/" do
|
||||
pipe_through([:read])
|
||||
get("/tasks", TaskController, :list)
|
||||
get("/tasks/:id", TaskController, :show)
|
||||
end
|
||||
|
||||
scope "/" do
|
||||
pipe_through([:submit])
|
||||
post("/tasks/:id/submit", TaskController, :submit)
|
||||
end
|
||||
|
||||
scope "/" do
|
||||
pipe_through([:request_approval])
|
||||
post("/tasks/:id/request_approval", TaskController, :request_approval)
|
||||
end
|
||||
|
||||
scope "/" do
|
||||
pipe_through([:approve])
|
||||
post("/tasks/:id/approve", TaskController, :approve)
|
||||
end
|
||||
|
||||
scope "/" do
|
||||
pipe_through([:complete])
|
||||
post("/tasks/:id/complete", TaskController, :complete)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Mix.env() == :dev do
|
||||
scope "/swaggerui" do
|
||||
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
|
||||
get("/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def introspect(conn, _opts) do
|
||||
zitadel = Application.get_env(:workload_service, :zitadel)
|
||||
|
||||
opts =
|
||||
Oidcc.Plug.IntrospectToken.init(
|
||||
provider: WorkloadService.ZitadelProvider,
|
||||
client_id: zitadel[:client_id],
|
||||
client_secret: zitadel[:client_secret],
|
||||
token_introspection_opts: %{client_self_only: false}
|
||||
)
|
||||
|
||||
Oidcc.Plug.IntrospectToken.call(conn, opts)
|
||||
end
|
||||
|
||||
def authorize_roles(conn, opts) do
|
||||
zitadel = Application.get_env(:workload_service, :zitadel)
|
||||
|
||||
o =
|
||||
WorkloadServiceWeb.Plugs.AuthorizeRoles.init(roles_claim: zitadel[:roles_claim])
|
||||
|
||||
WorkloadServiceWeb.Plugs.AuthorizeRoles.call(conn, Keyword.merge(opts, o))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -144,7 +144,10 @@ defmodule WorkloadServiceWeb.Schemas.Task do
|
||||
application_id: %Schema{type: :string},
|
||||
policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]},
|
||||
task_info: %Schema{oneOf: [QuoteTaskInfo, SolicitationTaskInfo]},
|
||||
status: %Schema{type: :string, enum: ["created", "draft", "approved", "completed"]},
|
||||
status: %Schema{
|
||||
type: :string,
|
||||
enum: ["created", "draft", "approval_requested", "approved", "completed"]
|
||||
},
|
||||
created_at: %Schema{type: :string, format: :"date-time"}
|
||||
}
|
||||
})
|
||||
@@ -167,7 +170,10 @@ defmodule WorkloadServiceWeb.Schemas.Task do
|
||||
nullable: true
|
||||
},
|
||||
attachments: %Schema{type: :array, items: %Schema{type: :string}},
|
||||
status: %Schema{type: :string, enum: ["created", "draft", "approved", "completed"]},
|
||||
status: %Schema{
|
||||
type: :string,
|
||||
enum: ["created", "draft", "approval_requested", "approved", "completed"]
|
||||
},
|
||||
created_at: %Schema{type: :string, format: :"date-time"},
|
||||
updated_at: %Schema{type: :string, format: :"date-time"}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user