From 6913689e3f94d4f54bcb7e3c4ff6a2a154266743 Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Fri, 3 Nov 2023 10:12:00 +0100 Subject: [PATCH 1/4] Support for authentication using external proxy (#33) * add options for HTTP header authentication to config * add template for handling error 401: Unauthorized * support external authentication Expects authentication to be done using an external tool (such as Apache), that fills the users UUID to a HTTP header and acts as a proxy. --- config.example.py | 6 +++++ flowapp/__init__.py | 47 ++++++++++++++++++++++----------- flowapp/auth.py | 11 +++++++- flowapp/templates/errors/401.j2 | 7 +++++ 4 files changed, 55 insertions(+), 16 deletions(-) create mode 100755 flowapp/templates/errors/401.j2 diff --git a/config.example.py b/config.example.py index e539040e..956ba963 100644 --- a/config.example.py +++ b/config.example.py @@ -9,6 +9,12 @@ class Config(): TESTING = False # SSO auth enabled SSO_AUTH = False + # Authentication is done outside the app, use HTTP header to get the user uuid. + # If SSO_AUTH is set to True, this option is ignored and SSO auth is used. + HEADER_AUTH = True + # Name of HTTP header containing the UUID of authenticated user. + # Only used when HEADER_AUTH is set to True + AUTH_HEADER_NAME = 'X-Authenticated-User' # SSO LOGOUT LOGOUT_URL = "https://flowspec.example.com/Shibboleth.sso/Logout" # SQL Alchemy config diff --git a/flowapp/__init__.py b/flowapp/__init__.py index a29297a7..83433e06 100644 --- a/flowapp/__init__.py +++ b/flowapp/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import babel -from flask import Flask, redirect, render_template, session, url_for +from flask import Flask, redirect, render_template, session, url_for, request from flask_sso import SSO from flask_sqlalchemy import SQLAlchemy from flask_wtf.csrf import CSRFProtect @@ -72,21 +72,9 @@ def login(user_info): else: user = db.session.query(models.User).filter_by(uuid=uuid).first() try: - session["user_uuid"] = user.uuid - session["user_email"] = user.uuid - session["user_name"] = user.name - session["user_id"] = user.id - session["user_roles"] = [role.name for role in user.role.all()] - session["user_orgs"] = ", ".join( - org.name for org in user.organization.all() - ) - session["user_role_ids"] = [role.id for role in user.role.all()] - session["user_org_ids"] = [org.id for org in user.organization.all()] - roles = [i > 1 for i in session["user_role_ids"]] - session["can_edit"] = True if all(roles) and roles else [] + _register_user_to_session(uuid) except AttributeError: - return redirect("/") - + pass return redirect("/") @app.route("/logout") @@ -96,6 +84,19 @@ def logout(): session.clear() return redirect(app.config.get("LOGOUT_URL")) + @app.route("/ext-login") + def ext_login(): + header_name = app.config.get("AUTH_HEADER_NAME", 'X-Authenticated-User') + if header_name not in request.headers: + return render_template("errors/401.j2") + uuid = request.headers.get(header_name) + if uuid: + try: + _register_user_to_session(uuid) + except AttributeError: + return render_template("errors/401.j2") + return redirect("/") + @app.route("/") @auth_required def index(): @@ -177,4 +178,20 @@ def format_datetime(value): return babel.dates.format_datetime(value, format) + def _register_user_to_session(uuid: str): + user = db.session.query(models.User).filter_by(uuid=uuid).first() + session["user_uuid"] = user.uuid + session["user_email"] = user.uuid + session["user_name"] = user.name + session["user_id"] = user.id + session["user_roles"] = [role.name for role in user.role.all()] + session["user_orgs"] = ", ".join( + org.name for org in user.organization.all() + ) + session["user_role_ids"] = [role.id for role in user.role.all()] + session["user_org_ids"] = [org.id for org in user.organization.all()] + roles = [i > 1 for i in session["user_role_ids"]] + session["can_edit"] = True if all(roles) and roles else [] + return app + diff --git a/flowapp/auth.py b/flowapp/auth.py index b4925d99..c4d942ff 100644 --- a/flowapp/auth.py +++ b/flowapp/auth.py @@ -14,7 +14,10 @@ def auth_required(f): @wraps(f) def decorated(*args, **kwargs): if not check_auth(get_user()): - return redirect("/login") + if current_app.config.get("SSO_AUTH"): + return redirect("/login") + elif current_app.config.get("HEADER_AUTH", False): + return redirect("/ext-login") return f(*args, **kwargs) return decorated @@ -99,6 +102,12 @@ def check_auth(uuid): if uuid: exist = db.session.query(User).filter_by(uuid=uuid).first() return exist + elif current_app.config.get("HEADER_AUTH", False): + # External auth (for example apache) + header_name = current_app.config.get("AUTH_HEADER_NAME", 'X-Authenticated-User') + if header_name not in request.headers or not session.get("user_uuid"): + return False + return db.session.query(User).filter_by(uuid=request.headers.get(header_name)) else: # Localhost login / no check session["user_email"] = current_app.config["LOCAL_USER_UUID"] diff --git a/flowapp/templates/errors/401.j2 b/flowapp/templates/errors/401.j2 new file mode 100755 index 00000000..6fea372d --- /dev/null +++ b/flowapp/templates/errors/401.j2 @@ -0,0 +1,7 @@ +{% extends 'layouts/default.j2' %} +{% block content %} +

Could not log you in.

+

401: Unauthorized

+

Please log out and try logging in again.

+

Log out

+{% endblock %} \ No newline at end of file From 4c1ece53bf19c9645c9cfcfe48fb640675db926f Mon Sep 17 00:00:00 2001 From: Jiri Vrany Date: Fri, 3 Nov 2023 13:01:12 +0100 Subject: [PATCH 2/4] version 0.7.3, simple auth mode available, docs for auth created --- README.md | 1 + docs/AUTH.md | 43 ++++++++++++++++++++++++++++++++++++++++ docs/INSTALL.md | 24 +++------------------- docs/apache.conf.example | 24 ++++++++++++++++++++++ flowapp/__about__.py | 2 +- flowapp/__init__.py | 2 -- 6 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 docs/AUTH.md create mode 100644 docs/apache.conf.example diff --git a/README.md b/README.md index caaba34f..e6c50587 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Last part of the system is Guarda service. This systemctl service is running in * [Local database instalation notes](./docs/DB_LOCAL.md) ## Change Log +- 0.7.3 - New possibility of external auth proxy. - 0.7.2 - Dashboard and Main menu are now customizable in config. App is ready to be packaged using setup.py. - 0.7.0 - ExaAPI now have two options - HTTP or RabbitMQ. ExaAPI process has been renamed, update of ExaBGP process value is needed for this version. - 0.6.2 - External config for ExaAPI diff --git a/docs/AUTH.md b/docs/AUTH.md new file mode 100644 index 00000000..9b6fdcb6 --- /dev/null +++ b/docs/AUTH.md @@ -0,0 +1,43 @@ +# ExaFS tool +## Auth mechanism + +Since version 0.7.3, the application supports three different forms of user authorization. + +* SSO using Shibboleth +* Simple Auth proxy +* Local single-user mode + +### SSO +To use SSO, you need to set up Apache + Shiboleth in the usual way. Then set `SSO_AUTH = True` in the application configuration file **config.py** + +Shibboleth configuration example: + +#### shibboleth config: +``` + + AuthType shibboleth + ShibRequestSetting requireSession 1 + require shib-session + + +``` + + +#### httpd ssl.conf +We recomend using app with https only. It's important to configure proxy pass to uwsgi in httpd config. +``` +# Proxy everything to the WSGI server except /Shibboleth.sso and +# /shibboleth-sp +ProxyPass /kon.php ! +ProxyPass /Shibboleth.sso ! +ProxyPass /shibboleth-sp ! +ProxyPass / uwsgi://127.0.0.1:8000/ +``` + +### Simple Auth +This mode uses a WWW server (usually Apache) as an auth proxy. It is thus possible to use an external user database. Everything needs to be set in the web server configuration, then in **config.py** enable `HEADER_AUTH = True` and set `AUTH_HEADER_NAME = 'X-Authenticated-User'` + +See [apache.conf.example]('./apache.example.conf') for more information about configuration. + +### Local single user mode +This mode is used as a fallback if neither SSO nor Simple Auth is enabled. Configuration is done using **config.py**. The mode is more for testing purposes, it does not allow to set up multiple users with different permission levels and also does not perform user authentication. \ No newline at end of file diff --git a/docs/INSTALL.md b/docs/INSTALL.md index f384845e..9032563f 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -8,30 +8,12 @@ The default Python for RHEL9 is Python 3.9 Virtualenv with Python39 is used by uWSGI server to keep the packages for app separated from system. ## Prerequisites +First, choose how to [authenticate and authorize users]('./AUTH.md'). The application currently supports three options. -ExaFS is using Shibboleth auth and therefore we suggest to use Apache web server. -Install the Apache httpd as usual and then continue with this guide. +Depending on the selected WWW server, set up a proxy. We recommend using Apache + mod_uwsgi. If you use another solution, set up the WWW server as you are used to. -First configure Shibboleth - -### shibboleth config: -``` - - AuthType shibboleth - ShibRequestSetting requireSession 1 - require shib-session - - -``` - -### httpd ssl.conf -We are using https only. It's important to configure proxy pass to uwsgi in httpd config. ``` -# Proxy everything to the WSGI server except /Shibboleth.sso and -# /shibboleth-sp -ProxyPass /kon.php ! -ProxyPass /Shibboleth.sso ! -ProxyPass /shibboleth-sp ! +# Proxy everything to the WSGI server ProxyPass / uwsgi://127.0.0.1:8000/ ``` diff --git a/docs/apache.conf.example b/docs/apache.conf.example new file mode 100644 index 00000000..d3cae299 --- /dev/null +++ b/docs/apache.conf.example @@ -0,0 +1,24 @@ +# mod_dbd configuration +DBDriver pgsql +DBDParams "dbname=exafs_users host=localhost user=exafs password=verysecurepassword" + +DBDMin 4 +DBDKeep 8 +DBDMax 20 +DBDExptime 300 + +# ExaFS authentication + + ServerName example.com + DocumentRoot /var/www/html + + + AuthType Basic + AuthName "Database Authentication" + AuthBasicProvider dbd + AuthDBDUserPWQuery "SELECT pass_hash AS password FROM \"users\" WHERE email = %s" + Require valid-user + RequestHeader set X-Authenticated-User expr=%{REMOTE_USER} + ProxyPass http://127.0.0.1:8080/ + + \ No newline at end of file diff --git a/flowapp/__about__.py b/flowapp/__about__.py index b7b96e61..be0f7d4e 100755 --- a/flowapp/__about__.py +++ b/flowapp/__about__.py @@ -1 +1 @@ -__version__ = "0.7.2" +__version__ = "0.7.3" diff --git a/flowapp/__init__.py b/flowapp/__init__.py index 83433e06..536ac51f 100644 --- a/flowapp/__init__.py +++ b/flowapp/__init__.py @@ -70,7 +70,6 @@ def login(user_info): uuid = False return redirect("/") else: - user = db.session.query(models.User).filter_by(uuid=uuid).first() try: _register_user_to_session(uuid) except AttributeError: @@ -194,4 +193,3 @@ def _register_user_to_session(uuid: str): session["can_edit"] = True if all(roles) and roles else [] return app - From cb93fbe925dfe21c408edc12e524964a8f1ae0d1 Mon Sep 17 00:00:00 2001 From: Jiri Vrany Date: Fri, 3 Nov 2023 13:04:14 +0100 Subject: [PATCH 3/4] version 0.7.3, simple auth mode available, docs for auth created --- docs/AUTH.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/AUTH.md b/docs/AUTH.md index 9b6fdcb6..d1b6a31b 100644 --- a/docs/AUTH.md +++ b/docs/AUTH.md @@ -37,7 +37,7 @@ ProxyPass / uwsgi://127.0.0.1:8000/ ### Simple Auth This mode uses a WWW server (usually Apache) as an auth proxy. It is thus possible to use an external user database. Everything needs to be set in the web server configuration, then in **config.py** enable `HEADER_AUTH = True` and set `AUTH_HEADER_NAME = 'X-Authenticated-User'` -See [apache.conf.example]('./apache.example.conf') for more information about configuration. +See [apache.conf.example](./apache.conf.example) for more information about configuration. ### Local single user mode This mode is used as a fallback if neither SSO nor Simple Auth is enabled. Configuration is done using **config.py**. The mode is more for testing purposes, it does not allow to set up multiple users with different permission levels and also does not perform user authentication. \ No newline at end of file From 061f40fd0629c79f7d21db972c3ec72c8783ba52 Mon Sep 17 00:00:00 2001 From: Jiri Vrany Date: Fri, 3 Nov 2023 13:07:55 +0100 Subject: [PATCH 4/4] typo in link --- docs/INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 9032563f..9965ee2e 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -8,7 +8,7 @@ The default Python for RHEL9 is Python 3.9 Virtualenv with Python39 is used by uWSGI server to keep the packages for app separated from system. ## Prerequisites -First, choose how to [authenticate and authorize users]('./AUTH.md'). The application currently supports three options. +First, choose how to [authenticate and authorize users](./AUTH.md). The application currently supports three options. Depending on the selected WWW server, set up a proxy. We recommend using Apache + mod_uwsgi. If you use another solution, set up the WWW server as you are used to.