Add minimal SSH key management
This commit is contained in:
parent
061a748c2a
commit
c2faafe3a3
9 changed files with 208 additions and 5 deletions
|
@ -5,7 +5,7 @@ defmodule RecycledCloud.Accounts do
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias RecycledCloud.Repo
|
alias RecycledCloud.Repo
|
||||||
alias RecycledCloud.Accounts.{User, UserToken, UserNotifier}
|
alias RecycledCloud.Accounts.{User, Key, UserToken, UserNotifier}
|
||||||
alias RecycledCloud.LDAP
|
alias RecycledCloud.LDAP
|
||||||
|
|
||||||
## Database getters
|
## Database getters
|
||||||
|
@ -356,4 +356,27 @@ defmodule RecycledCloud.Accounts do
|
||||||
{:error, :user, changeset, _} -> {:error, changeset}
|
{:error, :user, changeset, _} -> {:error, changeset}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## Keys
|
||||||
|
|
||||||
|
def get_key!(id), do: Repo.get!(Key, id)
|
||||||
|
|
||||||
|
def get_keys_for(%User{} = user) do
|
||||||
|
Repo.preload(user, :keys).keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def change_user_key(user, attrs \\ %{}) do
|
||||||
|
Key.changeset(user, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_key(%User{} = user, value, comment \\ nil) do
|
||||||
|
%Key{}
|
||||||
|
|> Key.changeset(%{value: value, comment: comment})
|
||||||
|
|> Ecto.Changeset.put_assoc(:user, user)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_key!(%Key{} = key) do
|
||||||
|
Repo.delete!(key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
48
lib/recycledcloud/accounts/key.ex
Normal file
48
lib/recycledcloud/accounts/key.ex
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
defmodule RecycledCloud.Accounts.Key do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "keys" do
|
||||||
|
field :comment, :string
|
||||||
|
field :value, :string
|
||||||
|
field :fingerprint, :string
|
||||||
|
belongs_to :user, RecycledCloud.Accounts.User
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_ssh_key(changeset, value_field, fingerprint_field, comment_field) do
|
||||||
|
raw_value = get_field(changeset, value_field)
|
||||||
|
comment = get_field(changeset, comment_field)
|
||||||
|
|
||||||
|
with {:is_valid, true} <- {:is_valid, changeset.valid?},
|
||||||
|
[{pk, attrs}] <- :public_key.ssh_decode(raw_value, :public_key) do
|
||||||
|
|
||||||
|
fingerprint = :public_key.ssh_hostkey_fingerprint(pk) |> List.to_string
|
||||||
|
changeset = put_change(changeset, fingerprint_field, fingerprint)
|
||||||
|
|
||||||
|
# Fetch comment from key, if not provided.
|
||||||
|
embedded_comment = attrs
|
||||||
|
|> Enum.into(%{})
|
||||||
|
|> Map.get(:comment)
|
||||||
|
|
||||||
|
case {comment, embedded_comment} do
|
||||||
|
{nil, nil} -> changeset
|
||||||
|
{nil, new_comment} ->
|
||||||
|
put_change(changeset, comment_field, List.to_string(new_comment))
|
||||||
|
{_, _} -> changeset
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:is_valid, false} -> changeset
|
||||||
|
_ -> add_error(changeset, value_field, "key is invalid")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(key, attrs) do
|
||||||
|
out = key
|
||||||
|
|> cast(attrs, [:comment, :value])
|
||||||
|
|> validate_required([:value])
|
||||||
|
|> process_ssh_key(:value, :fingerprint, :comment)
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,6 +15,8 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
field :email, :string, virtual: true
|
field :email, :string, virtual: true
|
||||||
field :confirmed_at, :naive_datetime
|
field :confirmed_at, :naive_datetime
|
||||||
|
|
||||||
|
has_many :keys, Accounts.Key
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
53
lib/recycledcloud_web/controllers/user_keys_controller.ex
Normal file
53
lib/recycledcloud_web/controllers/user_keys_controller.ex
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
defmodule RecycledCloudWeb.UserKeysController do
|
||||||
|
use RecycledCloudWeb, :controller
|
||||||
|
|
||||||
|
alias RecycledCloud.Accounts
|
||||||
|
|
||||||
|
def index(conn, %{"username" => username}) do
|
||||||
|
case Accounts.get_user_by_username(username) do
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> text("")
|
||||||
|
|
||||||
|
user ->
|
||||||
|
keys = Accounts.get_keys_for(user)
|
||||||
|
|> Enum.map(fn k -> k.value end)
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
body = "# Keys for #{user.username}\n" <> keys
|
||||||
|
conn |> text(body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(conn, %{"key" => key} = params) do
|
||||||
|
%{"value" => value, "comment" => comment} = key
|
||||||
|
user = conn.assigns.current_user
|
||||||
|
|
||||||
|
case Accounts.add_key(user, value, comment) do
|
||||||
|
{:ok, _} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:info, "Key added successfully.")
|
||||||
|
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Submitted key is invalid.")
|
||||||
|
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(conn, %{"key_id" => id}) do
|
||||||
|
user = conn.assigns.current_user
|
||||||
|
key = Accounts.get_key!(id)
|
||||||
|
|
||||||
|
if key.user_id == user.id do
|
||||||
|
Accounts.remove_key!(key)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_flash(:info, "Key removed.")
|
||||||
|
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -70,5 +70,7 @@ defmodule RecycledCloudWeb.UserSettingsController do
|
||||||
conn
|
conn
|
||||||
|> assign(:email_changeset, Accounts.change_user_email(user))
|
|> assign(:email_changeset, Accounts.change_user_email(user))
|
||||||
|> assign(:password_changeset, Accounts.change_user_password(user))
|
|> assign(:password_changeset, Accounts.change_user_password(user))
|
||||||
|
|> assign(:key_changeset, Accounts.change_user_key(%Accounts.Key{}))
|
||||||
|
|> assign(:keys, Accounts.get_keys_for(user))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,16 +16,21 @@ defmodule RecycledCloudWeb.Router do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pipeline :plain do
|
||||||
|
plug :accepts, ["text"]
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", RecycledCloudWeb do
|
scope "/", RecycledCloudWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :index
|
get "/", PageController, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
scope "/", RecycledCloudWeb do
|
||||||
# scope "/api", RecycledCloudWeb do
|
pipe_through :plain
|
||||||
# pipe_through :api
|
|
||||||
# end
|
get "/keys/:username", UserKeysController, :index
|
||||||
|
end
|
||||||
|
|
||||||
## Authentication routes
|
## Authentication routes
|
||||||
|
|
||||||
|
@ -48,6 +53,8 @@ defmodule RecycledCloudWeb.Router do
|
||||||
get "/users/settings", UserSettingsController, :edit
|
get "/users/settings", UserSettingsController, :edit
|
||||||
put "/users/settings", UserSettingsController, :update
|
put "/users/settings", UserSettingsController, :update
|
||||||
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
|
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
|
||||||
|
post "/users/settings/keys/new", UserKeysController, :new
|
||||||
|
get "/users/settings/keys/:key_id/delete", UserKeysController, :delete
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", RecycledCloudWeb do
|
scope "/", RecycledCloudWeb do
|
||||||
|
|
|
@ -58,3 +58,46 @@ method.
|
||||||
<%= submit "Change password" %>
|
<%= submit "Change password" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<h3>SSH Keys</h3>
|
||||||
|
|
||||||
|
<%= form_for @key_changeset, Routes.user_keys_path(@conn, :new), fn f -> %>
|
||||||
|
<%= if @key_changeset.action do %>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>Oops, something went wrong! Please check the errors below.</p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= text_input f, :comment, placeholder: "comment" %>
|
||||||
|
<%= error_tag f, :comment %>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<%= textarea f, :value, required: true, placeholder: "value" %>
|
||||||
|
<%= error_tag f, :value %>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= submit "Add key" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Comment</th>
|
||||||
|
<th>Imported on</th>
|
||||||
|
<th>Fingerprint</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<%= for key <- @keys do %>
|
||||||
|
<tr>
|
||||||
|
<td><%= key.comment %></td>
|
||||||
|
<td><%= key.inserted_at %></td>
|
||||||
|
<td><%= key.fingerprint %></td>
|
||||||
|
<td><%= link "Delete", to: Routes.user_keys_path(@conn, :delete, key.id) %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
15
priv/repo/migrations/20201222142443_create_keys.exs
Normal file
15
priv/repo/migrations/20201222142443_create_keys.exs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
defmodule RecycledCloud.Repo.Migrations.CreateKeys do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:keys) do
|
||||||
|
add :comment, :string
|
||||||
|
add :value, :text
|
||||||
|
add :user_id, references(:users, on_delete: :nothing)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:keys, [:user_id])
|
||||||
|
end
|
||||||
|
end
|
10
priv/repo/migrations/20201223114606_add_key_fingerprint.exs
Normal file
10
priv/repo/migrations/20201223114606_add_key_fingerprint.exs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule RecycledCloud.Repo.Migrations.AddKeyFingerprint do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:keys) do
|
||||||
|
add :fingerprint, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue