livebook-dev / kino

Client-driven interactive widgets for Livebook

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Kino.Hub.on_join

josevalim opened this issue · comments

The callback may return either :ok or :forbidden. If the latter is returned, the user is redirected back to the home page. If the callback errors, we should probably force a page reload or send them back to the home page.

I don’t mind taking a stab at this. I have some time this week.

if we returned:

{:ok, _} for approved
{:forbidden, “” <> reason} for forbidden

we could allow live books to give the reason if they chose to.

Since the ZTA already does the authentication check, at this level having groups would help be able to make the decisions.

At a high level I am thinking:

  • logger info log for audit events
    • at least
      • load
      • Login
      • Fail
  • add user groups to user map to allow for group checks
  • redirect with flash error message ? Or just go to the default 403 error controller with the override message

add user groups to user map to allow for group checks

Unfortunately there is no unified interface across ZTAs for groups. So I am giving you all JWT fields and you can decide it yourself. The logging also can be totally done by your own code for now. But other than that, we are good to go.

Watch out #405, because that will have a foundation for executing code for each user, which we can use as the foundation here.

Thinking out loud

Starting to look through the code more closely to start building a mental map. I am trying to figure out how to bridge the gaps to create the bridge call back between kino and livebook.

It looks like the user_info/0 potentially would need to get mapped out. https://github.com/livebook-dev/kino/blob/v0.12.3/lib/kino/hub.ex#L11

To do this it looks like we would need to do io_request

something like:

io_request(:livebook_get_user_info)

This would at least allow :multi_session types to be able to do checks before rendering the kino if they wanted to.

defp app_info_for_runtime(state) do
    case state.data do
      %{mode: :app, notebook: %{app_settings: %{multi_session: true}}} ->
        info = %{type: :multi_session}

        if user = state.started_by do
          started_by = user_info(user)
          Map.put(info, :started_by, started_by)
        else
          info
        end

      %{mode: :app, notebook: %{app_settings: %{multi_session: false}}} ->
        %{type: :single_session}

      _ ->
        %{type: :none}
    end
  end

So it looks like putting it in the Evaluator.ClientTracker for when the client joins to call the Kino.Hub.on_join 🤔

on_join

Trying to diget livebook and kino at the same time I am trying to think through how to link between the two. So I appreciate the grace as I am trying to help.

graph LR;

subgraph livebook

    subgraph runtime_server_gen_server
    runtime_server_g["Livebook.Runtime.ErlDist.RuntimeServer"]-->client_tracker["Livebook.Runtime.Evaluator.ClientTracker"]
    uknown
    end
    client_tracker-->has_callback{Do I have a client on join callback?}-- yes-->send_message
    has_callback-- no -->nothing
    subgraph kino
        callback-->register_callback-->io_request
        callback-->check_auth-->register_callback
    end
    io_request--add to state -->has_callback
    send_message-->check_auth-->uknown["TBD??"]-->403
end

It feels complex, and usually that is a sign I don't have a good understanding, so I figured I would take a step back and wait haha.

Hi @spunkedy, thanks for sparking the discussion here. I actually think you are on the right direction and it is even a bit trickier than that, because we need to make sure the page is not rendered until the callback runs, pushing synchronization all the way up to the LiveView.

For this reason, we chose to tackle the problem differently:

  1. for multi-session apps, you can use the Kino.Hub.app_info().started_by (the payload should already be available if using main on both)

  2. for single-session apps, the upcoming Kino.LiveFrame (#405) and the upcoming Kino.Hub.user_info(client_id) should also provide what is necessary to control access

We should have these in place soon (before the next release), thanks!

hah, glad I didn't go too far down the road :)

Here is a branch if you to try out #426. Note you will need Livebook from main and install Kino pointing to the branch. You can use Kino.Hub.user_info to get auth information about the user who starts the wizard.

Tried via getting the client id and then getting details as well as the user_info/0

image

image

<!-- livebook:{"app_settings":{"access_type":"public","slug":"asdf"}} -->

# Untitled notebook

```elixir
Mix.install([
  {:kino, git: "https://github.com/livebook-dev/kino.git", ref: "jv-kino-wizard"}
])

Section

import Kino.Control
      import Kino.Shorts
      import Kino.Wizard
      defmodule MyWizard do
        # @behaviour Kino.Wizard
        def init(_data, :ok) do
          {:ok, %{page: 1, name: nil, address: nil}}
        end
        defp step_one(%{data: %{name: name}}, state) do
          if name == "" do
            %{state | name: name}
          else
            %{state | name: name, page: 2}
          end
        end
        defp step_two(%{data: %{address: address}}, state) do
          case address do
            "BUMP" <> _ -> %{state | address: address <> "!"}
            "" -> %{state | address: ""}
            _ -> %{state | address: address, page: 3}
          end
        end
        defp go_back(_, state) do
          %{state | page: state.page - 1}
        end
        def render(%{page: 1} = state) do

          {:ok, [user]} = Kino.Bridge.monitor_clients(self()) |> IO.inspect

          details = user
          |> IO.inspect()
          |> Kino.Hub.user_info()
          |> IO.inspect

          details_via_bridge = user 
          |> Kino.Bridge.get_user_info()
          |> IO.inspect
          
          form(
            [name: Kino.Input.text("Name", default: state.name)],
            submit: "Step one"
          )
          |> control(&step_one/2)
          Kino.Markdown.new("""
          
          ```
          Running Details
          type: #{inspect(Kino.Bridge.get_app_info())}
          details for client_id: #{user}
          #{inspect(details)}

          details via bridge:

          #{inspect(details_via_bridge)}
          
          ```
          """)
        end
        def render(%{page: 2} = state) do
          Kino.Control.form(
            [address: Kino.Input.text("Address", default: state.address)],
            submit: "Step two"
          )
          |> control(&step_two/2)
          |> add_go_back()
        end
        def render(%{page: 3} = state) do
          "Well done, #{state.name}. You live in #{state.address}."
          |> add_go_back()
        end
        defp add_go_back(element) do
          button =
            button("Go back")
            |> control(&go_back/2)
          grid([element, button])
        end
      end
      Kino.Wizard.new(MyWizard, :ok, "Start Runbook")

@spunkedy you need to change the notebook to use your team workspace:

image

@spunkedy you need to change the notebook to use your team workspace:

image

We have no teams setup and unfortunately the deployment pattern of our livebooks is for an offline version where we deploy via docker and pre load the apps.

@spunkedy you can use Teams for offline deployments as well. It makes it easier because you can share configuration, secrets, and other data across your team members, making sure that everyone can deploy the same base version (instead of each of you deploying slightly different ones).

@josevalim

I know we talked earlier around parts of this. Maybe it's time to restart the conversation, will reply to the email.

@spunkedy Here's something you can do to get the user info:

CleanShot 2024-05-29 at 11 28 20


# Untitled notebook

```elixir
Mix.install([
  {:kino, github: "livebook-dev/kino"}
])

Section

button = Kino.Control.button("Print user info")

frame = Kino.Frame.new()
Kino.Frame.append(frame, button)

Kino.listen(button, fn event ->
  %{origin: origin} = event
  {:ok, user_info} = Kino.Workspace.user_info(origin)
  Kino.Frame.append(frame, user_info)
end)

frame

We've thinking of other APIs to expose the user_info when the Livebook app is accesed, without the need to require some user interaction like in the example above.