ideonate / cdsdashboards

JupyterHub extension for ContainDS Dashboards
https://cdsdashboards.readthedocs.io/
Other
200 stars 38 forks source link

Obtain authenticated username inside a dashboard #28

Closed danlester closed 3 years ago

danlester commented 4 years ago

Once authenticated into a dashboard, provide a way for dashboard apps to obtain the viewer's username from JupyterHub.

This would most likely be via a cookie.

Env vars aren't going to help here because each dashboard can have multiple visitors at the same time.

From @ricky-lim on issue 24 : With relation to the singleuser docker image, I have a question regarding how could streamlit uses the authenticated state of a user ? Within the singleuser image, the NB_USER env is set into jovyan and JUPYTERHUB_USER is set to the creator of the dashboard, instead of the authenticated user. I was wondering what is your advice for a dashboard creator to use the authenticated state, such as a username following the dashboard authentication?

ricky-lim commented 4 years ago

Hi Dan,

To add a background on this use case, I was thinking on a dashboard application that could be adjusted, depending on user information, such as a user's role.

I try to follow your advice via a cookie, with this following snippet:

      def my_hook(authenticator, handler, authentication):
        name = authentication.get('name', 'alien')
        handler._set_cookie('current_name', name)
        return authentication

      c.Authenticator.post_auth_hook = my_hook

This sets a cookie current_name for the response. I was wondering if you have any advice to get this cookie's value from a python script for streamlit ?

Thanks in advance.

Cheers

danlester commented 4 years ago

Very helpful, thank you. It would be possible to set the name of the user in a cookie like that but it can't be done securely without extra checks in the dashboard app (because the user could change it for themselves). At that point, we might as well just do the whole thing - get the username from the JupyterHub login cookie - within the dashboard.

That's not straightforward to do from the cookie since most dashboard frameworks (e.g. Streamlit and Voila) run your scripts in a difference 'space' to the web page the user is viewing. So we will need some interaction between server-side and client-side.

In Streamlit this will need a component (and in Voila, a widget of some sort).

It would make an HTTP request to /hub/api/user, except from a dashboard URL such as /user/dan/dash-test that API request would be blocked by JupyterHub due to cross-origin concerns. So we might need our own endpoint to get the current user based on cookie.

And then the component/widget would have to pass this information back to the dashboard script.

All this is possible for most frameworks, but would be different for each...

I'd like to look into this further, but the results of anyone else's experiments are welcome!

danlester commented 4 years ago

In master branch there is now an /dashboards-api/hub-info/user endpoint to get this information about the current user based on login cookies. This will now allow experimentation in the various frameworks.

ricky-lim commented 4 years ago

Hi Dan,

Thanks for the kind response.

I tried your master branch with streamlit using requests as

import requests

r = requests.get('https://jupyterhub.tst/hub/dashboards-api/hub-info/user')

st.write(r.json())

Unfortunately it does not seem to yield the user information.

What is your advise to experment with this API ?

Cheers

danlester commented 4 years ago

Yes, the request needs to be made on the Javascript side (where the user is logged in to JupyterHub in the browser). The script where you are trying it is being processed through the Streamlit server where you don't have access to cookies unless Streamlit passes them on (which I don't think it does).

So you would need to use Components, but it could be a bit fiddly. I'd like to try it out but not sure when I'll get a chance. Let me know if you get anywhere too!

danlester commented 4 years ago

Streamlit Components in general require a fix in order to work. This is now available in master branch, in addition to requiring the jupyterhub_config settings as listed here:

https://cdsdashboards.readthedocs.io/en/latest/chapters/troubleshooting.html#streamlit-components-aren-t-working

I have made a start at a component to get username: https://github.com/ideonate/streamlit-cds-username

But please note it doesn't yet work!

By the way, all of this would be much easier in something like Plotly Dash where you are already exposed to the web server and should be able to check cookies easily.

Streamlit is great for getting started, but its paradigm means some things get complicated quickly if you're really building a web app.

ricky-lim commented 4 years ago

Hi Dan,

Thanks for the advice and the update on Streamlit components.

From the widget, I tried with the hello-world custom widget from https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Custom.html.

This is what I got so far.

Python Object

from traitlets import Unicode, Dict
from ipywidgets import DOMWidget, register

