Flask_login current_user object with Background Callbacks

Hi everyone,

Just wondering if anyone has tried to use background callbacks with flask_login? I would like to use the current_user object in a background callback but I haven’t found a way to do this so far. Basically I am using Redis and Celery to do some back-end data parsing and saving of files on the server but I would like to first check that the user is logged in and has the right permissions.

I can almost achieve something similar by using a normal callback first as an intermediate step but of course there is still a security weakness that theoretically a well informed person could directly call the background callback even if not through the UI.

Does anybody have an idea or suggestion for getting the current_user object available in the celery worker?

Thanks!

Hi @andredfb, I don’t have any solution for you unfortunately, but I’m interested if you made progress on this? I’m going down the path of incorporating access controls in a dash app and so will likely be running into this soon as well.

Hello @benn0,

Background callbacks are outside of the scope of a flask request, so directly, no.

However, you could copy what info you want to a memory store for it.

@andredfb @jinnyzor

The ‘request’ context should be propagated to the background callback environment, imo. This would be a good feature request. I’m having the same problem.

A part of my user login process requires a bit of time to execute (getting preferences) so I’m hoping to make that run in a background callback. However, because the request context isn’t in the background callback, the user session ID isn’t available and the user can’t log in.

I could use localStorage but then I’m implementing stuff outside of the scope of what’s managed automatically by Flask-Login, which I don’t want to do by default.

Very interested in any other ideas.

@jinnyzor

I believe that background_callbacks are invoked via a standard callback targeting the _dash-update-component endpoint.
It also appears that the background_callback context could be enriched with the calling callback’s ‘request’ object via the following AttributeDict:

Given that at:

the background_callback is kicked off with the ‘context’ object available.

This suggests a relatively painless feature update that would require an architect green light: pass the then-contemporary request object into the background_callback because it’s reasonable given the abstraction that a background_callback is aiming to be, e.g. as though it were just a regular callback that doesn’t hog app server threads.

Please let me know what you think!

Hi all,

@benn0 I did not make much progress on this actually. At the moment I am using a dcc.Store to pass the current username to the background callback but this is not secure.

@jkunstle seems like a reasonable request and I think this would be quite useful for implementing either checks that a user is allowed to invoke the background callback but also having user or user group specific actions. Maybe we should raise an issue on GitHub?

@andredfb I created an issue for this previously, would love it if you’d comment on it as well for the sake of amplification and @-ed one of the maintainers.

It appears that there’s a reasonable trick available for this pattern, answered originally by @jinnyzor

if one passes ‘request=flask.request’ as a parameter into their background callback, the session token should be available and one can manually log in a user.

I finally got around to trying this and while it seems to work with Disk Cache, I cant seem to get it to work with celery at all. I tried passing both the request and the app context in the callback function but neither of them work. I still get operating outside the request context when trying to access the request. Will post back if I find anything useful.

Hello @andredfb,

Are you making sure that in your code you are using request and not flask.request?

Also, for this to work, I’m assuming you’d need to make sure you aren’t importing request from flask. You could try naming it something that won’t be reserved, like background_request=flask.request

Hey @jinnyzor,

Yep I passed request=flask.request and then tried to print(request). Got the error complaining about being out of the request context. I suspect my celery workers do not have either the app or request context but despite my googling I haven’t figured out a solution yet. I reverted to using diskcache for the moment.

Will stay on it, but any suggestions are most appreciated.

Hi there! @andredfb
Any updates on celery problem?
I’ve got the same trouble and did not found any solution so far…

Hi all, I finally had the time to revisit this and I think I have found a workaround. At least for my use case. Basically I am adding the user ID (from the User (customised UserMixin) class in flask_login) to the Dash callback context that is copied across to the Celery worker. Then in the Celery worker I make a new User object from that user ID. This then behaves the same way as the flask_login.current_user proxy. I am not sure if adding my own stuff to the Dash callback context will interfere with anything, I made sure to give it a unique name in the dict and will now be testing.

Firstly I edit the call_job_fn method in my new background manager class (thats the only change):

class BackgroundCallbacks(dash.CeleryManager):

    def call_job_fn(self, key, job_fn, args, context):
        user = flask_login.current_user #New
        context["whatever unique name"] = user.id if user.is_authenticated else None #New
        task = job_fn.delay(key, self._make_progress_key(key), args, context)
        return task.task_id

Then inside the callback you can create the User object again and use at just the same as before:

user_id = getattr(dash._callback_context.context_value.get(), "whatever unique name", None)
user_object = User(user_id) #Or however you defined your UserMixin Object

I haven’t tested this too much yet but it has to be JSON serializable, hence using just the ID and creating the object again from it.

In fact I buried some of this in a wrapper that has my own custom current_user property that will return the one from flask if available or check if the user ID is in the Dash callback context and give me that, otherwise return an AnonymousUserMixin like object.

I guess the generic approach from Plotly which might be nice is to have an easy way to add things to the callback context. Ie to give variables or functions that produce simple variable types to be added to the callback context before the job is handed off to Celery.