Skip to content

Commit

Permalink
Version 0.7.3. (#34)
Browse files Browse the repository at this point in the history
* 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.

* version 0.7.3, simple auth mode available, docs for auth created

* version 0.7.3, simple auth mode available, docs for auth created

* typo in link

---------

Co-authored-by: Jakub Man <jakub.man@pm.me>
  • Loading branch information
jirivrany and jakubman1 committed Nov 3, 2023
1 parent 4893aaa commit 432110d
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 39 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions config.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions docs/AUTH.md
Original file line number Diff line number Diff line change
@@ -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:
```
<Location />
AuthType shibboleth
ShibRequestSetting requireSession 1
require shib-session
</Location>
```


#### 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.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.
24 changes: 3 additions & 21 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
```
<Location />
AuthType shibboleth
ShibRequestSetting requireSession 1
require shib-session
</Location>
```

### 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/
```

Expand Down
24 changes: 24 additions & 0 deletions docs/apache.conf.example
Original file line number Diff line number Diff line change
@@ -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
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html

<Location />
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/
</Location>
</VirtualHost>
2 changes: 1 addition & 1 deletion flowapp/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.7.2"
__version__ = "0.7.3"
47 changes: 31 additions & 16 deletions flowapp/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -70,23 +70,10 @@ def login(user_info):
uuid = False
return redirect("/")
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")
Expand All @@ -96,6 +83,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():
Expand Down Expand Up @@ -177,4 +177,19 @@ 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
11 changes: 10 additions & 1 deletion flowapp/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand Down
7 changes: 7 additions & 0 deletions flowapp/templates/errors/401.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends 'layouts/default.j2' %}
{% block content %}
<h1>Could not log you in.</h1>
<p class="form-text">401: Unauthorized</p>
<p>Please log out and try logging in again.</p>
<p><a href="{{url_for('logout')}}">Log out</a></p>
{% endblock %}

0 comments on commit 432110d

Please sign in to comment.