From c6042973fe44c1984ad938378d218f7125e42b7d Mon Sep 17 00:00:00 2001 From: TuxCoder Date: Sat, 18 Jun 2022 19:35:05 +0200 Subject: [PATCH] more ldap migration --- lenticular_cloud/auth_providers.py | 37 +++++++++---------- ...0518a8625b50_remove_ldap_add_rest_to_db.py | 31 +++++++++++++++- lenticular_cloud/model.py | 4 +- lenticular_cloud/views/api.py | 5 +-- lenticular_cloud/views/auth.py | 1 + lenticular_cloud/views/frontend.py | 4 +- lenticular_cloud/views/oauth2.py | 4 +- 7 files changed, 55 insertions(+), 31 deletions(-) diff --git a/lenticular_cloud/auth_providers.py b/lenticular_cloud/auth_providers.py index a662d83..0570ed1 100644 --- a/lenticular_cloud/auth_providers.py +++ b/lenticular_cloud/auth_providers.py @@ -1,7 +1,8 @@ from flask import current_app +from flask_wtf import FlaskForm from .form.auth import PasswordForm, TotpForm, Fido2Form -from ldap3 import Server, Connection, HASHED_SALTED_SHA256 -from ldap3.core.exceptions import LDAPException +from hmac import compare_digest as compare_hash +import crypt from .model import User import logging @@ -17,11 +18,11 @@ class AuthProvider: return csl.__name__ @staticmethod - def get_form(): + def get_form() -> FlaskForm: return @staticmethod - def check_auth(user, form) -> bool: + def check_auth(user: User, form) -> bool: ''' checks the submited form is valid return true if user is allowed to auth @@ -29,30 +30,26 @@ class AuthProvider: return False -class LdapAuthProvider(AuthProvider): +class PasswordAuthProvider(AuthProvider): @staticmethod - def get_form(): + def get_form() -> FlaskForm: return PasswordForm(prefix='password') @staticmethod - def check_auth(user: User, form): - return LdapAuthProvider.check_auth_internal( - user, form.data['password']) - - @staticmethod - def check_auth_internal(user, password): - server = Server(current_app.config['LDAP_URL']) - ldap_conn = Connection(server, user.entry_dn, password) - try: - return ldap_conn.bind() - except LDAPException: + def check_auth(user: User, form: FlaskForm) -> bool: + if isinstance(form.data['password'], str): + return PasswordAuthProvider.check_auth_internal(user, form.data['password']) + else: return False + @staticmethod + def check_auth_internal(user: User, password: str) -> bool: + return compare_hash(crypt.crypt(password, user.password_hashed),user.password_hashed) class U2FAuthProvider(AuthProvider): @staticmethod - def get_from(): + def get_from() -> FlaskForm: return Fido2Form(prefix='fido2') @@ -67,7 +64,7 @@ class TotpAuthProvider(AuthProvider): return TotpForm(prefix='totp') @staticmethod - def check_auth(user, form): + def check_auth(user: User, form: FlaskForm) -> bool: data = form.data['totp'] if data is not None: #print(f'data totp: {data}') @@ -80,7 +77,7 @@ class TotpAuthProvider(AuthProvider): AUTH_PROVIDER_LIST = [ - LdapAuthProvider, + PasswordAuthProvider, TotpAuthProvider ] diff --git a/lenticular_cloud/migrations/versions/0518a8625b50_remove_ldap_add_rest_to_db.py b/lenticular_cloud/migrations/versions/0518a8625b50_remove_ldap_add_rest_to_db.py index 6c12fcf..f209d95 100644 --- a/lenticular_cloud/migrations/versions/0518a8625b50_remove_ldap_add_rest_to_db.py +++ b/lenticular_cloud/migrations/versions/0518a8625b50_remove_ldap_add_rest_to_db.py @@ -7,6 +7,12 @@ Create Date: 2022-06-17 13:15:33.450531 """ from alembic import op import sqlalchemy as sa +from flask import current_app +from lenticular_cloud.model import User +from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType +from ldap3_orm import Reader +from ldap3 import Connection, Server, ALL +import logging # revision identifiers, used by Alembic. @@ -17,6 +23,14 @@ depends_on = None def upgrade(): + app = current_app + server = Server(app.config['LDAP_URL'], get_info=ALL) + ldap_conn = Connection(server, app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True) # TODO auto_bind read docu + base_dn = app.config['LDAP_BASE_DN'] + object_def = ObjectDef(["inetOrgPerson"], ldap_conn) + user_base_dn = f"ou=users,{base_dn}" + + # ### commands auto generated by Alembic - please adjust! ### op.create_table('app_token', sa.Column('id', sa.Integer(), nullable=False), @@ -36,8 +50,21 @@ def upgrade(): op.add_column('user', sa.Column('enabled', sa.Boolean(), server_default="false", nullable=True)) # ### end Alembic commands ### - op.execute("UPDATE `user` SET enabled= 1;") - #op.execute('UPDATE `user` SET password_hashed = "";') + op.execute(User.__table__.update().values({'enabled': True})) + conn = op.get_bind() + users = conn.execute(User.__table__.select()) + + for user in users: + print(f"migrating user {user.username}") + reader = Reader(ldap_conn, object_def, user_base_dn, f'(uid={user.username})') + result = reader.search() + if len(result) == 0: + print(f"WARNING: could not migrate user {user.username}") + continue + ldap_object = result[0] + password_hashed = ldap_object.userPassword[0].decode().replace('{CRYPT}','') + op.execute(User.__table__.update().values({'password_hashed': password_hashed}).where(User.id == user.id)) + def downgrade(): diff --git a/lenticular_cloud/model.py b/lenticular_cloud/model.py index a51b217..1e14c63 100644 --- a/lenticular_cloud/model.py +++ b/lenticular_cloud/model.py @@ -49,7 +49,7 @@ class Service(object): } @staticmethod - def from_config(name, config) -> Service: + def from_config(name, config) -> 'Service': """ """ service = Service(name) @@ -189,7 +189,7 @@ class Totp(BaseModel): 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) - last_used = db.Column(db.DateTime, nullable=True) + #last_used = db.Column(db.DateTime, nullable=True) user_id = db.Column( db.String(length=36), diff --git a/lenticular_cloud/views/api.py b/lenticular_cloud/views/api.py index 3c497de..4ee6293 100644 --- a/lenticular_cloud/views/api.py +++ b/lenticular_cloud/views/api.py @@ -11,7 +11,6 @@ import logging import httpx from ..model import User -from ..auth_providers import LdapAuthProvider from ..hydra import hydra_service from ory_hydra_client.api.admin import introspect_o_auth_2_token from ory_hydra_client.models import GenericError @@ -37,7 +36,7 @@ def user_list() -> ResponseReturnValue: return jsonify([ {'username': str(user.username), 'email': str(user.email)} - for user in User.query_().all()]) + for user in User.query.all()]) @api_views.route('/introspect', methods=['POST']) def introspect() -> ResponseReturnValue: @@ -66,7 +65,7 @@ def email_login() -> ResponseReturnValue: if not request.is_json: return jsonify({}), 400 req_payload = request.get_json() - logger.error(f'{req_payload}') + logger.debug(f'{req_payload}') if not isinstance(req_payload, dict): return 'bad request', 400 password = req_payload["password"] diff --git a/lenticular_cloud/views/auth.py b/lenticular_cloud/views/auth.py index 2f8a6ba..442dfbe 100644 --- a/lenticular_cloud/views/auth.py +++ b/lenticular_cloud/views/auth.py @@ -147,6 +147,7 @@ async def login_auth() -> ResponseReturnValue: if auth_provider.get_name() not in session['auth_providers'] and\ auth_provider.check_auth(user, form): session['auth_providers'].append(auth_provider.get_name()) + session.modified = True if auth_provider.get_name() not in session['auth_providers']: auth_forms[auth_provider.get_name()]=form diff --git a/lenticular_cloud/views/frontend.py b/lenticular_cloud/views/frontend.py index fe40ab4..4fdb6e4 100644 --- a/lenticular_cloud/views/frontend.py +++ b/lenticular_cloud/views/frontend.py @@ -27,7 +27,7 @@ from ..model import db, User, SecurityUser, Totp, WebauthnCredential from ..form.frontend import ClientCertForm, TOTPForm, \ TOTPDeleteForm, PasswordChangeForm, WebauthnRegisterForm from ..form.base import ButtonForm -from ..auth_providers import LdapAuthProvider +from ..auth_providers import PasswordAuthProvider from .auth import webauthn from .oauth2 import redirect_login, oauth2 from ..hydra import hydra_service @@ -275,7 +275,7 @@ def password_change_post() -> ResponseReturnValue: if form.validate(): password_old = str(form.data['password_old']) password_new = str(form.data['password_new']) - if not LdapAuthProvider.check_auth_internal( + if not PasswordAuthProvider.check_auth_internal( current_user, password_old): return jsonify( {'errors': {'password_old': 'Old Password is invalid'}}) diff --git a/lenticular_cloud/views/oauth2.py b/lenticular_cloud/views/oauth2.py index 80349ff..8e34120 100644 --- a/lenticular_cloud/views/oauth2.py +++ b/lenticular_cloud/views/oauth2.py @@ -28,8 +28,8 @@ def redirect_login() -> ResponseReturnValue: session['next_url'] = request.path redirect_uri = url_for('oauth2.authorized', _external=True) response = oauth2.custom.authorize_redirect(redirect_uri) - #if isinstance(response, ResponseReturnValue): - # raise RuntimeError("invalid redirect") + if isinstance(response, Response): + raise RuntimeError("invalid redirect") return response