diff --git a/install.sh b/install.sh index ab194e2..8edca84 100755 --- a/install.sh +++ b/install.sh @@ -17,15 +17,15 @@ if ! type "add-apt-repository" > /dev/null; then fi # Elastic Search -wget -qO - http://packages.elasticsearch.org/GPG-KEY-elasticsearch | sudo apt-key add - -echo 'deb http://packages.elasticsearch.org/elasticsearch/1.4/debian stable main' | sudo tee /etc/apt/sources.list.d/elastic.list +# wget -qO - http://packages.elasticsearch.org/GPG-KEY-elasticsearch | sudo apt-key add - +# echo 'deb http://packages.elasticsearch.org/elasticsearch/1.4/debian stable main' | sudo tee /etc/apt/sources.list.d/elastic.list sudo add-apt-repository -y ppa:chris-lea/node.js sudo apt-get update sudo apt-get install -y python build-essential pkg-config git \ -python-pip python-virtualenv python-dev zlib1g-dev \ -libffi-dev libyaml-dev libssl-dev nodejs openjdk-7-jre-headless elasticsearch +python-pip python-virtualenv python-dev zlib1g-dev libldap2-dev libsasl2-dev \ +libffi-dev libyaml-dev libssl-dev nodejs # Create swap file because ES eats up RAM and 14.04 doesn't have swap by default sudo fallocate -l 1G /swapfile diff --git a/realms/__init__.py b/realms/__init__.py index 6c6c51a..4b51853 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -15,6 +15,7 @@ from flask.ext.cache import Cache from flask.ext.login import LoginManager, current_user from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.assets import Environment, Bundle +from flask_ldap_login import LDAPLoginManager from werkzeug.routing import BaseConverter from werkzeug.exceptions import HTTPException from sqlalchemy.ext.declarative import declarative_base @@ -163,6 +164,7 @@ def create_app(config=None): cache.init_app(app) assets.init_app(app) search.init_app(app) + ldap.init_app(app) db.Model = declarative_base(metaclass=HookModelMeta, cls=HookMixin) @@ -202,6 +204,7 @@ db = SQLAlchemy() cache = Cache() assets = Assets() search = Search() +ldap = LDAPLoginManager() assets.register('main.js', 'vendor/jquery/dist/jquery.js', diff --git a/realms/config/__init__.py b/realms/config/__init__.py index c9cf115..6895e1c 100644 --- a/realms/config/__init__.py +++ b/realms/config/__init__.py @@ -83,6 +83,38 @@ DB_URI = 'sqlite:////tmp/wiki.db' # DB_URI = 'oracle://scott:tiger@127.0.0.1:1521/sidname' # DB_URI = 'crate://' +LDAP = { + 'URI': 'ldap://localhost:8389', + + # This BIND_DN/BIND_PASSORD default to '', this is shown here for demonstrative purposes + # The values '' perform an anonymous bind so we may use search/bind method + 'BIND_DN': '', + 'BIND_AUTH': '', + + # Adding the USER_SEARCH field tells the flask-ldap-login that we are using + # the search/bind method + 'USER_SEARCH': {'base': 'dc=example,dc=com', 'filter': 'uid=%(username)s'}, + + # Map ldap keys into application specific keys + 'KEY_MAP': { + 'name': 'cn', + 'company': 'o', + 'location': 'l', + 'email': 'mail', + } +} + +OAUTH = { + 'twitter': { + 'key': '', + 'secret': '' + }, + 'github': { + 'key': '', + 'secret': '' + } +} + CACHE_TYPE = 'simple' # Redis @@ -161,4 +193,4 @@ if ENV != "DEV": ASSETS_DEBUG = False SQLALCHEMY_ECHO = False -MODULES = ['wiki', 'search', 'auth', 'auth.local', 'auth.oauth'] +MODULES = ['wiki', 'search', 'auth', 'auth.local', 'auth.oauth', 'auth.ldap'] diff --git a/realms/modules/auth/ldap/__init__.py b/realms/modules/auth/ldap/__init__.py index e69de29..cb45955 100644 --- a/realms/modules/auth/ldap/__init__.py +++ b/realms/modules/auth/ldap/__init__.py @@ -0,0 +1,4 @@ +from flask_ldap_login import LDAPLoginManager + +ldap_mgr = LDAPLoginManager() + diff --git a/realms/modules/auth/ldap/forms.py b/realms/modules/auth/ldap/forms.py new file mode 100644 index 0000000..ddbc54c --- /dev/null +++ b/realms/modules/auth/ldap/forms.py @@ -0,0 +1,7 @@ +from flask_wtf import Form +from wtforms import StringField, PasswordField, validators + + +class LoginForm(Form): + email = StringField('Email', [validators.DataRequired()]) + password = PasswordField('Password', [validators.DataRequired()]) \ No newline at end of file diff --git a/realms/modules/auth/ldap/models.py b/realms/modules/auth/ldap/models.py new file mode 100644 index 0000000..3cd8767 --- /dev/null +++ b/realms/modules/auth/ldap/models.py @@ -0,0 +1,31 @@ +from flask import current_app, render_template +from flask.ext.login import login_user +from realms import ldap +from flask_ldap_login import LDAPLoginForm +from ..models import BaseUser +import bcrypt + +users = {} + +@ldap.save_user +def save_user(username, userdata): + users[username] = User(username, userdata) + return users[username] + +class User(BaseUser): + type = 'ldap' + + def __init__(self, username, data): + self.id = username + self.username = username + self.data = data + + @staticmethod + def login_form(): + form = LDAPLoginForm() + return render_template('auth/ldap/login.html', form=form) + + @staticmethod + def auth(*args): + login_user(args[0].user, remember=True) + return True diff --git a/realms/modules/auth/ldap/views.py b/realms/modules/auth/ldap/views.py new file mode 100644 index 0000000..5d2a32f --- /dev/null +++ b/realms/modules/auth/ldap/views.py @@ -0,0 +1,18 @@ +from flask import current_app, request, redirect, Blueprint, flash, url_for +from ..ldap.models import User +from flask_ldap_login import LDAPLoginForm + +blueprint = Blueprint('auth.ldap', __name__) + +@blueprint.route("/login/ldap", methods=['POST']) +def login(): + form = LDAPLoginForm() + + if not form.validate(): + flash('Form invalid', 'warning') + return redirect(url_for('auth.login')) + + if User.auth(form.user): + return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT'])) + else: + return redirect(url_for('auth.login')) diff --git a/realms/modules/auth/models.py b/realms/modules/auth/models.py index 2c0ed7f..9b4c40f 100644 --- a/realms/modules/auth/models.py +++ b/realms/modules/auth/models.py @@ -19,13 +19,11 @@ class Auth(object): @staticmethod def get_auth_user(auth_type): - print auth_type mod = importlib.import_module('realms.modules.auth.%s.models' % auth_type) return mod.User @staticmethod def load_user(auth_id): - print auth_id auth_type, user_id = auth_id.split("/") return Auth.get_auth_user(auth_type).load_user(user_id) @@ -33,7 +31,7 @@ class Auth(object): def login_forms(): forms = [] # TODO be dynamic - for t in ['local']: + for t in ['local', 'ldap']: forms.append(Auth.get_auth_user(t).login_form()) return forms @@ -87,7 +85,7 @@ class BaseUser(UserMixin): @staticmethod def auth(email, password): - raise NotImplementedError() + raise NotImplementedError @staticmethod def hash_password(password): diff --git a/realms/modules/auth/oauth/models.py b/realms/modules/auth/oauth/models.py index 217e7b1..d67ce18 100644 --- a/realms/modules/auth/oauth/models.py +++ b/realms/modules/auth/oauth/models.py @@ -8,25 +8,31 @@ oauth = OAuth() class OAuthUser(BaseUser): # OAuth remote app - app = None + remote_app = None class TwitterUser(OAuthUser): - app = oauth.remote_app( - 'twitter', - base_url='https://api.twitter.com/1/', - request_token_url='https://api.twitter.com/oauth/request_token', - access_token_url='https://api.twitter.com/oauth/access_token', - authorize_url='https://api.twitter.com/oauth/authenticate', - consumer_key=config.TWITTER_KEY, - consumer_secret=config.TWITTER_SECRET) - def __init__(self, id_, username, email=None): self.id = id_ self.username = username self.email = email + @classmethod + def app(cls): + if cls.remote_app: + return cls.remote_app + + cls.remote_app = oauth.remote_app( + 'twitter', + base_url='https://api.twitter.com/1/', + request_token_url='https://api.twitter.com/oauth/request_token', + access_token_url='https://api.twitter.com/oauth/access_token', + authorize_url='https://api.twitter.com/oauth/authenticate', + consumer_key=config.OAUTH['twitter']['key'], + consumer_secret=config.OAUTH['twitter']['secret']) + return cls.remote_app + @staticmethod def load_user(*args, **kwargs): return TwitterUser(args[0]) diff --git a/realms/modules/auth/oauth/views.py b/realms/modules/auth/oauth/views.py index efe7434..fa2b5cf 100644 --- a/realms/modules/auth/oauth/views.py +++ b/realms/modules/auth/oauth/views.py @@ -1,22 +1,22 @@ -from flask import Blueprint, url_for, request, flash, redirect +from flask import Blueprint, url_for, request, flash, redirect, session from .models import TwitterUser blueprint = Blueprint('auth.oauth', __name__) def oauth_failed(next_url): - flash(u'You denied the request to sign in.') + flash('You denied the request to sign in.') return redirect(next_url) @blueprint.route("/login/twitter") def login_twitter(): - return TwitterUser.app.authorize(callback=url_for('twitter_callback', - next=request.args.get('next') or request.referrer or None)) + return TwitterUser.app().authorize(callback=url_for('twitter_callback', + next=request.args.get('next') or request.referrer or None)) @blueprint.route('/login/twitter/callback') def twitter_callback(): next_url = request.args.get('next') or url_for('index') - resp = TwitterUser.app.authorized_response() + resp = TwitterUser.app().authorized_response() if resp is None: return oauth_failed(next_url) @@ -27,4 +27,4 @@ def twitter_callback(): session['twitter_user'] = resp['screen_name'] flash('You were signed in as %s' % resp['screen_name']) - return redirect(next_url) \ No newline at end of file + return redirect(next_url) diff --git a/realms/templates/auth/ldap/login.html b/realms/templates/auth/ldap/login.html new file mode 100644 index 0000000..045040b --- /dev/null +++ b/realms/templates/auth/ldap/login.html @@ -0,0 +1,21 @@ +{% from 'macros.html' import render_form, render_field %} + + + \ No newline at end of file diff --git a/realms/templates/auth/login.html b/realms/templates/auth/login.html index adde149..ef129b1 100644 --- a/realms/templates/auth/login.html +++ b/realms/templates/auth/login.html @@ -2,5 +2,6 @@ {% block body %} {% for form in forms %} {{ form|safe }} +
{% endfor %} {% endblock %} diff --git a/setup.py b/setup.py index 1812098..193c596 100644 --- a/setup.py +++ b/setup.py @@ -28,12 +28,14 @@ setup(name='realms-wiki', 'Flask-Cache==0.13.1', 'Flask-Elastic==0.2', 'Flask-Login==0.2.11', + 'Flask-OAuthlib==0.9.1', 'Flask-SQLAlchemy==2.0', 'Flask-WTF==0.10.2', 'PyYAML==3.11', 'bcrypt==1.0.2', 'beautifulsoup4==4.3.2', 'click==3.3', + 'flask-ldap-login==0.3.0', 'gevent==1.0.2', 'ghdiff==0.4', 'gittle==0.4.0',