remove ldap
This commit is contained in:
parent
9387c44cd1
commit
161df8a473
|
@ -3,16 +3,17 @@ from flask import g, redirect, request
|
||||||
from flask.helpers import url_for
|
from flask.helpers import url_for
|
||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from lenticular_cloud.lenticular_services import lenticular_services
|
||||||
from ory_hydra_client import Client
|
from ory_hydra_client import Client
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ldap3 import Connection, Server, ALL
|
|
||||||
|
|
||||||
from . import model
|
from .pki import pki
|
||||||
from .pki import Pki
|
|
||||||
from .hydra import hydra_service
|
from .hydra import hydra_service
|
||||||
from .translations import init_babel
|
from .translations import init_babel
|
||||||
|
from .model import db, migrate
|
||||||
|
from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views, admin_views, oauth2_views
|
||||||
|
|
||||||
|
|
||||||
def get_git_hash():
|
def get_git_hash():
|
||||||
|
@ -31,13 +32,6 @@ def create_app() -> Flask:
|
||||||
|
|
||||||
app.jinja_env.globals['GIT_HASH'] = get_git_hash()
|
app.jinja_env.globals['GIT_HASH'] = get_git_hash()
|
||||||
|
|
||||||
#app.ldap_orm = Connection(app.config['LDAP_URL'], app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True)
|
|
||||||
server = Server(app.config['LDAP_URL'], get_info=ALL)
|
|
||||||
app.ldap_conn = Connection(server, app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True) # TODO auto_bind read docu
|
|
||||||
model.ldap_conn = app.ldap_conn
|
|
||||||
model.base_dn = app.config['LDAP_BASE_DN']
|
|
||||||
|
|
||||||
from .model import db, migrate
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
migration_dir = Path(app.root_path) / 'migrations'
|
migration_dir = Path(app.root_path) / 'migrations'
|
||||||
migrate.init_app(app, db, directory=str(migration_dir))
|
migrate.init_app(app, db, directory=str(migration_dir))
|
||||||
|
@ -53,9 +47,7 @@ def create_app() -> Flask:
|
||||||
# password=app.config['HYDRA_ADMIN_PASSWORD'])
|
# password=app.config['HYDRA_ADMIN_PASSWORD'])
|
||||||
hydra_service.set_hydra_client(Client(base_url=app.config['HYDRA_ADMIN_URL']))
|
hydra_service.set_hydra_client(Client(base_url=app.config['HYDRA_ADMIN_URL']))
|
||||||
|
|
||||||
from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views, admin_views, oauth2_views
|
|
||||||
init_login_manager(app)
|
init_login_manager(app)
|
||||||
#oauth2.init_app(app)
|
|
||||||
app.register_blueprint(auth_views)
|
app.register_blueprint(auth_views)
|
||||||
app.register_blueprint(frontend_views)
|
app.register_blueprint(frontend_views)
|
||||||
app.register_blueprint(api_views)
|
app.register_blueprint(api_views)
|
||||||
|
@ -68,11 +60,9 @@ def create_app() -> Flask:
|
||||||
request_start_time = time.time()
|
request_start_time = time.time()
|
||||||
g.request_time = lambda: "%.5fs" % (time.time() - request_start_time)
|
g.request_time = lambda: "%.5fs" % (time.time() - request_start_time)
|
||||||
|
|
||||||
app.lenticular_services = {}
|
lenticular_services.init_app(app)
|
||||||
for service_name, service_config in app.config['LENTICULAR_CLOUD_SERVICES'].items():
|
|
||||||
app.lenticular_services[service_name] = model.Service.from_config(service_name, service_config)
|
|
||||||
|
|
||||||
app.pki = Pki(app.config['PKI_PATH'], app.config['DOMAIN'])
|
pki.init_app(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import argparse
|
import argparse
|
||||||
from .model import db, User, UserSignUp
|
from .model import db, User
|
||||||
from .app import create_app
|
from .app import create_app
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from flask_migrate import upgrade
|
from flask_migrate import upgrade
|
||||||
|
@ -19,7 +19,7 @@ def entry_point():
|
||||||
parser_user.set_defaults(func=cli_user)
|
parser_user.set_defaults(func=cli_user)
|
||||||
|
|
||||||
parser_signup = subparsers.add_parser('signup')
|
parser_signup = subparsers.add_parser('signup')
|
||||||
parser_signup.add_argument('--signup_id', type=int)
|
parser_signup.add_argument('--signup_id', type=str)
|
||||||
parser_signup.set_defaults(func=cli_signup)
|
parser_signup.set_defaults(func=cli_signup)
|
||||||
|
|
||||||
parser_run = subparsers.add_parser('run')
|
parser_run = subparsers.add_parser('run')
|
||||||
|
@ -55,22 +55,25 @@ def entry_point():
|
||||||
|
|
||||||
|
|
||||||
def cli_user(args):
|
def cli_user(args):
|
||||||
print(User.query.all())
|
for user in User.query.all():
|
||||||
|
print(f'{user.id} - Enabled: {user.enabled} - Name:`{user.username}`')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def cli_signup(args):
|
def cli_signup(args):
|
||||||
|
|
||||||
print(args.signup_id)
|
|
||||||
if args.signup_id is not None:
|
if args.signup_id is not None:
|
||||||
user_data = UserSignUp.query.get(args.signup_id)
|
user = User.query.get(args.signup_id)
|
||||||
user = User.new(user_data)
|
if user == None:
|
||||||
|
print("user not found")
|
||||||
|
return
|
||||||
|
user.enabled = True
|
||||||
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.delete(user_data)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
# list
|
# list
|
||||||
print(UserSignUp.query.all())
|
print('disabled users:')
|
||||||
|
for user in User.query.filter_by(enabled=False).all():
|
||||||
|
print(f'<Signup id={user.id}, username={user.username}>')
|
||||||
|
|
||||||
|
|
||||||
def cli_run(app, args):
|
def cli_run(app, args):
|
||||||
|
|
15
lenticular_cloud/lenticular_services.py
Normal file
15
lenticular_cloud/lenticular_services.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from flask import Flask
|
||||||
|
from .model import Service
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LenticularServices(dict):
|
||||||
|
|
||||||
|
def init_app(self, app: Flask) -> None:
|
||||||
|
for service_name, service_config in app.config['LENTICULAR_CLOUD_SERVICES'].items():
|
||||||
|
self[service_name] = Service.from_config(service_name, service_config)
|
||||||
|
|
||||||
|
|
||||||
|
lenticular_services = LenticularServices()
|
|
@ -13,7 +13,9 @@ config = context.config
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
# Interpret the config file for Python logging.
|
||||||
# This line sets up loggers basically.
|
# This line sets up loggers basically.
|
||||||
fileConfig(config.config_file_name)
|
if type(config.config_file_name) == str:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
logger = logging.getLogger('alembic.env')
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""remove ldap, add rest to db
|
||||||
|
|
||||||
|
Revision ID: 0518a8625b50
|
||||||
|
Revises: 52a21983d2a8
|
||||||
|
Create Date: 2022-06-17 13:15:33.450531
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0518a8625b50'
|
||||||
|
down_revision = '52a21983d2a8'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('app_token',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('service_name', sa.String(), nullable=False),
|
||||||
|
sa.Column('token', sa.String(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('group',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.drop_table('user_sign_up')
|
||||||
|
op.add_column('user', sa.Column('password_hashed', sa.String(), server_default="", nullable=False))
|
||||||
|
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 = "";')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('user', 'enabled')
|
||||||
|
op.drop_column('user', 'password_hashed')
|
||||||
|
op.create_table('user_sign_up',
|
||||||
|
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('username', sa.VARCHAR(), nullable=False),
|
||||||
|
sa.Column('password', sa.VARCHAR(), nullable=False),
|
||||||
|
sa.Column('alternative_email', sa.VARCHAR(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DATETIME(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.drop_table('group')
|
||||||
|
op.drop_table('app_token')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -1,11 +1,5 @@
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
|
|
||||||
from ldap3_orm import Reader
|
|
||||||
from ldap3 import Connection, Entry, HASHED_SALTED_SHA256
|
|
||||||
from ldap3.utils.conv import escape_filter_chars
|
|
||||||
from ldap3.utils.hashed import hashed
|
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from ldap3.core.exceptions import LDAPSessionTerminatedByServerError
|
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from collections.abc import MutableSequence
|
from collections.abc import MutableSequence
|
||||||
|
@ -21,24 +15,17 @@ from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
import pyotp
|
import pyotp
|
||||||
from typing import Optional, Callable
|
from typing import Optional, Callable
|
||||||
|
from cryptography.x509 import Certificate as CertificateObj
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
ldap_conn = None # type: Connection
|
|
||||||
base_dn = ''
|
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy() # type: SQLAlchemy
|
db = SQLAlchemy() # type: SQLAlchemy
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
|
|
||||||
class UserSignUp(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
username = db.Column(db.String, nullable=False)
|
|
||||||
password = db.Column(db.String, nullable=False)
|
|
||||||
alternative_email = db.Column(db.String)
|
|
||||||
created_at = db.Column(db.DateTime, nullable=False,
|
|
||||||
default=datetime.now)
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityUser(UserMixin):
|
class SecurityUser(UserMixin):
|
||||||
|
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
|
@ -48,90 +35,6 @@ class SecurityUser(UserMixin):
|
||||||
return self._username
|
return self._username
|
||||||
|
|
||||||
|
|
||||||
class LambdaStr:
|
|
||||||
|
|
||||||
def __init__(self, lam: Callable[[],str]):
|
|
||||||
self.lam = lam
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.lam()
|
|
||||||
|
|
||||||
|
|
||||||
class EntryBase(db.Model):
|
|
||||||
__abstract__ = True # for sqlalchemy
|
|
||||||
|
|
||||||
_type = None # will get replaced by the local type
|
|
||||||
_ldap_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
|
|
||||||
dn = ''
|
|
||||||
base_dn = ''
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return str(self._ldap_object)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_object_def(cls) -> ObjectDef:
|
|
||||||
return ObjectDef(cls.object_classes, ldap_conn)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_entry_type(cls) -> EntryType:
|
|
||||||
return EntryType(cls.get_dn(), cls.object_classes, ldap_conn)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_base(cls) -> str:
|
|
||||||
return cls.base_dn.format(_base_dn=base_dn)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_dn(cls) -> str:
|
|
||||||
return cls.dn.replace('{base_dn}', cls.get_base())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_type(cls):
|
|
||||||
if cls._type is None:
|
|
||||||
cls._type = EntryType(cls.get_dn(), cls.object_classes, ldap_conn)
|
|
||||||
return cls._type
|
|
||||||
|
|
||||||
def ldap_commit(self):
|
|
||||||
self._ldap_object.entry_commit_changes()
|
|
||||||
|
|
||||||
def ldap_add(self):
|
|
||||||
ret = ldap_conn.add(
|
|
||||||
self.entry_dn, self.object_classes, self._ldap_object.entry_attributes_as_dict)
|
|
||||||
if not ret:
|
|
||||||
raise Exception('ldap error')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def query_(cls):
|
|
||||||
if cls._ldap_query_object is None:
|
|
||||||
cls._ldap_query_object = cls._query(cls)
|
|
||||||
return cls._ldap_query_object
|
|
||||||
|
|
||||||
class _query(object):
|
|
||||||
def __init__(self, clazz):
|
|
||||||
self._class = clazz
|
|
||||||
|
|
||||||
def _mapping(self, ldap_object):
|
|
||||||
return ldap_object
|
|
||||||
|
|
||||||
def _query(self, ldap_filter: str):
|
|
||||||
reader = Reader(ldap_conn, self._class.get_object_def(), self._class.get_base(), ldap_filter)
|
|
||||||
try:
|
|
||||||
reader.search()
|
|
||||||
except LDAPSessionTerminatedByServerError:
|
|
||||||
ldap_conn.bind()
|
|
||||||
reader.search()
|
|
||||||
return [self._mapping(entry) for entry in reader]
|
|
||||||
|
|
||||||
def all(self):
|
|
||||||
return self._query(None)
|
|
||||||
|
|
||||||
|
|
||||||
class Service(object):
|
class Service(object):
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
|
@ -171,7 +74,7 @@ class Service(object):
|
||||||
|
|
||||||
class Certificate(object):
|
class Certificate(object):
|
||||||
|
|
||||||
def __init__(self, cn, ca_name: str, cert_data, revoked=False):
|
def __init__(self, cn, ca_name: str, cert_data: CertificateObj, revoked=False):
|
||||||
self._cn = cn
|
self._cn = cn
|
||||||
self._ca_name = ca_name
|
self._ca_name = ca_name
|
||||||
self._cert_data = cert_data
|
self._cert_data = cert_data
|
||||||
|
@ -225,11 +128,13 @@ def generate_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
class User(EntryBase):
|
class User(db.Model):
|
||||||
id = db.Column(
|
id = db.Column(
|
||||||
db.String(length=36), primary_key=True, default=generate_uuid)
|
db.String(length=36), primary_key=True, default=generate_uuid)
|
||||||
username = db.Column(
|
username = db.Column(
|
||||||
db.String, unique=True, nullable=False)
|
db.String, unique=True, nullable=False)
|
||||||
|
password_hashed = db.Column(
|
||||||
|
db.String, nullable=False)
|
||||||
alternative_email = db.Column(
|
alternative_email = db.Column(
|
||||||
db.String, nullable=True)
|
db.String, nullable=True)
|
||||||
created_at = db.Column(db.DateTime, nullable=False,
|
created_at = db.Column(db.DateTime, nullable=False,
|
||||||
|
@ -238,15 +143,12 @@ class User(EntryBase):
|
||||||
default=datetime.now, onupdate=datetime.now)
|
default=datetime.now, onupdate=datetime.now)
|
||||||
last_login = db.Column(db.DateTime, nullable=True)
|
last_login = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
enabled = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
totps = db.relationship('Totp', back_populates='user')
|
totps = db.relationship('Totp', back_populates='user')
|
||||||
webauthn_credentials = db.relationship('WebauthnCredential', back_populates='user', cascade='delete,delete-orphan', passive_deletes=True)
|
webauthn_credentials = db.relationship('WebauthnCredential', back_populates='user', cascade='delete,delete-orphan', passive_deletes=True)
|
||||||
|
|
||||||
dn = "uid={uid},{base_dn}"
|
|
||||||
base_dn = "ou=users,{_base_dn}"
|
|
||||||
object_classes = ["inetOrgPerson"] #, "LenticularUser"]
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self._ldap_object = None
|
|
||||||
super(db.Model).__init__(**kwargs)
|
super(db.Model).__init__(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -256,9 +158,6 @@ class User(EntryBase):
|
||||||
def get(self, key):
|
def get(self, key):
|
||||||
print(f'getitem: {key}') # TODO
|
print(f'getitem: {key}') # TODO
|
||||||
|
|
||||||
def make_writeable(self):
|
|
||||||
self._ldap_object = self._ldap_object.entry_writable()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def groups(self) -> list[str]:
|
def groups(self) -> list[str]:
|
||||||
if self.username == 'tuxcoder':
|
if self.username == 'tuxcoder':
|
||||||
|
@ -266,58 +165,20 @@ class User(EntryBase):
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
|
||||||
def entry_dn(self) -> str:
|
|
||||||
return self._ldap_object.entry_dn
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self) -> str:
|
def email(self) -> str:
|
||||||
domain = current_app.config['DOMAIN']
|
domain = current_app.config['DOMAIN']
|
||||||
return f'{self.username}@{domain}'
|
return f'{self.username}@{domain}'
|
||||||
return self._ldap_object.mail
|
|
||||||
|
|
||||||
def change_password(self, password_new: str) -> bool:
|
def change_password(self, password_new: str) -> bool:
|
||||||
self.make_writeable()
|
|
||||||
password_hashed = crypt.crypt(password_new)
|
password_hashed = crypt.crypt(password_new)
|
||||||
self._ldap_object.userPassword = ('{CRYPT}' + password_hashed).encode()
|
|
||||||
self.ldap_commit()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class _query(EntryBase._query):
|
class AppToken(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
def _mapping(self, ldap_object):
|
service_name = db.Column(db.String, nullable=False)
|
||||||
user = User.query.filter(User.username == str(ldap_object.uid)).first()
|
token = db.Column(db.String, nullable=False)
|
||||||
if user is None:
|
name = db.Column(db.String, nullable=False)
|
||||||
# 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) -> Optional['User']:
|
|
||||||
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
|
|
||||||
if len(result) > 0 and isinstance(result[0], User):
|
|
||||||
return result[0]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def new(user_data: UserSignUp):
|
|
||||||
user = User()
|
|
||||||
user.username = user_data.username.lower()
|
|
||||||
domain = current_app.config['DOMAIN']
|
|
||||||
ldap_object = User.get_entry_type()(
|
|
||||||
uid=user_data.username.lower(),
|
|
||||||
sn=user_data.username,
|
|
||||||
cn=user_data.username,
|
|
||||||
userPassword='{CRYPT}' + user_data.password,
|
|
||||||
mail=f'{user_data.username}@{domain}')
|
|
||||||
user._ldap_object = ldap_object
|
|
||||||
user.ldap_add()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Totp(db.Model):
|
class Totp(db.Model):
|
||||||
|
@ -349,14 +210,7 @@ class WebauthnCredential(db.Model): # pylint: disable=too-few-public-methods
|
||||||
user = db.relationship('User', back_populates='webauthn_credentials')
|
user = db.relationship('User', back_populates='webauthn_credentials')
|
||||||
|
|
||||||
|
|
||||||
class Group(EntryBase):
|
class Group(db.Model):
|
||||||
__abstract__ = True # for sqlalchemy, disable for now
|
|
||||||
dn = "cn={cn},{base_dn}"
|
|
||||||
base_dn = "ou=Users,{_base_dn}"
|
|
||||||
object_classes = ["top"]
|
|
||||||
|
|
||||||
fullname = AttrDef("cn")
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(), nullable=False, unique=True)
|
name = db.Column(db.String(), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from flask import current_app
|
from flask import Flask
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
@ -39,12 +39,18 @@ def safe_filename(name):
|
||||||
|
|
||||||
class Pki(object):
|
class Pki(object):
|
||||||
|
|
||||||
def __init__(self, pki_path: str, domain: str):
|
def __init__(self):
|
||||||
|
self._pki_path = Path()
|
||||||
|
self._domain = ""
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(self, app: Flask) -> None:
|
||||||
'''
|
'''
|
||||||
pki_path: str base path from the pkis
|
pki_path: str base path from the pkis
|
||||||
'''
|
'''
|
||||||
self._pki_path = Path(pki_path)
|
self._pki_path = Path(os.getcwd()) / app.config['PKI_PATH']
|
||||||
self._domain = domain
|
self._domain = app.config['DOMAIN']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _init_ca(self, service: Service):
|
def _init_ca(self, service: Service):
|
||||||
|
@ -322,3 +328,4 @@ class Pki(object):
|
||||||
backend=default_backend())
|
backend=default_backend())
|
||||||
return crl
|
return crl
|
||||||
|
|
||||||
|
pki = Pki()
|
|
@ -11,7 +11,7 @@ from ory_hydra_client.models import OAuth2Client, GenericError
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..model import db, User, UserSignUp
|
from ..model import db, User
|
||||||
from .oauth2 import redirect_login, oauth2
|
from .oauth2 import redirect_login, oauth2
|
||||||
from ..form.admin import OAuth2ClientForm
|
from ..form.admin import OAuth2ClientForm
|
||||||
from ..hydra import hydra_service
|
from ..hydra import hydra_service
|
||||||
|
@ -50,28 +50,24 @@ async def users():
|
||||||
|
|
||||||
@admin_views.route('/registrations', methods=['GET'])
|
@admin_views.route('/registrations', methods=['GET'])
|
||||||
def registrations() -> ResponseReturnValue:
|
def registrations() -> ResponseReturnValue:
|
||||||
users = UserSignUp.query.all()
|
users = User.query.filter_by(enabled=False).all()
|
||||||
return render_template('admin/registrations.html.j2', users=users)
|
return render_template('admin/registrations.html.j2', users=users)
|
||||||
|
|
||||||
|
|
||||||
@admin_views.route('/registration/<registration_id>', methods=['DELETE'])
|
@admin_views.route('/registration/<registration_id>', methods=['DELETE'])
|
||||||
def registration_delete(registration_id) -> ResponseReturnValue:
|
def registration_delete(registration_id) -> ResponseReturnValue:
|
||||||
user_data = UserSignUp.query.get(registration_id)
|
user = User.query.get(registration_id)
|
||||||
if user_data is None:
|
if user is None:
|
||||||
return jsonify({}), 404
|
return jsonify({}), 404
|
||||||
db.session.delete(user_data)
|
db.session.delete(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({})
|
return jsonify({})
|
||||||
|
|
||||||
|
|
||||||
@admin_views.route('/registration/<registration_id>', methods=['PUT'])
|
@admin_views.route('/registration/<registration_id>', methods=['PUT'])
|
||||||
def registration_accept(registration_id) -> ResponseReturnValue:
|
def registration_accept(registration_id) -> ResponseReturnValue:
|
||||||
user_data = UserSignUp.query.get(registration_id)
|
user = User.query.get(registration_id)
|
||||||
#create user
|
user.enabled = True
|
||||||
user = User.new(user_data)
|
|
||||||
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.delete(user_data)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({})
|
return jsonify({})
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ def email_login() -> ResponseReturnValue:
|
||||||
logger.error(f'{request}')
|
logger.error(f'{request}')
|
||||||
logger.error(f'{request.headers}')
|
logger.error(f'{request.headers}')
|
||||||
if not request.is_json:
|
if not request.is_json:
|
||||||
return {}, 400
|
return jsonify({}), 400
|
||||||
req_payload = request.get_json()
|
req_payload = request.get_json()
|
||||||
logger.error(f'{req_payload}')
|
logger.error(f'{req_payload}')
|
||||||
password = req_payload["password"]
|
password = req_payload["password"]
|
||||||
|
|
|
@ -21,7 +21,7 @@ from ory_hydra_client.api.admin import get_consent_request, accept_consent_reque
|
||||||
from ory_hydra_client.models import AcceptLoginRequest, AcceptConsentRequest, ConsentRequestSession, GenericError, ConsentRequestSessionAccessToken, ConsentRequestSessionIdToken
|
from ory_hydra_client.models import AcceptLoginRequest, AcceptConsentRequest, ConsentRequestSession, GenericError, ConsentRequestSessionAccessToken, ConsentRequestSessionIdToken
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ..model import db, User, SecurityUser, UserSignUp
|
from ..model import db, User, SecurityUser
|
||||||
from ..form.auth import ConsentForm, LoginForm, RegistrationForm
|
from ..form.auth import ConsentForm, LoginForm, RegistrationForm
|
||||||
from ..auth_providers import AUTH_PROVIDER_LIST
|
from ..auth_providers import AUTH_PROVIDER_LIST
|
||||||
from ..hydra import hydra_service
|
from ..hydra import hydra_service
|
||||||
|
@ -118,7 +118,7 @@ async def login() -> ResponseReturnValue:
|
||||||
return redirect(resp.redirect_to)
|
return redirect(resp.redirect_to)
|
||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query_().by_username(form.data['name'])
|
user = User.query.filter_by(username=form.data['name']).first()
|
||||||
if user:
|
if user:
|
||||||
session['username'] = str(user.username)
|
session['username'] = str(user.username)
|
||||||
else:
|
else:
|
||||||
|
@ -141,7 +141,7 @@ async def login_auth() -> ResponseReturnValue:
|
||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
auth_forms = {}
|
auth_forms = {}
|
||||||
user = User.query_().by_username(session['username'])
|
user = User.query.filter_by(username=session['username']).first()
|
||||||
for auth_provider in AUTH_PROVIDER_LIST:
|
for auth_provider in AUTH_PROVIDER_LIST:
|
||||||
form = auth_provider.get_form()
|
form = auth_provider.get_form()
|
||||||
if auth_provider.get_name() not in session['auth_providers'] and\
|
if auth_provider.get_name() not in session['auth_providers'] and\
|
||||||
|
@ -216,9 +216,9 @@ def sign_up():
|
||||||
def sign_up_submit():
|
def sign_up_submit():
|
||||||
form = RegistrationForm()
|
form = RegistrationForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = UserSignUp()
|
user = User()
|
||||||
user.username = form.data['username']
|
user.username = form.data['username']
|
||||||
user.password = crypt.crypt(form.data['password'])
|
user.password_hashed = crypt.crypt(form.data['password'])
|
||||||
user.alternative_email = form.data['alternative_email']
|
user.alternative_email = form.data['alternative_email']
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
@ -31,6 +31,8 @@ from ..auth_providers import LdapAuthProvider
|
||||||
from .auth import webauthn
|
from .auth import webauthn
|
||||||
from .oauth2 import redirect_login, oauth2
|
from .oauth2 import redirect_login, oauth2
|
||||||
from ..hydra import hydra_service
|
from ..hydra import hydra_service
|
||||||
|
from ..pki import pki
|
||||||
|
from ..lenticular_services import lenticular_services
|
||||||
|
|
||||||
frontend_views = Blueprint('frontend', __name__, url_prefix='')
|
frontend_views = Blueprint('frontend', __name__, url_prefix='')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -43,8 +45,10 @@ def before_request() -> Optional[ResponseReturnValue]:
|
||||||
logger.info('user not logged in redirect')
|
logger.info('user not logged in redirect')
|
||||||
return redirect_login()
|
return redirect_login()
|
||||||
except MissingTokenError:
|
except MissingTokenError:
|
||||||
|
logger.info('MissingTokenError redirect user to login')
|
||||||
return redirect_login()
|
return redirect_login()
|
||||||
except InvalidTokenError:
|
except InvalidTokenError:
|
||||||
|
logger.info('InvalidTokenError redirect user to login')
|
||||||
return redirect_login()
|
return redirect_login()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -72,20 +76,20 @@ def index() -> ResponseReturnValue:
|
||||||
@frontend_views.route('/client_cert')
|
@frontend_views.route('/client_cert')
|
||||||
def client_cert() -> ResponseReturnValue:
|
def client_cert() -> ResponseReturnValue:
|
||||||
client_certs = {}
|
client_certs = {}
|
||||||
for service in current_app.lenticular_services.values():
|
for service in lenticular_services.values():
|
||||||
client_certs[str(service.name)] = \
|
client_certs[str(service.name)] = \
|
||||||
current_app.pki.get_client_certs(current_user, service)
|
pki.get_client_certs(current_user, service)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'frontend/client_cert.html.j2',
|
'frontend/client_cert.html.j2',
|
||||||
services=current_app.lenticular_services,
|
services=lenticular_services,
|
||||||
client_certs=client_certs)
|
client_certs=client_certs)
|
||||||
|
|
||||||
|
|
||||||
@frontend_views.route('/client_cert/<service_name>/<serial_number>')
|
@frontend_views.route('/client_cert/<service_name>/<serial_number>')
|
||||||
def get_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
def get_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
||||||
service = current_app.lenticular_services[service_name]
|
service = lenticular_services[service_name]
|
||||||
cert = current_app.pki.get_client_cert(
|
cert = pki.get_client_cert(
|
||||||
current_user, service, serial_number)
|
current_user, service, serial_number)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'data': {
|
'data': {
|
||||||
|
@ -96,10 +100,10 @@ def get_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
||||||
@frontend_views.route(
|
@frontend_views.route(
|
||||||
'/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
|
'/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
|
||||||
def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
||||||
service = current_app.lenticular_services[service_name]
|
service = lenticular_services[service_name]
|
||||||
cert = current_app.pki.get_client_cert(
|
cert = pki.get_client_cert(
|
||||||
current_user, service, serial_number)
|
current_user, service, serial_number)
|
||||||
current_app.pki.revoke_certificate(cert)
|
pki.revoke_certificate(cert)
|
||||||
return jsonify({})
|
return jsonify({})
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,11 +111,11 @@ def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue:
|
||||||
'/client_cert/<service_name>/new',
|
'/client_cert/<service_name>/new',
|
||||||
methods=['GET', 'POST'])
|
methods=['GET', 'POST'])
|
||||||
def client_cert_new(service_name) -> ResponseReturnValue:
|
def client_cert_new(service_name) -> ResponseReturnValue:
|
||||||
service = current_app.lenticular_services[service_name]
|
service = lenticular_services[service_name]
|
||||||
form = ClientCertForm()
|
form = ClientCertForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
valid_time = int(form.data['valid_time']) * timedelta(1, 0, 0)
|
valid_time = int(form.data['valid_time']) * timedelta(1, 0, 0)
|
||||||
cert = current_app.pki.signing_publickey(
|
cert = pki.signing_publickey(
|
||||||
current_user,
|
current_user,
|
||||||
service,
|
service,
|
||||||
form.data['publickey'],
|
form.data['publickey'],
|
||||||
|
@ -120,7 +124,7 @@ def client_cert_new(service_name) -> ResponseReturnValue:
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'data': {
|
'data': {
|
||||||
'cert': cert.pem(),
|
'cert': cert.pem(),
|
||||||
'ca_cert': current_app.pki.get_ca_cert_pem(service)
|
'ca_cert': pki.get_ca_cert_pem(service)
|
||||||
}})
|
}})
|
||||||
elif form.is_submitted():
|
elif form.is_submitted():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
@ -252,7 +256,7 @@ def webauthn_register_route() -> ResponseReturnValue:
|
||||||
|
|
||||||
return redirect(url_for('app.webauthn_list_route'))
|
return redirect(url_for('app.webauthn_list_route'))
|
||||||
except (KeyError, ValueError) as e:
|
except (KeyError, ValueError) as e:
|
||||||
current_app.logger.exception(e)
|
logger.exception(e)
|
||||||
flash('Error during registration.', 'error')
|
flash('Error during registration.', 'error')
|
||||||
|
|
||||||
return render_template('frontend/webauthn_register.html', form=form)
|
return render_template('frontend/webauthn_register.html', form=form)
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
from authlib.integrations.flask_client import OAuth
|
from authlib.integrations.flask_client import OAuth
|
||||||
from authlib.integrations.base_client.errors import MismatchingStateError
|
from authlib.integrations.base_client.errors import MismatchingStateError
|
||||||
from flask import Flask, Blueprint, session, request, redirect, url_for
|
from flask import Flask, Blueprint, Response, session, request, redirect, url_for
|
||||||
from flask_login import login_user, logout_user, current_user
|
from flask_login import login_user, logout_user, current_user
|
||||||
from flask.typing import ResponseReturnValue
|
from flask.typing import ResponseReturnValue
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
from ..model import User, SecurityUser
|
from ..model import User, SecurityUser
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def fetch_token(name: str) -> Optional[dict]:
|
def fetch_token(name: str) -> Optional[dict]:
|
||||||
token = session.get('token', None)
|
token = session.get('token', None)
|
||||||
if isinstance(token, dict):
|
if isinstance(token, dict):
|
||||||
|
@ -24,7 +27,10 @@ def redirect_login() -> ResponseReturnValue:
|
||||||
logout_user()
|
logout_user()
|
||||||
session['next_url'] = request.path
|
session['next_url'] = request.path
|
||||||
redirect_uri = url_for('oauth2.authorized', _external=True)
|
redirect_uri = url_for('oauth2.authorized', _external=True)
|
||||||
return oauth2.custom.authorize_redirect(redirect_uri)
|
response = oauth2.custom.authorize_redirect(redirect_uri)
|
||||||
|
#if isinstance(response, ResponseReturnValue):
|
||||||
|
# raise RuntimeError("invalid redirect")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@oauth2_views.route('/authorized')
|
@oauth2_views.route('/authorized')
|
||||||
|
@ -32,29 +38,38 @@ def authorized() -> ResponseReturnValue:
|
||||||
try:
|
try:
|
||||||
token = oauth2.custom.authorize_access_token()
|
token = oauth2.custom.authorize_access_token()
|
||||||
except MismatchingStateError:
|
except MismatchingStateError:
|
||||||
|
logger.warning("MismatchingStateError redirect user")
|
||||||
return redirect(url_for('oauth2.login'))
|
return redirect(url_for('oauth2.login'))
|
||||||
if token is None:
|
if token is None:
|
||||||
return 'bad request', 400
|
return 'bad request', 400
|
||||||
session['token'] = token
|
session['token'] = token
|
||||||
userinfo = oauth2.custom.get('/userinfo').json()
|
userinfo = oauth2.custom.get('/userinfo').json()
|
||||||
db_user = User.query.get(str(userinfo["sub"]))
|
logger.info(f"userinfo `{userinfo}`")
|
||||||
login_user(SecurityUser(db_user.username))
|
user = User.query.get(str(userinfo["sub"]))
|
||||||
|
if user is None:
|
||||||
|
return "user not found", 404
|
||||||
|
logger.info(f"login user `{user.username}`")
|
||||||
|
login_user(SecurityUser(user.username))
|
||||||
|
logger.info(f"session user `{session}`")
|
||||||
|
|
||||||
next_url = request.args.get('next_url')
|
next_url = request.args.get('next_url')
|
||||||
if next_url is None:
|
if next_url is None:
|
||||||
next_url = '/'
|
next_url = '/'
|
||||||
return redirect(next_url)
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
@oauth2_views.route('login')
|
@oauth2_views.route('login')
|
||||||
def login() -> ResponseReturnValue:
|
def login() -> ResponseReturnValue:
|
||||||
redirect_uri = url_for('.authorized', _external=True)
|
redirect_uri = url_for('.authorized', _external=True)
|
||||||
return oauth2.custom.authorize_redirect(redirect_uri)
|
response = oauth2.custom.authorize_redirect(redirect_uri)
|
||||||
|
#if type(response) != Response:
|
||||||
|
# raise RuntimeError("invalid redirect")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def user_loader(username) -> Optional[User]:
|
def user_loader(username) -> Optional[User]:
|
||||||
user = User.query_().by_username(username)
|
user = User.query.filter_by(username=username).first()
|
||||||
if isinstance(user, User):
|
if isinstance(user, User):
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
|
@ -65,12 +80,15 @@ def request_loader(_request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@login_manager.unauthorized_handler
|
@login_manager.unauthorized_handler
|
||||||
def unauthorized():
|
def unauthorized() -> Optional[User]:
|
||||||
redirect_login()
|
pass
|
||||||
|
|
||||||
def init_login_manager(app: Flask):
|
def init_login_manager(app: Flask) -> None:
|
||||||
|
|
||||||
base_url = app.config['HYDRA_PUBLIC_URL']
|
base_url = app.config['HYDRA_PUBLIC_URL']
|
||||||
|
if not isinstance(base_url, str):
|
||||||
|
raise RuntimeError("HYDRA_PUBLIC_URL not set")
|
||||||
|
|
||||||
oauth2.register(
|
oauth2.register(
|
||||||
name="custom",
|
name="custom",
|
||||||
client_id=app.config['OAUTH_ID'],
|
client_id=app.config['OAUTH_ID'],
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from flask import current_app, Blueprint
|
from flask import Blueprint
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from ..lenticular_services import lenticular_services
|
||||||
|
from ..pki import pki
|
||||||
|
|
||||||
|
|
||||||
pki_views = Blueprint('pki', __name__, url_prefix='/')
|
pki_views = Blueprint('pki', __name__, url_prefix='/')
|
||||||
|
@ -7,7 +9,7 @@ pki_views = Blueprint('pki', __name__, url_prefix='/')
|
||||||
|
|
||||||
@pki_views.route('/<service_name>.crl')
|
@pki_views.route('/<service_name>.crl')
|
||||||
def crl(service_name: str):
|
def crl(service_name: str):
|
||||||
service = current_app.lenticular_services[service_name]
|
service = lenticular_services[service_name]
|
||||||
crl = current_app.pki.get_crl(service)
|
crl = pki.get_crl(service)
|
||||||
return crl.public_bytes(encoding=serialization.Encoding.DER)
|
return crl.public_bytes(encoding=serialization.Encoding.DER)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue