mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-10 15:04:25 +08:00
Implement Basic Auth ZTA (#2564)
This commit is contained in:
parent
59deaa5329
commit
50f9bb2420
9 changed files with 106 additions and 1 deletions
|
@ -236,6 +236,7 @@ The following environment variables can be used to configure Livebook on boot:
|
||||||
Livebook inside a cloud platform, such as Cloudflare and Google.
|
Livebook inside a cloud platform, such as Cloudflare and Google.
|
||||||
Supported values are:
|
Supported values are:
|
||||||
|
|
||||||
|
* "basic_auth:<username>:<password>"
|
||||||
* "cloudflare:<your-team-name (domain)>"
|
* "cloudflare:<your-team-name (domain)>"
|
||||||
* "google_iap:<your-audience (aud)>"
|
* "google_iap:<your-audience (aud)>"
|
||||||
* "tailscale:<tailscale-cli-socket-path>"
|
* "tailscale:<tailscale-cli-socket-path>"
|
||||||
|
|
22
docs/deployment/basic_auth.md
Normal file
22
docs/deployment/basic_auth.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Authentication with Basic Auth
|
||||||
|
|
||||||
|
Setting up Basic Authentication will protect all routes of your notebook. It is particularly useful for adding authentication to deployed notebooks. Basic Authentication is provided in addition to [Livebook's authentication](../authentication.md) for authoring notebooks.
|
||||||
|
|
||||||
|
## How to
|
||||||
|
|
||||||
|
To integrate Basic Authentication with Livebook, set the `LIVEBOOK_IDENTITY_PROVIDER` environment variable to `basic_auth:<username>:<password>`.
|
||||||
|
|
||||||
|
To do it, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LIVEBOOK_IDENTITY_PROVIDER=basic_auth:user:pass \
|
||||||
|
livebook server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Livebook Teams
|
||||||
|
|
||||||
|
[Livebook Teams](https://livebook.dev/teams/) users have access to airgapped notebook deployment via Docker, with pre-configured Zero Trust Authentication, shared team secrets, and file storages.
|
||||||
|
|
||||||
|
Furthermore, if you are deploying multi-session apps via [Livebook Teams](https://livebook.dev/teams/), you can programmatically access data from the authenticated user by calling [`Kino.Hub.app_info/0`](https://hexdocs.pm/kino/Kino.Hub.html#app_info/0).
|
||||||
|
|
||||||
|
To get started, open up Livebook, click "Add Organization" on the sidebar, and visit the "Airgapped Deployment" section of your organization.
|
|
@ -10,6 +10,14 @@ defmodule Livebook.Config do
|
||||||
#
|
#
|
||||||
# IMPORTANT: this list must be in sync with Livebook Teams.
|
# IMPORTANT: this list must be in sync with Livebook Teams.
|
||||||
@identity_providers [
|
@identity_providers [
|
||||||
|
%{
|
||||||
|
type: :basic_auth,
|
||||||
|
name: "Basic Auth",
|
||||||
|
value: "Credentials (username:password)",
|
||||||
|
module: Livebook.ZTA.BasicAuth,
|
||||||
|
placeholder: "username:password",
|
||||||
|
input: "password"
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
type: :cloudflare,
|
type: :cloudflare,
|
||||||
name: "Cloudflare",
|
name: "Cloudflare",
|
||||||
|
|
|
@ -6,7 +6,7 @@ defmodule Livebook.Teams.DeploymentGroup do
|
||||||
alias Livebook.Teams.AgentKey
|
alias Livebook.Teams.AgentKey
|
||||||
|
|
||||||
# If this list is updated, it must also be mirrored on Livebook Teams Server.
|
# If this list is updated, it must also be mirrored on Livebook Teams Server.
|
||||||
@zta_providers ~w(cloudflare google_iap tailscale teleport)a
|
@zta_providers ~w(basic_auth cloudflare google_iap tailscale teleport)a
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
id: String.t() | nil,
|
id: String.t() | nil,
|
||||||
|
|
25
lib/livebook/zta/basic_auth.ex
Normal file
25
lib/livebook/zta/basic_auth.ex
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
defmodule Livebook.ZTA.BasicAuth do
|
||||||
|
def child_spec(opts) do
|
||||||
|
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_link(options) do
|
||||||
|
name = Keyword.fetch!(options, :name)
|
||||||
|
identity_key = Keyword.fetch!(options, :identity_key)
|
||||||
|
[username, password] = String.split(identity_key, ":", parts: 2)
|
||||||
|
|
||||||
|
Livebook.ZTA.put(name, {username, password})
|
||||||
|
:ignore
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate(name, conn, _options) do
|
||||||
|
{username, password} = Livebook.ZTA.get(name)
|
||||||
|
conn = Plug.BasicAuth.basic_auth(conn, username: username, password: password)
|
||||||
|
|
||||||
|
if conn.halted do
|
||||||
|
{conn, nil}
|
||||||
|
else
|
||||||
|
{conn, %{payload: %{}}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -134,6 +134,7 @@ defmodule LivebookWeb.AppComponents do
|
||||||
<.text_field
|
<.text_field
|
||||||
:if={zta_metadata = zta_metadata(@form[:zta_provider].value)}
|
:if={zta_metadata = zta_metadata(@form[:zta_provider].value)}
|
||||||
field={@form[:zta_key]}
|
field={@form[:zta_key]}
|
||||||
|
type={Map.get(zta_metadata, :input, "text")}
|
||||||
label={zta_metadata.value}
|
label={zta_metadata.value}
|
||||||
placeholder={zta_placeholder(zta_metadata)}
|
placeholder={zta_placeholder(zta_metadata)}
|
||||||
phx-debounce
|
phx-debounce
|
||||||
|
|
1
mix.exs
1
mix.exs
|
@ -219,6 +219,7 @@ defmodule Livebook.MixProject do
|
||||||
{"README.md", title: "Welcome to Livebook"},
|
{"README.md", title: "Welcome to Livebook"},
|
||||||
"docs/authentication.md",
|
"docs/authentication.md",
|
||||||
"docs/deployment/docker.md",
|
"docs/deployment/docker.md",
|
||||||
|
"docs/deployment/basic_auth.md",
|
||||||
"docs/deployment/cloudflare.md",
|
"docs/deployment/cloudflare.md",
|
||||||
"docs/deployment/google_iap.md",
|
"docs/deployment/google_iap.md",
|
||||||
"docs/deployment/tailscale.md",
|
"docs/deployment/tailscale.md",
|
||||||
|
|
|
@ -48,6 +48,11 @@ defmodule Livebook.ConfigTest do
|
||||||
assert Config.identity_provider!("TEST_IDENTITY_PROVIDER") ==
|
assert Config.identity_provider!("TEST_IDENTITY_PROVIDER") ==
|
||||||
{:zta, Livebook.ZTA.Cloudflare, "123"}
|
{:zta, Livebook.ZTA.Cloudflare, "123"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
with_env([TEST_IDENTITY_PROVIDER: "basic_auth:user:pass"], fn ->
|
||||||
|
assert Config.identity_provider!("TEST_IDENTITY_PROVIDER") ==
|
||||||
|
{:zta, Livebook.ZTA.BasicAuth, "user:pass"}
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
42
test/livebook/zta/basic_auth_test.exs
Normal file
42
test/livebook/zta/basic_auth_test.exs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
defmodule Livebook.ZTA.BasicAuthTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
use Plug.Test
|
||||||
|
|
||||||
|
alias Livebook.ZTA.BasicAuth
|
||||||
|
|
||||||
|
import Plug.BasicAuth, only: [encode_basic_auth: 2]
|
||||||
|
|
||||||
|
@name Context.Test.BasicAuth
|
||||||
|
|
||||||
|
setup do
|
||||||
|
username = "ChonkierCat"
|
||||||
|
password = Livebook.Utils.random_long_id()
|
||||||
|
options = [name: @name, identity_key: "#{username}:#{password}"]
|
||||||
|
|
||||||
|
{:ok, username: username, password: password, options: options, conn: conn(:get, "/")}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the user_identity when credentials are valid", context do
|
||||||
|
authorization = encode_basic_auth(context.username, context.password)
|
||||||
|
conn = put_req_header(context.conn, "authorization", authorization)
|
||||||
|
start_supervised!({BasicAuth, context.options})
|
||||||
|
|
||||||
|
assert {_conn, %{payload: %{}}} = BasicAuth.authenticate(@name, conn, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil when the username is invalid", context do
|
||||||
|
authorization = encode_basic_auth("foo", context.password)
|
||||||
|
conn = put_req_header(context.conn, "authorization", authorization)
|
||||||
|
start_supervised!({BasicAuth, context.options})
|
||||||
|
|
||||||
|
assert {_conn, nil} = BasicAuth.authenticate(@name, conn, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil when the password is invalid", context do
|
||||||
|
authorization = encode_basic_auth(context.username, Livebook.Utils.random_long_id())
|
||||||
|
conn = put_req_header(context.conn, "authorization", authorization)
|
||||||
|
start_supervised!({BasicAuth, context.options})
|
||||||
|
|
||||||
|
assert {_conn, nil} = BasicAuth.authenticate(@name, conn, [])
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue