How to customize "Active Bokeh Applications" welcome page

Hi, I have successfully developed a bokeh server running some Bokeh dashboard applications. When I access my root URL, I can see a default web page with a list of my current running bokeh app. Easy, I really would like to replace that default web with a custom one, so where I should place the index.htm and the correspondent static files?

Thanks

From bokeh serve --help

  --index INDEX         Path to a template to use for the site index

You can see the mosst relevant part of the default index here:

2 Likes

Thanks, @Bryan. Your solution works.

Thanks, @Bryan. Your solution also works for me. I am afraid I will go a bit out of the topic, but I couldn’t get any help from Tornado community.
So far, I have a two Bokeh apps in my server, and two users that I am letting access to his dashboard with that solution, bokeh/examples/howto/server_auth at branch-3.0 · bokeh/bokeh · GitHub. So far, everything is perfect, but I would like to keep user restricted just to the dashboard they were redirected when they logged in. In my bokeh server, they are being redirected to his dashboard when they login, but they can still access the dashboard from the other user if they write the URL.

I am truly sorry if I am wrongly putting that question here.

Thanks in advance

import tornado
from tornado.web import RequestHandler
from tornado.web import Application
import tornado.ioloop
import sqlite3
import functools

COOKIE_SECRET = 'L8LwECiNRxq2N0N2eGxx9MZlrpmuMEimlydNX/vt1LM='
# could also define get_login_url function (but must give up LoginHandler)
login_url = "/login"

users_list = {'user_1':'user_1_password', 'user_2': 'user_2_password'}

# could define get_user_async instead
def get_user(request_handler):
    return request_handler.get_cookie("user")

# optional login page for login_url
class LoginHandler(RequestHandler):
    def get(self):
        try:
            errormessage = self.get_argument("error")
        except Exception:
            errormessage = ""
        self.render("login.html", errormessage=errormessage)

    def check_permission(self, username, password):
        if username in users_list and users_list[username] == password:
            return True
        return False

    def post(self):
        username = self.get_argument("username", "")
        password = self.get_argument("password", "")
        auth = self.check_permission(username, password)
        if auth:
            self.set_current_user(username)
            self.redirect(self.get_argument("next", f"/{username}"))
        else:
            error_msg = "?error=" + tornado.escape.url_escape("Login problems")
            self.redirect(login_url + error_msg)

    def set_current_user(self, user):
        if user:
            # self.set_secure_cookie("user_id", user["id"])
            self.set_cookie("user", tornado.escape.json_encode(user), expires_days=None)
        else:
            self.clear_cookie("user")

class YourDashboardHandler(RequestHandler):
    def get(self, *arg, **kwargs):
        user_from_URL = kwargs["user_id"]
        user_from_cookie = self.get_cookie("user", "")
        if user_from_URL != user_from_cookie:
            self.redirect(r"^/(?P<user_id>\w+)$")

# optional logout_url, available as curdoc().session_context.logout_url
logout_url = "/logout"

# optional logout handler for logout_url
class LogoutHandler(RequestHandler):
    def get(self):
        self.clear_cookie("user")
        self.redirect(self.get_argument("next", "/login"))

