diff --git a/assets/js/app.js b/assets/js/app.js index ff0cae062..6c5c51089 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -64,3 +64,16 @@ window.deploymentPolling = (url) => { document.querySelectorAll('.date-time').forEach(d => { d.innerHTML = dates.formatDateTime(d.innerHTML) }) + +window.addEventListener('phx:sharedsecret:clipcopy', (event) => { + if ("clipboard" in navigator) { + const text = event.detail.secret; + navigator.clipboard.writeText(text).then(() => { + confirm('Content copied to clipboard'); + }, () => { + alert('Failed to copy'); + }); + } else { + alert("Sorry, your browser does not support clipboard copy."); + } +}); diff --git a/lib/nerves_hub/products.ex b/lib/nerves_hub/products.ex index b5d6cc098..bc7bf9ff4 100644 --- a/lib/nerves_hub/products.ex +++ b/lib/nerves_hub/products.ex @@ -140,6 +140,18 @@ defmodule NervesHub.Products do Product.changeset(product, %{}) end + def get_shared_secret_auth(product_id, auth_id) do + SharedSecretAuth + |> join(:inner, [ssa], p in assoc(ssa, :product)) + |> where([ssa], ssa.id == ^auth_id) + |> where([_, p], p.id == ^product_id) + |> Repo.one() + |> case do + nil -> {:error, :not_found} + auth -> {:ok, auth} + end + end + def get_shared_secret_auth(key) do SharedSecretAuth |> join(:inner, [ssa], p in assoc(ssa, :product)) @@ -163,6 +175,14 @@ defmodule NervesHub.Products do |> Repo.insert() end + def deactivate_shared_secret_auth(product, shared_secret_id) do + {:ok, auth} = get_shared_secret_auth(product.id, shared_secret_id) + + auth + |> SharedSecretAuth.deactivate_changeset() + |> Repo.update() + end + def devices_csv(%Product{} = product) do product = Repo.preload(product, [:org, devices: :device_certificates]) data = Enum.map(product.devices, &device_csv_line(&1, product)) diff --git a/lib/nerves_hub/products/product.ex b/lib/nerves_hub/products/product.ex index 6c640b375..5f22f7a7f 100644 --- a/lib/nerves_hub/products/product.ex +++ b/lib/nerves_hub/products/product.ex @@ -20,7 +20,10 @@ defmodule NervesHub.Products.Product do has_many(:firmwares, Firmware) has_many(:jitp, CACertificate.JITP) has_many(:archives, Archive) - has_many(:shared_secret_auth, SharedSecretAuth) + + has_many(:shared_secret_auth, SharedSecretAuth, + preload_order: [desc: :deactivated_at, asc: :id] + ) belongs_to(:org, Org, where: [deleted_at: nil]) diff --git a/lib/nerves_hub/products/shared_secret_auth.ex b/lib/nerves_hub/products/shared_secret_auth.ex index ff150fca8..88505c5c2 100644 --- a/lib/nerves_hub/products/shared_secret_auth.ex +++ b/lib/nerves_hub/products/shared_secret_auth.ex @@ -32,6 +32,11 @@ defmodule NervesHub.Products.SharedSecretAuth do |> unique_constraint(:secret) end + def deactivate_changeset(%__MODULE__{} = auth) do + change(auth) + |> put_change(:deactivated_at, DateTime.truncate(DateTime.utc_now(), :second)) + end + defp generate_token() do :crypto.strong_rand_bytes(32) |> Base.encode64(padding: false) end diff --git a/lib/nerves_hub_web/live/product/settings.ex b/lib/nerves_hub_web/live/product/settings.ex index d0bd91946..91f19b4cd 100644 --- a/lib/nerves_hub_web/live/product/settings.ex +++ b/lib/nerves_hub_web/live/product/settings.ex @@ -7,6 +7,7 @@ defmodule NervesHubWeb.Live.Product.Settings do def mount(_params, _session, socket) do socket = socket + |> assign(:shared_secrets, socket.assigns.product.shared_secret_auth) |> assign(:shared_auth_enabled, DeviceSocketSharedSecretAuth.enabled?()) {:ok, socket} @@ -17,14 +18,33 @@ defmodule NervesHubWeb.Live.Product.Settings do {:ok, product} = Products.update_product(socket.assigns.product, attrs) - {:noreply, assign(socket, :product, product)} + {:reply, assign(socket, :product, product)} end def handle_event("add-shared-secret", _params, socket) do - {:ok, _} = NervesHub.Products.create_shared_secret_auth(socket.assigns.product) + {:ok, _} = Products.create_shared_secret_auth(socket.assigns.product) - {:ok, product} = Products.load_shared_secret_auth(socket.assigns.product) + refreshed = Products.load_shared_secret_auth(socket.assigns.product) - {:noreply, assign(socket, :product, product)} + {:reply, assign(socket, :shared_secrets, refreshed.shared_secret_auth)} + end + + def handle_event("copy-shared-secret", %{"value" => shared_secret_id}, socket) do + auth = + Enum.find(socket.assigns.product.shared_secret_auth, fn ssa -> + ssa.id == String.to_integer(shared_secret_id) + end) + + {:noreply, push_event(socket, "sharedsecret:clipcopy", %{secret: auth.secret})} + end + + def handle_event("deactivate-shared-secret", %{"shared_secret_id" => shared_secret_id}, socket) do + product = socket.assigns.product + + {:ok, _} = Products.deactivate_shared_secret_auth(product, shared_secret_id) + + refreshed = Products.load_shared_secret_auth(product) + + {:reply, assign(socket, :shared_secrets, refreshed.shared_secret_auth)} end end diff --git a/lib/nerves_hub_web/live/product/settings.html.heex b/lib/nerves_hub_web/live/product/settings.html.heex index 21355ee9b..cac6d49a0 100644 --- a/lib/nerves_hub_web/live/product/settings.html.heex +++ b/lib/nerves_hub_web/live/product/settings.html.heex @@ -37,10 +37,10 @@
-- Key / Secret stuff +
+ Shared Secret authentication allows Devices to connect to Nerves Hub using a shared key and secret.
+ When a Device connects for the first time the Device will be registered with the Product ("Just-in-Time registration").
+
+ This authentication strategy is useful for small deployments of Devices, or when prototyping a new Product.
+ We highly recommend using Device Certificates for situations where security is paramount.
+
+ Please refer to the <.link navigate="https://docs.nerves-hub.org/nerves-hub-link/shared-secrets">documentation + on how to configure this with <.link navigate="https://github.com/nerves-hub/nerves_hub_link">NervesHubLink.
<%= if @shared_auth_enabled do %> - <%= if Enum.empty?(@product.shared_secret_auth) do %> + <%= if Enum.empty?(@shared_secrets) do %> No Shared Secrets added. <% end %> -Key | -Secret | +Created at | +Deactivated at | ++ | |
---|---|---|---|---|---|
Key
<%= auth.key %>
|
- Secret
- <%= auth.secret %>
+ Created at
+ <%= Date.to_string(auth.inserted_at) %>
+ |
+
+ Deactivated at
+ <%= if auth.deactivated_at, do: Date.to_string(auth.deactivated_at) %>
+ |
+ + + | ++ + + |