Friday, March 5, 2021

Pseudo-decorators for my Python Flask app

Secured Python Flask app on Code Engine
Recently, I migrated an existing Python app from a Cloud Foundry to an IBM Cloud Code Engine deployment. The Flask app uses decorators for the routes and for OIDC-based authentication. For usability, the app should start up even without anything configured yet and the OIDC decorators being invalid. It required to some IMHO tricky coding.

Python decorators

When working with Python, most of you have probably run into decorators. They are some form of metaprogramming where the decorating function is invoked with the decorated function as payload. Hence, the decorator adds its features to the other function. In frameworks like Flask, the routes are specified with decorators.

@app.route('/')
def index():
    return 'This is my index page'

@app.route('/hello')
def hello():
    return 'Hello and welcome to my world'

When working with authentication like OpenID connect and the Python module flask-pyoidc, routes can be protected by adding another decorator.

@app.route('/secure') 
@auth.oidc_auth('default')
def secure(): return 'This route is protected'

It would require a user to be authenticated to access that route.

Pseudo decorator

For my app, I have the Flask app either fully configured and initialized (typical situation) or not. The latter is the case when in a tutorial a user is in the process of binding other services and going through configuration steps. Typically, an app without a full configuration would error out during startup. So what to do? Some form of pseudo decorators to the rescue. Either have some simple pass-through decorator when the app is not ready yet or call the actual decorator when everything is in place.

@app.route('/secure') 
@security_decorator_auth
def secure(): return 'This route is protected'

The decorator security_decorator_auth is defined as one of:

    def security_decorator_auth(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            return f(*args, **kwargs)
        return decorated_function
    def security_decorator_auth(f):        
        @wraps(f)
        @auth.oidc_auth('default')
        def decorated_function(*args, **kwargs):
            return f(*args, **kwargs)
        return decorated_function

The first version just passes the decorated function without modifying it, the second version adds the OIDC decorator to the passed function.

For more context, see the file ghstats.py relating to this IBM Cloud Solution Tutorial on Serverless web app and eventing for data retrieval and analytics.

If you have feedback, suggestions, or questions about this post, please reach out to me on Twitter (@data_henrik) or LinkedIn.