0

I'm trying to adapt another StackOverflow answer on conditionally applying a decorator to only require login for a specific environment (eventually a staging environment, but development until I get this working). Toward that end, I started with the following

auth = HTTPDigestAuth()

def login_required(dec, condition):
    def decorator(func):
        if not condition:
            return func
        return dec(func)
    return decorator

@bp.route('/auth')
@login_required(auth.login_required, current_app.config['ENV'] != 'development')
def auth_route():
    return current_app.config['ENV']

When I launch the server, I get a RuntimeError: Working outside of application context error. After trying a few suggestions from an earlier version of this question, I got the RuntimeError to disappear, but the decorator still isn't being correctly applied when I want. Here's the current version:

def login_required(dec):
    def decorator(func):
        if not os.environ.get('ENV') != 'development':
            return func
        return dec(func)
    return decorator

@bp.route('/auth')
@login_required(auth.login_required)
def auth_route():
    return current_app.config['ENV']

This never returns the auth.login_reqired function. It always lets the browser in without authentication.

So, I tried changing the condition to

if not os.environ.get('ENV') is not None:

and then the authentication shows up.

Yes, I've done an export ENV=development in the shell and confirmed it with the env command. But even then it's not reading the environment variable as I would expect.

Perhaps this is simply the wrong way to go about it? My final goal is to require authentication on one particular environment. Is this possible with the path I'm on? Is it possible at all?

Chuck
  • 4,662
  • 2
  • 33
  • 55
  • 1
    Did you tried to put the condition inside decorator? Also keep in mind that using the same name for global variable and function name is not the best idea. – Konrad Mar 31 '19 at 22:41
  • @KonradSitarz Thanks for pointing out the global variable/function name collision. I hadn't noticed that. – Chuck Mar 31 '19 at 23:26

2 Answers2

1

current_app is a context local proxy that only has meaning during a request. This means you can't use it before a request, i.e. as part of a decorator.

Using current_app is generally good practice because Flask allows multiple apps to be configured. In your specific case however, it isn't actually necessary. For instance, the following would work, because it uses the app object directly instead of the current_app proxy:

from yourpackage import app

@bp.route('/auth')
@login_required(auth.login_required, app.config['ENV'] != 'development')
def auth():
    return current_app.config['ENV']
daveruinseverything
  • 4,775
  • 28
  • 40
  • I used your idea with some tweaking (because I'm using an app factory), but while it allowed the app to launch without crashing, the decorator still seems to be applied too early, perhaps before app.config is ready? Even though I'm comparing `ENV` to `development`, and `development` is the output on the page, when make my test `...config['ENV'] is not None`, authorization presents itself. I also tried comparing it to `os.environ.get('ENV')` with no success. I'll update the question to reflect this new information. – Chuck Mar 31 '19 at 23:45
  • To be honest if it were me writing this application, I would apply the logic inside a route. e.g. my auth function might add some values to the current user session, so if `current_app.config['ENV'] == 'dev'` I would add just optimistically set the session state as though the user had successfully authenticated, otherwise I would do real checks to set the session state. – daveruinseverything Apr 01 '19 at 03:10
  • Can you elaborate? I am still learning, but I don't know how to present the HTTP authorization to the user. I've read through the flask-httpauth source code, but I don't see where it's doing that. Is it possible to do that in a function that isn't decorator? I assume so, but perhaps you can provide a link where I can learn how to do it. – Chuck Apr 02 '19 at 00:56
1

Let me paste something from documentation of Flask

Lifetime of the Context The application context is created and destroyed as necessary. When a Flask application begins handling a request, it pushes an application context and a request context. When the request ends it pops the request context then the application context. Typically, an application context will have the same lifetime as a request.

Now let’s consider how decorators work. It is just a syntactic sugar see this answer.

So the login_required decorator is called while the module is loaded and the current app is not available yet because it’s not handling request.

I would do this way, move condition to the decorator function (relating to your example). It will be called while the request will be handled so you should have access to current_app.

Konrad
  • 952
  • 10
  • 25
  • I appreciate the idea, but I did try that (`@login_required(auth.login_required)` and `if not current_app.config...`) but got the same result. – Chuck Mar 31 '19 at 23:28
  • This isn't quite true, the decorator is applied outside of the application context when the module containing the route is first loaded – daveruinseverything Apr 01 '19 at 03:08