bugfixes, cleanup
2
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
|||
/venv
|
||||
/tmp
|
||||
/production.cfg
|
||||
/lenticular_cloud/production.cfg
|
||||
/pki
|
||||
signing_key.pem
|
||||
node_modules
|
||||
|
|
4
MANIFEST.in
Normal file
|
@ -0,0 +1,4 @@
|
|||
recursive-include lenticular_cloud/template *
|
||||
recursive-include lenticular_cloud/static **
|
||||
include lenticular_cloud/*.cfg
|
||||
|
|
@ -32,3 +32,9 @@ Tested Services
|
|||
* ~~Postfix~~/Dovecot (~~OIDC~~/client cert/password)
|
||||
* prosody (client cert/~~password~~)
|
||||
|
||||
|
||||
|
||||
Oauth2 Settings:
|
||||
----------------
|
||||
|
||||
callback url: `${domain}/
|
||||
|
|
8
cli.py
Normal 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
|
@ -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"
|
||||
}
|
|
@ -7,6 +7,7 @@ import time
|
|||
import subprocess
|
||||
import ory_hydra_client as hydra
|
||||
import ory_hydra_client.api.admin_api as hydra_admin_api
|
||||
import os
|
||||
|
||||
from ldap3 import Connection, Server, ALL
|
||||
|
||||
|
@ -26,11 +27,12 @@ def init_oauth2(app):
|
|||
|
||||
|
||||
|
||||
def init_app(name=None):
|
||||
name = name or __name__
|
||||
app = Flask(name)
|
||||
def create_app():
|
||||
name = "lenticular_cloud"
|
||||
app = Flask(name, template_folder='template')
|
||||
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()
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ SESSION_COOKIE_NAME='lc_session'
|
|||
SUBJECT_ID_HASH_SALT = 'salt'
|
||||
PREFERRED_URL_SCHEME = 'https'
|
||||
|
||||
DATA_FOLDER = "./data"
|
||||
DATA_FOLDER = "../data"
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATA_FOLDER}/db.sqlite'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
|
@ -70,7 +70,7 @@ class TotpAuthProvider(AuthProvider):
|
|||
def check_auth(user, form):
|
||||
data = form.data['totp']
|
||||
if data is not None:
|
||||
print(f'data totp: {data}')
|
||||
#print(f'data totp: {data}')
|
||||
if len(user.totps) == 0: # migration, TODO remove
|
||||
return True
|
||||
for totp in user.totps:
|
||||
|
@ -84,5 +84,5 @@ AUTH_PROVIDER_LIST = [
|
|||
TotpAuthProvider
|
||||
]
|
||||
|
||||
print(LdapAuthProvider.get_name())
|
||||
#print(LdapAuthProvider.get_name())
|
||||
|
||||
|
|
61
lenticular_cloud/cli.py
Normal 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()
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from flask import current_app
|
||||
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
|
||||
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.hashed import hashed
|
||||
from flask_login import UserMixin
|
||||
|
@ -19,6 +19,7 @@ from flask_sqlalchemy import SQLAlchemy, orm
|
|||
from datetime import datetime
|
||||
import uuid
|
||||
import pyotp
|
||||
from typing import Optional
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -226,6 +227,8 @@ class User(EntryBase):
|
|||
db.String(length=36), primary_key=True, default=generate_uuid)
|
||||
username = db.Column(
|
||||
db.String, unique=True, nullable=False)
|
||||
alternative_email = db.Column(
|
||||
db.String, nullable=True)
|
||||
created_at = db.Column(db.DateTime, nullable=False,
|
||||
default=datetime.now)
|
||||
modified_at = db.Column(db.DateTime, nullable=False,
|
||||
|
@ -236,7 +239,7 @@ class User(EntryBase):
|
|||
|
||||
dn = "uid={uid},{base_dn}"
|
||||
base_dn = "ou=users,{_base_dn}"
|
||||
object_classes = ["top", "inetOrgPerson", "LenticularUser"]
|
||||
object_classes = ["inetOrgPerson"] #, "LenticularUser"]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._ldap_object = None
|
||||
|
@ -247,57 +250,34 @@ class User(EntryBase):
|
|||
return True # TODO
|
||||
|
||||
def get(self, key):
|
||||
print(f'getitem: {key}')
|
||||
print(f'getitem: {key}') # TODO
|
||||
|
||||
def make_writeable(self):
|
||||
self._ldap_object = self._ldap_object.entry_writable()
|
||||
|
||||
@property
|
||||
def groups(self):
|
||||
def groups(self) -> list[str]:
|
||||
if self.username == 'tuxcoder':
|
||||
return [Group(name='admin')]
|
||||
else:
|
||||
return []
|
||||
|
||||
@property
|
||||
def entry_dn(self):
|
||||
def entry_dn(self) -> str:
|
||||
return self._ldap_object.entry_dn
|
||||
|
||||
@property
|
||||
def fullname(self):
|
||||
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):
|
||||
def email(self) -> str:
|
||||
domain = current_app.config['DOMAIN']
|
||||
return f'{self.username}@{domain}'
|
||||
return self._ldap_object.mail
|
||||
|
||||
@property
|
||||
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):
|
||||
def change_password(self, password_new: str) -> bool:
|
||||
self.make_writeable()
|
||||
password_hashed = crypt.crypt(password_new)
|
||||
self._ldap_object.userPassword = ('{CRYPT}' + password_hashed).encode()
|
||||
self.ldap_commit()
|
||||
return True
|
||||
|
||||
class _query(EntryBase._query):
|
||||
|
||||
|
@ -312,7 +292,7 @@ class User(EntryBase):
|
|||
user._ldap_object = ldap_object
|
||||
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)))
|
||||
if len(result) > 0:
|
||||
return result[0]
|
||||
|
|
Before Width: | Height: | Size: 876 KiB After Width: | Height: | Size: 876 KiB |
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 730 KiB |
Before Width: | Height: | Size: 699 KiB After Width: | Height: | Size: 699 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 898 KiB After Width: | Height: | Size: 898 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
@ -1,6 +1,7 @@
|
|||
from flask import g, request, Flask
|
||||
from flask_login import current_user
|
||||
from typing import Optional
|
||||
from lenticular_cloud.model import db, User
|
||||
|
||||
LANGUAGES = {
|
||||
'en': 'English',
|
||||
|
|
|
@ -15,9 +15,9 @@ def before_request():
|
|||
try:
|
||||
resp = current_app.oauth.session.get('/userinfo')
|
||||
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()
|
||||
if 'admin' not in data['groups']:
|
||||
if 'groups' not in data or 'admin' not in data['groups']:
|
||||
return 'Not an admin', 403
|
||||
except TokenExpiredError:
|
||||
return redirect_login()
|
||||
|
|
|
@ -86,7 +86,8 @@ def login():
|
|||
login_challenge = request.args.get('login_challenge')
|
||||
try:
|
||||
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'))
|
||||
|
||||
if login_request.skip:
|
||||
|
|
|
@ -31,7 +31,7 @@ def redirect_login():
|
|||
def before_request():
|
||||
try:
|
||||
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')
|
||||
return redirect_login()
|
||||
except TokenExpiredError:
|
||||
|
@ -72,7 +72,7 @@ def init_login_manager(app):
|
|||
if not token:
|
||||
flash("Failed to log in.", category="error")
|
||||
return False
|
||||
print(f'debug ---------------{token}')
|
||||
#print(f'debug ---------------{token}')
|
||||
|
||||
resp = blueprint.session.get("/userinfo")
|
||||
if not resp.ok:
|
||||
|
|
4
lenticular_cloud/wsgi.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from .app import create_app
|
||||
|
||||
application = create_app()
|
||||
|
1
requirements-dev.txt
Normal file
|
@ -0,0 +1 @@
|
|||
flask-debug
|
|
@ -17,5 +17,4 @@ requests_oauthlib
|
|||
blinker
|
||||
ory-hydra-client
|
||||
|
||||
flask-debug
|
||||
|
||||
|
|
60
schema/lenticular.ldif
Normal 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 ) )
|
||||
|
25
setup.cfg
|
@ -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
|
@ -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']
|
||||
}
|
||||
)
|
||||
|
4
tox.ini
|
@ -1,5 +1,5 @@
|
|||
[tox]
|
||||
isolated_build = true
|
||||
#[tox]
|
||||
#isolated_build = true
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
|
|