blacken
parent
fb48a67ac1
commit
bd6ec1ddc3
223
auth.py
223
auth.py
|
@ -8,8 +8,13 @@ from cached_property import cached_property
|
||||||
import flask
|
import flask
|
||||||
from flask import Flask, render_template, make_response, flash, redirect, url_for
|
from flask import Flask, render_template, make_response, flash, redirect, url_for
|
||||||
from flask_oauthlib.provider import OAuth2Provider
|
from flask_oauthlib.provider import OAuth2Provider
|
||||||
from flask_login import LoginManager, login_user, logout_user, \
|
from flask_login import (
|
||||||
login_required, current_user
|
LoginManager,
|
||||||
|
login_user,
|
||||||
|
logout_user,
|
||||||
|
login_required,
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, BooleanField
|
from wtforms import StringField, PasswordField, BooleanField
|
||||||
|
@ -17,14 +22,17 @@ from wtforms.validators import DataRequired
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)-15s] %(name)-10s %(levelname)7s: %(message)s')
|
logging.basicConfig(
|
||||||
app = Flask('auth')
|
level=logging.DEBUG,
|
||||||
|
format="[%(asctime)-15s] %(name)-10s %(levelname)7s: %(message)s",
|
||||||
|
)
|
||||||
|
app = Flask("auth")
|
||||||
app.config.from_object(__name__)
|
app.config.from_object(__name__)
|
||||||
app.config.from_pyfile('auth.cfg')
|
app.config.from_pyfile("auth.cfg")
|
||||||
oauth = OAuth2Provider(app)
|
oauth = OAuth2Provider(app)
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
login_manager.login_view = '/login'
|
login_manager.login_view = "/login"
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,8 +43,7 @@ class Client(db.Model):
|
||||||
description = db.Column(db.String(400))
|
description = db.Column(db.String(400))
|
||||||
|
|
||||||
client_id = db.Column(db.String(40), primary_key=True)
|
client_id = db.Column(db.String(40), primary_key=True)
|
||||||
client_secret = db.Column(db.String(55), unique=True, index=True,
|
client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False)
|
||||||
nullable=False)
|
|
||||||
|
|
||||||
# public or confidential
|
# public or confidential
|
||||||
is_confidential = db.Column(db.Boolean)
|
is_confidential = db.Column(db.Boolean)
|
||||||
|
@ -51,8 +58,8 @@ class Client(db.Model):
|
||||||
@property
|
@property
|
||||||
def client_type(self):
|
def client_type(self):
|
||||||
if self.is_confidential:
|
if self.is_confidential:
|
||||||
return 'confidential'
|
return "confidential"
|
||||||
return 'public'
|
return "public"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def redirect_uris(self):
|
def redirect_uris(self):
|
||||||
|
@ -71,8 +78,12 @@ class Client(db.Model):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def validate_scopes(self, scopes):
|
def validate_scopes(self, scopes):
|
||||||
return {'profile:read', 'profile:write', 'password:write', \
|
return {
|
||||||
'users:read'}.issuperset(scopes)
|
"profile:read",
|
||||||
|
"profile:write",
|
||||||
|
"password:write",
|
||||||
|
"users:read",
|
||||||
|
}.issuperset(scopes)
|
||||||
|
|
||||||
|
|
||||||
class Grant(db.Model):
|
class Grant(db.Model):
|
||||||
|
@ -81,10 +92,9 @@ class Grant(db.Model):
|
||||||
user = db.Column(db.String(40), nullable=False)
|
user = db.Column(db.String(40), nullable=False)
|
||||||
|
|
||||||
client_id = db.Column(
|
client_id = db.Column(
|
||||||
db.String(40), db.ForeignKey('client.client_id'),
|
db.String(40), db.ForeignKey("client.client_id"), nullable=False
|
||||||
nullable=False,
|
|
||||||
)
|
)
|
||||||
client = db.relationship('Client')
|
client = db.relationship("Client")
|
||||||
|
|
||||||
code = db.Column(db.String(255), index=True, nullable=False)
|
code = db.Column(db.String(255), index=True, nullable=False)
|
||||||
|
|
||||||
|
@ -108,14 +118,11 @@ class Grant(db.Model):
|
||||||
class Token(db.Model):
|
class Token(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
client_id = db.Column(
|
client_id = db.Column(
|
||||||
db.String(40), db.ForeignKey('client.client_id'),
|
db.String(40), db.ForeignKey("client.client_id"), nullable=False
|
||||||
nullable=False,
|
|
||||||
)
|
)
|
||||||
client = db.relationship('Client')
|
client = db.relationship("Client")
|
||||||
|
|
||||||
user = db.Column(
|
user = db.Column(db.String(40), nullable=False)
|
||||||
db.String(40), nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# currently only bearer is supported
|
# currently only bearer is supported
|
||||||
token_type = db.Column(db.String(40))
|
token_type = db.Column(db.String(40))
|
||||||
|
@ -138,22 +145,22 @@ class Token(db.Model):
|
||||||
|
|
||||||
|
|
||||||
def connect_to_ldap():
|
def connect_to_ldap():
|
||||||
conn = ldap.initialize(app.config['LDAP_URL'])
|
conn = ldap.initialize(app.config["LDAP_URL"])
|
||||||
conn.start_tls_s()
|
conn.start_tls_s()
|
||||||
conn.simple_bind(app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PASSWORD'])
|
conn.simple_bind(app.config["LDAP_BIND_DN"], app.config["LDAP_BIND_PASSWORD"])
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
def check_credentials(username, password):
|
def check_credentials(username, password):
|
||||||
conn = ldap.initialize(app.config['LDAP_URL'])
|
conn = ldap.initialize(app.config["LDAP_URL"])
|
||||||
conn.start_tls_s()
|
conn.start_tls_s()
|
||||||
try:
|
try:
|
||||||
conn.simple_bind_s(app.config['DN_STRING'] % username, password)
|
conn.simple_bind_s(app.config["DN_STRING"] % username, password)
|
||||||
return True
|
return True
|
||||||
except ldap.LDAPError:
|
except ldap.LDAPError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@oauth.clientgetter
|
@oauth.clientgetter
|
||||||
def load_client(client_id):
|
def load_client(client_id):
|
||||||
return Client.query.filter_by(client_id=client_id).first()
|
return Client.query.filter_by(client_id=client_id).first()
|
||||||
|
@ -170,11 +177,11 @@ def save_grant(client_id, code, request, *args, **kwargs):
|
||||||
expires = datetime.utcnow() + timedelta(seconds=100)
|
expires = datetime.utcnow() + timedelta(seconds=100)
|
||||||
grant = Grant(
|
grant = Grant(
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
code=code['code'],
|
code=code["code"],
|
||||||
redirect_uri=request.redirect_uri,
|
redirect_uri=request.redirect_uri,
|
||||||
_scopes=' '.join(request.scopes),
|
_scopes=" ".join(request.scopes),
|
||||||
user=current_user.username,
|
user=current_user.username,
|
||||||
expires=expires
|
expires=expires,
|
||||||
)
|
)
|
||||||
db.session.add(grant)
|
db.session.add(grant)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -191,20 +198,19 @@ def load_token(access_token=None, refresh_token=None):
|
||||||
|
|
||||||
@oauth.tokensetter
|
@oauth.tokensetter
|
||||||
def save_token(token, request, *args, **kwargs):
|
def save_token(token, request, *args, **kwargs):
|
||||||
toks = Token.query.filter_by(client_id=request.client.client_id,
|
toks = Token.query.filter_by(client_id=request.client.client_id, user=request.user)
|
||||||
user=request.user)
|
|
||||||
# make sure that every client has only one token connected to a user
|
# make sure that every client has only one token connected to a user
|
||||||
for t in toks:
|
for t in toks:
|
||||||
db.session.delete(t)
|
db.session.delete(t)
|
||||||
|
|
||||||
expires_in = token.get('expires_in')
|
expires_in = token.get("expires_in")
|
||||||
expires = datetime.utcnow() + timedelta(seconds=expires_in)
|
expires = datetime.utcnow() + timedelta(seconds=expires_in)
|
||||||
|
|
||||||
tok = Token(
|
tok = Token(
|
||||||
access_token=token['access_token'],
|
access_token=token["access_token"],
|
||||||
refresh_token=token.get('refresh_token'),
|
refresh_token=token.get("refresh_token"),
|
||||||
token_type=token['token_type'],
|
token_type=token["token_type"],
|
||||||
_scopes=token['scope'],
|
_scopes=token["scope"],
|
||||||
expires=expires,
|
expires=expires,
|
||||||
client_id=request.client.client_id,
|
client_id=request.client.client_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
@ -214,31 +220,31 @@ def save_token(token, request, *args, **kwargs):
|
||||||
return tok
|
return tok
|
||||||
|
|
||||||
|
|
||||||
@app.route('/oauth/authorize', methods=['GET', 'POST'])
|
@app.route("/oauth/authorize", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@oauth.authorize_handler
|
@oauth.authorize_handler
|
||||||
def authorize(*args, **kwargs):
|
def authorize(*args, **kwargs):
|
||||||
form = FlaskForm()
|
form = FlaskForm()
|
||||||
|
|
||||||
if Token.query.filter(
|
if Token.query.filter(
|
||||||
Token.client_id == kwargs.get('client_id'),
|
Token.client_id == kwargs.get("client_id"), Token.user == current_user.username
|
||||||
Token.user == current_user.username).count():
|
).count():
|
||||||
# User has unrevoked token already - grant by default
|
# User has unrevoked token already - grant by default
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if not form.validate_on_submit():
|
if not form.validate_on_submit():
|
||||||
client_id = kwargs.get('client_id')
|
client_id = kwargs.get("client_id")
|
||||||
client = Client.query.filter_by(client_id=client_id).first()
|
client = Client.query.filter_by(client_id=client_id).first()
|
||||||
kwargs['client'] = client
|
kwargs["client"] = client
|
||||||
kwargs['user'] = current_user
|
kwargs["user"] = current_user
|
||||||
kwargs['form'] = form
|
kwargs["form"] = form
|
||||||
return render_template('oauthorize.html', **kwargs)
|
return render_template("oauthorize.html", **kwargs)
|
||||||
|
|
||||||
confirm = flask.request.form.get('confirm', 'no')
|
confirm = flask.request.form.get("confirm", "no")
|
||||||
return confirm == 'yes'
|
return confirm == "yes"
|
||||||
|
|
||||||
|
|
||||||
@app.route('/oauth/token', methods=['GET', 'POST'])
|
@app.route("/oauth/token", methods=["GET", "POST"])
|
||||||
@oauth.token_handler
|
@oauth.token_handler
|
||||||
def access_token():
|
def access_token():
|
||||||
return None
|
return None
|
||||||
|
@ -246,37 +252,39 @@ def access_token():
|
||||||
|
|
||||||
class LDAPUserProxy(object):
|
class LDAPUserProxy(object):
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
self.username = re.sub(app.config['STRIP_RE'], '', username)
|
self.username = re.sub(app.config["STRIP_RE"], "", username)
|
||||||
self.is_authenticated = True
|
self.is_authenticated = True
|
||||||
self.is_anonymous = False
|
self.is_anonymous = False
|
||||||
conn = connect_to_ldap()
|
conn = connect_to_ldap()
|
||||||
res = conn.search_s(app.config['PEOPLE_BASEDN'], ldap.SCOPE_SUBTREE,
|
res = conn.search_s(
|
||||||
app.config['UID_LDAP_FILTER'] % self.username)
|
app.config["PEOPLE_BASEDN"],
|
||||||
|
ldap.SCOPE_SUBTREE,
|
||||||
|
app.config["UID_LDAP_FILTER"] % self.username,
|
||||||
|
)
|
||||||
if len(res) != 1:
|
if len(res) != 1:
|
||||||
raise Exception('No such username.')
|
raise Exception("No such username.")
|
||||||
dn, data = res[0]
|
dn, data = res[0]
|
||||||
|
|
||||||
self.username = data.get('uid', [None,])[0]
|
self.username = data.get("uid", [None])[0]
|
||||||
self.gecos = data.get('gecos', [None, ])[0]
|
self.gecos = data.get("gecos", [None])[0]
|
||||||
self.mifare_hashes = data.get('mifareIDHash', [])
|
self.mifare_hashes = data.get("mifareIDHash", [])
|
||||||
self.phone = data.get('mobile', [None, ])[0]
|
self.phone = data.get("mobile", [None])[0]
|
||||||
self.personal_email = data.get('mailRoutingAddress', [])
|
self.personal_email = data.get("mailRoutingAddress", [])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
active = 'active' if self.is_active else 'inactive'
|
active = "active" if self.is_active else "inactive"
|
||||||
return '<LDAPUserProxy {}, {}>'.format(self.username, active)
|
return "<LDAPUserProxy {}, {}>".format(self.username, active)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
return self.username + '@hackerspace.pl'
|
return self.username + "@hackerspace.pl"
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
url = 'https://kasownik.hackerspace.pl/api/judgement/{}.json'
|
url = "https://kasownik.hackerspace.pl/api/judgement/{}.json"
|
||||||
try:
|
try:
|
||||||
r = requests.get(url.format(self.username))
|
r = requests.get(url.format(self.username))
|
||||||
return bool(r.json()['content'])
|
return bool(r.json()["content"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("When getting data from Kasownik: {}".format(e))
|
logging.error("When getting data from Kasownik: {}".format(e))
|
||||||
# Fail-safe.
|
# Fail-safe.
|
||||||
|
@ -284,10 +292,10 @@ class LDAPUserProxy(object):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_staff(self):
|
def is_staff(self):
|
||||||
url = 'https://capacifier.hackerspace.pl/staff/{}'
|
url = "https://capacifier.hackerspace.pl/staff/{}"
|
||||||
try:
|
try:
|
||||||
r = requests.get(url.format(self.username))
|
r = requests.get(url.format(self.username))
|
||||||
return 'YES' in r.text
|
return "YES" in r.text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("When getting data from Capacifier: {}".format(e))
|
logging.error("When getting data from Capacifier: {}".format(e))
|
||||||
return False
|
return False
|
||||||
|
@ -297,53 +305,58 @@ class LDAPUserProxy(object):
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
username = StringField('username', validators=[DataRequired()])
|
username = StringField("username", validators=[DataRequired()])
|
||||||
password = PasswordField('password', validators=[DataRequired()])
|
password = PasswordField("password", validators=[DataRequired()])
|
||||||
remember = BooleanField('remember me')
|
remember = BooleanField("remember me")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route("/")
|
||||||
@app.route('/profile')
|
@app.route("/profile")
|
||||||
@login_required
|
@login_required
|
||||||
def profile():
|
def profile():
|
||||||
return render_template('profile.html', tokens=Token.query.filter(Token.user == current_user.username))
|
return render_template(
|
||||||
|
"profile.html", tokens=Token.query.filter(Token.user == current_user.username)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/token/<int:id>/revoke', methods=['POST'])
|
@app.route("/token/<int:id>/revoke", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def token_revoke(id):
|
def token_revoke(id):
|
||||||
token = Token.query.filter(Token.user == current_user.username, Token.id == id).first()
|
token = Token.query.filter(
|
||||||
|
Token.user == current_user.username, Token.id == id
|
||||||
|
).first()
|
||||||
if not token:
|
if not token:
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
token.delete()
|
token.delete()
|
||||||
return redirect('/')
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
next = flask.request.args.get('next')
|
next = flask.request.args.get("next")
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
username, password = form.data['username'], form.data['password']
|
username, password = form.data["username"], form.data["password"]
|
||||||
if not check_credentials(username, password):
|
if not check_credentials(username, password):
|
||||||
flash('Invalid username or password')
|
flash("Invalid username or password")
|
||||||
return render_template('login_oauth.html', form=form, next=next)
|
return render_template("login_oauth.html", form=form, next=next)
|
||||||
user = LDAPUserProxy(username)
|
user = LDAPUserProxy(username)
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
flash('User inactive - have you paid your membership fees?')
|
flash("User inactive - have you paid your membership fees?")
|
||||||
return render_template('login_oauth.html', form=form, next=next)
|
return render_template("login_oauth.html", form=form, next=next)
|
||||||
|
|
||||||
login_user(user, form.data['remember'])
|
login_user(user, form.data["remember"])
|
||||||
flash('Logged in successfully.')
|
flash("Logged in successfully.")
|
||||||
|
|
||||||
return redirect(next or url_for('profile'))
|
return redirect(next or url_for("profile"))
|
||||||
|
|
||||||
return render_template('login_oauth.html', form=form, next=next)
|
return render_template("login_oauth.html", form=form, next=next)
|
||||||
|
|
||||||
@app.route('/logout')
|
|
||||||
|
@app.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect('/')
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
|
@ -352,29 +365,39 @@ def load_user(user_id):
|
||||||
|
|
||||||
|
|
||||||
# HSWAW specific endpoint
|
# HSWAW specific endpoint
|
||||||
@app.route('/api/profile')
|
@app.route("/api/profile")
|
||||||
@app.route('/api/1/profile')
|
@app.route("/api/1/profile")
|
||||||
@oauth.require_oauth('profile:read')
|
@oauth.require_oauth("profile:read")
|
||||||
def api_profile():
|
def api_profile():
|
||||||
user = LDAPUserProxy(flask.request.oauth.user)
|
user = LDAPUserProxy(flask.request.oauth.user)
|
||||||
return flask.jsonify(
|
return flask.jsonify(
|
||||||
email=user.email, username=user.username,
|
email=user.email,
|
||||||
gecos=user.gecos, phone=user.phone,
|
username=user.username,
|
||||||
personal_email=user.personal_email)
|
gecos=user.gecos,
|
||||||
|
phone=user.phone,
|
||||||
|
personal_email=user.personal_email,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# OpenIDConnect userinfo
|
# OpenIDConnect userinfo
|
||||||
@app.route('/api/1/userinfo')
|
@app.route("/api/1/userinfo")
|
||||||
@oauth.require_oauth('profile:read')
|
@oauth.require_oauth("profile:read")
|
||||||
def api_userinfo():
|
def api_userinfo():
|
||||||
user = LDAPUserProxy(flask.request.oauth.user)
|
user = LDAPUserProxy(flask.request.oauth.user)
|
||||||
groups = []
|
groups = []
|
||||||
if user.is_staff:
|
if user.is_staff:
|
||||||
groups.append('staff')
|
groups.append("staff")
|
||||||
return flask.jsonify(sub=user.username, name=user.gecos, email=user.email,
|
return flask.jsonify(
|
||||||
preferred_username=user.username, nickname=user.username,
|
sub=user.username,
|
||||||
user_name=user.username, user_id=user.username, groups=groups)
|
name=user.gecos,
|
||||||
|
email=user.email,
|
||||||
|
preferred_username=user.username,
|
||||||
|
nickname=user.username,
|
||||||
|
user_name=user.username,
|
||||||
|
user_id=user.username,
|
||||||
|
groups=groups,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
app.run('0.0.0.0', 8082, debug=True)
|
app.run("0.0.0.0", 8082, debug=True)
|
||||||
|
|
Loading…
Reference in New Issue