merge ldap user and db user, cleanup, add password change

This commit is contained in:
TuxCoder 2020-05-27 17:56:10 +02:00
parent 6334c993a9
commit 4150853588
14 changed files with 280 additions and 259 deletions

View file

@ -40,7 +40,7 @@ def init_app(name=None):
model.ldap_conn = app.ldap_conn
model.base_dn = app.config['LDAP_BASE_DN']
from .model_db import db
from .model import db
db.init_app(app)
with app.app_context():
db.create_all()

View file

@ -1,9 +1,14 @@
from flask import current_app
from .form.auth import PasswordForm, TotpForm, Fido2Form
from ldap3 import Server, Connection
from ldap3 import Server, Connection, HASHED_SALTED_SHA256
from ldap3.core.exceptions import LDAPException
from .model import User
import logging
logger = logging.getLogger(__name__)
import pyotp
class AuthProvider:
@ -31,8 +36,9 @@ class LdapAuthProvider(AuthProvider):
return PasswordForm(prefix='password')
@staticmethod
def check_auth(user, form):
return LdapAuthProvider.check_auth_internal(user, form.data['password'])
def check_auth(user: User, form):
return LdapAuthProvider.check_auth_internal(
user, form.data['password'])
@staticmethod
def check_auth_internal(user, password):

View file

@ -35,9 +35,9 @@ class TOTPDeleteForm(FlaskForm):
class PasswordChangeForm(FlaskForm):
old_password = PasswordField(gettext('Old Password'), validators=[DataRequired()])
password = PasswordField(gettext('New Password'), validators=[DataRequired()])
password_repeat = PasswordField(gettext('Repeat Password'), validators=[DataRequired(),EqualTo('password')])
password_old = PasswordField(gettext('Old Password'), validators=[DataRequired()])
password_new = PasswordField(gettext('New Password'), validators=[DataRequired()])
password_repeat = PasswordField(gettext('Repeat Password'), validators=[DataRequired(),EqualTo('password_new')])
submit = SubmitField(gettext('Change Password'))
class OidcAuthenticationConfirm(FlaskForm):

View file

@ -1,8 +1,9 @@
from flask import current_app
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
from ldap3_orm import Reader
from ldap3 import Entry
from ldap3 import Entry, HASHED_SALTED_SHA256
from ldap3.utils.conv import escape_filter_chars
from ldap3.utils.hashed import hashed
from flask_login import UserMixin
from ldap3.core.exceptions import LDAPSessionTerminatedByServerError
from cryptography.hazmat.primitives import hashes
@ -12,10 +13,21 @@ from datetime import datetime
from dateutil import tz
import pyotp
import json
import logging
import crypt
from flask_sqlalchemy import SQLAlchemy, orm
from datetime import datetime
import uuid
import pyotp
logger = logging.getLogger(__name__)
ldap_conn = None # type: Connection
base_dn = ''
db = SQLAlchemy() # type: SQLAlchemy
class SecurityUser(UserMixin):
@ -35,16 +47,18 @@ class LambdaStr:
return self.lam()
class EntryBase(object):
class EntryBase(db.Model):
__abstract__ = True # for sqlalchemy
_type = None # will get replaced by the local type
_query_object = None # will get replaced by the local type
_base_dn = LambdaStr(lambda: base_dn)
def __init__(self, ldap_object=None, **kwargs):
if ldap_object is None:
self._ldap_object = self.get_type()(**kwargs)
else:
self._ldap_object = ldap_object
# def __init__(self, ldap_object=None, **kwargs):
# if ldap_object is None:
# self._ldap_object = self.get_type()(**kwargs)
# else:
# self._ldap_object = ldap_object
def __str__(self):
return str(self._ldap_object)
@ -64,14 +78,17 @@ class EntryBase(object):
return cls._type
def commit(self):
self._ldap_object.entry_commit_changes()
def add(self):
print(self._ldap_object.entry_attributes_as_dict)
ret = ldap_conn.add(
self.dn, self.object_classes, self._ldap_object.entry_attributes_as_dict)
print(ret)
logger.debug(ret)
pass
@classmethod
def query(cls):
def query_(cls):
if cls._query_object is None:
cls._query_object = cls._query(cls)
return cls._query_object
@ -188,77 +205,26 @@ class Certificate(object):
return f'Certificate(cn={self._cn}, ca_name={self._ca_name}, not_valid_before={self.not_valid_before}, not_valid_after={self.not_valid_after})'
class Totp(object):
def __init__(self, name, secret, created_at=datetime.now()):
self._secret = secret
self._name = name
self._created_at = created_at
@property
def name(self):
return self._name
@property
def created_at(self):
return self._created_at
def verify(self, token: str):
totp = pyotp.TOTP(self._secret)
return totp.verify(token)
def to_dict(self):
return {
'secret': self._secret,
'name': self._name,
'created_at': int(self._created_at.timestamp())}
@staticmethod
def from_dict(data):
return Totp(
name=data['name'],
secret=data['secret'],
created_at=datetime.fromtimestamp(data['created_at']))
class TotpList(MutableSequence):
def __init__(self, ldap_attr):
super().__init__()
self._ldap_attr = ldap_attr
def __getitem__(self, ii):
return Totp.from_dict(json.loads(self._ldap_attr[ii]))
def __setitem__(self, ii, val: Totp):
self._ldap_attr[ii] = json.dumps(val.to_dict()).encode()
def __len__(self):
return len(self._ldap_attr)
def __delitem__(self, ii):
del self._ldap_attr[ii]
def delete(self, totp_name):
for i in range(len(self)):
if self[i].name == totp_name:
self._ldap_attr.delete(self._ldap_attr[i])
def insert(self, ii, val):
self.append(val)
def append(self, val):
self._ldap_attr.add(json.dumps(val.to_dict()).encode())
def generate_uuid():
return str(uuid.uuid4())
class User(EntryBase):
id = db.Column(
db.String(length=36), primary_key=True, default=generate_uuid)
username = db.Column(
db.String, unique=True)
totps = db.relationship('Totp', back_populates='user')
dn = "uid={uid},{base_dn}"
base_dn = "ou=users,{_base_dn}"
object_classes = ["top", "inetOrgPerson", "LenticularUser"]
def __init__(self, ldap_object=None, **kwargs):
super().__init__(ldap_object, **kwargs)
self._totp_list = TotpList(ldap_object.totpSecret)
def __init__(self,**kwargs):
self._ldap_object = None
super(db.Model).__init__(**kwargs)
@property
def is_authenticated(self):
@ -269,24 +235,11 @@ class User(EntryBase):
def make_writeable(self):
self._ldap_object = self._ldap_object.entry_writable()
self._totp_list = TotpList(self._ldap_object.totpSecret)
@property
def entry_dn(self):
return self._ldap_object.entry_dn
@property
def username(self):
return self._ldap_object.uid
@username.setter
def username(self, value):
self._ldap_object.uid = value
@property
def userPassword(self):
return self._ldap_object.userPassword
@property
def fullname(self):
return self._ldap_object.fullname
@ -315,14 +268,24 @@ class User(EntryBase):
def gpg_public_key(self):
return self._ldap_object.gpgPublicKey
@property
def totps(self):
return self._totp_list
def change_password(self, password_new: str):
self.make_writeable()
password_hashed = crypt.crypt(password_new)
self._ldap_object.userPassword = ('{CRYPT}' + password_hashed).encode()
self.commit()
class _query(EntryBase._query):
def _mapping(self, ldap_object):
return User(ldap_object=ldap_object)
user = User.query.filter(User.username == str(ldap_object.uid)).first()
if user is None:
# migration time
user = User()
user.username = str(ldap_object.uid)
db.session.add(user)
db.session.commit()
user._ldap_object = ldap_object
return user
def by_username(self, username) -> 'User':
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
@ -333,7 +296,23 @@ class User(EntryBase):
class Totp(db.Model):
id = db.Column(db.Integer, primary_key=True)
secret = db.Column(db.String, nullable=False)
name = db.Column(db.String, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
user_id = db.Column(
db.String(length=36),
db.ForeignKey(User.id), nullable=False)
user = db.relationship(User)
def verify(self, token: str):
totp = pyotp.TOTP(self.secret)
return totp.verify(token)
class Group(EntryBase):
__abstract__ = True # for sqlalchemy, disable for now
dn = "cn={cn},{base_dn}"
base_dn = "ou=Users,{_base_dn}"
object_classes = ["top"]

View file

@ -1,5 +1,7 @@
from flask_sqlalchemy import SQLAlchemy, orm
from datetime import datetime
import uuid
import pyotp
db = SQLAlchemy() # type: SQLAlchemy
@ -8,39 +10,26 @@ def generate_uuid():
return str(uuid.uuid4())
class OAuth(db.Model):
token = db.Column(db.Text, primary_key=True)
provider = db.Column(db.Text)
provider_username = db.Column(db.Text)
class User(db.Model):
id = db.Column(db.String(length=36), primary_key=True, default=generate_uuid)
username = db.Column(db.String, unique=True)
id = db.Column(
db.String(length=36), primary_key=True, default=generate_uuid)
username = db.Column(
db.String, unique=True)
totps = db.relationship('Totp', back_populates='user')
class Client(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class AuthzCode(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class AccessToken(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class RefreshToken(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class SubjectIdentifier(db.Model):
key = db.Column(db.Text, primary_key=True)
value = db.Column(db.Text)
class Totp(object):
id = db.Column(db.Integer, primary_key=True)
secret = db.Column(db.String, nullable=False)
name = db.Column(db.String, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
user_id = db.Column(
db.Integer,
db.ForeignKey(User.id), nullable=False)
user = db.relationship(User)
def verify(self, token: str):
totp = pyotp.TOTP(self._secret)
return totp.verify(token)

View file

@ -11,7 +11,6 @@ import logging
import requests
from ..model import User
from ..model_db import User as DbUser
from ..auth_providers import LdapAuthProvider
@ -19,11 +18,15 @@ api_views = Blueprint('api', __name__, url_prefix='/api')
@api_views.route('/userinfo', methods=['GET', 'POST'])
def userinfo():
if 'authorization' not in request.headers:
return 'not token found', 400
token = request.headers['authorization'].replace('Bearer ', '')
token_info = current_app.hydra_api.introspect_o_auth2_token(token=token)
if not token_info.active:
return 'token not valid', 403
user_db = DbUser.query.get(token_info.sub)
user = User.query().by_username(user_db.username)
user_db = User.query.get(token_info.sub)
user = User.query_().by_username(user_db.username)
public_url = current_app.config.get('HYDRA_PUBLIC_URL')
r = requests.get(
@ -50,5 +53,6 @@ def user_list():
if 'lc_i_userlist' not in token_info.scope.split(' '):
return '', 403
return jsonify([{'username': str(user.username), 'email': str(user.email)}
for user in User.query().all()])
return jsonify([
{'username': str(user.username), 'email': str(user.email)}
for user in User.query_().all()])

View file

@ -14,8 +14,7 @@ from urllib.parse import urlparse
from base64 import b64decode, b64encode
import http
from ..model import User, SecurityUser
from ..model_db import db, User as DbUser
from ..model import db, User, SecurityUser
from ..form.auth import ConsentForm, LoginForm
from ..auth_providers import AUTH_PROVIDER_LIST
@ -29,7 +28,7 @@ def consent():
# DUMMPY ONLY
form = ConsentForm()
remember_for = 60*60*24*7 # remember for 7 days
remember_for = 60*60*24*30 # remember for 7 days
consent_request = current_app.hydra_api.get_consent_request(
request.args['consent_challenge'])
@ -42,7 +41,7 @@ def consent():
'grant_scope': requested_scope,
'grant_access_token_audience': requested_audiences,
'remember': form.data['remember'],
# 'remember_for': remember_for,
'remember_for': remember_for,
})
return redirect(resp.redirect_to)
return render_template(
@ -65,7 +64,7 @@ def login():
return redirect(resp.redirect_to)
form = LoginForm()
if form.validate_on_submit():
user = User.query().by_username(form.data['name'])
user = User.query_().by_username(form.data['name'])
if user:
session['username'] = str(user.username)
else:
@ -83,7 +82,7 @@ def login_auth():
if 'username' not in session:
return redirect(url_for('auth.login'))
auth_forms = {}
user = User.query().by_username(session['username'])
user = User.query_().by_username(session['username'])
for auth_provider in AUTH_PROVIDER_LIST:
form = auth_provider.get_form()
if auth_provider.get_name() not in session['auth_providers'] and\
@ -95,31 +94,18 @@ def login_auth():
if len(session['auth_providers']) >= 2:
remember_me = True
db_user = DbUser.query.filter(DbUser.username == session['username']).one_or_none()
if db_user is None:
db_user = DbUser(username=session['username'])
db.session.add(db_user)
db.session.commit()
# if db_user is None:
# db_user = User(username=session['username'])
# db.session.add(db_user)
# db.session.commit()
subject = db_user.id
subject = user.id
resp = current_app.hydra_api.accept_login_request(
login_challenge, body={
'subject': subject,
'remember': remember_me})
return redirect(resp.redirect_to)
login_user(SecurityUser(session['username']))
# TODO use this var
_next = None
try:
_next_url = urlparse(b64decode(request.args.get('next')).decode())
_host_url = urlparse(request.url)
if _next_url.scheme == _host_url.scheme and _next_url.netloc == _host_url.netloc :
_next = _next_url.geturl()
except TypeError:
_next = None
return redirect(_next or url_for('frontend.index'))
return render_template('auth/login_auth.html.j2', forms=auth_forms)
@ -128,7 +114,7 @@ def logout():
logout_challenge = request.args.get('logout_challenge')
logout_request = current_app.hydra_api.get_logout_request(logout_challenge)
resp = current_app.hydra_api.accept_logout_request(logout_challenge)
logout_user()
# TODO confirm
return redirect(resp.redirect_to)

View file

@ -1,46 +1,47 @@
from urllib.parse import urlencode, parse_qs
import flask
from flask import Blueprint, redirect
from flask import current_app, session
from flask import jsonify, send_file
from flask.helpers import make_response
from flask.templating import render_template
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
from pyop.access_token import AccessToken, BearerTokenError
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for, flash
from flask_login import login_required, login_user, logout_user, current_user
from flask import current_app
from flask import jsonify
from flask import render_template, url_for, flash
from flask_login import login_user, logout_user, current_user
from werkzeug.utils import redirect
import logging
from datetime import timedelta
import pyotp
from base64 import b64decode, b64encode
from base64 import b64decode
from flask_dance.consumer import oauth_authorized
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.consumer import OAuth2ConsumerBlueprint
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
from ..model import User, SecurityUser, Totp
from ..model_db import OAuth, db, User as DbUser
from ..form.frontend import ClientCertForm, TOTPForm, TOTPDeleteForm, PasswordChangeForm
from ..auth_providers import AUTH_PROVIDER_LIST
from ..model import db, User, SecurityUser, Totp
from ..form.frontend import ClientCertForm, TOTPForm, \
TOTPDeleteForm, PasswordChangeForm
from ..auth_providers import LdapAuthProvider
frontend_views = Blueprint('frontend', __name__, url_prefix='')
logger = logging.getLogger(__name__)
def before_request():
try:
resp = current_app.oauth.session.get('/userinfo')
if not current_user.is_authenticated:
return redirect(url_for('oauth.login'))
except TokenExpiredError:
return redirect(url_for('oauth.login'))
frontend_views.before_request(before_request)
def init_login_manager(app):
@app.login_manager.user_loader
def user_loader(username):
return User.query().by_username(username)
return User.query_().by_username(username)
@app.login_manager.request_loader
def request_loader(request):
def request_loader(_request):
pass
@app.login_manager.unauthorized_handler
@ -75,25 +76,9 @@ def init_login_manager(app):
oauth_info = resp.json()
db_user = DbUser.query.get(str(oauth_info["sub"]))
oauth_username = db_user.username
db_user = User.query.get(str(oauth_info["sub"]))
# Find this OAuth token in the database, or create it
query = OAuth.query.filter_by(
provider=blueprint.name,
provider_username=oauth_username,
)
try:
oauth = query.one()
except NoResultFound:
oauth = OAuth(
provider=blueprint.name,
provider_username=oauth_username,
token=token,
)
login_user(SecurityUser(oauth.provider_username))
login_user(SecurityUser(db_user.username))
#flash("Successfully signed in with GitHub.")
# Since we're manually creating the OAuth model in the database,
@ -106,27 +91,29 @@ def init_login_manager(app):
@frontend_views.route('/logout')
def logout():
logout_user()
return redirect(f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout')
return redirect(
f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout')
@frontend_views.route('/', methods=['GET'])
@login_required
def index():
return render_template('frontend/index.html.j2')
@frontend_views.route('/client_cert')
@login_required
def client_cert():
client_certs = {}
for service in current_app.lenticular_services.values():
client_certs[str(service.name)] = current_app.pki.get_client_certs(current_user, service)
client_certs[str(service.name)] = \
current_app.pki.get_client_certs(current_user, service)
return render_template('frontend/client_cert.html.j2', services=current_app.lenticular_services, client_certs=client_certs)
return render_template(
'frontend/client_cert.html.j2',
services=current_app.lenticular_services,
client_certs=client_certs)
@frontend_views.route('/client_cert/<service_name>/<serial_number>')
@login_required
def get_client_cert(service_name, serial_number):
service = current_app.lenticular_services[service_name]
cert = current_app.pki.get_client_cert(
@ -137,8 +124,8 @@ def get_client_cert(service_name, serial_number):
})
@frontend_views.route('/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
@login_required
@frontend_views.route(
'/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
def revoke_client_cert(service_name, serial_number):
service = current_app.lenticular_services[service_name]
cert = current_app.pki.get_client_cert(
@ -150,7 +137,6 @@ def revoke_client_cert(service_name, serial_number):
@frontend_views.route(
'/client_cert/<service_name>/new',
methods=['GET', 'POST'])
@login_required
def client_cert_new(service_name):
service = current_app.lenticular_services[service_name]
form = ClientCertForm()
@ -180,23 +166,20 @@ def client_cert_new(service_name):
@frontend_views.route('/totp')
@login_required
def totp():
delete_form = TOTPDeleteForm()
return render_template('frontend/totp.html.j2', delete_form=delete_form)
@frontend_views.route('/totp/new', methods=['GET', 'POST'])
@login_required
def totp_new():
form = TOTPForm()
if form.validate_on_submit():
totp = Totp(name=form.data['name'], secret=form.data['secret'])
if totp.verify(form.data['token']):
current_user.make_writeable()
current_user.totps.append(totp)
current_user._ldap_object.entry_commit_changes()
db.session.commit()
return jsonify({
'status': 'ok'})
else:
@ -208,47 +191,55 @@ def totp_new():
return render_template('frontend/totp_new.html.j2', form=form)
@frontend_views.route('/totp/<totp_name>/delete', methods=['GET','POST'])
@login_required
@frontend_views.route('/totp/<totp_name>/delete', methods=['GET', 'POST'])
def totp_delete(totp_name):
current_user.make_writeable()
current_user.totps.delete(totp_name)
current_user._ldap_object.entry_commit_changes()
totp = Totp.query.filter(Totp.name == totp_name).first()
db.session.delete(totp)
db.session.commit()
return jsonify({
'status': 'ok'})
@frontend_views.route('/password_change')
@login_required
def password_change():
form = PasswordChangeForm()
return render_template('frontend/password_change.html.j2', form=form)
@frontend_views.route('/password_change', methods=['POST'])
@login_required
def password_change_post():
form = PasswordChangeForm()
if form.validate():
return jsonify({})
password_old = str(form.data['password_old'])
password_new = str(form.data['password_new'])
if not LdapAuthProvider.check_auth_internal(
current_user, password_old):
return jsonify(
{'errors': {'password_old': 'Old Password is invalid'}})
resp = current_user.change_password(password_new)
if resp:
print(current_user)
return jsonify({})
else:
return jsonify({'errors': {'internal': 'internal server errror'}})
return jsonify({'errors': form.errors})
@frontend_views.route('/oauth2_token')
@login_required
def oauth2_tokens():
subject = current_app.oauth.session.get('/userinfo').json()['sub']
consent_sessions = current_app.hydra_api.list_subject_consent_sessions(
subject)
return render_template('frontend/oauth2_tokens.html.j2', consent_sessions=consent_sessions)
print(consent_sessions)
return render_template(
'frontend/oauth2_tokens.html.j2',
consent_sessions=consent_sessions)
@frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE'])
@login_required
def oauth2_token_revoke(client_id: str):
subject = current_app.oauth.session.get('/userinfo').json()['sub']
current_app.hydra_api.revoke_consent_sessions(