authentication by reverse proxy

This commit is contained in:
Stephane Martin 2016-09-05 22:54:53 +02:00
parent c6db7909b8
commit 509f97a9f5
10 changed files with 120 additions and 13 deletions

View file

@ -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 ## Running
realms-wiki start realms-wiki start

View file

@ -1,6 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import sys import sys
import logging
# Set default encoding to UTF-8 # Set default encoding to UTF-8
reload(sys) reload(sys)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@ -17,7 +18,7 @@ from functools import update_wrapper
import click import click
from flask import Flask, request, render_template, url_for, redirect, g from flask import Flask, request, render_template, url_for, redirect, g
from flask_cache import Cache 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_sqlalchemy import SQLAlchemy
from flask_assets import Environment, Bundle from flask_assets import Environment, Bundle
from flask_ldap_login import LDAPLoginManager from flask_ldap_login import LDAPLoginManager
@ -215,6 +216,23 @@ def create_app(config=None):
if app.config.get('DB_URI'): if app.config.get('DB_URI'):
db.metadata.create_all(db.get_engine(app)) 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 return app
# Init plugins here if possible # Init plugins here if possible

View file

@ -100,6 +100,10 @@ class Config(object):
# Name of page that will act as home # Name of page that will act as home
WIKI_HOME = '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 AUTH_LOCAL_ENABLE = True
ALLOW_ANON = True ALLOW_ANON = True
REGISTRATION_ENABLED = True REGISTRATION_ENABLED = True

View file

@ -5,8 +5,10 @@ from flask_login import login_url
from realms import login_manager from realms import login_manager
modules = set() modules = set()
@login_manager.unauthorized_handler @login_manager.unauthorized_handler
def unauthorized(): def unauthorized():
if request.method == 'GET': if request.method == 'GET':

View file

@ -17,6 +17,7 @@ from . import modules
def load_user(auth_id): def load_user(auth_id):
return Auth.load_user(auth_id) return Auth.load_user(auth_id)
auth_users = {} auth_users = {}
@ -40,7 +41,9 @@ class Auth(object):
def login_forms(): def login_forms():
forms = [] forms = []
for t in modules: 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) return "<hr />".join(forms)

View file

@ -0,0 +1,5 @@
from __future__ import absolute_import
from realms.modules.auth.models import Auth
Auth.register('proxy')

View file

@ -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

View file

@ -1,7 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for, session 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 from .models import Auth
@ -12,6 +12,8 @@ blueprint = Blueprint('auth', __name__, template_folder='templates')
@blueprint.route("/login", methods=['GET', 'POST']) @blueprint.route("/login", methods=['GET', 'POST'])
def login(): def login():
next_url = request.args.get('next') or url_for(current_app.config['ROOT_ENDPOINT']) 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 session['next_url'] = next_url
return render_template("auth/login.html", forms=Auth.login_forms()) return render_template("auth/login.html", forms=Auth.login_forms())

View file

@ -17,7 +17,7 @@ blueprint = Blueprint('wiki', __name__, template_folder='templates',
@blueprint.route("/_commit/<sha>/<path:name>") @blueprint.route("/_commit/<sha>/<path:name>")
def commit(name, sha): 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() return current_app.login_manager.unauthorized()
cname = to_canonical(name) 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>") @blueprint.route(r"/_compare/<path:name>/<regex('\w+'):fsha><regex('\.{2,3}'):dots><regex('\w+'):lsha>")
def compare(name, fsha, dots, 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() return current_app.login_manager.unauthorized()
diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha) diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha)
@ -47,7 +47,7 @@ def revert():
commit = request.form.get('commit') commit = request.form.get('commit')
message = request.form.get('message', "Reverting %s" % cname) 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 return dict(error=True, message="Anonymous posting not allowed"), 403
if cname in current_app.config.get('WIKI_LOCKED_PAGES'): if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
@ -69,7 +69,7 @@ def revert():
@blueprint.route("/_history/<path:name>") @blueprint.route("/_history/<path:name>")
def history(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 current_app.login_manager.unauthorized()
return render_template('wiki/history.html', name=name) 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", defaults={"path": ""})
@blueprint.route("/_index/<path:path>") @blueprint.route("/_index/<path:path>")
def index(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() return current_app.login_manager.unauthorized()
items = g.current_wiki.get_index() items = g.current_wiki.get_index()
@ -192,7 +192,7 @@ def page_write(name):
if not cname: if not cname:
return dict(error=True, message="Invalid name") 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 return dict(error=True, message="Anonymous posting not allowed"), 403
if request.method == 'POST': if request.method == 'POST':
@ -235,7 +235,7 @@ def page_write(name):
@blueprint.route("/", defaults={'name': 'home'}) @blueprint.route("/", defaults={'name': 'home'})
@blueprint.route("/<path:name>") @blueprint.route("/<path:name>")
def page(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() return current_app.login_manager.unauthorized()
cname = to_canonical(name) cname = to_canonical(name)

View file

@ -58,7 +58,7 @@
</div> </div>
</form> </form>
</li> </li>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated() %}
<li class="dropdown user-avatar"> <li class="dropdown user-avatar">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span> <span>
@ -68,7 +68,11 @@
</a> </a>
<ul class="dropdown-menu"> <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.settings') }}"><i class="fa fa-gear"></i> Settings</a></li>-->
{% if current_user.type != "proxy" %}
<li><a href="{{ url_for('auth.logout') }}"><i class="fa fa-power-off"></i> Logout</a></li> <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> </ul>
</li> </li>
{% else %} {% else %}
@ -109,7 +113,7 @@
{% endfor %} {% endfor %}
var User = {}; var User = {};
User.is_authenticated = {{ current_user.is_authenticated|tojson }}; User.is_authenticated = {{ current_user.is_authenticated()|tojson }};
{% for attr in ['username', 'email'] %} {% for attr in ['username', 'email'] %}
User.{{ attr }} = {{ current_user[attr]|tojson }}; User.{{ attr }} = {{ current_user[attr]|tojson }};
{% endfor %} {% endfor %}