Remove teleport, as it does not support smart cells (#2589)

This commit is contained in:
José Valim 2024-05-06 23:58:50 +02:00 committed by GitHub
parent 24efee3b82
commit eb4887657a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1 additions and 263 deletions

View file

@ -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

View file

@ -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.

View file

@ -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"
}
]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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