From 509f97a9f58bc7138fbbc3a0e6fcb7ecb33096a3 Mon Sep 17 00:00:00 2001 From: Stephane Martin Date: Mon, 5 Sep 2016 22:54:53 +0200 Subject: [PATCH 1/3] authentication by reverse proxy --- README.md | 27 +++++++++++++++++ realms/__init__.py | 20 ++++++++++++- realms/config/__init__.py | 4 +++ realms/modules/auth/__init__.py | 2 ++ realms/modules/auth/models.py | 5 +++- realms/modules/auth/proxy/__init__.py | 5 ++++ realms/modules/auth/proxy/models.py | 42 +++++++++++++++++++++++++++ realms/modules/auth/views.py | 4 ++- realms/modules/wiki/views.py | 14 ++++----- realms/templates/layout.html | 10 +++++-- 10 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 realms/modules/auth/proxy/__init__.py create mode 100644 realms/modules/auth/proxy/models.py diff --git a/README.md b/README.md index 3767104..91e2233 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,33 @@ Put them in your `realms-wiki.json` config file. Use the example below. } } +### Authentication by reverse proxy + +If you configured realms behind a reverse-proxy or a single-sign-on, it is possible to delegate authentication to +the proxy. + + "AUTH_PROXY": true + +Note: of course with that setup you must ensure that **Realms is only accessible through the proxy**. + +Example Nginx configuration: + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header REMOTE_USER $remote_user; + + proxy_pass http://127.0.0.1:5000/; + proxy_redirect off; + } + +By default, Realms will look for the user ID in `REMOTE_USER` HTTP header. You can specify another header name with: + + "AUTH_PROXY_HEADER_NAME": "LOGGED_IN_USER" + + + ## Running realms-wiki start diff --git a/realms/__init__.py b/realms/__init__.py index 273e277..580bf32 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import sys +import logging # Set default encoding to UTF-8 reload(sys) # noinspection PyUnresolvedReferences @@ -17,7 +18,7 @@ from functools import update_wrapper import click from flask import Flask, request, render_template, url_for, redirect, g from flask_cache import Cache -from flask_login import LoginManager, current_user +from flask_login import LoginManager, current_user, logout_user from flask_sqlalchemy import SQLAlchemy from flask_assets import Environment, Bundle from flask_ldap_login import LDAPLoginManager @@ -215,6 +216,23 @@ def create_app(config=None): if app.config.get('DB_URI'): db.metadata.create_all(db.get_engine(app)) + if app.config["AUTH_PROXY"]: + logger = logging.getLogger("realms.auth") + + @app.before_request + def proxy_auth(): + from realms.modules.auth.proxy.models import User as ProxyUser + remote_user = request.environ.get(app.config["AUTH_PROXY_HEADER_NAME"]) + if remote_user: + if current_user.is_authenticated(): + if current_user.id == remote_user: + return + logger.info("login in realms and login by proxy are different: '{}'/'{}'".format( + current_user.id, remote_user)) + logout_user() + logger.info("User logged in by proxy as '{}'".format(remote_user)) + ProxyUser.do_login(remote_user) + return app # Init plugins here if possible diff --git a/realms/config/__init__.py b/realms/config/__init__.py index 8bc8e17..3c5da25 100644 --- a/realms/config/__init__.py +++ b/realms/config/__init__.py @@ -100,6 +100,10 @@ class Config(object): # Name of page that will act as home WIKI_HOME = 'home' + # Should we trust authentication set by a proxy + AUTH_PROXY = False + AUTH_PROXY_HEADER_NAME = "REMOTE_USER" + AUTH_LOCAL_ENABLE = True ALLOW_ANON = True REGISTRATION_ENABLED = True diff --git a/realms/modules/auth/__init__.py b/realms/modules/auth/__init__.py index 1908ef5..b362d66 100644 --- a/realms/modules/auth/__init__.py +++ b/realms/modules/auth/__init__.py @@ -5,8 +5,10 @@ from flask_login import login_url from realms import login_manager + modules = set() + @login_manager.unauthorized_handler def unauthorized(): if request.method == 'GET': diff --git a/realms/modules/auth/models.py b/realms/modules/auth/models.py index 94a67eb..a6ee6c3 100644 --- a/realms/modules/auth/models.py +++ b/realms/modules/auth/models.py @@ -17,6 +17,7 @@ from . import modules def load_user(auth_id): return Auth.load_user(auth_id) + auth_users = {} @@ -40,7 +41,9 @@ class Auth(object): def login_forms(): forms = [] for t in modules: - forms.append(Auth.get_auth_user(t).login_form()) + form = Auth.get_auth_user(t).login_form() + if form: + forms.append(form) return "
".join(forms) diff --git a/realms/modules/auth/proxy/__init__.py b/realms/modules/auth/proxy/__init__.py new file mode 100644 index 0000000..4c37ffc --- /dev/null +++ b/realms/modules/auth/proxy/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import + +from realms.modules.auth.models import Auth + +Auth.register('proxy') diff --git a/realms/modules/auth/proxy/models.py b/realms/modules/auth/proxy/models.py new file mode 100644 index 0000000..c45e87f --- /dev/null +++ b/realms/modules/auth/proxy/models.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import + +from flask_login import login_user + +from realms.modules.auth.models import BaseUser + + +users = {} + + +class User(BaseUser): + type = 'proxy' + + def __init__(self, username, email='null@localhost.local', password="dummypassword"): + self.id = username + self.username = username + self.email = email + self.password = password + + @property + def auth_token_id(self): + return self.password + + @staticmethod + def load_user(*args, **kwargs): + return User.get_by_id(args[0]) + + @staticmethod + def get_by_id(user_id): + return users.get(user_id) + + @staticmethod + def login_form(): + return None + + @staticmethod + def do_login(user_id): + user = User(user_id) + users[user_id] = user + login_user(user, remember=True) + return True + diff --git a/realms/modules/auth/views.py b/realms/modules/auth/views.py index f11eee5..181248a 100644 --- a/realms/modules/auth/views.py +++ b/realms/modules/auth/views.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for, session -from flask_login import logout_user +from flask_login import logout_user, current_user from .models import Auth @@ -12,6 +12,8 @@ blueprint = Blueprint('auth', __name__, template_folder='templates') @blueprint.route("/login", methods=['GET', 'POST']) def login(): next_url = request.args.get('next') or url_for(current_app.config['ROOT_ENDPOINT']) + if current_user.is_authenticated(): + return redirect(next_url) session['next_url'] = next_url return render_template("auth/login.html", forms=Auth.login_forms()) diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py index 2db60ed..562411b 100644 --- a/realms/modules/wiki/views.py +++ b/realms/modules/wiki/views.py @@ -17,7 +17,7 @@ blueprint = Blueprint('wiki', __name__, template_folder='templates', @blueprint.route("/_commit//") def commit(name, sha): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): return current_app.login_manager.unauthorized() cname = to_canonical(name) @@ -32,7 +32,7 @@ def commit(name, sha): @blueprint.route(r"/_compare//") def compare(name, fsha, dots, lsha): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): return current_app.login_manager.unauthorized() diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha) @@ -47,7 +47,7 @@ def revert(): commit = request.form.get('commit') message = request.form.get('message', "Reverting %s" % cname) - if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous: + if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous(): return dict(error=True, message="Anonymous posting not allowed"), 403 if cname in current_app.config.get('WIKI_LOCKED_PAGES'): @@ -69,7 +69,7 @@ def revert(): @blueprint.route("/_history/") def history(name): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): return current_app.login_manager.unauthorized() return render_template('wiki/history.html', name=name) @@ -171,7 +171,7 @@ def _tree_index(items, path=""): @blueprint.route("/_index", defaults={"path": ""}) @blueprint.route("/_index/") def index(path): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): return current_app.login_manager.unauthorized() items = g.current_wiki.get_index() @@ -192,7 +192,7 @@ def page_write(name): if not cname: return dict(error=True, message="Invalid name") - if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous: + if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous(): return dict(error=True, message="Anonymous posting not allowed"), 403 if request.method == 'POST': @@ -235,7 +235,7 @@ def page_write(name): @blueprint.route("/", defaults={'name': 'home'}) @blueprint.route("/") def page(name): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): return current_app.login_manager.unauthorized() cname = to_canonical(name) diff --git a/realms/templates/layout.html b/realms/templates/layout.html index fbe097f..4f763f8 100644 --- a/realms/templates/layout.html +++ b/realms/templates/layout.html @@ -58,7 +58,7 @@ - {% if current_user.is_authenticated %} + {% if current_user.is_authenticated() %} {% else %} @@ -109,7 +113,7 @@ {% endfor %} var User = {}; - User.is_authenticated = {{ current_user.is_authenticated|tojson }}; + User.is_authenticated = {{ current_user.is_authenticated()|tojson }}; {% for attr in ['username', 'email'] %} User.{{ attr }} = {{ current_user[attr]|tojson }}; {% endfor %} From d7d8ef6d7635c06d4ab9c31377b91868e40f1bdf Mon Sep 17 00:00:00 2001 From: Stephane Martin Date: Mon, 5 Sep 2016 23:19:28 +0200 Subject: [PATCH 2/3] flask 0.11 compatibility --- realms/__init__.py | 2 +- realms/modules/auth/views.py | 2 +- realms/modules/search/views.py | 2 +- realms/modules/wiki/views.py | 16 ++++++++-------- realms/templates/layout.html | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/realms/__init__.py b/realms/__init__.py index 580bf32..ec00c76 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -224,7 +224,7 @@ def create_app(config=None): from realms.modules.auth.proxy.models import User as ProxyUser remote_user = request.environ.get(app.config["AUTH_PROXY_HEADER_NAME"]) if remote_user: - if current_user.is_authenticated(): + if current_user.is_authenticated: if current_user.id == remote_user: return logger.info("login in realms and login by proxy are different: '{}'/'{}'".format( diff --git a/realms/modules/auth/views.py b/realms/modules/auth/views.py index 181248a..e8dd2b7 100644 --- a/realms/modules/auth/views.py +++ b/realms/modules/auth/views.py @@ -12,7 +12,7 @@ blueprint = Blueprint('auth', __name__, template_folder='templates') @blueprint.route("/login", methods=['GET', 'POST']) def login(): next_url = request.args.get('next') or url_for(current_app.config['ROOT_ENDPOINT']) - if current_user.is_authenticated(): + if current_user.is_authenticated: return redirect(next_url) session['next_url'] = next_url return render_template("auth/login.html", forms=Auth.login_forms()) diff --git a/realms/modules/search/views.py b/realms/modules/search/views.py index 7d74e20..ab48f54 100644 --- a/realms/modules/search/views.py +++ b/realms/modules/search/views.py @@ -11,7 +11,7 @@ blueprint = Blueprint('search', __name__, template_folder='templates') @blueprint.route('/_search') def search(): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() results = search_engine.wiki(request.args.get('q')) diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py index 562411b..4506beb 100644 --- a/realms/modules/wiki/views.py +++ b/realms/modules/wiki/views.py @@ -17,7 +17,7 @@ blueprint = Blueprint('wiki', __name__, template_folder='templates', @blueprint.route("/_commit//") def commit(name, sha): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() cname = to_canonical(name) @@ -32,7 +32,7 @@ def commit(name, sha): @blueprint.route(r"/_compare//") def compare(name, fsha, dots, lsha): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha) @@ -47,7 +47,7 @@ def revert(): commit = request.form.get('commit') message = request.form.get('message', "Reverting %s" % cname) - if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous(): + if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous: return dict(error=True, message="Anonymous posting not allowed"), 403 if cname in current_app.config.get('WIKI_LOCKED_PAGES'): @@ -69,7 +69,7 @@ def revert(): @blueprint.route("/_history/") def history(name): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() return render_template('wiki/history.html', name=name) @@ -77,7 +77,7 @@ def history(name): @blueprint.route("/_history_data/") def history_data(name): """Ajax provider for paginated history data.""" - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() draw = int(request.args.get('draw', 0)) start = int(request.args.get('start', 0)) @@ -171,7 +171,7 @@ def _tree_index(items, path=""): @blueprint.route("/_index", defaults={"path": ""}) @blueprint.route("/_index/") def index(path): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() items = g.current_wiki.get_index() @@ -192,7 +192,7 @@ def page_write(name): if not cname: return dict(error=True, message="Invalid name") - if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous(): + if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous: return dict(error=True, message="Anonymous posting not allowed"), 403 if request.method == 'POST': @@ -235,7 +235,7 @@ def page_write(name): @blueprint.route("/", defaults={'name': 'home'}) @blueprint.route("/") def page(name): - if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous(): + if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() cname = to_canonical(name) diff --git a/realms/templates/layout.html b/realms/templates/layout.html index 4f763f8..7c1b9bc 100644 --- a/realms/templates/layout.html +++ b/realms/templates/layout.html @@ -58,7 +58,7 @@ - {% if current_user.is_authenticated() %} + {% if current_user.is_authenticated %}