diff --git a/README.md b/README.md index 3767104..e98786a 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,36 @@ 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 / { + auth_basic "Restricted"; + auth_basic_user_file /etc/nginx/.htpasswd; + + 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..7298a0e 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -17,7 +17,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 @@ -26,9 +26,8 @@ from werkzeug.exceptions import HTTPException from sqlalchemy.ext.declarative import declarative_base from realms.modules.search.models import Search -from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict +from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict, is_su, in_virtualenv from realms.lib.hook import HookModelMeta, HookMixin -from realms.lib.util import is_su, in_virtualenv from realms.version import __version__ diff --git a/realms/config/__init__.py b/realms/config/__init__.py index 8bc8e17..860837f 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 @@ -156,6 +160,8 @@ class Config(object): self.MODULES.append('auth.oauth') if hasattr(self, 'LDAP'): self.MODULES.append('auth.ldap') + if hasattr(self, "AUTH_PROXY"): + self.MODULES.append('auth.proxy') if in_vagrant(): self.USE_X_SENDFILE = False if self.ENV == "DEV": 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/hooks.py b/realms/modules/auth/proxy/hooks.py new file mode 100644 index 0000000..e7dcae4 --- /dev/null +++ b/realms/modules/auth/proxy/hooks.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import + +import logging + +from flask import request, current_app +from flask_login import current_user, logout_user + +from .models import User as ProxyUser + + +logger = logging.getLogger("realms.auth") + + +def before_request(): + header_name = current_app.config["AUTH_PROXY_HEADER_NAME"] + remote_user = request.headers.get(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) 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..e8dd2b7 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/templates/layout.html b/realms/templates/layout.html index fbe097f..7c1b9bc 100644 --- a/realms/templates/layout.html +++ b/realms/templates/layout.html @@ -68,7 +68,11 @@ {% else %}