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 528e29a..f46c3ec 100644 --- a/realms/modules/wiki/views.py +++ b/realms/modules/wiki/views.py @@ -18,7 +18,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) @@ -35,7 +35,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) @@ -50,7 +50,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'): @@ -72,7 +72,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) @@ -197,7 +197,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() @@ -218,7 +218,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': @@ -261,7 +261,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 %}