Reputation: 1196
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
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
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