(Nginx) + Gunicorn + Flask + Bokeh on Azure with AD - python

(Apologies: I'm a webapp novice.)
REQUIREMENTS:
Azure Application Services Web App
Need Azure AD to authenticate users
Python Bokeh app to display data webapp once user is authenticated
ATTEMPT:
I tried wrapping a Flask app around Bokeh using this example After many iterations I can't get all traffic to flow to bokeh after authenticating. I've tried flask redirect(), and my latest iteration involves using requests' get() from the example in the link.
.
...
from requests import get
...
#app.route('/')
def bkapp_page():
script = server_document('https://myapp.azurewebsites.net/bkapp')
return render_template("embed.html", script=script, template="Flask")
#app.route('/bkapp', defaults={'path': ''})
#app.route('/bkapp/<path:path>")
def bkapp(path):
return get(f'http://localhost:5006/bkapp/{path}').content
...
PROBLEM:
- Despite many iterations, I can't get the template bokeh renders (which points to javascript the server serves) to point to a path that successfully renders the page. The best I have done is to get the /bkapp endpoint to not give me a 404 error.
- Python Bokeh runs on port 5006 and flask is running on 8000. I need to forward all traffic destined to bokeh (after authentication) to bokeh through flask. (Azure App Services appear to only allow one open port unless you customize a container, which I'd like to avoid.)
ALTERNATIVES?:
Sit nginx in front of gunicorn and use a 'reverse proxy' to send all traffic to bokeh. (Can I do this securely?) But I will have to create a custom container. I'd rather do this with just gunicorn + flask + bokeh, if possible.
Feel free to propose anything else. I'd like to use just gunicorn + flask + bokeh, but if I need a different method, I'll result to that.
Thanks!

Related

How to change the Redirect URI from HTTP to HTTPS within a FastAPI web app using fastapi_msal?

I am trying to setup Azure AD authentication for a web application using FastAPI. I am using the fastapi_msal python package to do this. The problem I am having is that when I go to the web app, I am able to login, but once i am authenticated, it says the redirect URI that the application is using begins with HTTP. However, Azure requires the redirect uri begin with HTTPS unless running the app locally. Does anyone know how I can change the redirect uri to begin with https instead?
The code for my project pretty much exactly resembles the code from this example project here. However, I have found a similar project using Flask instead of FastAPI. And there is a specific portion of the code that addresses this redirect uri problem:
# This section is needed for url_for("foo", _external=True) to automatically
# generate http scheme when this sample is running on localhost,
# and to generate https scheme when it is deployed behind reversed proxy.
# See also https://flask.palletsprojects.com/en/1.0.x/deploying/wsgi-standalone/#proxy-setups
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
Does anyone know how I can do something like this for a web app using FastAPI instead?
The full source code for the Flask app can be found here

Flask redirect url_for generates Cloud Run service URL instead of domain

Background:
I've built and deployed an app with Google Cloud Firebase. At a high level, I have a Python Flask server running on Cloud Run, and I serve static JS files with Firebase hosting.
Issue:
Sometimes, I want to redirect the user, but I'm sending them to the Cloud Run service URL rather than my app domain.
EDIT: I'm NOT experiencing this in the JS on the browser, but ONLY in the Python on the server.
Python
If a user navigates to a page without being signed in, e.g. following a link, they are redirected to my login page. For example, if someone who is not signed in tries to look at someone else's profile, the following code redirects them to the authentication blueprint's login endpoint:
if not session.get('user'):
return redirect(url_for('authentication.login'))
I would expect them to be redirected to my-app-name.web.app/auth/login but instead they're routed to my-cloudrun-service-name-XXXXX-region.run.app/auth/login. While the pages loaded look the same, they're really not. The second one redirects to the Cloud Run service URL, which doesn't have my static files served by Firebase Hosting.
I'm not really sure, but I believe this happens because Flask is generating the URL using the request context. So the request from the browser hits Cloud Run Load Balancer, which directs the request to my Cloud Run instance, but that means the Flask app in my Cloud Run instance only sees the internal Google Cloud redirect, and it doesn't know my domain.
I've tried solving this by setting app.config['SEVER_NAME'] = my-app-name.web.app, but I just get the "Not Found" page on each request. Is SEVER_NAME the solution but I'm not implementing it correctly, or is there another way to fix the Flask url_for generation?
I've found what I deem to be an effective solution / workaround.
I set the app config to store my BASE_URL when the app is created:
app.config['BASE_URL'] = 'https://my-url.domain'
Then I can access this as application context during requests, even from blueprints:
#blueprint.route('my_route/')
def my_route():
if not session.get('user'):
return redirect(current_app.config['BASE_URL'] + url_for('authentication.login', 302)
This has two key benefits for me:
There's only one place, the app config, to update if I change the domain
url_for is still used, so there's no hardcoding in case blueprints or routes change

Can't serve swagger on remotely hosted server with different url

I'm trying to serve some simple service using flask and flask_restx (a forked project of flask-restplus, that would be eventually served on AWS.
When it is served, I want to generate swagger page for others to test it easily.
from flask import Flask
from flask_restx import Api
from my_service import service_namespace
app = Flask(__name__)
api = Api(app, version='1.0')
api.add_namespace(service_namespace)
if __name__ == '__main__':
app.run(debug=True)
When I test it locally (e.g. localhost:5000), it works just fine. Problem is, when it is hosted on AWS, because it has a specific domain (gets redirected?) (e.g. my-company.com/chris-service to a container), the document page is unable to find its required files like css and so:
What I've looked and tried
Python (Flask + Swagger) Flasgger throwing 404 error
flask python creating swagger document error
404 error in Flask
Also tried adding Blueprint (albeit without knowing exactly what it does):
app = Flask(__name__)
blueprint = Blueprint("api", __name__,
root_path="/chris-service",
# url_prefix="/chris-service", # doesn't work
)
api = Api(blueprint)
app.register_blueprint(blueprint)
...
And still no luck.
Update
So here's more information as per the comments (pseudo, but technically identical)
Access point for the swagger is my-company.com/chris (with or without http:// or https:// doesn't make difference)
When connecting to the above address, the request URL for the assets are my-company.com/swaggerui/swagger-ui.css
You can access the asset in my-company.com/chris/swaggerui/swagger-ui.css
So I my resolution (which didn't work) was to somehow change the root_path (not even sure if it's the correct wording), as shown in What I've looked and tried.
I've spent about a week to solve this but can't find a way.
Any help will be greatful :) Thanks
Swagger parameters defined at apidoc.py file. Default apidoc object also created in this file. So if you want to customize it you have change it before app and api initialization.
In your case url_prefix should be changed (I recommend to use environment variables to be able set url_prefix flexibly):
$ export URL_PREFIX='/chris'
from os import environ
from flask import Flask
from flask_restx import Api, apidoc
if (url_prefix := environ.get('URL_PREFIX', None)) is not None:
apidoc.apidoc.url_prefix = url_prefix
app = Flask(__name__)
api = Api(app)
...
if __name__ == '__main__':
app.run()
Always very frustrating when stuff is working locally but not when deployed to AWS. Reading this github issue, these 404 errors on swagger assets are probably caused by:
Missing javascript swagger packages
Probably not the case, since flask-restx does this for you. And running it locally should also not work in this case.
Missing gunicorn settings
Make sure that you are also setting gunicorn up correctly as well with
--forwarded-allow-ips if deploying with it (you should be). If you are in a kubernetes cluster you can set this to *
https://docs.gunicorn.org/en/stable/settings.html#forwarded-allow-ips
According to this post, you also have to explicitly set
settings.FLASK_SERVER_NAME to something like http://ec2-10-221-200-56.us-west-2.compute.amazonaws.com:5000
If that does not work, try to deploy a flask-restx example, that should definetely work. This rules out any errors on your end.

Using flask to return server side processed URL

I am hosting my web app on my in-house server using flask (100% accessible online - running on host 0.0.0.0), however, I am trying to use flask to navigate to other locally hosted web apps while browsing from a public network .. is it possible? i.e. onclick() i would like to redirect to URLs that are only available on the local network .. is this possible? I am essentially looking to use my flask app to access my LAN .. thanks!
from flask import Flask, redirect
app = Flask(__name__)
#app.route('/<ip>')//local ip
def redirect_to_link(ip):
return redirect('ip') #would like this to be processed server side and displayed through flask
If the other apps are accessible from extern , yes , you can. If not , you can try serving other app pages by making a request to your flask app first. Flask application should download the webpage for you and then serve it to you externally. Basically you want to make a proxy server.
Try this:
import requests
from flask import Flask, redirect
app = Flask(__name__)
#app.route('/<ip>')//local ip
def redirect_to_link(ip):
return redirect('ip') #would like this to be processed server side and displayed through flask
#local url address to serve examples:
#192.168.1.123:8888/whatyouwant
#192.168.1.123:5656/
#192.168.1.123/somepage
#192.168.1.123
#app.route('/lan/<url>')
def serve_from_lan(url):
r = requests.get("http://"+url, allow_redirects=True)
return r.content
So simple it's not secure but can do the job . then you can think about security.
Also consider that all the addresses present in the application pages in lan should refer to your flask app to be navigable-

Using Flask with subdomain breaks Google Task Queue routing

I set SERVER_NAME in my Flask app to start using subdomains so I can have e.g. frontend and backend on two different subdomains:
frontend.domain.com
backend.domain.com
I set Flask like this:
app.config['SERVER_NAME'] = 'domain.com'
app.url_map.default_subdomain = "frontend"
The app is published using Google App Engine, everything works fine, except the default App Engine domain https://PROJECT_ID.REGION_ID.r.appspot.com now returns a 404 because I understand Flask is not recognising any matching route.
I thought it was fine since I never used https://PROJECT_ID.REGION_ID.r.appspot.com, now I know I was wrong...
https://PROJECT_ID.REGION_ID.r.appspot.com is used by Google Task Cloud to route tasks and e.g. myapp.ey.r.appspot.com/my_task_worker, which is called by Cloud Tasks create_task, now hits a Not Found 404 while it worked before I set SERVER_NAME
How do I fix this? Do I have to hardcode myapp.ey.r.appspot.com in my Flask app somehow?
Here's an extract of my app.yaml, adapted:
runtime: python37
handlers:
- url: /.*
secure: always
redirect_http_response_code: 301
script: auto
env_variables:
DEBUG: False
SERVER_NAME: 'domain.com'
DEFAULT_SUBDOMAIN: 'frontend'
GCP_PROJECT: 'myapp'
CLOUD_TASK_LOCATION: 'europe-west3'
CLOUD_TASK_QUEUE: 'default'
GOOGLE_CLOUD_PLATFORM_API_KEY: 'xxxxxxxx'
...
Do I have to hardcode myapp.ey.r.appspot.com in my Flask app somehow?
Yes. The problem here is that you're managing the redirection from your App instead of leaving App engine to do it. Although this isn't a bad practice by its own, it leaves many of the App Engine features out and most important, as you already mentioned, other GCP products like Cloud Tasks expect a specific behaviour in order to work properly.
How do I fix this?
Under your current architecture you would have to add a routing to the default URL, however as far as I know Flask doesn't allow to route more than one domain, so you would have to switch the 'SERVER_NAME' to the default app engine or change into something like Django that supports multiple domains.
My suggestion is to map your subdomains to App Engine services (one for your frontend and one for your backend) and leave the routing to GCP (and remove the 'SERVER_NAME'). You can make use of the dispatch.yaml to do the routing, you can for example create the next routes:
dispatch:
# Default service serves the typical web resources and all static resources.
- url: "myapp.ey.r.appspot.com/*"
service: default
- url: "frontend.domain.com/*"
service: frontend
- url: "backend.domain.com/*"
service: backend

Categories