Skip to content

Commit

Permalink
Device Shared Secret Authentication (#1139)
Browse files Browse the repository at this point in the history
this is based upon 0d44fc2 with the addition of Product Shared Secrets

* device token auth strategy requires opt in during runtime
* add an `Experimental` badge
* improvements to the shared secrets UI
* add authorization to the Product Settings LiveView

---------

Co-authored-by: Jon Carstens <[email protected]>
  • Loading branch information
joshk and jjcarstens committed Jan 7, 2024
1 parent a2294c7 commit b83ea21
Show file tree
Hide file tree
Showing 32 changed files with 1,012 additions and 33 deletions.
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", "[email protected]")

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
47 changes: 45 additions & 2 deletions lib/nerves_hub/devices.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,31 @@ defmodule NervesHub.Devices do
alias NervesHub.Firmwares
alias NervesHub.Firmwares.Firmware
alias NervesHub.Firmwares.FirmwareMetadata
alias NervesHub.Products
alias NervesHub.Products.Product
alias NervesHub.Products.SharedSecretAuth
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,24 @@ defmodule NervesHub.Devices do
end
end

@spec get_or_create_device(SharedSecretAuth.t(), String.t()) ::
{:ok, Device.t()} | {:error, :not_found}
def get_or_create_device(%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
55 changes: 54 additions & 1 deletion lib/nerves_hub/products.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule NervesHub.Products do

alias Ecto.Multi
alias NervesHub.{Certificate, Repo}
alias NervesHub.Products.Product
alias NervesHub.Products.{Product, SharedSecretAuth}
alias NervesHub.Accounts.{User, Org, OrgUser}

alias NimbleCSV.RFC4180, as: CSV
Expand Down Expand Up @@ -54,6 +54,16 @@ defmodule NervesHub.Products do
|> Repo.get!(id)
end

def get_product(id) do
Product
|> Repo.exclude_deleted()
|> Repo.get(id)
|> case do
nil -> {:error, :not_found}
product -> {:ok, product}
end
end

def get_product_by_org_id_and_name(org_id, name) do
Product
|> Repo.exclude_deleted()
Expand Down Expand Up @@ -130,6 +140,49 @@ 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))
|> where([ssa], ssa.key == ^key)
|> where([ssa], is_nil(ssa.deactivated_at))
|> where([_, p], is_nil(p.deleted_at))
|> Repo.one()
|> case do
nil -> {:error, :not_found}
auth -> {:ok, auth}
end
end

def load_shared_secret_auth(product) do
Repo.preload(product, :shared_secret_auths)
end

def create_shared_secret_auth(product) do
product
|> SharedSecretAuth.create_changeset()
|> 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))
Expand Down
5 changes: 5 additions & 0 deletions lib/nerves_hub/products/product.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule NervesHub.Products.Product do
alias NervesHub.Devices.CACertificate
alias NervesHub.Devices.Device
alias NervesHub.Firmwares.Firmware
alias NervesHub.Products.SharedSecretAuth
alias NervesHub.Repo

@required_params [:name, :org_id]
Expand All @@ -20,6 +21,10 @@ defmodule NervesHub.Products.Product do
has_many(:jitp, CACertificate.JITP)
has_many(:archives, Archive)

has_many(:shared_secret_auths, SharedSecretAuth,
preload_order: [desc: :deactivated_at, asc: :id]
)

belongs_to(:org, Org, where: [deleted_at: nil])

field(:name, :string)
Expand Down
Loading

0 comments on commit b83ea21

Please sign in to comment.