feat(portal): Add REST API (#5579)

Why:

* In order to manage a large number of Firezone Sites, Resources,
Policies, etc... a REST API is needed as clicking through the UI is too
time consuming, as well as prone to error. By providing a REST API
Firezone customers will be able to manage things within their Firezone
accounts with code.
This commit is contained in:
Brian Manifold
2024-07-20 00:20:43 -04:00
committed by GitHub
parent 18394e3dcb
commit 79c815fbbc
75 changed files with 4946 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
[
import_deps: [:phoenix],
import_deps: [:phoenix, :open_api_spex],
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@@ -0,0 +1,27 @@
defmodule API.ApiSpec do
alias OpenApiSpex.{Components, Info, OpenApi, Paths, SecurityScheme, Server}
alias API.{Endpoint, Router}
@behaviour OpenApi
@impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(Endpoint)
],
info: %Info{
title: "Firezone API",
version: "1.0"
},
# Populate the paths from a phoenix router
paths: Paths.from_router(Router),
components: %Components{
securitySchemes: %{"authorization" => %SecurityScheme{type: "http", scheme: "bearer"}}
},
security: [%{"authorization" => []}]
}
# Discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules()
end
end

View File

@@ -0,0 +1,128 @@
defmodule API.ActorController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Actors
action_fallback API.FallbackController
tags ["Actors"]
operation :index,
summary: "List Actors",
parameters: [
limit: [in: :query, description: "Limit Users returned", type: :integer, example: 10],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok: {"ActorsResponse", "application/json", API.Schemas.Actor.ListResponse}
]
# List Actors
def index(conn, params) do
list_opts = Pagination.params_to_list_opts(params)
with {:ok, actors, metadata} <- Actors.list_actors(conn.assigns.subject, list_opts) do
render(conn, :index, actors: actors, metadata: metadata)
end
end
operation :show,
summary: "Show Actor",
parameters: [
id: [
in: :path,
description: "Actor ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"ActorResponse", "application/json", API.Schemas.Actor.Response}
]
# Show a specific Actor
def show(conn, %{"id" => id}) do
with {:ok, actor} <- Actors.fetch_actor_by_id(id, conn.assigns.subject) do
render(conn, :show, actor: actor)
end
end
operation :create,
summary: "Create an Actor",
request_body:
{"Actor attributes", "application/json", API.Schemas.Actor.Request, required: true},
responses: [
ok: {"ActorResponse", "application/json", API.Schemas.Actor.Response}
]
# Create a new Actor
def create(conn, %{"actor" => params}) do
subject = conn.assigns.subject
with {:ok, actor} <- Actors.create_actor(subject.account, params, subject) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/actors/#{actor}")
|> render(:show, actor: actor)
end
end
def create(_conn, _params) do
{:error, :bad_request}
end
operation :update,
summary: "Update an Actor",
parameters: [
id: [
in: :path,
description: "Actor ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
request_body:
{"Actor attributes", "application/json", API.Schemas.Actor.Request, required: true},
responses: [
ok: {"ActorResponse", "application/json", API.Schemas.Actor.Response}
]
# Update an Actor
def update(conn, %{"id" => id, "actor" => params}) do
subject = conn.assigns.subject
with {:ok, actor} <- Actors.fetch_actor_by_id(id, subject),
{:ok, actor} <- Actors.update_actor(actor, params, subject) do
render(conn, :show, actor: actor)
end
end
def update(_conn, _params) do
{:error, :bad_request}
end
operation :delete,
summary: "Delete an Actor",
parameters: [
id: [
in: :path,
description: "Actor ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"ActorResponse", "application/json", API.Schemas.Actor.Response}
]
# Delete an Actor
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, actor} <- Actors.fetch_actor_by_id(id, subject),
{:ok, actor} <- Actors.delete_actor(actor, subject) do
render(conn, :show, actor: actor)
end
end
end

View File

@@ -0,0 +1,131 @@
defmodule API.ActorGroupController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Actors
action_fallback API.FallbackController
tags ["Actor Groups"]
operation :index,
summary: "List Actor Groups",
parameters: [
limit: [in: :query, description: "Limit Actor Groups returned", type: :integer, example: 10],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok: {"Actor Group Response", "application/json", API.Schemas.ActorGroup.ListResponse}
]
# List Actor Groups
def index(conn, params) do
list_opts = Pagination.params_to_list_opts(params)
with {:ok, actor_groups, metadata} <- Actors.list_groups(conn.assigns.subject, list_opts) do
render(conn, :index, actor_groups: actor_groups, metadata: metadata)
end
end
operation :show,
summary: "Show Actor Group",
parameters: [
id: [
in: :path,
description: "Actor Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Actor Group Response", "application/json", API.Schemas.ActorGroup.Response}
]
# Show a specific Actor Group
def show(conn, %{"id" => id}) do
with {:ok, actor_group} <- Actors.fetch_group_by_id(id, conn.assigns.subject) do
render(conn, :show, actor_group: actor_group)
end
end
operation :create,
summary: "Create Actor Group",
parameters: [],
request_body:
{"Actor Group Attributes", "application/json", API.Schemas.ActorGroup.Request,
required: true},
responses: [
ok: {"Actor Group Response", "application/json", API.Schemas.ActorGroup.Response}
]
# Create a new Actor Group
def create(conn, %{"actor_group" => params}) do
params = Map.put(params, "type", "static")
with {:ok, actor_group} <- Actors.create_group(params, conn.assigns.subject) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/actor_groups/#{actor_group}")
|> render(:show, actor_group: actor_group)
end
end
def create(_conn, _params) do
{:error, :bad_request}
end
operation :update,
summary: "Update a Actor Group",
parameters: [
id: [
in: :path,
description: "Actor Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
request_body:
{"Actor Group Attributes", "application/json", API.Schemas.ActorGroup.Request,
required: true},
responses: [
ok: {"Actor Group Response", "application/json", API.Schemas.ActorGroup.Response}
]
# Update an Actor Group
def update(conn, %{"id" => id, "actor_group" => params}) do
subject = conn.assigns.subject
with {:ok, actor_group} <- Actors.fetch_group_by_id(id, subject),
{:ok, actor_group} <- Actors.update_group(actor_group, params, subject) do
render(conn, :show, actor_group: actor_group)
end
end
def update(_conn, _params) do
{:error, :bad_request}
end
operation :delete,
summary: "Delete a Actor Group",
parameters: [
id: [
in: :path,
description: "Actor Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Actor Group Response", "application/json", API.Schemas.ActorGroup.Response}
]
# Delete an Actor Group
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, actor_group} <- Actors.fetch_group_by_id(id, subject),
{:ok, actor_group} <- Actors.delete_group(actor_group, subject) do
render(conn, :show, actor_group: actor_group)
end
end
end

View File

@@ -0,0 +1,28 @@
defmodule API.ActorGroupJSON do
alias API.Pagination
alias Domain.Actors
@doc """
Renders a list of Actor Groups.
"""
def index(%{actor_groups: actor_groups, metadata: metadata}) do
%{
data: Enum.map(actor_groups, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Render a single Actor Group
"""
def show(%{actor_group: actor_group}) do
%{data: data(actor_group)}
end
defp data(%Actors.Group{} = actor_group) do
%{
id: actor_group.id,
name: actor_group.name
}
end
end

View File

@@ -0,0 +1,135 @@
defmodule API.ActorGroupMembershipController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Actors
action_fallback API.FallbackController
tags ["Actor Group Memberships"]
operation :index,
summary: "List Actor Group Memberships",
parameters: [
actor_group_id: [
in: :path,
description: "Actor Group ID",
example: "00000000-0000-0000-0000-000000000000"
],
limit: [
in: :query,
description: "Limit Actor Group Memberships returned",
type: :integer,
example: 10
],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok:
{"Actor Group Membership Response", "application/json",
API.Schemas.ActorGroupMembership.ListResponse}
]
# List members for a given Actor Group
def index(conn, %{"actor_group_id" => actor_group_id} = params) do
list_opts =
Pagination.params_to_list_opts(params)
|> Keyword.put(:filter, group_id: actor_group_id)
with {:ok, actors, metadata} <- Actors.list_actors(conn.assigns.subject, list_opts) do
render(conn, :index, actors: actors, metadata: metadata)
end
end
operation :update_put,
summary: "Update Actor Group Memberships",
parameters: [
actor_group_id: [
in: :path,
description: "Actor Group ID",
example: "00000000-0000-0000-0000-000000000000"
]
],
request_body:
{"Actor Group Membership Attributes", "application/json",
API.Schemas.ActorGroupMembership.PutRequest, required: true},
responses: [
ok:
{"Actor Group Membership Response", "application/json",
API.Schemas.ActorGroupMembership.MembershipResponse}
]
def update_put(
conn,
%{"actor_group_id" => actor_group_id, "memberships" => attrs}
) do
subject = conn.assigns.subject
preload = [:memberships]
filter = [deleted?: false, editable?: true]
with {:ok, group} <-
Actors.fetch_group_by_id(actor_group_id, subject, preload: preload, filter: filter),
{:ok, group} <- Actors.update_group(group, %{memberships: attrs}, subject) do
render(conn, :memberships, memberships: group.memberships)
end
end
def update_put(_conn, _params) do
{:error, :bad_request}
end
operation :update_patch,
summary: "Update an Actor Group Membership",
parameters: [
actor_group_id: [
in: :path,
description: "Actor Group ID",
example: "00000000-0000-0000-0000-000000000000"
]
],
request_body:
{"Actor Group Membership Attributes", "application/json",
API.Schemas.ActorGroupMembership.PatchRequest, required: true},
responses: [
ok:
{"Actor Group Membership Response", "application/json",
API.Schemas.ActorGroupMembership.MembershipResponse}
]
# Update Actor Group Memberships
def update_patch(
conn,
%{"actor_group_id" => actor_group_id, "memberships" => params}
) do
add = Map.get(params, "add", [])
remove = Map.get(params, "remove", [])
subject = conn.assigns.subject
preload = [:memberships]
filter = [deleted?: false, editable?: true]
with {:ok, group} <-
Actors.fetch_group_by_id(actor_group_id, subject, preload: preload, filter: filter),
membership_attrs <- prepare_membership_attrs(group, add, remove),
{:ok, group} <- Actors.update_group(group, %{memberships: membership_attrs}, subject) do
render(conn, :memberships, memberships: group.memberships)
end
end
def update_patch(_conn, _params) do
{:error, :bad_request}
end
defp prepare_membership_attrs(group, add, remove) do
to_add = MapSet.new(add) |> MapSet.reject(&(String.trim(&1) == ""))
to_remove = MapSet.new(remove) |> MapSet.reject(&(String.trim(&1) == ""))
member_ids = Enum.map(group.memberships, & &1.actor_id) |> MapSet.new()
membership_ids =
MapSet.difference(member_ids, to_remove)
|> MapSet.union(to_add)
if MapSet.size(membership_ids) == 0,
do: [],
else: Enum.map(membership_ids, &%{actor_id: &1})
end
end

View File

@@ -0,0 +1,30 @@
defmodule API.ActorGroupMembershipJSON do
alias API.Pagination
alias Domain.Actors
@doc """
Renders a list of Actors.
"""
def index(%{actors: actors, metadata: metadata}) do
%{
data: Enum.map(actors, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Renders a list of Actor IDs for an Actor Group
"""
def memberships(%{memberships: memberships}) do
actor_ids = for(membership <- memberships, do: membership.actor_id)
%{data: %{actor_ids: actor_ids}}
end
defp data(%Actors.Actor{} = actor) do
%{
id: actor.id,
name: actor.name,
type: actor.type
}
end
end

View File

@@ -0,0 +1,29 @@
defmodule API.ActorJSON do
alias API.Pagination
alias Domain.Actors
@doc """
Renders a list of Actors.
"""
def index(%{actors: actors, metadata: metadata}) do
%{
data: Enum.map(actors, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Render a single Actor
"""
def show(%{actor: actor}) do
%{data: data(actor)}
end
defp data(%Actors.Actor{} = actor) do
%{
id: actor.id,
name: actor.name,
type: actor.type
}
end
end

View File

@@ -0,0 +1,16 @@
defmodule API.ChangesetJSON do
def error(%{status: status, changeset: changeset}) do
%{
error: %{
reason: Plug.Conn.Status.reason_phrase(status),
validation_errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
}
}
end
defp translate_error({msg, opts}) do
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
end)
end
end

View File

@@ -0,0 +1,5 @@
defmodule API.ErrorJSON do
def render(template, _assigns) do
%{error: %{reason: Phoenix.Controller.status_message_from_template(template)}}
end
end

View File

@@ -1,9 +0,0 @@
defmodule API.ExampleController do
use API, :controller
def echo(conn, params) do
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(params))
end
end

View File

@@ -0,0 +1,38 @@
defmodule API.FallbackController do
use Phoenix.Controller
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(json: API.ErrorJSON)
|> render(:"404")
end
def call(conn, {:error, :unauthorized}) do
conn
|> put_status(:unauthorized)
|> put_view(json: API.ErrorJSON)
|> render(:"401")
end
def call(conn, {:error, :bad_request}) do
conn
|> put_status(:bad_request)
|> put_view(json: API.ErrorJSON)
|> render(:"400")
end
def call(conn, {:error, :unprocessable_entity}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: API.ErrorJSON)
|> render(:"422")
end
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: API.ChangesetJSON)
|> render(:error, status: 422, changeset: changeset)
end
end

View File

@@ -0,0 +1,96 @@
defmodule API.GatewayController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Gateways
action_fallback API.FallbackController
tags ["Gateways"]
operation :index,
summary: "List Gateways",
parameters: [
gateway_group_id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
],
limit: [in: :query, description: "Limit Gateways returned", type: :integer, example: 10],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok: {"Gateway Response", "application/json", API.Schemas.Gateway.ListResponse}
]
# List Gateways
def index(conn, params) do
list_opts =
params
|> Pagination.params_to_list_opts()
|> Keyword.put(:preload, :online?)
with {:ok, gateways, metadata} <- Gateways.list_gateways(conn.assigns.subject, list_opts) do
render(conn, :index, gateways: gateways, metadata: metadata)
end
end
operation :show,
summary: "Show Gateway",
parameters: [
gateway_group_id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
],
id: [
in: :path,
description: "Gateway ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Gateway Response", "application/json", API.Schemas.Gateway.Response}
]
# Show a specific Gateway
def show(conn, %{"id" => id}) do
with {:ok, gateway} <-
Gateways.fetch_gateway_by_id(id, conn.assigns.subject, preload: :online?) do
render(conn, :show, gateway: gateway)
end
end
operation :delete,
summary: "Delete a Gateway",
parameters: [
gateway_group_id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
],
id: [
in: :path,
description: "Gateway ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Gateway Response", "application/json", API.Schemas.Gateway.Response}
]
# Delete a Gateway
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, gateway} <- Gateways.fetch_gateway_by_id(id, subject),
{:ok, gateway} <- Gateways.delete_gateway(gateway, subject) do
render(conn, :show, gateway: gateway)
end
end
end

View File

@@ -0,0 +1,211 @@
defmodule API.GatewayGroupController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.{Gateways, Tokens}
action_fallback API.FallbackController
tags ["Gateway Groups"]
operation :index,
summary: "List Gateway Groups",
parameters: [
limit: [
in: :query,
description: "Limit Gateway Groups returned",
type: :integer,
example: 10
],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok: {"Gateway Group Response", "application/json", API.Schemas.GatewayGroup.ListResponse}
]
# List Gateway Groups / Sites
def index(conn, params) do
list_opts = Pagination.params_to_list_opts(params)
with {:ok, gateway_groups, metadata} <- Gateways.list_groups(conn.assigns.subject, list_opts) do
render(conn, :index, gateway_groups: gateway_groups, metadata: metadata)
end
end
operation :show,
summary: "Show Gateway Group",
parameters: [
id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Gateway Group Response", "application/json", API.Schemas.GatewayGroup.Response}
]
# Show a specific Gateway Group / Site
def show(conn, %{"id" => id}) do
with {:ok, gateway_group} <- Gateways.fetch_group_by_id(id, conn.assigns.subject) do
render(conn, :show, gateway_group: gateway_group)
end
end
operation :create,
summary: "Create Gateway Group",
parameters: [],
request_body:
{"Gateway Group Attributes", "application/json", API.Schemas.GatewayGroup.Request,
required: true},
responses: [
ok: {"Gateway Group Response", "application/json", API.Schemas.GatewayGroup.Response}
]
# Create a new Gateway Group / Site
def create(conn, %{"gateway_group" => params}) do
with {:ok, gateway_group} <- Gateways.create_group(params, conn.assigns.subject) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/gateway_groups/#{gateway_group}")
|> render(:show, gateway_group: gateway_group)
end
end
def create(_conn, _params) do
{:error, :bad_request}
end
operation :update,
summary: "Update a Gateway Group",
request_body:
{"Gateway Group Attributes", "application/json", API.Schemas.GatewayGroup.Request,
required: true},
responses: [
ok: {"Gateway Group Response", "application/json", API.Schemas.GatewayGroup.Response}
]
# Update a Gateway Group / Site
def update(conn, %{"id" => id, "gateway_group" => params}) do
subject = conn.assigns.subject
with {:ok, gateway_group} <- Gateways.fetch_group_by_id(id, subject),
{:ok, gateway_group} <- Gateways.update_group(gateway_group, params, subject) do
render(conn, :show, gateway_group: gateway_group)
end
end
def update(_conn, _params) do
{:error, :bad_request}
end
operation :delete,
summary: "Delete a Gateway Group",
parameters: [
id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Gateway Group Response", "application/json", API.Schemas.GatewayGroup.Response}
]
# Delete a Gateway Group / Site
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, gateway_group} <- Gateways.fetch_group_by_id(id, subject),
{:ok, gateway_group} <- Gateways.delete_group(gateway_group, subject) do
render(conn, :show, gateway_group: gateway_group)
end
end
operation :create_token,
summary: "Create a Gateway Token",
parameters: [
gateway_group_id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok:
{"New Gateway Token Response", "application/json", API.Schemas.GatewayGroupToken.NewToken}
]
# Create a Gateway Group Token (used for deploying a gateway)
def create_token(conn, %{"gateway_group_id" => gateway_group_id}) do
subject = conn.assigns.subject
with {:ok, gateway_group} <- Gateways.fetch_group_by_id(gateway_group_id, subject),
{:ok, gateway_token, encoded_token} <-
Gateways.create_token(gateway_group, %{}, subject) do
conn
|> put_status(:created)
|> render(:token, gateway_token: gateway_token, encoded_token: encoded_token)
end
end
operation :delete_token,
summary: "Delete a Gateway Token",
parameters: [
gateway_group_id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
],
id: [
in: :path,
description: "Gateway Token ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok:
{"Deleted Gateway Token Response", "application/json",
API.Schemas.GatewayGroupToken.DeletedToken}
]
# Delete/Revoke a Gateway Group Token
def delete_token(conn, %{"gateway_group_id" => _gateway_group_id, "id" => token_id}) do
subject = conn.assigns.subject
with {:ok, token} <- Tokens.fetch_token_by_id(token_id, subject),
{:ok, token} <- Tokens.delete_token(token, subject) do
render(conn, :deleted_token, gateway_token: token)
end
end
operation :delete_all_tokens,
summary: "Delete all Gateway Tokens for a given Gateway Group",
parameters: [
gateway_group_id: [
in: :path,
description: "Gateway Group ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok:
{"Deleted Gateway Tokens Response", "application/json",
API.Schemas.GatewayGroupToken.DeletedTokens}
]
def delete_all_tokens(conn, %{"gateway_group_id" => gateway_group_id}) do
subject = conn.assigns.subject
with {:ok, gateway_group} <- Gateways.fetch_group_by_id(gateway_group_id, subject),
{:ok, deleted_tokens} <- Tokens.delete_tokens_for(gateway_group, subject) do
render(conn, :deleted_tokens, %{tokens: deleted_tokens})
end
end
end

View File

@@ -0,0 +1,58 @@
defmodule API.GatewayGroupJSON do
alias API.Pagination
alias Domain.Gateways
@doc """
Renders a list of Sites / Gateway Groups.
"""
def index(%{gateway_groups: gateway_groups, metadata: metadata}) do
%{
data: Enum.map(gateway_groups, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Render a single Site / Gateway Group
"""
def show(%{gateway_group: group}) do
%{data: data(group)}
end
@doc """
Render a Gateway Group Token
"""
def token(%{gateway_token: token, encoded_token: encoded_token}) do
%{
data: %{
id: token.id,
token: encoded_token
}
}
end
@doc """
Render a deleted Gateway Group Token
"""
def deleted_token(%{gateway_token: token}) do
%{
data: %{
id: token.id
}
}
end
@doc """
Render all deleted Gateway Group Tokens
"""
def deleted_tokens(%{tokens: tokens}) do
%{data: for(token <- tokens, do: %{id: token.id})}
end
defp data(%Gateways.Group{} = group) do
%{
id: group.id,
name: group.name
}
end
end

View File

@@ -0,0 +1,31 @@
defmodule API.GatewayJSON do
alias API.Pagination
alias Domain.Gateways
@doc """
Renders a list of Gateways.
"""
def index(%{gateways: gateways, metadata: metadata}) do
%{
data: Enum.map(gateways, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Render a single Gateway
"""
def show(%{gateway: gateway}) do
%{data: data(gateway)}
end
defp data(%Gateways.Gateway{} = gateway) do
%{
id: gateway.id,
name: gateway.name,
ipv4: gateway.ipv4,
ipv6: gateway.ipv6,
online: gateway.online?
}
end
end

View File

@@ -0,0 +1,136 @@
defmodule API.IdentityController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Auth
action_fallback API.FallbackController
tags ["Identities"]
operation :index,
summary: "List Identities for an Actor",
parameters: [
actor_id: [in: :path, description: "Actor ID", type: :string],
limit: [in: :query, description: "Limit Identities returned", type: :integer, example: 10],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok: {"Identity List Response", "application/json", API.Schemas.Identity.ListResponse}
]
# List Identities
def index(conn, %{"actor_id" => actor_id} = params) do
subject = conn.assigns.subject
list_opts = Pagination.params_to_list_opts(params)
with {:ok, actor} <- Domain.Actors.fetch_actor_by_id(actor_id, subject),
{:ok, identities, metadata} <- Auth.list_identities_for(actor, subject, list_opts) do
render(conn, :index, identities: identities, metadata: metadata)
end
end
operation :create,
summary: "Create an Identity for an Actor",
parameters: [
actor_id: [in: :path, description: "Actor ID", type: :string]
],
request_body:
{"Identity Attributes", "application/json", API.Schemas.Identity.Request, required: true},
responses: [
ok: {"Identity Response", "application/json", API.Schemas.Identity.Response}
]
# Create an Identity
def create(conn, %{
"actor_id" => actor_id,
"provider_id" => provider_id,
"identity" => params
}) do
subject = conn.assigns.subject
params =
Map.put_new(
params,
"provider_identifier_confirmation",
Map.get(params, "provider_identifier")
)
with {:ok, actor} <- Domain.Actors.fetch_actor_by_id(actor_id, subject),
{:ok, provider} <- Auth.fetch_provider_by_id(provider_id, subject),
{:provider_check, true} <- valid_provider?(provider),
{:ok, identity} <- Auth.create_identity(actor, provider, params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/actors/#{actor_id}/identities/#{identity.id}")
|> render(:show, identity: identity)
else
{:provider_check, _false} -> {:error, :unprocessable_entity}
other -> other
end
end
def create(_conn, _params) do
{:error, :bad_request}
end
operation :show,
summary: "Show Identity",
parameters: [
id: [
in: :path,
description: "Identity ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Identity Response", "application/json", API.Schemas.Identity.Response}
]
# Show a specific Identity
def show(conn, %{"id" => id}) do
with {:ok, identity} <- Auth.fetch_identity_by_id(id, conn.assigns.subject) do
render(conn, :show, identity: identity)
end
end
operation :delete,
summary: "Delete an Identity",
parameters: [
actor_id: [
in: :path,
description: "Actor ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
],
id: [
in: :path,
description: "Identity ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Identity Response", "application/json", API.Schemas.Identity.Response}
]
# Delete an Identity
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, identity} <- Auth.fetch_identity_by_id(id, subject),
{:ok, identity} <- Auth.delete_identity(identity, subject) do
render(conn, :show, identity: identity)
end
end
defp valid_provider?(provider) do
valid? =
Auth.fetch_provider_capabilities!(provider)
|> Keyword.fetch!(:provisioners)
|> Enum.member?(:manual)
{:provider_check, valid?}
end
end

View File

@@ -0,0 +1,30 @@
defmodule API.IdentityJSON do
alias API.Pagination
alias Domain.Auth.Identity
@doc """
Renders a list of Identities.
"""
def index(%{identities: identities, metadata: metadata}) do
%{
data: Enum.map(identities, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Render a single Identity
"""
def show(%{identity: identity}) do
%{data: data(identity)}
end
defp data(%Identity{} = identity) do
%{
id: identity.id,
actor_id: identity.actor_id,
provider_id: identity.provider_id,
provider_identifier: identity.provider_identifier
}
end
end

View File

@@ -0,0 +1,81 @@
defmodule API.IdentityProviderController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Auth
action_fallback API.FallbackController
tags ["Identity Providers"]
operation :index,
summary: "List Identity Providers",
parameters: [
limit: [
in: :query,
description: "Limit Identity Providers returned",
type: :integer,
example: 10
],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok:
{"Identity Provider Response", "application/json",
API.Schemas.IdentityProvider.ListResponse}
]
def index(conn, params) do
list_opts = Pagination.params_to_list_opts(params)
with {:ok, identity_providers, metadata} <-
Auth.list_providers(conn.assigns.subject, list_opts) do
render(conn, :index, identity_providers: identity_providers, metadata: metadata)
end
end
operation :show,
summary: "Show Identity Provider",
parameters: [
id: [
in: :path,
description: "Identity Provider ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok:
{"Identity Provider Response", "application/json", API.Schemas.IdentityProvider.Response}
]
def show(conn, %{"id" => id}) do
with {:ok, identity_provider} <- Auth.fetch_provider_by_id(id, conn.assigns.subject) do
render(conn, :show, identity_provider: identity_provider)
end
end
operation :delete,
summary: "Delete a Identity Provider",
parameters: [
id: [
in: :path,
description: "Identity Provider ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok:
{"Identity Provider Response", "application/json", API.Schemas.IdentityProvider.Response}
]
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, identity_provider} <- Auth.fetch_provider_by_id(id, subject),
{:ok, identity_provider} <- Auth.delete_provider(identity_provider, subject) do
render(conn, :show, identity_provider: identity_provider)
end
end
end

View File

@@ -0,0 +1,28 @@
defmodule API.IdentityProviderJSON do
alias API.Pagination
alias Domain.Auth.Provider
@doc """
Renders a list of Identity Providers.
"""
def index(%{identity_providers: identity_providers, metadata: metadata}) do
%{
data: Enum.map(identity_providers, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Renders a single Identity Provider.
"""
def show(%{identity_provider: identity_provider}) do
%{data: data(identity_provider)}
end
defp data(%Provider{} = provider) do
%{
id: provider.id,
name: provider.name
}
end
end

View File

@@ -0,0 +1,35 @@
defmodule API.Pagination do
alias LoggerJSON.Formatter.Metadata
alias Domain.Repo.Paginator.Metadata
def params_to_list_opts(params) do
[
page: params_to_page(params)
]
end
def metadata(%Metadata{} = metadata) do
%{
count: metadata.count,
limit: metadata.limit,
next_page: metadata.next_page_cursor,
prev_page: metadata.previous_page_cursor
}
end
defp params_to_page(%{"limit" => limit, "page_cursor" => cursor}) do
[cursor: cursor, limit: String.to_integer(limit)]
end
defp params_to_page(%{"limit" => limit}) do
[limit: String.to_integer(limit)]
end
defp params_to_page(%{"page_cursor" => cursor}) do
[cursor: cursor]
end
defp params_to_page(_params) do
[]
end
end

View File

@@ -0,0 +1,119 @@
defmodule API.PolicyController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Policies
action_fallback API.FallbackController
tags ["Policies"]
operation :index,
summary: "List Policies",
parameters: [
limit: [in: :query, description: "Limit Policies returned", type: :integer, example: 10],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok: {"Policy Response", "application/json", API.Schemas.Policy.ListResponse}
]
# List Policies
def index(conn, params) do
list_opts = Pagination.params_to_list_opts(params)
with {:ok, policies, metadata} <- Policies.list_policies(conn.assigns.subject, list_opts) do
render(conn, :index, policies: policies, metadata: metadata)
end
end
operation :show,
summary: "Show Policy",
parameters: [
id: [
in: :path,
description: "Policy ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Policy Response", "application/json", API.Schemas.Policy.Response}
]
# Show a specific Policy
def show(conn, %{"id" => id}) do
with {:ok, policy} <- Policies.fetch_policy_by_id(id, conn.assigns.subject) do
render(conn, :show, policy: policy)
end
end
operation :create,
summary: "Create Policy",
parameters: [],
request_body:
{"Policy Attributes", "application/json", API.Schemas.Policy.Request, required: true},
responses: [
ok: {"Policy Response", "application/json", API.Schemas.Policy.Response}
]
# Create a new Policy
def create(conn, %{"policy" => params}) do
with {:ok, policy} <- Policies.create_policy(params, conn.assigns.subject) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/policies/#{policy}")
|> render(:show, policy: policy)
end
end
def create(_conn, _params) do
{:error, :bad_request}
end
operation :update,
summary: "Update a Policy",
request_body:
{"Policy Attributes", "application/json", API.Schemas.Policy.Request, required: true},
responses: [
ok: {"Policy Response", "application/json", API.Schemas.Policy.Response}
]
# Update a Policy
def update(conn, %{"id" => id, "policy" => params}) do
subject = conn.assigns.subject
with {:ok, policy} <- Policies.fetch_policy_by_id(id, subject),
{:ok, policy} <- Policies.update_policy(policy, params, subject) do
render(conn, :show, policy: policy)
end
end
def update(_conn, _params) do
{:error, :bad_request}
end
operation :delete,
summary: "Delete a Policy",
parameters: [
id: [
in: :path,
description: "Policy ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Policy Response", "application/json", API.Schemas.Policy.Response}
]
# Delete a Policy
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, policy} <- Policies.fetch_policy_by_id(id, subject),
{:ok, policy} <- Policies.delete_policy(policy, subject) do
render(conn, :show, policy: policy)
end
end
end

View File

@@ -0,0 +1,30 @@
defmodule API.PolicyJSON do
alias API.Pagination
alias Domain.Policies
@doc """
Renders a list of Policies.
"""
def index(%{policies: policies, metadata: metadata}) do
%{
data: Enum.map(policies, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Render a single Policy
"""
def show(%{policy: policy}) do
%{data: data(policy)}
end
defp data(%Policies.Policy{} = policy) do
%{
id: policy.id,
actor_group_id: policy.actor_group_id,
resource_id: policy.resource_id,
description: policy.description
}
end
end

View File

@@ -0,0 +1,122 @@
defmodule API.ResourceController do
use API, :controller
use OpenApiSpex.ControllerSpecs
alias API.Pagination
alias Domain.Resources
action_fallback API.FallbackController
tags ["Resources"]
operation :index,
summary: "List Resources",
parameters: [
limit: [in: :query, description: "Limit Resources returned", type: :integer, example: 10],
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
],
responses: [
ok: {"Resource Response", "application/json", API.Schemas.Resource.ListResponse}
]
def index(conn, params) do
list_opts = Pagination.params_to_list_opts(params)
with {:ok, resources, metadata} <-
Resources.list_resources(conn.assigns.subject, list_opts) do
render(conn, :index, resources: resources, metadata: metadata)
end
end
operation :show,
summary: "Show Resource",
parameters: [
id: [
in: :path,
description: "Resource ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Resource Response", "application/json", API.Schemas.Resource.Response}
]
def show(conn, %{"id" => id}) do
with {:ok, resource} <- Resources.fetch_resource_by_id(id, conn.assigns.subject) do
render(conn, :show, resource: resource)
end
end
operation :create,
summary: "Create Resource",
parameters: [],
request_body:
{"Resource Attributes", "application/json", API.Schemas.Resource.Request, required: true},
responses: [
ok: {"Resource Response", "application/json", API.Schemas.Resource.Response}
]
def create(conn, %{"resource" => params}) do
attrs = set_param_defaults(params)
with {:ok, resource} <- Resources.create_resource(attrs, conn.assigns.subject) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/resources/#{resource}")
|> render(:show, resource: resource)
end
end
def create(_conn, _params) do
{:error, :bad_request}
end
operation :update,
summary: "Update Resource",
request_body:
{"Resource Attributes", "application/json", API.Schemas.Resource.Request, required: true},
responses: [
ok: {"Resource Response", "application/json", API.Schemas.Resource.Response}
]
def update(conn, %{"id" => id, "resource" => params}) do
subject = conn.assigns.subject
attrs = set_param_defaults(params)
with {:ok, resource} <- Resources.fetch_resource_by_id(id, subject),
{:ok, resource} <- Resources.update_resource(resource, attrs, subject) do
render(conn, :show, resource: resource)
end
end
def update(_conn, _params) do
{:error, :bad_request}
end
operation :delete,
summary: "Delete Resource",
parameters: [
id: [
in: :path,
description: "Resource ID",
type: :string,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {"Resource Response", "application/json", API.Schemas.Resource.Response}
]
def delete(conn, %{"id" => id}) do
subject = conn.assigns.subject
with {:ok, resource} <- Resources.fetch_resource_by_id(id, subject),
{:ok, resource} <- Resources.delete_resource(resource, subject) do
render(conn, :show, resource: resource)
end
end
defp set_param_defaults(params) do
Map.put_new(params, "filters", %{})
end
end

View File

@@ -0,0 +1,31 @@
defmodule API.ResourceJSON do
alias API.Pagination
alias Domain.Resources.Resource
@doc """
Renders a list of resources.
"""
def index(%{resources: resources, metadata: metadata}) do
%{
data: Enum.map(resources, &data/1),
metadata: Pagination.metadata(metadata)
}
end
@doc """
Renders a single resource.
"""
def show(%{resource: resource}) do
%{data: data(resource)}
end
defp data(%Resource{} = resource) do
%{
id: resource.id,
name: resource.name,
address: resource.address,
description: resource.address_description,
type: resource.type
}
end
end

View File

@@ -1,12 +1,12 @@
defmodule API.ErrorView do
def render("500.json", _assigns) do
%{errors: %{detail: "internal_error"}}
%{error: %{reason: "internal_error"}}
end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
%{error: %{reason: Phoenix.Controller.status_message_from_template(template)}}
end
end

View File

@@ -11,9 +11,16 @@ defmodule API.Plugs.Auth do
assign(conn, :subject, subject)
else
_ ->
# conn
# |> put_resp_content_type("application/json")
# |> send_resp(401, Jason.encode!(%{"error" => "invalid_access_token"}))
# |> halt()
# TODO: BRIAN - Confirm that this change won't break anything with the clients or gateways
conn
|> put_resp_content_type("application/json")
|> send_resp(401, Jason.encode!(%{"error" => "invalid_access_token"}))
|> put_status(401)
|> Phoenix.Controller.put_view(json: API.ErrorJSON)
|> Phoenix.Controller.render(:"401")
|> halt()
end
end

View File

@@ -15,16 +15,53 @@ defmodule API.Router do
plug :accepts, ["html", "xml", "json"]
end
pipeline :openapi do
plug OpenApiSpex.Plug.PutApiSpec, module: API.ApiSpec
end
scope "/openapi" do
pipe_through :openapi
get "/", OpenApiSpex.Plug.RenderSpec, []
end
scope "/swaggerui" do
pipe_through :public
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/openapi"
end
scope "/", API do
pipe_through :public
get "/healthz", HealthController, :healthz
end
scope "/v1", API do
scope "/", API do
pipe_through :api
post "/echo", ExampleController, :echo
resources "/resources", ResourceController, except: [:new, :edit]
resources "/policies", PolicyController, except: [:new, :edit]
resources "/gateway_groups", GatewayGroupController, except: [:new, :edit] do
post "/tokens", GatewayGroupController, :create_token
delete "/tokens", GatewayGroupController, :delete_all_tokens
delete "/tokens/:id", GatewayGroupController, :delete_token
resources "/gateways", GatewayController, except: [:new, :edit, :create, :update]
end
resources "/actors", ActorController, except: [:new, :edit] do
resources "/identities", IdentityController, except: [:new, :edit, :update]
post "/providers/:provider_id/identities/", IdentityController, :create
end
resources "/actor_groups", ActorGroupController, except: [:new, :edit] do
get "/memberships", ActorGroupMembershipController, :index
put "/memberships", ActorGroupMembershipController, :update_put
patch "/memberships", ActorGroupMembershipController, :update_patch
end
resources "/identity_providers", IdentityProviderController, only: [:index, :show, :delete]
end
scope "/integrations", API.Integrations do

View File

@@ -0,0 +1,170 @@
defmodule API.Schemas.ActorGroupMembership do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Actor Group Membership",
description: "Actor Group Membership",
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{type: :string, description: "Actor ID"},
name: %Schema{type: :string, description: "Actor Name"},
type: %Schema{type: :string, description: "Actor Type"}
}
},
example: [
%{
"id" => "7cb89288-1fb3-433e-a522-2d087e45988d",
"name" => "John Doe",
"type" => "account_user"
}
]
})
end
defmodule PatchRequest do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.ActorGroupMembership
OpenApiSpex.schema(%{
title: "Actor Group Membership Patch Request",
description: "PATCH body for updating Actor Group Memberships",
type: :object,
properties: %{
memberships: %Schema{
type: :object,
properties: %{
add: %Schema{
type: :array,
description: "Array of Actor IDs",
items: %Schema{type: :string, description: "Actor ID"}
},
remove: %Schema{
type: :array,
description: "Array of Actor IDs",
items: %Schema{type: :string, description: "Actor ID"}
}
}
}
},
required: [:memberships],
example: %{
"memberships" => %{
"add" => ["1234-1234"],
"remove" => ["2345-2345"]
}
}
})
end
defmodule PutRequest do
require OpenApiSpex
alias Ecto.Adapter.Schema
alias Ecto.Adapter.Schema
alias Ecto.Adapter.Schema
alias OpenApiSpex.Schema
alias API.Schemas.ActorGroupMembership
OpenApiSpex.schema(%{
title: "Actor Group Membership Put Request",
description: "PUT body for updating Actor Group Memberships",
type: :object,
properties: %{
memberships: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
actor_id: %Schema{type: :string, description: "Actor ID"}
}
}
}
},
required: [:memberships],
example: %{
"memberships" => [
%{"actor_id" => "1234-1234"},
%{"actor_id" => "2345-2345"}
]
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.ActorGroupMembership
OpenApiSpex.schema(%{
title: "Actor Group Membership List Response",
description: "Response schema for Actor Group Memberships",
type: :object,
properties: %{
data: %Schema{
description: "Actor Group Membership details",
type: :array,
items: ActorGroupMembership.Schema
},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "John Doe",
"type" => "account_user"
},
%{
"id" => "cc9f561a-444d-4083-ab38-0abc6cf2314c",
"name" => "Jane Smith",
"type" => "account_admin_user"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
defmodule MembershipResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Actor Group Membership Response",
description: "Response schema for Actor Group Membership Updates",
type: :object,
properties: %{
data: %Schema{
type: :object,
description: "Actor Group Memberships",
properties: %{
actor_ids: %Schema{
description: "Actor IDs",
type: :array,
items: %Schema{type: :string, description: "Actor ID"}
}
}
}
},
example: %{
"data" => %{
"actor_ids" => [
"4ddfa557-7dfc-484f-894c-2024ec3fe9f7",
"89d22f71-939d-442d-b148-897b730adfb4"
]
}
}
})
end
end

View File

@@ -0,0 +1,99 @@
defmodule API.Schemas.ActorGroup do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Actor Group",
description: "Actor Group",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Actor Group ID"},
name: %Schema{type: :string, description: "Actor Group Name"}
},
required: [:id, :name],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Engineering"
}
})
end
defmodule Request do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.ActorGroup
OpenApiSpex.schema(%{
title: "Actor Group Request",
description: "POST body for creating an Actor Group",
type: :object,
properties: %{
actor_group: %Schema{anyOf: [ActorGroup.Schema]}
},
required: [:actor_group],
example: %{
"actor_group" => %{
"name" => "Engineering"
}
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.ActorGroup
OpenApiSpex.schema(%{
title: "Actor GroupResponse",
description: "Response schema for single Actor Group",
type: :object,
properties: %{
data: ActorGroup.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Engineering"
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.ActorGroup
OpenApiSpex.schema(%{
title: "Actor Group List Response",
description: "Response schema for multiple Actor Groups",
type: :object,
properties: %{
data: %Schema{description: "Actor Group details", type: :array, items: ActorGroup.Schema},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Engineering"
},
%{
"id" => "4ae929a7-1973-43f2-a1a8-9221b91a4c0e",
"name" => "Finance"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -0,0 +1,109 @@
defmodule API.Schemas.Actor do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Actor",
description: "Actor",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Actor ID"},
name: %Schema{
type: :string,
description: "Actor Name",
pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/
},
type: %Schema{type: :string, description: "Actor Type"}
},
required: [:name, :type],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "John Doe",
"type" => "account_admin_user"
}
})
end
defmodule Request do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Actor
OpenApiSpex.schema(%{
title: "ActorRequest",
description: "POST body for creating an Actor",
type: :object,
properties: %{
actor: %Schema{anyOf: [Actor.Schema]}
},
required: [:actor],
example: %{
"actor" => %{
"name" => "Joe User",
"type" => "account_admin_user"
}
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Actor
OpenApiSpex.schema(%{
title: "ActorResponse",
description: "Response schema for single Actor",
type: :object,
properties: %{
data: Actor.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "John Doe",
"type" => "account_admin_user"
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Actor
OpenApiSpex.schema(%{
title: "ActorsResponse",
description: "Response schema for multiple Actors",
type: :object,
properties: %{
data: %Schema{description: "Actors details", type: :array, items: Actor.Schema},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "John Doe",
"type" => "account_admin_user"
},
%{
"id" => "84e7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Jane Smith",
"type" => "account_user"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -0,0 +1,103 @@
defmodule API.Schemas.GatewayGroup do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Gateway Group",
description: "Gateway Group",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Gateway Group ID"},
name: %Schema{type: :string, description: "Gateway Group Name"}
},
required: [:id, :name],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "vpc-us-east"
}
})
end
defmodule Request do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.GatewayGroup
OpenApiSpex.schema(%{
title: "Gateway GroupRequest",
description: "POST body for creating a Gateway Group",
type: :object,
properties: %{
gateway_group: %Schema{anyOf: [GatewayGroup.Schema]}
},
required: [:gateway_group],
example: %{
"gateway_group" => %{
"name" => "vpc-us-east"
}
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.GatewayGroup
OpenApiSpex.schema(%{
title: "Gateway GroupResponse",
description: "Response schema for single Gateway Group",
type: :object,
properties: %{
data: GatewayGroup.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "vpc-us-east"
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.GatewayGroup
OpenApiSpex.schema(%{
title: "Gateway Group List Response",
description: "Response schema for multiple Gateway Groups",
type: :object,
properties: %{
data: %Schema{
description: "Gateway Group details",
type: :array,
items: GatewayGroup.Schema
},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "vpc-us-east"
},
%{
"id" => "6301d7d2-4938-4123-87de-282c01cca656",
"name" => "vpc-us-west"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -0,0 +1,93 @@
defmodule API.Schemas.GatewayGroupToken do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Gateway Group Token",
description: "Gateway Group Token",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Gateway Group Token ID"},
token: %Schema{type: :string, description: "Gateway Group Token"}
},
required: [:id, :token],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"token" => "secret-token-here"
}
})
end
defmodule NewToken do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.GatewayGroupToken
OpenApiSpex.schema(%{
title: "New Gateway Group Token Response",
description: "Response schema for a new Gateway Group Token",
type: :object,
properties: %{
data: GatewayGroupToken.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"token" => "secret-token-here"
}
}
})
end
defmodule DeletedToken do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.GatewayGroupToken
OpenApiSpex.schema(%{
title: "Deleted Gateway Group Token Response",
description: "Response schema for a new Gateway Group Token",
type: :object,
properties: %{
data: GatewayGroupToken.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205"
}
}
})
end
defmodule DeletedTokens do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.GatewayGroupToken
OpenApiSpex.schema(%{
title: "Deleted Gateway Group Token List Response",
description: "Response schema for deleted Gateway Group Tokens",
type: :object,
properties: %{
data: %Schema{
description: "Deleted Gateway Group Tokens",
type: :array,
items: GatewayGroupToken.Schema
}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205"
},
%{
"id" => "6301d7d2-4938-4123-87de-282c01cca656"
}
]
}
})
end
end

View File

@@ -0,0 +1,106 @@
defmodule API.Schemas.Gateway do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Gateway",
description: "Gateway",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Gateway ID"},
name: %Schema{
type: :string,
description: "Gateway Name",
pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/
},
ipv4: %Schema{
type: :string,
description: "IPv4 Address of Gateway"
},
ipv6: %Schema{
type: :string,
description: "IPv6 Address of Gateway"
},
online: %Schema{
type: :boolean,
description: "Online status of Gateway"
}
},
required: [:name, :type],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "vpc-us-east",
"ipv4" => "1.2.3.4",
"ipv6" => "",
"online" => true
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Gateway
OpenApiSpex.schema(%{
title: "Gateway Response",
description: "Response schema for single Gateway",
type: :object,
properties: %{
data: Gateway.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "vpc-us-east",
"ipv4" => "1.2.3.4",
"ipv6" => "",
"online" => true
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Gateway
OpenApiSpex.schema(%{
title: "GatewaysResponse",
description: "Response schema for multiple Gateways",
type: :object,
properties: %{
data: %Schema{description: "Gateways details", type: :array, items: Gateway.Schema},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "vpc-us-east",
"ipv4" => "1.2.3.4",
"ipv6" => "",
"online" => true
},
%{
"id" => "6ecc106b-75c1-48a5-846c-14782180c1ff",
"name" => "vpc-us-west",
"ipv4" => "5.6.7.8",
"ipv6" => "",
"online" => true
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -0,0 +1,82 @@
defmodule API.Schemas.IdentityProvider do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Identity Provider",
description: "Identity Provider",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Identity Provider ID"},
name: %Schema{type: :string, description: "Identity Provider name"}
},
required: [:id, :name],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "OIDC Provider"
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.IdentityProvider
OpenApiSpex.schema(%{
title: "Identity Provider Response",
description: "Response schema for single Identity Provider",
type: :object,
properties: %{
data: IdentityProvider.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "OIDC Provider"
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.IdentityProvider
OpenApiSpex.schema(%{
title: "Identity Provider List Response",
description: "Response schema for multiple Identity Providers",
type: :object,
properties: %{
data: %Schema{
description: "Identity Provider details",
type: :array,
items: IdentityProvider.Schema
},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "OIDC Provider"
},
%{
"id" => "23ca9d03-c904-42c9-bd38-f89a6d57d3a8",
"name" => "Okta"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -0,0 +1,111 @@
defmodule API.Schemas.Identity do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Identity",
description: "Identity",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Identity ID"},
actor_id: %Schema{type: :string, description: "Actor ID"},
provider_id: %Schema{type: :string, description: "Identity Provider ID"},
provider_identifier: %Schema{type: :string, description: "Identifier from Provider"}
},
required: [:id, :actor_id, :provider_id, :provider_identifier],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"actor_id" => "cdfa97e6-cca1-41db-8fc7-864daedb46df",
"provider_id" => "989f9e96-e348-47ec-ba85-869fcd7adb19",
"provider_identifier" => "foo@bar.com"
}
})
end
defmodule Request do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Identity
OpenApiSpex.schema(%{
title: "IdentityRequest",
description: "POST body for creating a Identity",
type: :object,
properties: %{
identity: %Schema{anyOf: [Identity.Schema]}
},
required: [:identity],
example: %{
"identity" => %{
"actor_id" => "cdfa97e6-cca1-41db-8fc7-864daedb46df",
"provider_id" => "989f9e96-e348-47ec-ba85-869fcd7adb19",
"provider_identifier" => "foo@bar.com"
}
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Identity
OpenApiSpex.schema(%{
title: "IdentityResponse",
description: "Response schema for single Identity",
type: :object,
properties: %{
data: Identity.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"actor_id" => "cdfa97e6-cca1-41db-8fc7-864daedb46df",
"provider_id" => "989f9e96-e348-47ec-ba85-869fcd7adb19",
"provider_identifier" => "foo@bar.com"
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Identity
OpenApiSpex.schema(%{
title: "Identity List Response",
description: "Response schema for multiple Identities",
type: :object,
properties: %{
data: %Schema{description: "Identity details", type: :array, items: Identity.Schema},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"actor_id" => "8f44a02b-b8eb-406f-8202-4274bf60ebd0",
"provider_id" => "6472d898-5b98-4c3b-b4b9-d3158c1891be",
"provider_identifier" => "foo@bar.com"
},
%{
"id" => "8a70eb96-e74b-4cdc-91b8-48c05ef74d4c",
"actor_id" => "38c92cda-1ddb-45b3-9d1a-7efc375e00c1",
"provider_id" => "04f13eed-4845-47c3-833e-fdd869fab96f",
"provider_identifier" => "baz@bar.com"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -0,0 +1,111 @@
defmodule API.Schemas.Policy do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Policy",
description: "Policy",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Policy ID"},
actor_group_id: %Schema{type: :string, description: "Actor Group ID"},
resource_id: %Schema{type: :string, description: "Resource ID"},
description: %Schema{type: :string, description: "Policy Description"}
},
required: [:name, :type],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"actor_group_id" => "88eae9ce-9179-48c6-8430-770e38dd4775",
"resource_id" => "a9f60587-793c-46ae-8525-597f43ab2fb1",
"description" => "Policy to allow something"
}
})
end
defmodule Request do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Policy
OpenApiSpex.schema(%{
title: "Policy Request",
description: "POST body for creating a Policy",
type: :object,
properties: %{
policy: %Schema{anyOf: [Policy.Schema]}
},
required: [:policy],
example: %{
"policy" => %{
"resource_id" => "a9f60587-793c-46ae-8525-597f43ab2fb1",
"actor_group_id" => "88eae9ce-9179-48c6-8430-770e38dd4775",
"description" => "Policy to allow something"
}
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Policy
OpenApiSpex.schema(%{
title: "Policy Response",
description: "Response schema for single Policy",
type: :object,
properties: %{
data: Policy.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"resource_id" => "a9f60587-793c-46ae-8525-597f43ab2fb1",
"actor_group_id" => "88eae9ce-9179-48c6-8430-770e38dd4775",
"description" => "Policy to allow something"
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Policy
OpenApiSpex.schema(%{
title: "Policy List Response",
description: "Response schema for multiple Policies",
type: :object,
properties: %{
data: %Schema{description: "Policy details", type: :array, items: Policy.Schema},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"resource_id" => "a9f60587-793c-46ae-8525-597f43ab2fb1",
"actor_group_id" => "88eae9ce-9179-48c6-8430-770e38dd4775",
"description" => "Policy to allow something"
},
%{
"id" => "6301d7d2-4938-4123-87de-282c01cca656",
"resource_id" => "9876bd25-0f6c-48fb-a9fd-196ba9be86e5",
"actor_group_id" => "343385a2-5437-4c66-8744-1332421ff736",
"description" => "Policy to allow something else"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -0,0 +1,118 @@
defmodule API.Schemas.Resource do
alias OpenApiSpex.Schema
defmodule Schema do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Resource",
description: "Resource",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Resource ID"},
name: %Schema{type: :string, description: "Resource name"},
address: %Schema{type: :string, description: "Resource address"},
description: %Schema{type: :string, description: "Resource description"},
type: %Schema{type: :string, description: "Resource type"}
},
required: [:name, :type],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Prod DB",
"address" => "10.0.0.10",
"description" => "Production Database",
"type" => "ip"
}
})
end
defmodule Request do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Resource
OpenApiSpex.schema(%{
title: "ResourceRequest",
description: "POST body for creating a Resource",
type: :object,
properties: %{
resource: %Schema{anyOf: [Resource.Schema]}
},
required: [:resource],
example: %{
"resource" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Prod DB",
"address" => "10.0.0.10",
"description" => "Production Database",
"type" => "ip"
}
}
})
end
defmodule Response do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Resource
OpenApiSpex.schema(%{
title: "ResourceResponse",
description: "Response schema for single Resource",
type: :object,
properties: %{
data: Resource.Schema
},
example: %{
"data" => %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Prod DB",
"address" => "10.0.0.10",
"description" => "Production Database",
"type" => "ip"
}
}
})
end
defmodule ListResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
alias API.Schemas.Resource
OpenApiSpex.schema(%{
title: "Gateway List Response",
description: "Response schema for multiple Gateways",
type: :object,
properties: %{
data: %Schema{description: "Resource details", type: :array, items: Resource.Schema},
metadata: %Schema{description: "Pagination metadata", type: :object}
},
example: %{
"data" => [
%{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"name" => "Prod DB",
"address" => "10.0.0.10",
"description" => "Production Database",
"type" => "ip"
},
%{
"id" => "3b9451c9-5616-48f8-827f-009ace22d015",
"name" => "Admin Dashboard",
"address" => "10.0.0.20",
"description" => "Production Admin Dashboard",
"type" => "ip"
}
],
"metadata" => %{
"limit" => 10,
"total" => 100,
"prev_page" => "123123425",
"next_page" => "98776234123"
}
}
})
end
end

View File

@@ -58,6 +58,8 @@ defmodule API.MixProject do
# Other deps
{:jason, "~> 1.2"},
{:remote_ip, "~> 1.1"},
{:open_api_spex, "~> 3.18"},
{:ymlr, "~> 2.0"},
# Test deps
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},

View File

@@ -0,0 +1,236 @@
defmodule API.ActorControllerTest do
use API.ConnCase, async: true
alias Domain.Actors.Actor
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn} do
conn = get(conn, "/actors")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all actors", %{conn: conn, account: account, actor: actor} do
actors = for _ <- 1..3, do: Fixtures.Actors.create_actor(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actors")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 4
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
actor_ids = Enum.map(actors, & &1.id) ++ [actor.id]
assert equal_ids?(data_ids, actor_ids)
end
test "lists actors with limit", %{conn: conn, account: account, actor: actor} do
actors = for _ <- 1..3, do: Fixtures.Actors.create_actor(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actors", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 4
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
actor_ids = (Enum.map(actors, & &1.id) ++ [actor.id]) |> MapSet.new()
assert MapSet.subset?(data_ids, actor_ids)
end
end
describe "show/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
actor = Fixtures.Actors.create_actor(%{account: account})
conn = get(conn, "/actors/#{actor.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single actor", %{conn: conn, account: account, actor: api_actor} do
actor = Fixtures.Actors.create_actor(%{account: account})
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> get("/actors/#{actor.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => actor.id,
"name" => actor.name,
"type" => Atom.to_string(actor.type)
}
}
end
end
describe "create/2" do
test "returns error when not authorized", %{conn: conn} do
conn = post(conn, "/actors", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, actor: api_actor} do
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "returns error on invalid attrs", %{conn: conn, actor: api_actor} do
attrs = %{}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors", actor: attrs)
assert resp = json_response(conn, 422)
assert resp ==
%{
"error" => %{
"reason" => "Unprocessable Entity",
"validation_errors" => %{
"name" => ["can't be blank"],
"type" => ["can't be blank"]
}
}
}
end
test "creates a actor with valid attrs", %{conn: conn, actor: api_actor} do
# TODO: At the moment, API clients aren't allowed to create admin users
attrs = %{
"name" => "Test User",
"type" => "account_user"
}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors", actor: attrs)
assert resp = json_response(conn, 201)
assert resp["data"]["name"] == attrs["name"]
assert resp["data"]["type"] == attrs["type"]
end
end
describe "update/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
actor = Fixtures.Actors.create_actor(%{account: account})
conn = put(conn, "/actors/#{actor.id}", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, account: account, actor: api_actor} do
actor = Fixtures.Actors.create_actor(%{account: account, type: :account_admin_user})
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> put("/actors/#{actor.id}")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "updates an actor", %{conn: conn, account: account, actor: api_actor} do
actor = Fixtures.Actors.create_actor(%{account: account, type: :account_admin_user})
_other_admin = Fixtures.Actors.create_actor(%{account: account, type: :account_admin_user})
attrs = %{"type" => "account_user"}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> put("/actors/#{actor.id}", actor: attrs)
assert resp = json_response(conn, 200)
assert resp["data"]["id"] == actor.id
assert resp["data"]["name"] == actor.name
assert resp["data"]["type"] == attrs["type"]
end
end
describe "delete/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
actor = Fixtures.Actors.create_actor(%{account: account})
conn = delete(conn, "/actors/#{actor.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes a resource", %{conn: conn, account: account, actor: api_actor} do
actor = Fixtures.Actors.create_actor(%{account: account})
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> delete("/actors/#{actor.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => actor.id,
"name" => actor.name,
"type" => Atom.to_string(actor.type)
}
}
assert actor = Repo.get(Actor, actor.id)
assert actor.deleted_at
end
end
end

View File

@@ -0,0 +1,226 @@
defmodule API.ActorGroupControllerTest do
use API.ConnCase, async: true
alias Domain.Actors.Group
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn} do
conn = get(conn, "/actor_groups")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all actor groups", %{conn: conn, account: account, actor: actor} do
actor_groups = for _ <- 1..3, do: Fixtures.Actors.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actor_groups")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
actor_group_ids = Enum.map(actor_groups, & &1.id)
assert equal_ids?(data_ids, actor_group_ids)
end
test "lists actor groups with limit", %{conn: conn, account: account, actor: actor} do
actor_groups = for _ <- 1..3, do: Fixtures.Actors.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actor_groups", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
actor_group_ids = Enum.map(actor_groups, & &1.id) |> MapSet.new()
assert MapSet.subset?(data_ids, actor_group_ids)
end
end
describe "show/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn = get(conn, "/actor_groups/#{actor_group.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single actor group", %{conn: conn, account: account, actor: actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actor_groups/#{actor_group.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => actor_group.id,
"name" => actor_group.name
}
}
end
end
describe "create/2" do
test "returns error when not authorized", %{conn: conn} do
conn = post(conn, "/actor_groups", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, actor: actor} do
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/actor_groups")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "returns error on invalid attrs", %{conn: conn, actor: actor} do
attrs = %{}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/actor_groups", actor_group: attrs)
assert resp = json_response(conn, 422)
assert resp ==
%{
"error" => %{
"reason" => "Unprocessable Entity",
"validation_errors" => %{"name" => ["can't be blank"]}
}
}
end
test "creates an actor group with valid attrs", %{conn: conn, actor: actor} do
attrs = %{
"name" => "Test Actor Group"
}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/actor_groups", actor_group: attrs)
assert resp = json_response(conn, 201)
assert resp["data"]["name"] == attrs["name"]
end
end
describe "update/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn = put(conn, "/actor_groups/#{actor_group.id}", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, account: account, actor: actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/actor_groups/#{actor_group.id}")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "updates an actor group", %{conn: conn, account: account, actor: actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
attrs = %{"name" => "Updated Actor Group"}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/actor_groups/#{actor_group.id}", actor_group: attrs)
assert resp = json_response(conn, 200)
assert resp["data"]["id"] == actor_group.id
assert resp["data"]["name"] == attrs["name"]
end
end
describe "delete/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn = delete(conn, "/actor_groups/#{actor_group.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes an actor group", %{conn: conn, account: account, actor: actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/actor_groups/#{actor_group.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => actor_group.id,
"name" => actor_group.name
}
}
assert actor_group = Repo.get(Group, actor_group.id)
assert actor_group.deleted_at
end
end
end

View File

@@ -0,0 +1,253 @@
defmodule API.ActorGroupMembershipControllerTest do
use API.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn = get(conn, "/actor_groups/#{actor_group.id}/memberships")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all memberships", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
memberships =
for _ <- 1..3,
do: Fixtures.Actors.create_membership(%{account: account, group: actor_group})
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> get("/actor_groups/#{actor_group.id}/memberships")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
membership_ids = Enum.map(memberships, & &1.actor_id)
assert equal_ids?(membership_ids, data_ids)
end
test "lists identity providers with limit", %{conn: conn, account: account, actor: actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
memberships =
for _ <- 1..3,
do: Fixtures.Actors.create_membership(%{account: account, group: actor_group})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actor_groups/#{actor_group.id}/memberships", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
assert MapSet.size(data_ids) == 2
membership_ids =
Enum.map(memberships, & &1.actor_id) |> MapSet.new()
assert MapSet.subset?(data_ids, membership_ids)
end
end
describe "update_patch/2" do
test "adds actor to group", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
actor = Fixtures.Actors.create_actor(%{account: account})
attrs = %{"add" => [actor.id]}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> patch("/actor_groups/#{actor_group.id}/memberships", memberships: attrs)
assert %{"data" => data} = json_response(conn, 200)
assert data == %{"actor_ids" => [actor.id]}
end
test "returns error on empty params/body", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> patch("/actor_groups/#{actor_group.id}/memberships")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "removes actor from group", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
actor1 = Fixtures.Actors.create_actor(%{account: account})
actor2 = Fixtures.Actors.create_actor(%{account: account})
Fixtures.Actors.create_membership(%{account: account, actor: actor1, group: actor_group})
Fixtures.Actors.create_membership(%{account: account, actor: actor2, group: actor_group})
attrs = %{"remove" => [actor2.id]}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> patch("/actor_groups/#{actor_group.id}/memberships", memberships: attrs)
assert %{"data" => data} = json_response(conn, 200)
assert data == %{"actor_ids" => [actor1.id]}
end
test "adds and removes actors from group", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
actor1 = Fixtures.Actors.create_actor(%{account: account})
actor2 = Fixtures.Actors.create_actor(%{account: account})
actor3 = Fixtures.Actors.create_actor(%{account: account})
Fixtures.Actors.create_membership(%{account: account, actor: actor1, group: actor_group})
Fixtures.Actors.create_membership(%{account: account, actor: actor2, group: actor_group})
attrs = %{"add" => [actor3.id], "remove" => [actor2.id]}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> patch("/actor_groups/#{actor_group.id}/memberships", memberships: attrs)
assert %{"data" => data} = json_response(conn, 200)
assert Enum.sort(data["actor_ids"]) == Enum.sort([actor1.id, actor3.id])
end
test "group remains the same on empty params", %{
conn: conn,
account: account,
actor: api_actor
} do
actor_group = Fixtures.Actors.create_group(%{account: account})
actor1 = Fixtures.Actors.create_actor(%{account: account})
actor2 = Fixtures.Actors.create_actor(%{account: account})
Fixtures.Actors.create_membership(%{account: account, actor: actor1, group: actor_group})
Fixtures.Actors.create_membership(%{account: account, actor: actor2, group: actor_group})
attrs = %{}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> patch("/actor_groups/#{actor_group.id}/memberships", memberships: attrs)
assert %{"data" => data} = json_response(conn, 200)
assert Enum.sort(data["actor_ids"]) == Enum.sort([actor1.id, actor2.id])
end
end
describe "update_put/2" do
test "adds actor to group", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
actor = Fixtures.Actors.create_actor(%{account: account})
attrs = [%{"actor_id" => actor.id}]
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> put("/actor_groups/#{actor_group.id}/memberships", memberships: attrs)
assert %{"data" => data} = json_response(conn, 200)
assert data == %{"actor_ids" => [actor.id]}
end
test "returns error on empty params/body", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> put("/actor_groups/#{actor_group.id}/memberships")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "removes actor from group", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
actor1 = Fixtures.Actors.create_actor(%{account: account})
actor2 = Fixtures.Actors.create_actor(%{account: account})
Fixtures.Actors.create_membership(%{account: account, actor: actor1, group: actor_group})
Fixtures.Actors.create_membership(%{account: account, actor: actor2, group: actor_group})
attrs = [%{"actor_id" => actor1.id}]
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> put("/actor_groups/#{actor_group.id}/memberships", memberships: attrs)
assert %{"data" => data} = json_response(conn, 200)
assert data == %{"actor_ids" => [actor1.id]}
end
test "adds and removes actors from group", %{conn: conn, account: account, actor: api_actor} do
actor_group = Fixtures.Actors.create_group(%{account: account})
actor1 = Fixtures.Actors.create_actor(%{account: account})
actor2 = Fixtures.Actors.create_actor(%{account: account})
actor3 = Fixtures.Actors.create_actor(%{account: account})
Fixtures.Actors.create_membership(%{account: account, actor: actor1, group: actor_group})
Fixtures.Actors.create_membership(%{account: account, actor: actor2, group: actor_group})
attrs = [%{"actor_id" => actor1.id}, %{"actor_id" => actor3.id}]
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> put("/actor_groups/#{actor_group.id}/memberships", memberships: attrs)
assert %{"data" => data} = json_response(conn, 200)
assert Enum.sort(data["actor_ids"]) == Enum.sort([actor1.id, actor3.id])
end
end
end

View File

@@ -1,22 +0,0 @@
defmodule API.ExampleControllerTest do
use API.ConnCase, async: true
describe "echo/2" do
test "returns error when not authorized", %{conn: conn} do
conn = post(conn, "/v1/echo", %{"message" => "Hello, world!"})
assert json_response(conn, 401) == %{"error" => "invalid_access_token"}
end
test "returns 200 OK with the request body", %{conn: conn} do
actor = Fixtures.Actors.create_actor(type: :api_client)
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/v1/echo", Jason.encode!(%{"message" => "Hello, world!"}))
assert json_response(conn, 200) == %{"message" => "Hello, world!"}
end
end
end

View File

@@ -0,0 +1,174 @@
defmodule API.GatewayControllerTest do
use API.ConnCase, async: true
alias Domain.Gateways.Gateway
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
gateway_group = Fixtures.Gateways.create_group(%{account: account})
%{
account: account,
actor: actor,
gateway_group: gateway_group
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn, gateway_group: gateway_group} do
conn = get(conn, "/gateway_groups/#{gateway_group.id}/gateways")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all gateways for a gateway group", %{
conn: conn,
account: account,
actor: actor,
gateway_group: gateway_group
} do
gateways =
for _ <- 1..3,
do: Fixtures.Gateways.create_gateway(%{account: account, group: gateway_group})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/gateway_groups/#{gateway_group.id}/gateways")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
gateway_ids = Enum.map(gateways, & &1.id)
assert equal_ids?(data_ids, gateway_ids)
end
test "lists gateways with limit", %{
conn: conn,
account: account,
actor: actor,
gateway_group: gateway_group
} do
gateways =
for _ <- 1..3,
do: Fixtures.Gateways.create_gateway(%{account: account, group: gateway_group})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/gateway_groups/#{gateway_group.id}/gateways", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
gateway_ids = Enum.map(gateways, & &1.id) |> MapSet.new()
assert MapSet.subset?(data_ids, gateway_ids)
end
end
describe "show/2" do
test "returns error when not authorized", %{
conn: conn,
account: account,
gateway_group: gateway_group
} do
gateway = Fixtures.Gateways.create_gateway(%{account: account, group: gateway_group})
conn = get(conn, "/gateway_groups/#{gateway_group.id}/gateways/#{gateway.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single gateway", %{
conn: conn,
account: account,
actor: actor,
gateway_group: gateway_group
} do
gateway = Fixtures.Gateways.create_gateway(%{account: account, group: gateway_group})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/gateway_groups/#{gateway_group.id}/gateways/#{gateway.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => gateway.id,
"name" => gateway.name,
"ipv4" => Domain.Types.IP.to_string(gateway.ipv4),
"ipv6" => Domain.Types.IP.to_string(gateway.ipv6),
"online" => false
}
}
end
end
describe "delete/2" do
test "returns error when not authorized", %{
conn: conn,
account: account,
gateway_group: gateway_group
} do
gateway = Fixtures.Gateways.create_gateway(%{account: account, group: gateway_group})
conn = delete(conn, "/gateway_groups/#{gateway_group.id}/gateways/#{gateway.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes a gateway", %{
conn: conn,
account: account,
actor: actor,
gateway_group: gateway_group
} do
gateway = Fixtures.Gateways.create_gateway(%{account: account, group: gateway_group})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/gateway_groups/#{gateway_group.id}/gateways/#{gateway.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => gateway.id,
"name" => gateway.name,
"ipv4" => Domain.Types.IP.to_string(gateway.ipv4),
"ipv6" => Domain.Types.IP.to_string(gateway.ipv6),
"online" => false
}
}
assert gateway = Repo.get(Gateway, gateway.id)
assert gateway.deleted_at
end
end
end

View File

@@ -0,0 +1,308 @@
defmodule API.GatewayGroupControllerTest do
use API.ConnCase, async: true
alias Domain.Gateways.Group
alias Domain.Tokens.Token
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn} do
conn = get(conn, "/gateway_groups")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all gateway groups", %{conn: conn, account: account, actor: actor} do
gateway_groups = for _ <- 1..3, do: Fixtures.Gateways.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/gateway_groups")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
gateway_group_ids = Enum.map(gateway_groups, & &1.id)
assert equal_ids?(data_ids, gateway_group_ids)
end
test "lists gateway groups with limit", %{conn: conn, account: account, actor: actor} do
gateway_groups = for _ <- 1..3, do: Fixtures.Gateways.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/gateway_groups", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
gateway_group_ids = Enum.map(gateway_groups, & &1.id) |> MapSet.new()
assert MapSet.subset?(data_ids, gateway_group_ids)
end
end
describe "show/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn = get(conn, "/gateway_groups/#{gateway_group.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single gateway_group", %{conn: conn, account: account, actor: actor} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/gateway_groups/#{gateway_group.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => gateway_group.id,
"name" => gateway_group.name
}
}
end
end
describe "create/2" do
test "returns error when not authorized", %{conn: conn} do
conn = post(conn, "/gateway_groups", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, actor: actor} do
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/gateway_groups")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "returns error on invalid attrs", %{conn: conn, actor: actor} do
attrs = %{"name" => String.duplicate("a", 65)}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/gateway_groups", gateway_group: attrs)
assert resp = json_response(conn, 422)
assert resp ==
%{
"error" => %{
"reason" => "Unprocessable Entity",
"validation_errors" => %{"name" => ["should be at most 64 character(s)"]}
}
}
end
test "creates a gateway group with valid attrs", %{conn: conn, actor: actor} do
attrs = %{
"name" => "Example Site"
}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/gateway_groups", gateway_group: attrs)
assert resp = json_response(conn, 201)
assert resp["data"]["name"] == attrs["name"]
end
end
describe "update/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn = put(conn, "/gateway_groups/#{gateway_group.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, account: account, actor: actor} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/gateway_groups/#{gateway_group.id}")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "updates a gateway group", %{conn: conn, account: account, actor: actor} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
attrs = %{"name" => "Updated Site Name"}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/gateway_groups/#{gateway_group.id}", gateway_group: attrs)
assert resp = json_response(conn, 200)
assert resp["data"]["name"] == attrs["name"]
end
end
describe "delete/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn = delete(conn, "/gateway_groups/#{gateway_group.id}", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes a gateway group", %{conn: conn, account: account, actor: actor} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/gateway_groups/#{gateway_group.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => gateway_group.id,
"name" => gateway_group.name
}
}
assert gateway_group = Repo.get(Group, gateway_group.id)
assert gateway_group.deleted_at
end
end
describe "gateway group token create/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn = post(conn, "/gateway_groups/#{gateway_group.id}/tokens")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "creates a gateway token", %{
conn: conn,
account: account,
actor: actor
} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/gateway_groups/#{gateway_group.id}/tokens")
assert %{"data" => %{"id" => _id, "token" => _token}} = json_response(conn, 201)
end
end
describe "delete single gateway token" do
test "returns error when not authorized", %{conn: conn, account: account} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
token = Fixtures.Gateways.create_token(%{account: account, group: gateway_group})
conn = delete(conn, "/gateway_groups/#{gateway_group.id}/tokens/#{token.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes gateway token", %{conn: conn, account: account, actor: actor} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
token = Fixtures.Gateways.create_token(%{account: account, group: gateway_group})
conn =
conn
|> authorize_conn(actor)
|> delete("/gateway_groups/#{gateway_group.id}/tokens/#{token.id}")
assert %{"data" => %{"id" => _id}} = json_response(conn, 200)
assert token = Repo.get(Token, token.id)
assert token.deleted_at
end
end
describe "delete all gateway tokens" do
test "returns error when not authorized", %{conn: conn, account: account} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
conn = delete(conn, "/gateway_groups/#{gateway_group.id}/tokens")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes all gateway tokens", %{
conn: conn,
account: account,
actor: actor
} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
tokens =
for _ <- 1..3,
do: Fixtures.Gateways.create_token(%{account: account, group: gateway_group})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/gateway_groups/#{gateway_group.id}/tokens")
assert %{"data" => [%{"id" => _id1}, %{"id" => _id2}, %{"id" => _id3}]} =
json_response(conn, 200)
Enum.map(tokens, fn token ->
assert token = Repo.get(Token, token.id)
assert token.deleted_at
end)
end
end
end

View File

@@ -0,0 +1,246 @@
defmodule API.IdentityControllerTest do
use API.ConnCase, async: true
alias Domain.Auth.Identity
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn, actor: actor} do
conn = get(conn, "/actors/#{actor.id}/identities")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all identities for actor", %{conn: conn, account: account, actor: actor} do
identities =
for _ <- 1..3, do: Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actors/#{actor.id}/identities")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
identity_ids = Enum.map(identities, & &1.id)
assert equal_ids?(data_ids, identity_ids)
end
test "lists resources with limit", %{conn: conn, account: account, actor: actor} do
identities =
for _ <- 1..3, do: Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actors/#{actor.id}/identities", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
identity_ids = Enum.map(identities, & &1.id) |> MapSet.new()
assert MapSet.subset?(data_ids, identity_ids)
end
end
describe "show/2" do
test "returns error when not authorized", %{conn: conn, account: account, actor: actor} do
identity = Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn = get(conn, "/actors/#{actor.id}/identities/#{identity.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single resource", %{conn: conn, account: account, actor: actor} do
identity = Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actors/#{actor.id}/identities/#{identity.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => identity.id,
"actor_id" => actor.id,
"provider_id" => identity.provider_id,
"provider_identifier" => identity.provider_identifier
}
}
end
end
describe "create/2" do
test "returns error when not authorized", %{conn: conn, actor: actor} do
conn = post(conn, "/actors/#{actor.id}/identities", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{
conn: conn,
account: account,
actor: api_actor
} do
actor = Fixtures.Actors.create_actor(account: account)
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/identities")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "returns error on invalid identity provider", %{
conn: conn,
account: account,
actor: api_actor
} do
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/identities",
provider_id: "1234",
identity: attrs
)
assert resp = json_response(conn, 404)
assert resp == %{"error" => %{"reason" => "Not Found"}}
end
test "returns error on invalid identity attrs", %{
conn: conn,
account: account,
actor: api_actor
} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/identities",
provider_id: oidc_provider.id,
identity: attrs
)
assert resp = json_response(conn, 422)
assert resp ==
%{
"error" => %{
"reason" => "Unprocessable Entity",
"validation_errors" => %{"provider_identifier" => ["can't be blank"]}
}
}
end
test "creates a resource with valid attrs", %{
conn: conn,
account: account,
actor: api_actor
} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{"provider_identifier" => "foo@local"}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/identities",
provider_id: oidc_provider.id,
identity: attrs
)
assert resp = json_response(conn, 201)
assert resp["data"]["provider_identifier"] == attrs["provider_identifier"]
assert resp["data"]["provider_id"] == oidc_provider.id
assert resp["data"]["actor_id"] == actor.id
end
end
describe "delete/2" do
test "returns error when not authorized", %{conn: conn, account: account, actor: actor} do
identity = Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn = delete(conn, "/actors/#{actor.id}/identities/#{identity.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes a resource", %{conn: conn, account: account, actor: actor} do
identity = Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/actors/#{actor.id}/identities/#{identity.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => identity.id,
"actor_id" => actor.id,
"provider_id" => identity.provider_id,
"provider_identifier" => identity.provider_identifier
}
}
assert identity = Repo.get(Identity, identity.id)
assert identity.deleted_at
end
end
end

View File

@@ -0,0 +1,146 @@
defmodule API.IdentityProviderControllerTest do
use API.ConnCase, async: true
alias Domain.Auth.Provider
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn} do
conn = get(conn, "/identity_providers")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all identity_providers", %{conn: conn, account: account, actor: actor} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
{google_provider, _bypass} =
Fixtures.Auth.start_and_create_google_workspace_provider(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/identity_providers")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
provider_ids =
Enum.map([oidc_provider, google_provider], & &1.id) |> MapSet.new()
assert MapSet.subset?(provider_ids, data_ids)
end
test "lists identity providers with limit", %{conn: conn, account: account, actor: actor} do
Fixtures.Auth.start_and_create_openid_connect_provider(%{account: account})
Fixtures.Auth.start_and_create_google_workspace_provider(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/identity_providers", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
assert length(data_ids) == 2
end
end
describe "show/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
{identity_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(%{account: account})
conn = get(conn, "/identity_providers/#{identity_provider.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single resource", %{conn: conn, account: account, actor: actor} do
{identity_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/identity_providers/#{identity_provider.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => identity_provider.id,
"name" => identity_provider.name
}
}
end
end
describe "delete/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
{identity_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(%{account: account})
conn = delete(conn, "/identity_providers/#{identity_provider.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes an identity provider", %{conn: conn, account: account, actor: actor} do
{identity_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/identity_providers/#{identity_provider.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => identity_provider.id,
"name" => identity_provider.name
}
}
assert identity_provider = Repo.get(Provider, identity_provider.id)
assert identity_provider.deleted_at
end
end
end

View File

@@ -0,0 +1,238 @@
defmodule API.PolicyControllerTest do
use API.ConnCase, async: true
alias Domain.Policies.Policy
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn} do
conn = get(conn, "/policies")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all policies", %{conn: conn, account: account, actor: actor} do
policies = for _ <- 1..3, do: Fixtures.Policies.create_policy(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/policies", Jason.encode!(%{}))
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
policy_ids = Enum.map(policies, & &1.id)
assert equal_ids?(data_ids, policy_ids)
end
test "lists policies with limit", %{conn: conn, account: account, actor: actor} do
policies = for _ <- 1..3, do: Fixtures.Policies.create_policy(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/policies", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
policy_ids = Enum.map(policies, & &1.id) |> MapSet.new()
assert MapSet.subset?(data_ids, policy_ids)
end
end
describe "show/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
policy = Fixtures.Policies.create_policy(%{account: account})
conn = get(conn, "/policies/#{policy.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single policy", %{conn: conn, account: account, actor: actor} do
policy = Fixtures.Policies.create_policy(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/policies/#{policy.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => policy.id,
"actor_group_id" => policy.actor_group_id,
"resource_id" => policy.resource_id,
"description" => policy.description
}
}
end
end
describe "create/2" do
test "returns error when not authorized", %{conn: conn} do
conn = post(conn, "/policies", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, actor: actor} do
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/policies")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "returns error on invalid attrs", %{conn: conn, actor: actor} do
attrs = %{}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/policies", policy: attrs)
assert resp = json_response(conn, 422)
assert resp ==
%{
"error" => %{
"reason" => "Unprocessable Entity",
"validation_errors" => %{
"actor_group_id" => ["can't be blank"],
"resource_id" => ["can't be blank"]
}
}
}
end
test "creates a policy with valid attrs", %{conn: conn, account: account, actor: actor} do
resource = Fixtures.Resources.create_resource(%{account: account})
actor_group = Fixtures.Actors.create_group(%{account: account})
attrs = %{
"actor_group_id" => actor_group.id,
"resource_id" => resource.id,
"description" => "test policy"
}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/policies", policy: attrs)
assert resp = json_response(conn, 201)
assert resp["data"]["actor_group_id"] == attrs["actor_group_id"]
assert resp["data"]["resource_id"] == attrs["resource_id"]
end
end
describe "update/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
policy = Fixtures.Policies.create_policy(%{account: account})
conn = put(conn, "/policies/#{policy.id}", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, account: account, actor: actor} do
policy = Fixtures.Policies.create_policy(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/policies/#{policy.id}")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "updates a policy", %{conn: conn, account: account, actor: actor} do
policy = Fixtures.Policies.create_policy(%{account: account})
attrs = %{"description" => "updated policy description"}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/policies/#{policy.id}", policy: attrs)
assert resp = json_response(conn, 200)
assert resp["data"]["description"] == attrs["description"]
end
end
describe "delete/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
policy = Fixtures.Policies.create_policy(%{account: account})
conn = delete(conn, "/policies/#{policy.id}", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes a policy", %{conn: conn, account: account, actor: actor} do
policy = Fixtures.Policies.create_policy(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/policies/#{policy.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => policy.id,
"actor_group_id" => policy.actor_group_id,
"resource_id" => policy.resource_id,
"description" => policy.description
}
}
assert policy = Repo.get(Policy, policy.id)
assert policy.deleted_at
end
end
end

View File

@@ -0,0 +1,248 @@
defmodule API.ResourceControllerTest do
use API.ConnCase, async: true
alias Domain.Resources.Resource
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
%{
account: account,
actor: actor
}
end
describe "index/2" do
test "returns error when not authorized", %{conn: conn} do
conn = get(conn, "/resources")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "lists all resources", %{conn: conn, account: account, actor: actor} do
resources = for _ <- 1..3, do: Fixtures.Resources.create_resource(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/resources")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert count == 3
assert limit == 50
assert is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"])
resource_ids = Enum.map(resources, & &1.id)
assert equal_ids?(data_ids, resource_ids)
end
test "lists resources with limit", %{conn: conn, account: account, actor: actor} do
resources = for _ <- 1..3, do: Fixtures.Resources.create_resource(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/resources", limit: "2")
assert %{
"data" => data,
"metadata" => %{
"count" => count,
"limit" => limit,
"next_page" => next_page,
"prev_page" => prev_page
}
} = json_response(conn, 200)
assert limit == 2
assert count == 3
refute is_nil(next_page)
assert is_nil(prev_page)
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
resource_ids = Enum.map(resources, & &1.id) |> MapSet.new()
assert MapSet.subset?(data_ids, resource_ids)
end
end
describe "show/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
resource = Fixtures.Resources.create_resource(%{account: account})
conn = get(conn, "/resources/#{resource.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single resource", %{conn: conn, account: account, actor: actor} do
resource = Fixtures.Resources.create_resource(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/resources/#{resource.id}")
assert json_response(conn, 200) == %{
"data" => %{
"address" => resource.address,
"description" => resource.address_description,
"id" => resource.id,
"name" => resource.name,
"type" => Atom.to_string(resource.type)
}
}
end
end
describe "create/2" do
test "returns error when not authorized", %{conn: conn} do
conn = post(conn, "/resources", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, actor: actor} do
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/resources")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "returns error on invalid attrs", %{conn: conn, actor: actor} do
attrs = %{}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/resources", resource: attrs)
assert resp = json_response(conn, 422)
assert resp ==
%{
"error" => %{
"reason" => "Unprocessable Entity",
"validation_errors" => %{
"address" => ["can't be blank"],
"connections" => ["can't be blank"],
"name" => ["can't be blank"],
"type" => ["can't be blank"]
}
}
}
end
test "creates a resource with valid attrs", %{conn: conn, account: account, actor: actor} do
gateway_group = Fixtures.Gateways.create_group(%{account: account})
attrs = %{
"address" => "google.com",
"name" => "Google",
"type" => "dns",
"connections" => [
%{"gateway_group_id" => gateway_group.id}
]
}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> post("/resources", resource: attrs)
assert resp = json_response(conn, 201)
assert resp["data"]["address"] == attrs["address"]
assert resp["data"]["description"] == nil
assert resp["data"]["name"] == attrs["name"]
assert resp["data"]["type"] == attrs["type"]
end
end
describe "update/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
resource = Fixtures.Resources.create_resource(%{account: account})
conn = put(conn, "/resources/#{resource.id}", %{})
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns error on empty params/body", %{conn: conn, account: account, actor: actor} do
resource = Fixtures.Resources.create_resource(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/resources/#{resource.id}")
assert resp = json_response(conn, 400)
assert resp == %{"error" => %{"reason" => "Bad Request"}}
end
test "updates a resource", %{conn: conn, account: account, actor: actor} do
resource = Fixtures.Resources.create_resource(%{account: account})
attrs = %{"name" => "Google"}
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> put("/resources/#{resource.id}", resource: attrs)
assert resp = json_response(conn, 200)
assert resp["data"]["address"] == resource.address
assert resp["data"]["description"] == resource.address_description
assert resp["data"]["name"] == attrs["name"]
end
end
describe "delete/2" do
test "returns error when not authorized", %{conn: conn, account: account} do
resource = Fixtures.Resources.create_resource(%{account: account})
conn = delete(conn, "/resources/#{resource.id}")
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes a resource", %{conn: conn, account: account, actor: actor} do
resource = Fixtures.Resources.create_resource(%{account: account})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> delete("/resources/#{resource.id}")
assert json_response(conn, 200) == %{
"data" => %{
"address" => resource.address,
"description" => resource.address_description,
"id" => resource.id,
"name" => resource.name,
"type" => Atom.to_string(resource.type)
}
}
assert resource = Repo.get(Resource, resource.id)
assert resource.deleted_at
end
end
end

View File

@@ -56,4 +56,8 @@ defmodule API.ConnCase do
Plug.Conn.put_req_header(conn, "authorization", "Bearer " <> encoded_fragment)
end
def equal_ids?(list1, list2) do
MapSet.equal?(MapSet.new(list1), MapSet.new(list2))
end
end

View File

@@ -11,6 +11,12 @@ defmodule Domain.Accounts.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_own_account_permission()
]
end
def list_permissions_for_role(_) do
[]
end

View File

@@ -13,6 +13,13 @@ defmodule Domain.Actors.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_actors_permission(),
edit_own_profile_permission()
]
end
def list_permissions_for_role(:account_user) do
[
edit_own_profile_permission()

View File

@@ -20,8 +20,9 @@ defmodule Domain.Actors.Group do
# ref https://github.com/firezone/firezone/issues/2162
has_many :actors, through: [:memberships, :actor]
field :created_by, Ecto.Enum, values: ~w[system identity provider]a
field :created_by, Ecto.Enum, values: ~w[actor identity provider system]a
belongs_to :created_by_identity, Domain.Auth.Identity
belongs_to :created_by_actor, Domain.Actors.Actor
belongs_to :account, Domain.Accounts.Account

View File

@@ -19,8 +19,7 @@ defmodule Domain.Actors.Group.Changeset do
|> changeset()
|> put_change(:account_id, account.id)
|> cast_membership_assocs(account.id)
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
|> put_created_by(subject)
end
def create(%Accounts.Account{} = account, attrs) do

View File

@@ -54,6 +54,15 @@ defmodule Domain.Auth.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_providers_permission(),
manage_service_accounts_permission(),
manage_own_identities_permission(),
manage_identities_permission()
]
end
def list_permissions_for_role(_role) do
[]
end

View File

@@ -14,6 +14,13 @@ defmodule Domain.Clients.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_own_clients_permission(),
manage_clients_permission()
]
end
def list_permissions_for_role(:account_user) do
[
manage_own_clients_permission()

View File

@@ -13,6 +13,13 @@ defmodule Domain.Flows.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_flows_permission(),
create_flows_permission()
]
end
def list_permissions_for_role(:account_user) do
[
create_flows_permission()

View File

@@ -14,6 +14,13 @@ defmodule Domain.Gateways.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_gateways_permission(),
connect_gateways_permission()
]
end
def list_permissions_for_role(_) do
[
connect_gateways_permission()

View File

@@ -13,8 +13,9 @@ defmodule Domain.Gateways.Group do
has_many :connections, Domain.Resources.Connection, foreign_key: :gateway_group_id
field :created_by, Ecto.Enum, values: ~w[identity]a
field :created_by, Ecto.Enum, values: ~w[actor identity]a
belongs_to :created_by_identity, Domain.Auth.Identity
belongs_to :created_by_actor, Domain.Actors.Actor
field :deleted_at, :utc_datetime_usec
timestamps()

View File

@@ -9,8 +9,7 @@ defmodule Domain.Gateways.Group.Changeset do
%Gateways.Group{account: account}
|> changeset(attrs)
|> put_change(:account_id, account.id)
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
|> put_created_by(subject)
end
def update(%Gateways.Group{} = group, attrs, %Auth.Subject{}) do

View File

@@ -13,6 +13,13 @@ defmodule Domain.Policies.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_policies_permission(),
view_available_policies_permission()
]
end
def list_permissions_for_role(:account_user) do
[
# TODO: view_assigned_policies_permission()

View File

@@ -10,8 +10,9 @@ defmodule Domain.Policies.Policy do
belongs_to :resource, Domain.Resources.Resource
belongs_to :account, Domain.Accounts.Account
field :created_by, Ecto.Enum, values: ~w[identity]a
field :created_by, Ecto.Enum, values: ~w[actor identity]a
belongs_to :created_by_identity, Domain.Auth.Identity
belongs_to :created_by_actor, Domain.Actors.Actor
field :disabled_at, :utc_datetime_usec
field :deleted_at, :utc_datetime_usec

View File

@@ -14,8 +14,7 @@ defmodule Domain.Policies.Policy.Changeset do
|> cast_embed(:conditions, with: &Domain.Policies.Condition.Changeset.changeset/3)
|> changeset()
|> put_change(:account_id, subject.account.id)
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
|> put_created_by(subject)
end
def update(%Policy{} = policy, attrs) do

View File

@@ -139,6 +139,19 @@ defmodule Domain.Repo.Changeset do
end
end
def put_created_by(changset, %Domain.Auth.Subject{identity: nil} = subject) do
changset
|> put_change(:created_by_actor_id, subject.actor.id)
|> put_change(:created_by, :actor)
end
def put_created_by(changeset, %Domain.Auth.Subject{} = subject) do
changeset
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
|> put_change(:created_by_actor_id, subject.actor.id)
end
# Validations
def validate_list(

View File

@@ -19,6 +19,13 @@ defmodule Domain.Resources.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_resources_permission(),
view_available_resources_permission()
]
end
def list_permissions_for_role(:service_account) do
[
view_available_resources_permission()

View File

@@ -6,8 +6,9 @@ defmodule Domain.Resources.Connection do
belongs_to :resource, Domain.Resources.Resource, primary_key: true
belongs_to :gateway_group, Domain.Gateways.Group, primary_key: true
field :created_by, Ecto.Enum, values: ~w[identity]a
field :created_by, Ecto.Enum, values: ~w[actor identity]a
belongs_to :created_by_identity, Domain.Auth.Identity
belongs_to :created_by_actor, Domain.Actors.Actor
belongs_to :account, Domain.Accounts.Account
end

View File

@@ -7,8 +7,7 @@ defmodule Domain.Resources.Connection.Changeset do
def changeset(account_id, connection, attrs, %Auth.Subject{} = subject) do
changeset(account_id, connection, attrs)
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
|> put_created_by(subject)
end
def changeset(account_id, connection, attrs) do

View File

@@ -26,7 +26,8 @@ defmodule Domain.Resources.Resource do
# because the actual preload query should also use joins and process policy conditions
has_many :authorized_by_policies, Domain.Policies.Policy, where: [id: {:fragment, "FALSE"}]
field :created_by, Ecto.Enum, values: ~w[identity]a
field :created_by, Ecto.Enum, values: ~w[identity actor]a
belongs_to :created_by_actor, Domain.Actors.Actor
belongs_to :created_by_identity, Domain.Auth.Identity
field :deleted_at, :utc_datetime_usec

View File

@@ -18,8 +18,7 @@ defmodule Domain.Resources.Resource.Changeset do
with: &Connection.Changeset.changeset(account.id, &1, &2, subject),
required: true
)
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
|> put_created_by(subject)
end
def create(%Accounts.Account{} = account, attrs) do

View File

@@ -13,6 +13,13 @@ defmodule Domain.Tokens.Authorizer do
]
end
def list_permissions_for_role(:api_client) do
[
manage_tokens_permission(),
manage_own_tokens_permission()
]
end
def list_permissions_for_role(:account_user) do
[
manage_own_tokens_permission()

View File

@@ -43,8 +43,9 @@ defmodule Domain.Tokens.Token do
field :last_seen_at, :utc_datetime_usec
# Maybe this is not needed and they should be in the join tables (eg. relay_group_tokens)
field :created_by, Ecto.Enum, values: ~w[system identity]a
field :created_by, Ecto.Enum, values: ~w[actor identity system]a
belongs_to :created_by_identity, Domain.Auth.Identity
belongs_to :created_by_actor, Domain.Actors.Actor
field :created_by_user_agent, :string
field :created_by_remote_ip, Domain.Types.IP

View File

@@ -46,8 +46,7 @@ defmodule Domain.Tokens.Token.Changeset do
:service_account_client
])
|> changeset()
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
|> put_created_by(subject)
end
defp changeset(changeset) do

View File

@@ -0,0 +1,37 @@
defmodule Domain.Repo.Migrations.AddCreatedByActor do
use Ecto.Migration
@table_names ~w[
actor_groups
auth_identities
auth_providers
gateway_groups
policies
relay_groups
resources
resource_connections
tokens
]a
defp migrate_data(table_name) do
"""
UPDATE #{table_name} AS t
SET created_by_actor_id = ai.actor_id
FROM auth_identities AS ai
WHERE t.created_by_identity_id = ai.id
AND t.created_by = 'identity'
AND t.created_by_identity_id is not null;
"""
|> execute("")
end
def change do
for table_name <- @table_names do
alter table(table_name) do
add(:created_by_actor_id, :uuid)
end
migrate_data(table_name)
end
end
end

View File

@@ -95,6 +95,9 @@ config :api, API.Endpoint,
# Do not include metadata nor timestamps in development logs
config :logger, :default_formatter, format: "[$level] $message\n"
# Disable caching for OpenAPI spec to ensure it is refreshed
config :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

View File

@@ -61,6 +61,7 @@
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"number": {:hex, :number, "1.0.5", "d92136f9b9382aeb50145782f116112078b3465b7be58df1f85952b8bb399b0f", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c0733a0a90773a66582b9e92a3f01290987f395c972cb7d685f51dd927cd5169"},
"observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"},
"open_api_spex": {:hex, :open_api_spex, "3.20.0", "d4fcf1ee297aa94a673cddb92734eb0bc7cac698be93949a223a50f724e3af89", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "2e9beea71142ff09f8f935579b39406e2c6b5a3978e7235978d7faf2f90cd081"},
"openid_connect": {:git, "https://github.com/firezone/openid_connect.git", "e4d9dca8ae43c765c00a7d3dfa12d6f24f5b3418", [ref: "e4d9dca8ae43c765c00a7d3dfa12d6f24f5b3418"]},
"opentelemetry": {:hex, :opentelemetry, "1.4.0", "f928923ed80adb5eb7894bac22e9a198478e6a8f04020ae1d6f289fdcad0b498", [:rebar3], [{:opentelemetry_api, "~> 1.3.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "50b32ce127413e5d87b092b4d210a3449ea80cd8224090fe68d73d576a3faa15"},
"opentelemetry_api": {:hex, :opentelemetry_api, "1.3.0", "03e2177f28dd8d11aaa88e8522c81c2f6a788170fe52f7a65262340961e663f9", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "b9e5ff775fd064fa098dba3c398490b77649a352b40b0b730a6b7dc0bdd68858"},
@@ -112,4 +113,5 @@
"workos": {:git, "https://github.com/firezone/workos-elixir.git", "6d6995e2a765656a6834577837433393fac9b35b", [branch: "main"]},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
"ymlr": {:hex, :ymlr, "2.0.0", "7525b6da40250777c35456017ef44f7faec06da254eafcf9f9cfb0d65f4c8cb7", [:mix], [], "hexpm", "f9301ad7ea377213b506f6e58ddffd1a7743e24238bb70e572ee510bdc2d1d5a"},
}