if __name__ == "__main__":
    application = tornado.web.Application([
        (r"/login", LoginHandler),
        (r"^/(?P<user_id>\w+)$", YourDashboardHandler)
    ], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")

@Mauro_Navarro If you want me to run your code to investigate, you need to update it to be complete. Running the code above just immediately exits and does nothing. I also need an exact sequence of steps to follow to see what you want me to see.

Otherwise, all I can note offhand is that:

  • passing credentials in HTTP arguments (which appears to be the case above) is not a real auth solution, and is probably part of the problem.
  • the basic Bokeh example is an extremely simplified toy example to show how the different hooks interact. They are building blocks you can use to implement a real auth workflow on top of.

You might want to look at Panel: https://panel.holoviz.org/

It can drive Bokeh applications from a higher level, and I believe they have already implemented real auto workflows (e.g. oauth, etc) on top of Bokeh’s auth hooks.

@Bryan many thanks for your advice. I am aware of the constraints of my simple implementation, I was just trying to know if it was possible to do what I want effortless. My application will be working in a very restricted environment, with just few users, so the idea is just to keep users looking they own data/dashboard without the possibility to move somewhere else. I am invoking the authentication module when I start the bokeh server with --auth-module=/path/to/auth.py.

Thanks again for your time

import tornado
from tornado.web import RequestHandler

# could also define get_login_url function (but must give up LoginHandler)
login_url = "/login"

users_list = {'user_1':'user_1_password', 'user_2': 'user_2_password'}

# could define get_user_async instead
def get_user(request_handler):
    return request_handler.get_cookie("user")

# optional login page for login_url
class LoginHandler(RequestHandler):
    def get(self):
        try:
            errormessage = self.get_argument("error")
        except Exception:
            errormessage = ""
        self.render("login.html", errormessage=errormessage)

    def check_permission(self, username, password):
        if username in users_list and users_list[username] == password:
            return True
        return False

    def post(self):
        username = self.get_argument("username", "")
        password = self.get_argument("password", "")
        auth = self.check_permission(username, password)
        if auth:
            self.set_current_user(username)
            self.redirect(self.get_argument("next", f"/{username}"))
        else:
            error_msg = "?error=" + tornado.escape.url_escape("Wrong username or passwort")
            self.redirect(login_url + error_msg)

    def set_current_user(self, user):
        if user:
            self.set_cookie("user", tornado.escape.json_encode(user), expires_days=None)
        else:
            self.clear_cookie("user")

# optional logout_url, available as curdoc().session_context.logout_url
logout_url = "/logout"

# optional logout handler for logout_url
class LogoutHandler(RequestHandler):
    def get(self):
        self.clear_cookie("user")
        self.redirect(self.get_argument("next", "/login"))

You’ve only provided the auth module. I want to actually run this to see how it performs by direct investigation. For that to happen, you need to provide the other half (a bokeh server app to start that pairs with this auth module) .

I did and small working demo that resemble the structure of my project and behaves similarly.
https://github.com/noctiluka/Bokeh-authentification-hooks
Thanks.

Hi @Mauro_Navarro thanks for that, having concrete code really helps to focus thinking about this. The crux is really that the auth module was imagined for the Bokeh server as a whole, there’s been no consideration to “per-route” auth. So what can you do? A few ideas:

  • Your YourDashboardHandler idea might work, but you need to go a little further. There is more work to do to actually start a Bokeh server programmatically with custom handlers. See these docs: Embedding Bokeh server as a library

  • Alternatively, keep using bokeh serve but run every “sub”-app as a separate bokeh serve process, each with its own separate auth module (or at least separately configured auth module)

  • It might also work to let something else handle the auth entirely in front of the bokeh server, and refuse or pass connections before anything even hits the Bokeh server. You could restrict the domains that the apps are embeddable in, and use the auth module just to confirm that front-end login has already happened.

@Mauro_Navarro I spoke to soon. I forgot that the get_user function is passed the request hander. But the request handler also has the actual request attached:

(Pdb) request_handler.request
HTTPServerRequest(protocol='http', host='localhost:5006', method='GET', uri='/user_1', version='HTTP/1.1', remote_ip='::1')

That is where you can compare the current user (cookie, or whatever) to the request URL and permit or deny it. No need for any custom handlers or anything else. Right now it lets any user access any route because all you do is check that some user cookie has been set.

Hi @Bryan, that sounds easy but after many tries I have no success so far. This is my try:

def get_user(self, request_handler):
    user_cookie = request_handler.get_current_user("user")
    user_url = HTTPServerRequest({"uri"})
    if user_cookie != user_url:
        return self.redirect("/login")
    return request_handler.get_cookie("user")

Do you know any example?

@Mauro_Navarro the get_user method should return None if there not an authenticated user. In that case, a redirect will automatically happen to whatever you specified as login_url (or if you defined a get_login_url, then whatever that returns).

Edit: perhaps it would be helpful to refer to some of Tornado’s documentation for more context, e,g.

Authentication and security — Tornado 6.1 documentation

Because the Bokeh auth hooks are nothing but pass-throughs for Tornado’s auth hooks. For instance, literally all Bokeh’s get_user is used for is to implement Tornado’s get_current_user:

Hi @Bryan, thanks so much for all your helpful tips and answers! Ultimately, I’ve settled on using a flask app in front of my Bokeh server.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.