mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 04:54:29 +08:00
Remove teleport, as it does not support smart cells (#2589)
This commit is contained in:
parent
24efee3b82
commit
eb4887657a
7 changed files with 1 additions and 263 deletions
|
@ -243,7 +243,6 @@ The following environment variables can be used to configure Livebook on boot:
|
|||
* "cloudflare:<your-team-name (domain)>"
|
||||
* "google_iap:<your-audience (aud)>"
|
||||
* "tailscale:<tailscale-cli-socket-path>"
|
||||
* "teleport:<teleport-cluster-address>"
|
||||
* "custom:YourElixirModule"
|
||||
|
||||
See our authentication docs for more information: https://hexdocs.pm/livebook/authentication.html
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
# Teleport
|
||||
|
||||
Setting up Teleport authentication will protect all routes of your Livebook instance. It is particularly useful for adding authentication to Livebook instances with deployed notebooks. Teleport authentication occurs in addition to [Livebook's authentication](../authentication.md) for deployed notebooks and admins.
|
||||
|
||||
## How to
|
||||
|
||||
To integrate Teleport authentication with Livebook,
|
||||
set the `LIVEBOOK_IDENTITY_PROVIDER` environment variable to `LIVEBOOK_IDENTITY_PROVIDER=teleport:https://[cluster-name]:3080`.
|
||||
|
||||
```bash
|
||||
LIVEBOOK_IDENTITY_PROVIDER=teleport:https://[cluster-name]:3080 \
|
||||
livebook server
|
||||
```
|
||||
|
||||
See https://goteleport.com/docs/application-access/jwt/introduction/ for more information
|
||||
on how Teleport authentication works.
|
||||
|
||||
## Livebook Teams
|
||||
|
||||
[Livebook Teams](https://livebook.dev/teams/) users can deploy notebooks with the click of a button with pre-configured Zero Trust Authentication, shared team secrets, and file storages. Both online and airgapped deployment mechanisms are supported.
|
||||
|
||||
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.
|
|
@ -35,13 +35,6 @@ defmodule Livebook.Config do
|
|||
name: "Tailscale",
|
||||
value: "Tailscale CLI socket path",
|
||||
module: Livebook.ZTA.Tailscale
|
||||
},
|
||||
%{
|
||||
type: :teleport,
|
||||
name: "Teleport",
|
||||
value: "Teleport cluster address",
|
||||
module: Livebook.ZTA.Teleport,
|
||||
placeholder: "https://[cluster-name]:3080"
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -5,20 +5,7 @@ defmodule Livebook.Teams.DeploymentGroup do
|
|||
alias Livebook.Secrets.Secret
|
||||
alias Livebook.Teams.AgentKey
|
||||
|
||||
# If this list is updated, it must also be mirrored on Livebook Teams Server.
|
||||
@zta_providers ~w(basic_auth cloudflare google_iap tailscale teleport)a
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t() | nil,
|
||||
name: String.t() | nil,
|
||||
mode: :online | :offline,
|
||||
hub_id: String.t() | nil,
|
||||
clustering: :fly_io | nil,
|
||||
zta_provider: :cloudflare | :google_iap | :tailscale | :teleport,
|
||||
zta_key: String.t(),
|
||||
secrets: [Secret.t()],
|
||||
agent_keys: [AgentKey.t()]
|
||||
}
|
||||
@zta_providers Enum.map(Livebook.Config.identity_providers(), & &1.type)
|
||||
|
||||
@primary_key {:id, :string, autogenerate: false}
|
||||
embedded_schema do
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
defmodule Livebook.ZTA.Teleport do
|
||||
@behaviour Livebook.ZTA
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
defstruct [:req_options, :name]
|
||||
|
||||
@renew_afer 24 * 60 * 60 * 1000
|
||||
@fields %{"sub" => :id, "username" => :username}
|
||||
@assertion "teleport-jwt-assertion"
|
||||
@well_known_jwks_path "/.well-known/jwks.json"
|
||||
|
||||
def start_link(opts) do
|
||||
url =
|
||||
opts[:identity_key]
|
||||
|> URI.parse()
|
||||
|> URI.append_path(@well_known_jwks_path)
|
||||
|> URI.to_string()
|
||||
|
||||
name = Keyword.fetch!(opts, :name)
|
||||
options = [req_options: [url: url], name: name]
|
||||
GenServer.start_link(__MODULE__, options, name: name)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def authenticate(name, conn, _opts) do
|
||||
token = Plug.Conn.get_req_header(conn, @assertion)
|
||||
jwks = Livebook.ZTA.get(name)
|
||||
{conn, authenticate_user(token, jwks)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(options) do
|
||||
state = struct!(__MODULE__, options)
|
||||
{:ok, renew(state)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:renew, state) do
|
||||
{:noreply, renew(state)}
|
||||
end
|
||||
|
||||
defp authenticate_user(token, jwks) do
|
||||
with [encoded_token] <- token,
|
||||
{:ok, %{fields: %{"exp" => exp, "nbf" => nbf}} = token} <-
|
||||
verify_token(encoded_token, jwks),
|
||||
:ok <- verify_timestamps(exp, nbf) do
|
||||
for(
|
||||
{k, v} <- token.fields,
|
||||
new_k = @fields[k],
|
||||
do: {new_k, v},
|
||||
into: %{payload: token.fields}
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_token(token, keys) do
|
||||
Enum.find_value(keys, :error, fn key ->
|
||||
case JOSE.JWT.verify(key, token) do
|
||||
{true, token, _s} -> {:ok, token}
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp verify_timestamps(exp, nbf) do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
with {:ok, exp} <- DateTime.from_unix(exp),
|
||||
{:ok, nbf} <- DateTime.from_unix(nbf),
|
||||
true <- DateTime.after?(exp, now),
|
||||
true <- DateTime.after?(now, nbf) do
|
||||
:ok
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp renew(state) do
|
||||
keys = Req.request!(state.req_options).body["keys"]
|
||||
jwks = JOSE.JWK.from_map(keys)
|
||||
Process.send_after(self(), :renew, @renew_afer)
|
||||
Livebook.ZTA.put(state.name, jwks)
|
||||
state
|
||||
end
|
||||
end
|
1
mix.exs
1
mix.exs
|
@ -230,7 +230,6 @@ defmodule Livebook.MixProject do
|
|||
"docs/authentication/cloudflare.md",
|
||||
"docs/authentication/google_iap.md",
|
||||
"docs/authentication/tailscale.md",
|
||||
"docs/authentication/teleport.md",
|
||||
"docs/authentication/custom_auth.md"
|
||||
]
|
||||
end
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
defmodule Livebook.ZTA.TeleportTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Plug.Test
|
||||
|
||||
alias Livebook.ZTA.Teleport
|
||||
|
||||
@fields [:id, :name, :email]
|
||||
@name Context.Test.Teleport
|
||||
|
||||
@public_key """
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
|
||||
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
@private_key """
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
|
||||
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
|
||||
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
setup do
|
||||
bypass = Bypass.open()
|
||||
|
||||
options = [
|
||||
name: @name,
|
||||
identity_key: "http://localhost:#{bypass.port}"
|
||||
]
|
||||
|
||||
Bypass.expect(bypass, "GET", "/.well-known/jwks.json", fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(%{keys: get_well_known_jwks()}))
|
||||
end)
|
||||
|
||||
token = create_token()
|
||||
|
||||
conn = conn(:get, "/") |> put_req_header("teleport-jwt-assertion", token)
|
||||
|
||||
{:ok, bypass: bypass, options: options, conn: conn, token: token}
|
||||
end
|
||||
|
||||
test "returns the user when it's valid", %{options: options, conn: conn} do
|
||||
start_supervised!({Teleport, options})
|
||||
{_conn, user} = Teleport.authenticate(@name, conn, fields: @fields)
|
||||
assert %{id: "my-user-id", username: "myusername", payload: %{}} = user
|
||||
end
|
||||
|
||||
test "returns nil when the exp is in the past", %{options: options, conn: conn} do
|
||||
iat = DateTime.utc_now() |> DateTime.add(-10000)
|
||||
exp = DateTime.utc_now() |> DateTime.add(-1000)
|
||||
conn = put_req_header(conn, "teleport-jwt-assertion", create_token(iat, exp))
|
||||
start_supervised!({Teleport, options})
|
||||
|
||||
assert {_conn, nil} = Teleport.authenticate(@name, conn, fields: @fields)
|
||||
end
|
||||
|
||||
test "returns nil when the nbf is not reached yet", %{options: options, conn: conn} do
|
||||
iat = DateTime.utc_now() |> DateTime.add(1000)
|
||||
exp = DateTime.utc_now() |> DateTime.add(10000)
|
||||
conn = put_req_header(conn, "teleport-jwt-assertion", create_token(iat, exp))
|
||||
start_supervised!({Teleport, options})
|
||||
|
||||
assert {_conn, nil} = Teleport.authenticate(@name, conn, fields: @fields)
|
||||
end
|
||||
|
||||
test "returns nil when the token is invalid", %{options: options} do
|
||||
conn = conn(:get, "/") |> put_req_header("teleport-jwt-assertion", "invalid_token")
|
||||
start_supervised!({Teleport, options})
|
||||
|
||||
assert {_conn, nil} = Teleport.authenticate(@name, conn, fields: @fields)
|
||||
end
|
||||
|
||||
test "returns nil when the assertion is invalid", %{options: options} do
|
||||
conn = conn(:get, "/") |> put_req_header("invalid_assertion", create_token())
|
||||
start_supervised!({Teleport, options})
|
||||
|
||||
assert {_conn, nil} = Teleport.authenticate(@name, conn, fields: @fields)
|
||||
end
|
||||
|
||||
test "fails to start the process when the key is invalid", %{bypass: bypass, options: options} do
|
||||
Bypass.expect(bypass, "GET", "/.well-known/jwks.json", fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(%{keys: ["invalid_key"]}))
|
||||
end)
|
||||
|
||||
assert_raise RuntimeError, fn ->
|
||||
start_supervised!({Teleport, options})
|
||||
end
|
||||
end
|
||||
|
||||
defp get_well_known_jwks() do
|
||||
jwk = @public_key |> JOSE.JWK.from_pem() |> JOSE.JWK.to_map() |> elem(1) |> Map.put("kid", "")
|
||||
[jwk]
|
||||
end
|
||||
|
||||
defp create_token(
|
||||
iat \\ DateTime.utc_now(),
|
||||
exp \\ DateTime.add(DateTime.utc_now(), 1000)
|
||||
) do
|
||||
iat = DateTime.to_unix(iat)
|
||||
exp = DateTime.to_unix(exp)
|
||||
|
||||
payload = %{
|
||||
"aud" => ["http://localhost:4000"],
|
||||
"exp" => exp,
|
||||
"iat" => iat,
|
||||
"iss" => "my-teleport-custer",
|
||||
"nbf" => iat,
|
||||
"roles" => ["access", "editor", "member"],
|
||||
"sub" => "my-user-id",
|
||||
"traits" => %{"host_user_gid" => [""], "host_user_uid" => [""]},
|
||||
"username" => "myusername"
|
||||
}
|
||||
|
||||
@private_key
|
||||
|> JOSE.JWK.from_pem()
|
||||
|> JOSE.JWT.sign(payload)
|
||||
|> JOSE.JWS.compact()
|
||||
|> elem(1)
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue