@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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': | |||
@@ -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 "<hr />".join(forms) | |||
@@ -0,0 +1,5 @@ | |||
from __future__ import absolute_import | |||
from realms.modules.auth.models import Auth | |||
Auth.register('proxy') |
@@ -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 | |||
@@ -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()) | |||
@@ -17,7 +17,7 @@ blueprint = Blueprint('wiki', __name__, template_folder='templates', | |||
@blueprint.route("/_commit/<sha>/<path:name>") | |||
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/<path:name>/<regex('\w+'):fsha><regex('\.{2,3}'):dots><regex('\w+'):lsha>") | |||
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/<path:name>") | |||
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/<path:path>") | |||
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("/<path:name>") | |||
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) | |||
@@ -58,7 +58,7 @@ | |||
</div> | |||
</form> | |||
</li> | |||
{% if current_user.is_authenticated %} | |||
{% if current_user.is_authenticated() %} | |||
<li class="dropdown user-avatar"> | |||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> | |||
<span> | |||
@@ -68,7 +68,11 @@ | |||
</a> | |||
<ul class="dropdown-menu"> | |||
<!--<li><a href="{{ url_for('auth.settings') }}"><i class="fa fa-gear"></i> Settings</a></li>--> | |||
<li><a href="{{ url_for('auth.logout') }}"><i class="fa fa-power-off"></i> Logout</a></li> | |||
{% if current_user.type != "proxy" %} | |||
<li><a href="{{ url_for('auth.logout') }}"><i class="fa fa-power-off"></i> Logout</a></li> | |||
{% else %} | |||
<li><button class="btn btn-block" disabled="disabled"><i class="fa fa-power-off"></i> Logout</button></li> | |||
{% endif %} | |||
</ul> | |||
</li> | |||
{% 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 %} | |||