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() %}
@@ -68,7 +68,11 @@
{% 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 %}