bugfixes, cleanup

This commit is contained in:
TuxCoder 2022-02-06 23:57:01 +01:00
parent 1bf474045a
commit 17c30128ae
82 changed files with 216 additions and 76 deletions

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
/venv /venv
/tmp /tmp
/production.cfg /lenticular_cloud/production.cfg
/pki /pki
signing_key.pem signing_key.pem
node_modules node_modules

4
MANIFEST.in Normal file
View file

@ -0,0 +1,4 @@
recursive-include lenticular_cloud/template *
recursive-include lenticular_cloud/static **
include lenticular_cloud/*.cfg

View file

@ -32,3 +32,9 @@ Tested Services
* ~~Postfix~~/Dovecot (~~OIDC~~/client cert/password) * ~~Postfix~~/Dovecot (~~OIDC~~/client cert/password)
* prosody (client cert/~~password~~) * prosody (client cert/~~password~~)
Oauth2 Settings:
----------------
callback url: `${domain}/

8
cli.py Normal file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from lenticular_cloud.cli import entry_point
entry_point()

13
hydra_client.json Normal file
View file

@ -0,0 +1,13 @@
{
"client_id": "identiy_provider",
"client_secret": "",
"redirect_uris": [
"{domain}/oauth/authorized"
],
"response_types": [
"code", "id_token"
],
"scope": "openid profile manage",
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "client_secret_basic"
}

View file

@ -7,6 +7,7 @@ import time
import subprocess import subprocess
import ory_hydra_client as hydra import ory_hydra_client as hydra
import ory_hydra_client.api.admin_api as hydra_admin_api import ory_hydra_client.api.admin_api as hydra_admin_api
import os
from ldap3 import Connection, Server, ALL from ldap3 import Connection, Server, ALL
@ -26,11 +27,12 @@ def init_oauth2(app):
def init_app(name=None): def create_app():
name = name or __name__ name = "lenticular_cloud"
app = Flask(name) app = Flask(name, template_folder='template')
app.config.from_pyfile('application.cfg') app.config.from_pyfile('application.cfg')
app.config.from_pyfile('production.cfg') active_cfg = os.getenv('CONFIG_FILE', 'production.cfg')
app.config.from_pyfile(active_cfg)
app.jinja_env.globals['GIT_HASH'] = get_git_hash() app.jinja_env.globals['GIT_HASH'] = get_git_hash()

View file

@ -3,7 +3,7 @@ SESSION_COOKIE_NAME='lc_session'
SUBJECT_ID_HASH_SALT = 'salt' SUBJECT_ID_HASH_SALT = 'salt'
PREFERRED_URL_SCHEME = 'https' PREFERRED_URL_SCHEME = 'https'
DATA_FOLDER = "./data" DATA_FOLDER = "../data"
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATA_FOLDER}/db.sqlite' SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATA_FOLDER}/db.sqlite'
SQLALCHEMY_TRACK_MODIFICATIONS=False SQLALCHEMY_TRACK_MODIFICATIONS=False

View file

@ -70,7 +70,7 @@ class TotpAuthProvider(AuthProvider):
def check_auth(user, form): def check_auth(user, form):
data = form.data['totp'] data = form.data['totp']
if data is not None: if data is not None:
print(f'data totp: {data}') #print(f'data totp: {data}')
if len(user.totps) == 0: # migration, TODO remove if len(user.totps) == 0: # migration, TODO remove
return True return True
for totp in user.totps: for totp in user.totps:
@ -84,5 +84,5 @@ AUTH_PROVIDER_LIST = [
TotpAuthProvider TotpAuthProvider
] ]
print(LdapAuthProvider.get_name()) #print(LdapAuthProvider.get_name())

61
lenticular_cloud/cli.py Normal file
View file

@ -0,0 +1,61 @@
import argparse
from .model import db, User, UserSignUp
from .app import create_app
def entry_point():
parser = argparse.ArgumentParser(description='lenticular_cloud cli')
subparsers = parser.add_subparsers()
parser_user = subparsers.add_parser('user')
parser_user.set_defaults(func=cli_user)
parser_signup = subparsers.add_parser('signup')
parser_signup.add_argument('--signup_id', type=int)
parser_signup.set_defaults(func=cli_signup)
'''
parser_upcoming = subparsers.add_parser('upcoming')
parser_upcoming.set_defaults(func=cli_upcoming)
parser_upcoming.add_argument('-a', '--all', help='shows all single order`', nargs='?', default=False, const=True,
type=bool,
required=False)
parser_upcoming.add_argument('-n', '--no-import', dest='noimport', help='do not a automatic import', nargs='?',
default=False, type=bool, required=False)
parser_upcoming.add_argument('-F', '--format', help='format can be `d`|`m`|`y`', default='d', required=False)
# parser_select.add_argument('-F', '--format', help='format can be `d`|`m`|`y`', default='d', required=False)
# parser_select.add_argument('-f', '--from', help='from date in the format `yyyy-mm-dd`', required=False)
# parser_select.add_argument('-t', '--to', help='to date in the format `yyyy-mm-dd`', required=False)
'''
args = parser.parse_args()
if 'func' not in args:
parser.print_help()
return
app = create_app()
with app.app_context():
args.func(args)
def cli_user(args):
print(User.query.all())
pass
def cli_signup(args):
print(args.signup_id)
if args.signup_id is not None:
user_data = UserSignUp.query.get(args.signup_id)
user = User.new(user_data)
db.session.add(user)
db.session.delete(user_data)
db.session.commit()
else:
# list
print(UserSignUp.query.all())
if __name__ == "__main__":
entry_point()

View file

@ -1,7 +1,7 @@
from flask import current_app from flask import current_app
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
from ldap3_orm import Reader from ldap3_orm import Reader
from ldap3 import Entry, HASHED_SALTED_SHA256 from ldap3 import Connection, Entry, HASHED_SALTED_SHA256
from ldap3.utils.conv import escape_filter_chars from ldap3.utils.conv import escape_filter_chars
from ldap3.utils.hashed import hashed from ldap3.utils.hashed import hashed
from flask_login import UserMixin from flask_login import UserMixin
@ -19,6 +19,7 @@ from flask_sqlalchemy import SQLAlchemy, orm
from datetime import datetime from datetime import datetime
import uuid import uuid
import pyotp import pyotp
from typing import Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -226,6 +227,8 @@ class User(EntryBase):
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)
alternative_email = db.Column(
db.String, nullable=True)
created_at = db.Column(db.DateTime, nullable=False, created_at = db.Column(db.DateTime, nullable=False,
default=datetime.now) default=datetime.now)
modified_at = db.Column(db.DateTime, nullable=False, modified_at = db.Column(db.DateTime, nullable=False,
@ -236,7 +239,7 @@ class User(EntryBase):
dn = "uid={uid},{base_dn}" dn = "uid={uid},{base_dn}"
base_dn = "ou=users,{_base_dn}" base_dn = "ou=users,{_base_dn}"
object_classes = ["top", "inetOrgPerson", "LenticularUser"] object_classes = ["inetOrgPerson"] #, "LenticularUser"]
def __init__(self, **kwargs): def __init__(self, **kwargs):
self._ldap_object = None self._ldap_object = None
@ -247,57 +250,34 @@ class User(EntryBase):
return True # TODO return True # TODO
def get(self, key): def get(self, key):
print(f'getitem: {key}') print(f'getitem: {key}') # TODO
def make_writeable(self): def make_writeable(self):
self._ldap_object = self._ldap_object.entry_writable() self._ldap_object = self._ldap_object.entry_writable()
@property @property
def groups(self): def groups(self) -> list[str]:
if self.username == 'tuxcoder': if self.username == 'tuxcoder':
return [Group(name='admin')] return [Group(name='admin')]
else: else:
return [] return []
@property @property
def entry_dn(self): def entry_dn(self) -> str:
return self._ldap_object.entry_dn return self._ldap_object.entry_dn
@property @property
def fullname(self): def email(self) -> str:
return self._ldap_object.fullname
@property
def givenname(self):
return self._ldap_object.givenname
@property
def surname(self):
return self._ldap_object.surname
@property
def email(self):
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 return self._ldap_object.mail
@property def change_password(self, password_new: str) -> bool:
def alternative_email(self):
return self._ldap_object.altMail
@property
def auth_role(self):
return self._ldap_object.authRole
@property
def gpg_public_key(self):
return self._ldap_object.gpgPublicKey
def change_password(self, password_new: str):
self.make_writeable() 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_object.userPassword = ('{CRYPT}' + password_hashed).encode()
self.ldap_commit() self.ldap_commit()
return True
class _query(EntryBase._query): class _query(EntryBase._query):
@ -312,7 +292,7 @@ class User(EntryBase):
user._ldap_object = ldap_object user._ldap_object = ldap_object
return user return user
def by_username(self, username) -> 'User': def by_username(self, username) -> Optional['User']:
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username))) result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
if len(result) > 0: if len(result) > 0:
return result[0] return result[0]

View file

Before

Width:  |  Height:  |  Size: 876 KiB

After

Width:  |  Height:  |  Size: 876 KiB

View file

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 730 KiB

View file

Before

Width:  |  Height:  |  Size: 699 KiB

After

Width:  |  Height:  |  Size: 699 KiB

View file

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View file

Before

Width:  |  Height:  |  Size: 898 KiB

After

Width:  |  Height:  |  Size: 898 KiB

View file

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View file

@ -1,6 +1,7 @@
from flask import g, request, Flask from flask import g, request, Flask
from flask_login import current_user from flask_login import current_user
from typing import Optional from typing import Optional
from lenticular_cloud.model import db, User
LANGUAGES = { LANGUAGES = {
'en': 'English', 'en': 'English',

View file

@ -15,9 +15,9 @@ def before_request():
try: try:
resp = current_app.oauth.session.get('/userinfo') resp = current_app.oauth.session.get('/userinfo')
data = resp.json() data = resp.json()
if not current_user.is_authenticated or resp.status_code is not 200: if not current_user.is_authenticated or resp.status_code != 200:
return redirect_login() return redirect_login()
if 'admin' not in data['groups']: if 'groups' not in data or 'admin' not in data['groups']:
return 'Not an admin', 403 return 'Not an admin', 403
except TokenExpiredError: except TokenExpiredError:
return redirect_login() return redirect_login()

View file

@ -86,7 +86,8 @@ def login():
login_challenge = request.args.get('login_challenge') login_challenge = request.args.get('login_challenge')
try: try:
login_request = current_app.hydra_api.get_login_request(login_challenge) login_request = current_app.hydra_api.get_login_request(login_challenge)
except ory_hydra_client.exceptions.ApiException: except ory_hydra_client.exceptions.ApiException as e:
logger.exception("could not fetch login request")
return redirect(url_for('frontend.index')) return redirect(url_for('frontend.index'))
if login_request.skip: if login_request.skip:

View file

@ -31,7 +31,7 @@ def redirect_login():
def before_request(): def before_request():
try: try:
resp = current_app.oauth.session.get('/userinfo') resp = current_app.oauth.session.get('/userinfo')
if not current_user.is_authenticated or resp.status_code is not 200: if not current_user.is_authenticated or resp.status_code != 200:
logger.info('user not logged in redirect') logger.info('user not logged in redirect')
return redirect_login() return redirect_login()
except TokenExpiredError: except TokenExpiredError:
@ -72,7 +72,7 @@ def init_login_manager(app):
if not token: if not token:
flash("Failed to log in.", category="error") flash("Failed to log in.", category="error")
return False return False
print(f'debug ---------------{token}') #print(f'debug ---------------{token}')
resp = blueprint.session.get("/userinfo") resp = blueprint.session.get("/userinfo")
if not resp.ok: if not resp.ok:

4
lenticular_cloud/wsgi.py Normal file
View file

@ -0,0 +1,4 @@
from .app import create_app
application = create_app()

1
requirements-dev.txt Normal file
View file

@ -0,0 +1 @@
flask-debug

View file

@ -17,5 +17,4 @@ requests_oauthlib
blinker blinker
ory-hydra-client ory-hydra-client
flask-debug

60
schema/lenticular.ldif Normal file
View file

@ -0,0 +1,60 @@
dn: cn=lenticular,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: lenticular
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.1
NAME 'masterPasswordEnable'
DESC 'is the master password enabled'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
SINGLE-VALUE )
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.2
NAME 'authRole'
DESC 'is the master password enabled'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.3
NAME ( 'altMail' )
DESC 'RFC1274: RFC822 Mailbox'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.4
NAME 'gpgPublicKey'
DESC 'pgpPublicKey as ascii text'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.5
NAME 'totpSecret'
DESC 'TOTP secret as base32'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.1
NAME 'service'
DESC 'schema for a service'
SUP top STRUCTURAL
MUST uid
MAY ( masterPasswordEnable $ mail ) )
olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.2
NAME 'LenticularUser'
DESC 'a Lenticular user'
SUP top AUXILIARY
MUST uid
MAY ( authRole $ altMail $ gpgPublicKey ) )
olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.3
NAME 'LenticularGroup'
DESC 'a Lenticular group'
SUP top AUXILIARY
MUST cn
MAY ( authRole ) )
#olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.4
# NAME 'posixAccountAux'
# DESC 'Abstraction of an account with POSIX attributes'
# SUP top AUXILIARY
# MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
# MAY ( userPassword $ loginShell $ gecos $ description ) )
#olcObjectClasses: ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top AUXILIARY
# DESC 'Abstraction of a group of accounts'
# MUST gidNumber
# MAY ( userPassword $ memberUid $
# description ) )

View file

@ -1,25 +0,0 @@
[metadata]
name = lenticular_cloud
version = 0.0.1
description = user management portal
[options]
packages = lenticular_cloud
install_requires =
Flask
gunicorn
flask_babel
flask_wtf
flask_login
flask_sqlalchemy
Flask-Dance
ldap3
ldap3_orm
python-u2flib-server
pyotp
cryptography
requests
requests_oauthlib
blinker
ory-hydra-client
flask-debug

26
setup.py Normal file
View file

@ -0,0 +1,26 @@
from setuptools import setup, find_packages
from pathlib import Path
TESTS_DIRECTORY = 'test'
def get_requirements():
return []
with Path("./requirements.txt").open('r') as fd:
return fd.readlines()
setup(
name='lenticular_cloud',
version='0.0.1',
description='A useful module',
author='tuxcoder',
author_email='git@o-g.at',
packages=find_packages(exclude=(TESTS_DIRECTORY,)),
include_package_data=True,
install_requires=get_requirements(), # external packages as dependencies
entry_points = {
'console_scripts': ['lenticular_cloud-cli=lenticular_cloud.cli:entry_point']
}
)

View file

@ -1,5 +1,5 @@
[tox] #[tox]
isolated_build = true #isolated_build = true
[testenv] [testenv]
deps = deps =

View file

@ -1,9 +1,8 @@
import logging import logging
from lenticular_cloud.app import init_app from lenticular_cloud.app import create_app
name = 'oidc_provider' app = create_app()
app = init_app(name)
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)