Skip to content

Commit

Permalink
Shared Secret Auth (#1176)
Browse files Browse the repository at this point in the history
* Device Shared Secret Authentication (#1139)

this is based upon 0d44fc2 with the addition of Product Shared Secrets
  • Loading branch information
joshk authored Jan 13, 2024
1 parent e71a0b1 commit c259b84
Show file tree
Hide file tree
Showing 36 changed files with 1,197 additions and 34 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ jobs:
tags: |
# short sha
type=sha,enable={{is_default_branch}},prefix=,suffix=,format=short
# branch image names, except for main
type=ref,enable=${{ github.ref != format('refs/heads/{0}', 'main') }},event=branch
# latest tag for main branch
type=raw,enable={{is_default_branch}},value=latest
# tag event (eg. "v1.2.3")
Expand Down
7 changes: 6 additions & 1 deletion assets/css/_navigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,16 @@ nav.navbar {

.nav-item {
margin-right: 2.75rem;
padding: 0 1.25rem;

@media (max-width: 1100px) {
margin-right: 0;
}

&:first-child {
padding: 0 1.25rem 0 0;
}

&:last-child {
margin-right: 0;
}
Expand All @@ -290,7 +295,7 @@ nav.navbar {
height: 76px;
display: flex;
align-items: center;
padding: 0 1.25rem;
padding: 0 0 0 0;

@media (max-width: 1100px) {
height: 60px;
Expand Down
13 changes: 13 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
});
2 changes: 2 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ config :nerves_hub, NervesHub.Uploads.File,
##
# Other
#
config :nerves_hub, NervesHubWeb.DeviceSocketSharedSecretAuth, enabled: true

config :nerves_hub, NervesHub.SwooshMailer, adapter: Swoosh.Adapters.Local

config :nerves_hub, NervesHub.RateLimit, limit: 10
Expand Down
7 changes: 5 additions & 2 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ config :nerves_hub,
deploy_env: System.get_env("DEPLOY_ENV", to_string(config_env())),
from_email: System.get_env("FROM_EMAIL", "no-reply@nerves-hub.org")

if log_level = System.get_env("LOG_LEVEL") do
config :logger, level: String.to_atom(log_level)
if level = System.get_env("LOG_LEVEL") do
config :logger, level: String.to_atom(level)
end

dns_cluster_query =
Expand Down Expand Up @@ -55,6 +55,9 @@ if config_env() == :prod do
signing_salt: System.fetch_env!("LIVE_VIEW_SIGNING_SALT")
],
server: true

config :nerves_hub, NervesHubWeb.DeviceSocketSharedSecretAuth,
enabled: System.get_env("DEVICE_SHARED_SECRET_AUTH", "false") == "true"
end

if nerves_hub_app in ["all", "device"] do
Expand Down
9 changes: 5 additions & 4 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ config :logger, :default_handler, false
# NervesHub Web
#
config :nerves_hub, NervesHubWeb.Endpoint,
http: [port: 5000],
server: false,
http: [port: 4100],
server: true,
secret_key_base: "x7Vj9rmmRke//ctlapsPNGHXCRTnArTPbfsv6qX4PChFT9ARiNR5Ua8zoRilNCmX",
live_view: [signing_salt: "FnV9rP_c2BL11dvh"]

Expand Down Expand Up @@ -41,8 +41,7 @@ config :nerves_hub, NervesHubWeb.DeviceEndpoint,
##
# Firmware uploader
#
config :nerves_hub,
firmware_upload: NervesHub.UploadMock
config :nerves_hub, firmware_upload: NervesHub.UploadMock

config :nerves_hub, NervesHub.Firmwares.Upload.S3, bucket: "mybucket"

Expand Down Expand Up @@ -74,6 +73,8 @@ config :nerves_hub, Oban, queues: false, plugins: false
##
# Other
#
config :nerves_hub, NervesHubWeb.DeviceSocketSharedSecretAuth, enabled: true

config :nerves_hub, delta_updater: NervesHub.DeltaUpdaterMock

config :nerves_hub, NervesHub.SwooshMailer, adapter: Swoosh.Adapters.Test
Expand Down
23 changes: 13 additions & 10 deletions lib/nerves_hub/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ defmodule NervesHub.Accounts do
RemoveAccount
}

alias NervesHub.Products.Product

alias NervesHub.Repo

@spec create_org(User.t(), map) ::
Expand Down Expand Up @@ -128,12 +130,9 @@ defmodule NervesHub.Accounts do
end

def get_org_user(org, user) do
from(
ou in OrgUser,
where:
ou.org_id == ^org.id and
ou.user_id == ^user.id
)
OrgUser
|> where([ou], ou.org_id == ^org.id)
|> where([ou], ou.user_id == ^user.id)
|> OrgUser.with_user()
|> Repo.exclude_deleted()
|> Repo.one()
Expand Down Expand Up @@ -224,12 +223,16 @@ defmodule NervesHub.Accounts do
|> Repo.get!(user_id)
end

def get_user_with_all_orgs(user_id) do
query = from(u in User, where: u.id == ^user_id)
def get_user_with_all_orgs_and_products(user_id) do
org_query = from(o in Org, where: is_nil(o.deleted_at))
product_query = from(p in Product, where: is_nil(p.deleted_at))

query
orgs_preload = {org_query, products: product_query}

User
|> where([u], u.id == ^user_id)
|> Repo.exclude_deleted()
|> User.with_all_orgs()
|> preload(orgs: ^orgs_preload)
|> Repo.one()
|> case do
nil -> {:error, :not_found}
Expand Down
17 changes: 10 additions & 7 deletions lib/nerves_hub/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ defmodule NervesHub.Application do
{Cluster.Supervisor, [topologies]},
{Task.Supervisor, name: NervesHub.TaskSupervisor},
{Oban, Application.fetch_env!(:nerves_hub, Oban)},
NervesHub.Tracker
NervesHub.Tracker,
NervesHub.Devices.Supervisor
] ++
endpoints(Application.get_env(:nerves_hub, :deploy_env))
deployments_supervisor(deploy_env()) ++
endpoints(deploy_env())

opts = [strategy: :one_for_one, name: NervesHub.Supervisor]
Supervisor.start_link(children, opts)
Expand All @@ -52,9 +54,14 @@ defmodule NervesHub.Application do
[NervesHub.Metrics]
end

defp deployments_supervisor("test"), do: []

defp deployments_supervisor(_) do
[NervesHub.Deployments.Supervisor]
end

defp endpoints("test") do
[
NervesHub.Devices.Supervisor,
NervesHubWeb.DeviceEndpoint,
NervesHubWeb.Endpoint
]
Expand All @@ -64,16 +71,12 @@ defmodule NervesHub.Application do
case Application.get_env(:nerves_hub, :app) do
"all" ->
[
NervesHub.Deployments.Supervisor,
NervesHub.Devices.Supervisor,
NervesHubWeb.DeviceEndpoint,
NervesHubWeb.Endpoint
] ++ device_socket_drainer()

"device" ->
[
NervesHub.Deployments.Supervisor,
NervesHub.Devices.Supervisor,
NervesHubWeb.DeviceEndpoint
] ++ device_socket_drainer()

Expand Down
71 changes: 69 additions & 2 deletions lib/nerves_hub/devices.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,36 @@ defmodule NervesHub.Devices do
alias NervesHub.Devices.CACertificate
alias NervesHub.Devices.Device
alias NervesHub.Devices.DeviceCertificate
alias NervesHub.Devices.SharedSecretAuth
alias NervesHub.Devices.InflightUpdate
alias NervesHub.Devices.UpdatePayload
alias NervesHub.Firmwares
alias NervesHub.Firmwares.Firmware
alias NervesHub.Firmwares.FirmwareMetadata
alias NervesHub.Products
alias NervesHub.Products.Product
alias NervesHub.Repo
alias NervesHub.TaskSupervisor, as: Tasks

@min_fwup_delta_updatable_version ">=1.10.0"

def get_device(device_id), do: Repo.get(Device, device_id)
def get_device!(device_id), do: Repo.get!(Device, device_id)
def get_device!(device_id) do
Repo.get!(Device, device_id)
end

def get_device(device_id) when is_integer(device_id) do
Repo.get(Device, device_id)
end

def get_active_device(filters) do
Device
|> Repo.exclude_deleted()
|> Repo.get_by(filters)
|> case do
nil -> {:error, :not_found}
device -> {:ok, device}
end
end

def get_devices_by_org_id(org_id) do
query =
Expand Down Expand Up @@ -154,6 +171,14 @@ defmodule NervesHub.Devices do
|> Repo.one!()
end

def get_device_count_by_product_id(product_id) do
Device
|> where([d], d.product_id == ^product_id)
|> Repo.exclude_deleted()
|> select([d], count(d))
|> Repo.one!()
end

defp device_by_org_query(org_id, device_id) do
from(
d in Device,
Expand Down Expand Up @@ -221,6 +246,48 @@ defmodule NervesHub.Devices do
end
end

@spec get_shared_secret_auth(String.t()) ::
{:ok, SharedSecretAuth.t()} | {:error, :not_found}
def get_shared_secret_auth(key) do
SharedSecretAuth
|> join(:inner, [ssa], d in assoc(ssa, :device))
|> where([ssa], ssa.key == ^key)
|> where([ssa], is_nil(ssa.deactivated_at))
|> where([_, d], is_nil(d.deleted_at))
|> preload([:device, :product_shared_secret_auth])
|> Repo.one()
|> case do
nil -> {:error, :not_found}
auth -> {:ok, auth}
end
end

@spec create_shared_secret_auth(Device.t(), %{product_shared_secret_auth_id: pos_integer()}) ::
{:ok, SharedSecretAuth.t()} | {:error, Changeset.t()}
def create_shared_secret_auth(device, attrs \\ %{}) do
device
|> SharedSecretAuth.create_changeset(attrs)
|> Repo.insert()
end

@spec get_or_create_device(Products.SharedSecretAuth.t(), String.t()) ::
{:ok, Device.t()} | {:error, :not_found}
def get_or_create_device(%Products.SharedSecretAuth{} = auth, identifier) do
with {:error, :not_found} <-
get_active_device(product_id: auth.product_id, identifier: identifier),
{:ok, product} <-
Products.get_product(auth.product_id) do
create_device(%{
org_id: product.org_id,
product_id: product.id,
identifier: identifier
})
else
result ->
result
end
end

def get_device_by(filters) do
Repo.get_by(Device, filters)
|> case do
Expand Down
45 changes: 45 additions & 0 deletions lib/nerves_hub/devices/shared_secret_auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule NervesHub.Devices.SharedSecretAuth do
use Ecto.Schema

import Ecto.Changeset

alias NervesHub.Devices.Device
alias NervesHub.Products

@type t :: %__MODULE__{}

@key_prefix "nhd"

schema "device_shared_secret_auths" do
belongs_to(:device, Device)
belongs_to(:product_shared_secret_auth, Products.SharedSecretAuth)

field(:key, :string)
field(:secret, :string)

field(:deactivated_at, :utc_datetime)

timestamps()
end

def create_changeset(%Device{id: device_id}, attrs \\ %{}) do
cast(%__MODULE__{device_id: device_id}, attrs, [:product_shared_secret_auth_id])
|> put_change(:key, "#{@key_prefix}_#{generate_token()}")
|> put_change(:secret, generate_token())
|> validate_required([:device_id, :key, :secret])
|> validate_format(:key, ~r/^#{@key_prefix}_[a-zA-Z0-9\-\/\+]{43}$/)
|> validate_format(:secret, ~r/^[a-zA-Z0-9\-\/\+]{43}$/)
|> foreign_key_constraint(:device_id)
|> foreign_key_constraint(:product_shared_secret_auth_id)
|> unique_constraint(:key)
|> unique_constraint(:secret)
end

def deactivate_changeset(%__MODULE__{} = auth) do
change(auth, %{deactivated_at: DateTime.truncate(DateTime.utc_now(), :second)})
end

defp generate_token() do
:crypto.strong_rand_bytes(32) |> Base.encode64(padding: false)
end
end
Loading

0 comments on commit c259b84

Please sign in to comment.