mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 21:14:26 +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.
|
||||
Supported values are:
|
||||
|
||||
* "basic_auth:<username>:<password>"
|
||||
* "cloudflare:<your-team-name (domain)>"
|
||||
* "google_iap:<your-audience (aud)>"
|
||||
* "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.
|
||||
@identity_providers [
|
||||
%{
|
||||
type: :basic_auth,
|
||||
name: "Basic Auth",
|
||||
value: "Credentials (username:password)",
|
||||
module: Livebook.ZTA.BasicAuth,
|
||||
placeholder: "username:password",
|
||||
input: "password"
|
||||
},
|
||||
%{
|
||||
type: :cloudflare,
|
||||
name: "Cloudflare",
|
||||
|
|
|
@ -6,7 +6,7 @@ defmodule Livebook.Teams.DeploymentGroup do
|
|||
alias Livebook.Teams.AgentKey
|
||||
|
||||
# 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__{
|
||||
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
|
||||
:if={zta_metadata = zta_metadata(@form[:zta_provider].value)}
|
||||
field={@form[:zta_key]}
|
||||
type={Map.get(zta_metadata, :input, "text")}
|
||||
label={zta_metadata.value}
|
||||
placeholder={zta_placeholder(zta_metadata)}
|
||||
phx-debounce
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -219,6 +219,7 @@ defmodule Livebook.MixProject do
|
|||
{"README.md", title: "Welcome to Livebook"},
|
||||
"docs/authentication.md",
|
||||
"docs/deployment/docker.md",
|
||||
"docs/deployment/basic_auth.md",
|
||||
"docs/deployment/cloudflare.md",
|
||||
"docs/deployment/google_iap.md",
|
||||
"docs/deployment/tailscale.md",
|
||||
|
|
|
@ -48,6 +48,11 @@ defmodule Livebook.ConfigTest do
|
|||
assert Config.identity_provider!("TEST_IDENTITY_PROVIDER") ==
|
||||
{:zta, Livebook.ZTA.Cloudflare, "123"}
|
||||
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
|
||||
|
||||
|
|
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