Skip to content

Commit

Permalink
Half CRUD to manage autohost credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
geekingfrog committed Jul 27, 2024
1 parent b163fe5 commit 950294f
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 13 deletions.
8 changes: 8 additions & 0 deletions lib/teiserver/o_auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ defmodule Teiserver.OAuth do
end
end

@spec delete_credential(Credential.t() | Credential.id()) :: :ok | {:error, term()}
def delete_credential(%Credential{} = cred) do
case Repo.delete(cred) do
{:ok, _} -> :ok
{:error, err} -> {:error, err}
end
end

@doc """
Given a client_id and a cleartext secret, check the secret matches and returns the credentials
"""
Expand Down
52 changes: 51 additions & 1 deletion lib/teiserver/o_auth/queries/credential_query.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Teiserver.OAuth.CredentialQueries do
use TeiserverWeb, :queries
alias Teiserver.OAuth.Credential
alias Teiserver.OAuth.{Credential, Application}
alias Teiserver.Autohost.Autohost

def get_credential(nil), do: nil

Expand All @@ -11,6 +12,40 @@ defmodule Teiserver.OAuth.CredentialQueries do
|> Repo.one()
end

def get_credential_by_id(nil), do: nil

def get_credential_by_id(id) do
base_query()
|> preload(:application)
|> where_id(id)
|> Repo.one()
end

@spec for_autohost(Autohost.t() | Autohost.id()) :: [
Credential.t()
]
def for_autohost(%Autohost{} = autohost) do
for_autohost(autohost.id)
end

def for_autohost(autohost_id) do
base_query() |> preload(:application) |> where_autohost_id(autohost_id) |> Repo.all()
end

@spec count_per_autohosts([Autohost.id()]) :: %{Autohost.id() => non_neg_integer()}
def count_per_autohosts(autohost_ids) do
query =
base_query()
|> where_autohost_ids(autohost_ids)

from([credential: credential] in query,
group_by: credential.autohost_id,
select: {credential.autohost_id, count(credential.id)}
)
|> Repo.all()
|> Enum.into(%{})
end

def base_query() do
from credential in Credential,
as: :credential
Expand All @@ -21,6 +56,11 @@ defmodule Teiserver.OAuth.CredentialQueries do
where: credential.client_id == ^client_id
end

def where_id(query, cred_id) do
from [credential: credential] in query,
where: credential.id == ^cred_id
end

@spec count_per_apps([Application.id()]) :: %{Application.id() => non_neg_integer()}
def count_per_apps(app_ids) do
query =
Expand All @@ -39,4 +79,14 @@ defmodule Teiserver.OAuth.CredentialQueries do
from [credential: credential] in query,
where: credential.application_id in ^app_ids
end

def where_autohost_id(query, autohost_id) do
from [credential: credential] in query,
where: credential.autohost_id == ^autohost_id
end

def where_autohost_ids(query, autohost_ids) do
from [credential: credential] in query,
where: credential.autohost_id in ^autohost_ids
end
end
1 change: 1 addition & 0 deletions lib/teiserver/o_auth/schemas/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Teiserver.OAuth.Application do
use TeiserverWeb, :schema
alias Teiserver.Account.User

@type id() :: non_neg_integer()
@type app_id() :: String.t()
@type scopes() :: nonempty_list(String.t())
@type t :: %__MODULE__{
Expand Down
1 change: 1 addition & 0 deletions lib/teiserver/o_auth/schemas/credential.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Teiserver.OAuth.Credential do

alias Teiserver.OAuth

@type id :: non_neg_integer()
@type t :: %__MODULE__{
application: OAuth.Application.t(),
autohost: Teiserver.Autohost.Autohost.t(),
Expand Down
89 changes: 83 additions & 6 deletions lib/teiserver_web/controllers/admin/autohost_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ defmodule TeiserverWeb.Admin.AutohostController do

use TeiserverWeb, :controller

alias Teiserver.{Autohost, AutohostQueries}
alias Teiserver.{Autohost, AutohostQueries, OAuth}
alias Teiserver.OAuth.{ApplicationQueries, CredentialQueries}

plug Bodyguard.Plug.Authorize,
# The policy should be Admin or something fairly high. But while we're
Expand All @@ -21,9 +22,10 @@ defmodule TeiserverWeb.Admin.AutohostController do
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
def index(conn, _params) do
autohosts = AutohostQueries.list_autohosts()
cred_counts = CredentialQueries.count_per_autohosts(Enum.map(autohosts, fn a -> a.id end))

conn
|> render("index.html", autohosts: autohosts)
|> render("index.html", autohosts: autohosts, cred_counts: cred_counts)
end

@spec new(Plug.Conn.t(), map()) :: Plug.Conn.t()
Expand Down Expand Up @@ -62,9 +64,7 @@ defmodule TeiserverWeb.Admin.AutohostController do
def show(conn, assigns) do
case Autohost.get_by_id(Map.get(assigns, "id")) do
%Autohost.Autohost{} = autohost ->
conn
|> assign(:page_title, "BAR - autohost #{autohost.name}")
|> render("show.html", autohost: autohost)
render_show(conn, autohost)

nil ->
conn
Expand Down Expand Up @@ -98,7 +98,7 @@ defmodule TeiserverWeb.Admin.AutohostController do
{:ok, autohost} ->
conn
|> put_flash(:info, "Autohost updated")
|> render(:show, autohost: autohost)
|> render_show(autohost)

{:error, changeset} ->
conn
Expand Down Expand Up @@ -141,4 +141,81 @@ defmodule TeiserverWeb.Admin.AutohostController do
|> render("not_found.html")
end
end

@spec create_credential(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create_credential(conn, assigns) do
with autohost when not is_nil(autohost) <- Autohost.get_by_id(Map.get(assigns, "id")),
app when not is_nil(app) <-
ApplicationQueries.get_application_by_id(Map.get(assigns, "application")) do
client_id = UUID.uuid4()
secret = Base.hex_encode32(:crypto.strong_rand_bytes(32))

case OAuth.create_credentials(app, autohost, client_id, secret) do
{:ok, _cred} ->
conn
|> put_flash(:info, "credential created")
|> Plug.Conn.put_resp_cookie("client_secret", secret, sign: true, max_age: 60)
|> redirect(to: ~p"/teiserver/admin/autohost/#{autohost.id}")

{:error, err} ->
conn
|> put_flash(:danger, inspect(err))
|> render_show(autohost)
end
else
nil ->
conn
|> put_status(:not_found)
|> render("not_found.html")
end
end

@spec delete_credential(Plug.Conn.t(), map()) :: Plug.Conn.t()
def delete_credential(conn, assigns) do
with autohost when not is_nil(autohost) <- Autohost.get_by_id(Map.get(assigns, "id")),
cred when not is_nil(cred) <-
CredentialQueries.get_credential_by_id(Map.get(assigns, "cred_id")) do
# _ when cred.autohost_id == autohost.id <- nil do
if cred.autohost_id != autohost.id do
conn
|> put_status(:bad_request)
|> put_flash(:danger, "credential doesn't match autohost")
|> render_show(autohost)
else
case OAuth.delete_credential(cred) do
:ok ->
conn
|> put_flash(:info, "credential deleted")
|> redirect(to: ~p"/teiserver/admin/autohost/#{autohost.id}")

{:error, err} ->
conn
|> put_flash(:danger, inspect(err))
|> render_show(autohost)
end
end
else
nil ->
conn
|> put_status(:not_found)
|> render("not_found.html")
end
end

defp render_show(conn, autohost) do
applications = ApplicationQueries.list_applications()
credentials = CredentialQueries.for_autohost(autohost)
cookies = Plug.Conn.fetch_cookies(conn, signed: ["client_secret"]).cookies
client_secret = Map.get(cookies, "client_secret")

conn
|> assign(:page_title, "BAR - autohost #{autohost.name}")
|> Plug.Conn.delete_resp_cookie("client_secret", sign: true)
|> render("show.html",
autohost: autohost,
applications: applications,
credentials: credentials,
client_secret: client_secret
)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ defmodule TeiserverWeb.Admin.OAuthApplicationController do

conn
|> assign(:page_title, "BAR - oauth apps")
|> render("index.html", app_and_stats: Enum.zip(applications, stats)
)
|> render("index.html", app_and_stats: Enum.zip(applications, stats))
end

@spec new(Plug.Conn.t(), map()) :: Plug.Conn.t()
Expand Down
6 changes: 6 additions & 0 deletions lib/teiserver_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -689,5 +689,11 @@ defmodule TeiserverWeb.Router do

resources("/oauth_application", OAuthApplicationController)
resources("/autohost", AutohostController)

# Ideally credentials would get the full CRUD routes, but I can't be arsed
# right now for an "mvp". Only autohost need credentials so far, so bind
# the two
post("/autohost/:id/credential", AutohostController, :create_credential)
delete("/autohost/:id/credential/:cred_id", AutohostController, :delete_credential)
end
end
2 changes: 2 additions & 0 deletions lib/teiserver_web/templates/admin/autohost/index.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<tr>
<th>id</th>
<th>name</th>
<th>credentials</th>
<th>actions</th>
</tr>
</thead>
Expand All @@ -21,6 +22,7 @@
<tr>
<td><%= autohost.id %></td>
<td><%= autohost.name %></td>
<td><%= Map.get(@cred_counts, autohost.id, 0) %></td>
<td>
<a href={~p"/teiserver/admin/autohost/#{autohost.id}"}>
<button type="button" class="btn btn-primary btn-sm">show</button>
Expand Down
58 changes: 56 additions & 2 deletions lib/teiserver_web/templates/admin/autohost/show.html.heex
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<div class="container">
<h1>Autohost: <%= @autohost.name %></h1>
<%!-- TODO: add a list of oauth credentials + action to delete them --%>

<div :if={@client_secret} class="note note-danger">
Secret only shown once! <pre><%= @client_secret %></pre>
</div>

<p>
<a href={~p"/teiserver/admin/autohost/#{@autohost.id}/edit"}>
<button type="button" class="btn btn-primary">Edit</button>
<button type="button" class="btn btn-primary">Edit autohost</button>
</a>

<%!-- TODO: add a modal confirmation for deleting an autohost --%>
Expand All @@ -14,4 +17,55 @@
</:actions>
</CC.simple_form>
</p>

<h2>Existing credentials</h2>
<%= if length(@credentials) > 0 do %>
<table class="table">
<thead>
<tr>
<th>application</th>
<th>client id</th>
<th>actions</th>
</tr>
</thead>
<tbody>
<%= for credential <- @credentials do %>
<tr>
<td><%= credential.application.name %></td>
<td><pre> <%= credential.client_id %> </pre></td>
<td>
<form
method="POST"
action={~p"/teiserver/admin/autohost/#{@autohost.id}/credential/#{credential.id}"}
>
<input type="hidden" name="_method" value="delete" />
<input
type="hidden"
name="_csrf_token"
value={Phoenix.Controller.get_csrf_token()}
/>
<CC.button type="submit" class="btn btn-danger">DELETE</CC.button>
</form>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
No credentials.
<% end %>

<p>
<h2>Create credential</h2>
<form method="POST" action={~p"/teiserver/admin/autohost/#{@autohost.id}/credential"}>
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<CC.label for="application_select">application</CC.label>
<select id="application_select" name="application" class="form_control">
<%= for app <- @applications do %>
<option value={app.id}><%= app.name %></option>
<% end %>
</select>
<CC.button type="submit" class="btn-primary">Create credentials</CC.button>
</form>
</p>
</div>
8 changes: 8 additions & 0 deletions test/support/fixtures/autohost.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Teiserver.AutohostFixture do
alias Teiserver.Autohost

def create_autohost(name) do
{:ok, autohost} = Autohost.create_autohost(%{name: name})
autohost
end
end
Loading

0 comments on commit 950294f

Please sign in to comment.