@register
class User(DOMWidget):
    _view_name = Unicode('UserView').tag(sync=True)
    _view_module = Unicode('user_widget').tag(sync=True)
    _view_module_version = Unicode('0.1.0').tag(sync=True)

    value = Dict({}, help="User info").tag(sync=True)

JS object

%%javascript
require.undef('user_widget');

define('user_widget', ["@jupyter-widgets/base"], function (widgets) {

    var UserView = widgets.DOMWidgetView.extend({
        render: function () {
            const request = async () => {
                const response = await fetch(
                    'https://jupyterhub.tst/hub/dashboards-api/hub-info/user',
                     { 
                     mode: 'no-cors', 
                     credentials: 'same-origin',
                     headers: new Headers({'Access-Control-Allow-Origin':'*'}) 
                });
                const result = await response.json();
                this.model.set('value', result);
                this.model.save_changes();
            }
            request();
        },

    });

    return {
        UserView: UserView
    };
});

To call:

user = User()
user

To retrieve the value:

user.value

{'kind': 'user',
 'name': 'ricky_lim',

I could retrieve the user.value on the notebook, however it is not rendered by voila. I think it might be due to the async part of the render function.

I'd be very grateful if you could share some advice to troubleshoot this.

Cheers and thank you for your kind guidance.

danlester commented 4 years ago

Thanks for this code. Have you been able to try it directly in JupyterLab?

I just tried and got "Javascript Error: require is not defined".

Maybe I'm being a bit ambitious to try this directly in a notebook... if you were making a real widget, it would be great if you can share via GitHub or similar.

Or if you were just trying this in JupyterLab, maybe you have some other extensions installed that are giving you 'require'. Please let me know a list if so as that might help get this proof of concept working.

It might make more sense if I was able to get as far as you, but I'm not sure what you mean by "not rendered by Voila" - there isn't really anything to display on the Javascript side.

You may need to make that a blocking call so that Voila definitely has the username available when it goes on to execute the next cell.

ricky-lim commented 4 years ago

Hi Dan,

Yes, correct. This snippet won't work with JupyterLab, unfortunately. It should work only in jupyter notebook.

It still needs to be wrapped into a proper widget package, such as with this cookiecutter, https://github.com/jupyter-widgets/widget-cookiecutter

To clarify what I meant with rendered is to get the returned username, in this snippet. As the render is done in async-way, it might be the source of the issue.

Do you have any idea to make it into a blocking call ?

Cheers

ricky-lim commented 4 years ago

I found another issue that I think is also relevant to this use case: https://github.com/martinRenou/ipycanvas/issues/77

danlester commented 3 years ago

Thank you for all your input.

I have created a Jupyter notebook that shows how this works differently for Voilà compared to just running one cell at a time in Jupyter.

This does work for me in Voilà too - I am able to use the username text inside a Python function. It needs to be inside an event handler though, which doesn't give the 'procedural' notebook feel that I think you were hoping for. But that's the way Voilà widgets always work, e.g. if you asked the user to type into a text box.

Anyway, please take a look and see if it makes sense to you:

I think if we could get something like this working in Streamlit, it could feel more 'natural' depending on how well components can keep track of state. As I've said before, I think Plotly Dash would be a better fit for this kind of work since the web server is more completely exposed to you as the developer.

ricky-lim commented 3 years ago

Hi Dan,

Thank you for your input and helpful explanation.

As for now, with voila I could retrieve the user info from a 'callback' function. It's indeed not natural and makes also rather complex for a dashboard developer.

I created a package to wrap this as a widget to work with jupyterlab, as well. The repo: https://github.com/ricky-lim/ipyfetch And with a simple example at: https://github.com/ricky-lim/ipyfetch/blob/master/examples/ipyfetch.ipynb

Hopefully this may help.

Thank you again for your kind help and your feedback is always welcome.

Cheers :)

danlester commented 3 years ago

@ricky-lim That's really exciting, good work!

Yes, I think this is just how Voilà works and is exactly what you would have to do for other widgets too. We would need to build the username check into Voilà itself, before execution stage, to be able to use it in the code cells directly.

I think I will close this issue for now, but it would definitely be interesting if anyone wants to continue the work I did for Streamlit at https://github.com/ideonate/streamlit-cds-username

Feel free to reopen or just get in touch on email etc.