From c2faafe3a3d8d789494309051a80a9e2291f09fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 23 Dec 2020 12:51:05 +0100 Subject: [PATCH] Add minimal SSH key management --- lib/recycledcloud/accounts.ex | 25 ++++++++- lib/recycledcloud/accounts/key.ex | 48 +++++++++++++++++ lib/recycledcloud/accounts/user.ex | 2 + .../controllers/user_keys_controller.ex | 53 +++++++++++++++++++ .../controllers/user_settings_controller.ex | 2 + lib/recycledcloud_web/router.ex | 15 ++++-- .../templates/user_settings/edit.html.eex | 43 +++++++++++++++ .../migrations/20201222142443_create_keys.exs | 15 ++++++ .../20201223114606_add_key_fingerprint.exs | 10 ++++ 9 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 lib/recycledcloud/accounts/key.ex create mode 100644 lib/recycledcloud_web/controllers/user_keys_controller.ex create mode 100644 priv/repo/migrations/20201222142443_create_keys.exs create mode 100644 priv/repo/migrations/20201223114606_add_key_fingerprint.exs diff --git a/lib/recycledcloud/accounts.ex b/lib/recycledcloud/accounts.ex index 94ce1ce..3cd13d6 100644 --- a/lib/recycledcloud/accounts.ex +++ b/lib/recycledcloud/accounts.ex @@ -5,7 +5,7 @@ defmodule RecycledCloud.Accounts do import Ecto.Query, warn: false alias RecycledCloud.Repo - alias RecycledCloud.Accounts.{User, UserToken, UserNotifier} + alias RecycledCloud.Accounts.{User, Key, UserToken, UserNotifier} alias RecycledCloud.LDAP ## Database getters @@ -356,4 +356,27 @@ defmodule RecycledCloud.Accounts do {:error, :user, changeset, _} -> {:error, changeset} 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 diff --git a/lib/recycledcloud/accounts/key.ex b/lib/recycledcloud/accounts/key.ex new file mode 100644 index 0000000..7fd2a37 --- /dev/null +++ b/lib/recycledcloud/accounts/key.ex @@ -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 diff --git a/lib/recycledcloud/accounts/user.ex b/lib/recycledcloud/accounts/user.ex index c9a1653..139508a 100644 --- a/lib/recycledcloud/accounts/user.ex +++ b/lib/recycledcloud/accounts/user.ex @@ -15,6 +15,8 @@ defmodule RecycledCloud.Accounts.User do field :email, :string, virtual: true field :confirmed_at, :naive_datetime + has_many :keys, Accounts.Key + timestamps() end diff --git a/lib/recycledcloud_web/controllers/user_keys_controller.ex b/lib/recycledcloud_web/controllers/user_keys_controller.ex new file mode 100644 index 0000000..ee4742a --- /dev/null +++ b/lib/recycledcloud_web/controllers/user_keys_controller.ex @@ -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 diff --git a/lib/recycledcloud_web/controllers/user_settings_controller.ex b/lib/recycledcloud_web/controllers/user_settings_controller.ex index d8aa79b..e4711ba 100644 --- a/lib/recycledcloud_web/controllers/user_settings_controller.ex +++ b/lib/recycledcloud_web/controllers/user_settings_controller.ex @@ -70,5 +70,7 @@ defmodule RecycledCloudWeb.UserSettingsController do conn |> assign(:email_changeset, Accounts.change_user_email(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 diff --git a/lib/recycledcloud_web/router.ex b/lib/recycledcloud_web/router.ex index 74b72a6..7b31bba 100644 --- a/lib/recycledcloud_web/router.ex +++ b/lib/recycledcloud_web/router.ex @@ -16,16 +16,21 @@ defmodule RecycledCloudWeb.Router do plug :accepts, ["json"] end + pipeline :plain do + plug :accepts, ["text"] + end + scope "/", RecycledCloudWeb do pipe_through :browser get "/", PageController, :index end - # Other scopes may use custom stacks. - # scope "/api", RecycledCloudWeb do - # pipe_through :api - # end + scope "/", RecycledCloudWeb do + pipe_through :plain + + get "/keys/:username", UserKeysController, :index + end ## Authentication routes @@ -48,6 +53,8 @@ defmodule RecycledCloudWeb.Router do get "/users/settings", UserSettingsController, :edit put "/users/settings", UserSettingsController, :update 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 scope "/", RecycledCloudWeb do diff --git a/lib/recycledcloud_web/templates/user_settings/edit.html.eex b/lib/recycledcloud_web/templates/user_settings/edit.html.eex index 554883a..4bd62b6 100644 --- a/lib/recycledcloud_web/templates/user_settings/edit.html.eex +++ b/lib/recycledcloud_web/templates/user_settings/edit.html.eex @@ -58,3 +58,46 @@ method. <%= submit "Change password" %> <% end %> + +

SSH Keys

+ +<%= form_for @key_changeset, Routes.user_keys_path(@conn, :new), fn f -> %> + <%= if @key_changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= text_input f, :comment, placeholder: "comment" %> + <%= error_tag f, :comment %> + +
+ + <%= textarea f, :value, required: true, placeholder: "value" %> + <%= error_tag f, :value %> + +
+ <%= submit "Add key" %> +
+<% end %> + + + + + + + + + + + + <%= for key <- @keys do %> + + + + + + + <% end %> + +
CommentImported onFingerprintActions
<%= key.comment %><%= key.inserted_at %><%= key.fingerprint %><%= link "Delete", to: Routes.user_keys_path(@conn, :delete, key.id) %>
diff --git a/priv/repo/migrations/20201222142443_create_keys.exs b/priv/repo/migrations/20201222142443_create_keys.exs new file mode 100644 index 0000000..54611d5 --- /dev/null +++ b/priv/repo/migrations/20201222142443_create_keys.exs @@ -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 diff --git a/priv/repo/migrations/20201223114606_add_key_fingerprint.exs b/priv/repo/migrations/20201223114606_add_key_fingerprint.exs new file mode 100644 index 0000000..424d17b --- /dev/null +++ b/priv/repo/migrations/20201223114606_add_key_fingerprint.exs @@ -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