B B
B B

Reputation: 1196

How to limit access to google app engine flask endpoints to just application code (or app engine service accounts)

Currently building an app on app engine standard environment, with python 3.7 and the flask framework. I need to schedule some tasks which will require the app to run several sensitive endpoints periodically.

I want to limit access to these endpoints to the application itself, preventing (non-admin) users from accessing these. In the Python 2 version of app engine, it is possible by specifying login: admin in the app.yaml file like so:

# app.yaml for google app engine standard env python 2

handlers:

  - url: /this_is/my_protected/endpoint
    script: main.app
    login: admin

However, in the Python 3.7 incarnation of the app engine environment, this is no longer possible.

I understand that it may be necessary to do the authentication in the main.py file of my flask app, but I'm not certain where to start. I already have firebase auth working, and the app is authenticating users fine for several user facing endpoints. However I am not certain how to go about authenticating my own app-engine application (or possibly the service account) to run several of its own endpoints. I've tried checking the docs, but they're either sparse, or I simply can't find the information I require.

Is there a straightforward way to accomplish this?

Upvotes: 4

Views: 563

Answers (2)

Erwan
Erwan

Reputation: 3729

It still works in Python 3.x, I use the original approach in my own Flask AppEngine app running Python 3.8

Here is a simplified version of my app.yaml with everything you need:

runtime: python38
app_engine_apis: true

handlers:
- url: /admin/.*
  secure: always
  script: auto
  login: admin

- url: /.*
  secure: always
  script: auto

Both scripts are set to auto and point to main.py by default. In main.py, I define my routes and all routes starting with /admin will force the user to login with a Google Account which has owner/admin rights for the application.

Just make sure you include app_engine_apis: true in your app.yaml file as it is required for login to work.

Upvotes: 1

B B
B B

Reputation: 1196

As suggested in a comment, here's my simplified (simplistic?) solution to make it such that specific flask end points in google app engine are only accessibly by application code or app engine service accounts. The answer is based on the documentation regarding validating cron requests and validating task requests.

Basically, we write a decorator that will validate whether or not X-Appengine-Cron: true is in the headers (implying that the end point is being called by your code, not a remote user). If the header is not found, then we do not run the protected function.

# python
# main.py

from flask import Flask, request, redirect, render_template

app = Flask(__name__)

# Define the decorator to protect your end points
def validate_cron_header(protected_function):
    def cron_header_validator_wrapper(*args, **kwargs):
        # https://cloud.google.com/appengine/docs/standard/python3/scheduling-jobs-with-cron-yaml#validating_cron_requests
        header = request.headers.get('X-Appengine-Cron')
        # If you are validating a TASK request from a TASK QUEUE instead of a CRON request, then use 'X-Appengine-TaskName' instead of 'X-Appengine-Cron'
        # example:
        # header = request.headers.get('X-Appengine-TaskName')
        # Other possible headers to check can be found here: https://cloud.google.com/tasks/docs/creating-appengine-handlers#reading_app_engine_task_request_headers

        # If the header does not exist, then don't run the protected function
        if not header:
            # here you can raise an error, redirect to a page, etc.
            return redirect("/")

        # Run and return the protected function
        return protected_function(*args, **kwargs)

    # The line below is necessary to allow the use of the wrapper on multiple endpoints
    # https://stackoverflow.com/a/42254713
    cron_header_validator_wrapper.__name__ = protected_function.__name__
    return cron_header_validator_wrapper


@app.route("/example/protected/handler")
@validate_cron_header
def a_protected_handler():
    # Run your code here
    your_response_or_error_etc = "text"
    return your_response_or_error_etc


@app.route("/yet/another/example/protected/handler/<myvar>")
@validate_cron_header
def another_protected_handler(some_var=None):
    # Run your code here
    return render_template("my_sample_template", some_var=some_var)

Upvotes: 3

Related Questions