add nix flake
make a restart
This commit is contained in:
tuxcoder 2023-10-09 21:58:44 +02:00
parent 536668d8b9
commit eee18c1785
24 changed files with 509 additions and 231 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ node_modules
/dist /dist
build build
result result
nixos.qcow2

View file

@ -1,5 +1,5 @@
recursive-include lenticular_cloud/template * recursive-include lenticular_cloud/template *
recursive-include lenticular_cloud/static ** recursive-include lenticular_cloud/static **
recursive-include lenticular_cloud/migrations ** recursive-include lenticular_cloud/migrations **
include lenticular_cloud/*.cfg include lenticular_cloud/*.toml

View file

@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1673956053, "lastModified": 1696426674,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -52,17 +52,17 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1696022471, "lastModified": 1696374741,
"narHash": "sha256-3U5nqHQ9JFUoY4GJ89ErqzRmkgAhPYjbPn4vP+CpltM=", "narHash": "sha256-gt8B3G0ryizT9HSB4cCO8QoxdbsHnrQH+/BdKxOwqF0=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "336d12bc4cdb0980f5ff450d7bc599bdb4ed5e74", "rev": "8a4c17493e5c39769f79117937c79e1c88de6729",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "id": "nixpkgs",
"repo": "nixpkgs", "ref": "nixos-23.05",
"type": "github" "type": "indirect"
} }
}, },
"root": { "root": {
@ -70,7 +70,8 @@
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nix-node-package": "nix-node-package", "nix-node-package": "nix-node-package",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"tuxpkgs": "tuxpkgs"
} }
}, },
"systems": { "systems": {
@ -87,6 +88,29 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"tuxpkgs": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1696700871,
"narHash": "sha256-9VFEJEfnfnCS1+kLznxd+OiDYdMnLP00+XR53iPfnK4=",
"ref": "refs/heads/master",
"rev": "a25f5792a256beaed2a9f944fccdea8ea7a8d44b",
"revCount": 6,
"type": "git",
"url": "ssh://git@git.o-g.at/nixpkg/tuxpkgs.git"
},
"original": {
"type": "git",
"url": "ssh://git@git.o-g.at/nixpkg/tuxpkgs.git"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -1,7 +1,7 @@
{ {
description = "Lenticular cloud interface"; description = "Lenticular cloud interface";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs"; nixpkgs.url = "nixpkgs/nixos-23.05";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
flake-compat = { # for shell.nix flake-compat = { # for shell.nix
url = "github:edolstra/flake-compat"; url = "github:edolstra/flake-compat";
@ -11,16 +11,23 @@
url = "github:mkg20001/nix-node-package"; url = "github:mkg20001/nix-node-package";
flake = false; flake = false;
}; };
tuxpkgs = {
url = "git+ssh://git@git.o-g.at/nixpkg/tuxpkgs.git";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
}; };
outputs = { self, nixpkgs, nix-node-package, flake-utils, ... }: outputs = { self, nixpkgs, nix-node-package, flake-utils, tuxpkgs, ... }:
flake-utils.lib.eachDefaultSystem (system: let flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system}.extend (import ./overlay.nix); pkgs = nixpkgs.legacyPackages.${system}.extend (import ./overlay.nix);
in rec { in rec {
formatter = pkgs.nixpkgs-fmt; formatter = pkgs.nixpkgs-fmt;
devShells.default = pkgs.python3.withPackages (ps: ( devShells.default = pkgs.mkShell {packages = [
pkgs.lenticular-cloud.propagatedBuildInputs ++ (pkgs.python3.withPackages (ps: (
pkgs.lenticular-cloud.testBuildInputs pkgs.lenticular-cloud.propagatedBuildInputs ++
)); pkgs.lenticular-cloud.testBuildInputs
)))
];};
packages.default = pkgs.lenticular-cloud; packages.default = pkgs.lenticular-cloud;
@ -37,7 +44,18 @@
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
self.nixosModules.default self.nixosModules.default
tuxpkgs.nixosModules.ory-hydra
"${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix" "${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix"
({...}:{
services.lenticular-cloud.enable = true;
services.ory-hydra = {
enable = true;
admin_domain = "admin-hydra.local";
};
networking.hosts = {"::1" = [ "admin-hydra.local" ]; };
services.getty.autologinUser = "root";
virtualisation.qemu.options = ["-vga none"];
})
]; ];
}; };
}; };

View file

@ -1,11 +1,14 @@
from flask.app import Flask from flask.app import Flask
from flask import g, redirect, request from flask import g
from flask.helpers import url_for from flask.json.provider import DefaultJSONProvider
import time import time
import subprocess import subprocess
from lenticular_cloud.lenticular_services import lenticular_services from lenticular_cloud.lenticular_services import lenticular_services
from ory_hydra_client import Client
import os import os
import toml
import json
import logging
from uuid import UUID
from pathlib import Path from pathlib import Path
@ -15,6 +18,7 @@ from .translations import init_babel
from .model import db, migrate from .model import db, migrate
from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views, admin_views, oauth2_views from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views, admin_views, oauth2_views
logger = logging.getLogger(__name__)
def get_git_hash(): def get_git_hash():
try: try:
@ -23,13 +27,33 @@ def get_git_hash():
return '' return ''
def create_app() -> Flask:
class CustomJSONEncoder(DefaultJSONProvider):
def default(self, obj):
if isinstance(obj, UUID):
# if the obj is uuid, we simply return the value of uuid
return obj.hex
return super().default(obj)
def create_app_raw(config_files: list[Path]) -> Flask:
name = "lenticular_cloud" name = "lenticular_cloud"
app = Flask(name, template_folder='template') app = Flask(name, template_folder='template')
app.config.from_pyfile('application.cfg') app.json_provider_class = CustomJSONEncoder
active_cfg = os.getenv('CONFIG_FILE', 'production.cfg')
app.config.from_pyfile(active_cfg) # config
app.config.from_file('config_development.toml', toml.load)
for config_file in config_files:
active_cfg = str(config_file.absolute())
if active_cfg.endswith(".toml"):
logger.info(f"load toml config file from {active_cfg}")
app.config.from_file(active_cfg, toml.load)
elif active_cfg.endswith(".json"):
logger.info(f"load json config file from {active_cfg}")
app.config.from_file(active_cfg, json.load)
else:
logger.info(f"load pyfile config file from {active_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()
db.init_app(app) db.init_app(app)
@ -45,9 +69,9 @@ def create_app() -> Flask:
# host=app.config['HYDRA_ADMIN_URL'], # host=app.config['HYDRA_ADMIN_URL'],
# username=app.config['HYDRA_ADMIN_USER'], # username=app.config['HYDRA_ADMIN_USER'],
# 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.init_app(app)
init_login_manager(app) init_login_manager(app) # has to be after hydra_service
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)
@ -66,3 +90,12 @@ def create_app() -> Flask:
return app return app
def create_app() -> Flask:
evn_var = os.getenv('CONFIG_FILE', None)
if isinstance(evn_var, str):
active_cfgs = list(map(Path, evn_var.split(':')))
else:
active_cfgs = [ Path() / 'production.toml' ]
return create_app_raw(active_cfgs)

View file

@ -1,4 +1,5 @@
import argparse import argparse
from typing import Optional
from .model import db, User 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
@ -64,7 +65,7 @@ def cli_signup(args) -> None:
if args.signup_id is not None: if args.signup_id is not None:
user = User.query.get(args.signup_id) user = User.query.get(args.signup_id)
if user == None: if user is None:
print("user not found") print("user not found")
return return
user.enabled = True user.enabled = True
@ -87,7 +88,7 @@ def cli_run(app: Flask, args) -> None:
def cli_db_upgrade(args) -> None: def cli_db_upgrade(args) -> None:
app = create_app() app = create_app()
migration_dir = Path(app.root_path) / 'migrations' migration_dir = Path(app.root_path) / 'migrations'
upgrade( str(migration_dir) ) upgrade( str(migration_dir), revision='head' )
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -6,16 +6,11 @@ PREFERRED_URL_SCHEME = 'https'
DATA_FOLDER = "../data" DATA_FOLDER = "../data"
SCRIPT_LOCATION="lenticular_cloud:migrations" SCRIPT_LOCATION="lenticular_cloud:migrations"
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATA_FOLDER}/db.sqlite' SQLALCHEMY_DATABASE_URI = 'sqlite:///../data/db.sqlite'
SQLALCHEMY_TRACK_MODIFICATIONS=False SQLALCHEMY_TRACK_MODIFICATIONS = false
LDAP_URL = 'ldaps://ldap.example.org'
LDAP_BASE_DN = 'dc=example,dc=com'
LDAP_BIND_DN = 'cn=admin,' + LDAP_BASE_DN
LDAP_BIND_PW = '123456'
PKI_PATH = f'{DATA_FOLDER}/pki' PKI_PATH = "../data/pki"
DOMAIN = 'example.com' DOMAIN = 'example.com'
#SERVER_NAME = f'account.{ DOMAIN }:9090' #SERVER_NAME = f'account.{ DOMAIN }:9090'
@ -30,17 +25,24 @@ OAUTH_ID = 'identiy_provider'
OAUTH_SECRET = 'ThisIsNotSafe' OAUTH_SECRET = 'ThisIsNotSafe'
[LENTICULAR_CLOUD_SERVICES.jabber]
app_token = true
# client_cert= true
LENTICULAR_CLOUD_SERVICES = {
'jabber': { [LENTICULAR_CLOUD_SERVICES.mail-cardav]
'client_cert': True, app_token = true
'pki_config':{
'email': '{username}@jabber.{domain}' # LENTICULAR_CLOUD_SERVICES = {
}, # 'jabber': {
'app_token': True # 'client_cert': True,
}, # 'pki_config':{
'mail-cardav': { # 'email': '{username}@jabber.{domain}'
'client_cert': False, # },
'app_token': True # 'app_token': True
} # },
} # 'mail-cardav': {
# 'client_cert': False,
# 'app_token': True
# }
# }

View file

@ -1,14 +1,56 @@
from flask import Flask
from ory_hydra_client import Client from ory_hydra_client import Client
from typing import Optional from typing import Optional
from ory_hydra_client.api.o_auth_2 import list_o_auth_2_clients, create_o_auth_2_client
from ory_hydra_client.models.o_auth_20_client import OAuth20Client
class HydraService: class HydraService:
def __init__(self): def __init__(self) -> None:
self._hydra_client: Optional[Client] = None self._hydra_client: Optional[Client] = None
self._oauth_client: Optional[Client] = None self._oauth_client: Optional[Client] = None
self.client_id = ''
self.client_secret = ''
def init_app(self, app: Flask) -> None:
self.set_hydra_client(Client(base_url=app.config['HYDRA_ADMIN_URL']))
client_name = app.config['OAUTH_ID']
client_secret = app.config['OAUTH_SECRET']
clients = list_o_auth_2_clients.sync_detailed(_client=self.hydra_client).parsed
if clients is None:
raise RuntimeError("could not get clients list")
client: Optional[OAuth20Client] = None
for c in clients:
if c.client_name == client_name:
client = c
break
if client is None:
domain = app.config['DOMAIN']
client = OAuth20Client(
client_name="identiy_provider",
# client_id=client_id,
client_secret=client_secret,
response_types=["code", "id_token"],
scope="openid profile manage",
grant_types=["authorization_code", "refresh_token"],
redirect_uris=[ f"https://{domain}/oauth/authorized" ],
token_endpoint_auth_method="client_secret_basic",
)
ret = create_o_auth_2_client.sync(json_body=client, _client=self.hydra_client)
if ret is None:
raise RuntimeError("could not crate account")
if type(client.client_id) is not str:
raise RuntimeError("could not parse client_id from ory-hydra")
self.client_id = client.client_id
self.client_secret = client_secret
@property @property
def hydra_client(self) -> Client: def hydra_client(self) -> Client:
if self._hydra_client is None: if self._hydra_client is None:

View file

@ -0,0 +1,63 @@
"""init
Revision ID: a74320a5d7a1
Revises:
Create Date: 2023-10-01 20:15:53.795636
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a74320a5d7a1'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('group',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('modified_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('user',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('password_hashed', sa.String(), nullable=False),
sa.Column('alternative_email', sa.String(), nullable=True),
sa.Column('last_login', sa.DateTime(), nullable=True),
sa.Column('enabled', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('modified_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_table('app_token',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('service_name', sa.String(), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('token', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('last_used', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('modified_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('webauthn_credential')
op.drop_table('totp')
op.drop_table('app_token')
op.drop_table('user')
op.drop_table('group')
# ### end Alembic commands ###

View file

@ -20,18 +20,6 @@ depends_on = None
def upgrade(): def upgrade():
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
from ldap3_orm import Reader
from ldap3 import Connection, Server, ALL
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! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('app_token', op.create_table('app_token',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
@ -50,21 +38,36 @@ def upgrade():
op.add_column('user', sa.Column('password_hashed', sa.String(), server_default="", nullable=False)) 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)) op.add_column('user', sa.Column('enabled', sa.Boolean(), server_default="false", nullable=True))
# ### end Alembic commands ### # ### end Alembic commands ###
try:
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
from ldap3_orm import Reader
from ldap3 import Connection, Server, ALL
op.execute(User.__table__.update().values({'enabled': True})) app = current_app
conn = op.get_bind() server = Server(app.config['LDAP_URL'], get_info=ALL)
users = conn.execute(User.__table__.select()) 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}"
for user in users:
print(f"migrating user {user.username}")
reader = Reader(ldap_conn, object_def, user_base_dn, f'(uid={user.username})') op.execute(User.__table__.update().values({'enabled': True}))
result = reader.search() conn = op.get_bind()
if len(result) == 0: users = conn.execute(User.__table__.select())
print(f"WARNING: could not migrate user {user.username}")
continue for user in users:
ldap_object = result[0] print(f"migrating user {user.username}")
password_hashed = ldap_object.userPassword[0].decode().replace('{CRYPT}','') reader = Reader(ldap_conn, object_def, user_base_dn, f'(uid={user.username})')
op.execute(User.__table__.update().values({'password_hashed': password_hashed}).where(User.id == user.id)) 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))
except ModuleNotFoundError:
print("ignore import warning")

View file

@ -18,10 +18,10 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.add_column('app_token', sa.Column('user_id', sa.String(length=36), nullable=False)) with op.batch_alter_table('app_token') as batch_op:
op.add_column('app_token', sa.Column('last_used', sa.DateTime(), nullable=True)) batch_op.add_column(sa.Column('user_id', sa.Uuid, nullable=False))
batch_op.add_column(sa.Column('last_used', sa.DateTime(), nullable=True))
op.create_foreign_key(None, 'app_token', 'user', ['user_id'], ['id']) op.create_foreign_key(None, 'app_token', 'user', ['user_id'], ['id'])
op.add_column('totp', sa.Column('last_used', sa.DateTime(), nullable=True))
tmp_table = sa.Table('_alembic_tmp_user', sa.MetaData()) tmp_table = sa.Table('_alembic_tmp_user', sa.MetaData())
op.execute(sa.schema.DropTable(tmp_table, if_exists=True)) op.execute(sa.schema.DropTable(tmp_table, if_exists=True))
with op.batch_alter_table('user') as batch_op: with op.batch_alter_table('user') as batch_op:

View file

@ -20,7 +20,7 @@ def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('webauthn_credential', op.create_table('webauthn_credential',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.String(length=36), nullable=False), sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('user_handle', sa.String(length=64), nullable=False), sa.Column('user_handle', sa.String(length=64), nullable=False),
sa.Column('credential_data', sa.LargeBinary(), nullable=False), sa.Column('credential_data', sa.LargeBinary(), nullable=False),
sa.Column('name', sa.String(length=250), nullable=True), sa.Column('name', sa.String(length=250), nullable=True),

View file

@ -11,6 +11,7 @@ import logging
import crypt import crypt
import secrets import secrets
import string import string
from sqlalchemy import null
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column, relationship, declarative_base from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column, relationship, declarative_base
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.model import Model, DefaultMeta from flask_sqlalchemy.model import Model, DefaultMeta
@ -38,9 +39,10 @@ if TYPE_CHECKING:
pass pass
else: else:
BaseModel: Type[_FSAModel] = db.Model BaseModel: Type[_FSAModel] = db.Model
class ModelUpdatedMixin: class ModelUpdatedMixin:
created_at: Mapped[datetime] = mapped_column(db.DateTime, default=datetime.now()) created_at: Mapped[datetime] = mapped_column(db.DateTime, default=datetime.now(), nullable=False)
last_update: Mapped[datetime] = mapped_column(db.DateTime, default=datetime.now(), onupdate=datetime.now) modified_at: Mapped[datetime] = mapped_column(db.DateTime, default=datetime.now(), onupdate=datetime.now, nullable=False)
class SecurityUser(UserMixin): class SecurityUser(UserMixin):
@ -151,28 +153,27 @@ def generate_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
class User(BaseModel): class User(BaseModel, ModelUpdatedMixin):
id = db.Column( id: Mapped[uuid.UUID] = mapped_column(db.Uuid, primary_key=True, default=uuid.uuid4)
db.String(length=36), primary_key=True, default=generate_uuid) username: Mapped[str] = mapped_column(db.String, unique=True, nullable=False)
username = db.Column( password_hashed: Mapped[str] = mapped_column(db.String, nullable=False)
db.String, unique=True, nullable=False) alternative_email: Mapped[Optional[str]] = mapped_column( db.String, nullable=True)
password_hashed = db.Column( last_login: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True)
db.String, 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,
default=datetime.now, onupdate=datetime.now)
last_login = db.Column(db.DateTime, nullable=True)
enabled = db.Column(db.Boolean, nullable=False, default=False) enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, default=False)
app_tokens = db.relationship('AppToken', back_populates='user') app_tokens: Mapped[List['AppToken']] = relationship('AppToken', back_populates='user')
totps = db.relationship('Totp', back_populates='user') # totps: Mapped[List['Totp']] = relationship('Totp', back_populates='user', default_factory=list)
webauthn_credentials = db.relationship('WebauthnCredential', back_populates='user', cascade='delete,delete-orphan', passive_deletes=True) # webauthn_credentials: Mapped[List['WebauthnCredential']] = relationship('WebauthnCredential', back_populates='user', cascade='delete,delete-orphan', passive_deletes=True, default_factory=list)
def __init__(self, **kwargs): @property
def totps(self) -> List['Totp']:
return []
@property
def webauthn_credentials(self) -> List['WebauthnCredential']:
return []
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
@property @property
@ -207,56 +208,53 @@ class User(BaseModel):
return None return None
class AppToken(BaseModel): class AppToken(BaseModel, ModelUpdatedMixin):
id = db.Column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
service_name = db.Column(db.String, nullable=False) service_name: Mapped[str] = mapped_column(nullable=False)
user_id = db.Column( user_id: Mapped[uuid.UUID] = mapped_column(
db.String(length=36), db.Uuid,
db.ForeignKey(User.id), nullable=False) db.ForeignKey(User.id), nullable=False)
user = db.relationship(User) user: Mapped[User] = relationship(User, back_populates="app_tokens")
token = db.Column(db.String, nullable=False) token: Mapped[str] = mapped_column(nullable=False)
name = db.Column(db.String, nullable=False) name: Mapped[str] = mapped_column(nullable=False)
last_used = db.Column(db.DateTime, nullable=True) last_used: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True, default=None)
@staticmethod @staticmethod
def new(service: Service): def new(user: User, service: Service, name: str):
app_token = AppToken()
app_token.service_name = service.name
alphabet = string.ascii_letters + string.digits alphabet = string.ascii_letters + string.digits
app_token.token = ''.join(secrets.choice(alphabet) for i in range(12)) token = ''.join(secrets.choice(alphabet) for i in range(12))
return app_token return AppToken(service_name=service.name, token=token, user=user, name=name)
class Totp(BaseModel): class Totp(BaseModel, ModelUpdatedMixin):
id = db.Column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
secret = db.Column(db.String, nullable=False) secret: Mapped[str] = mapped_column(db.String, nullable=False)
name = db.Column(db.String, nullable=False) name: Mapped[str] = mapped_column(db.String, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
last_used = db.Column(db.DateTime, nullable=True)
user_id = db.Column( user_id: Mapped[uuid.UUID] = mapped_column(
db.String(length=36), db.Uuid,
db.ForeignKey(User.id), nullable=False) db.ForeignKey(User.id), nullable=False)
user = db.relationship(User) # user: Mapped[User] = relationship(User, back_populates="totp")
last_used: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True, default=None)
def verify(self, token: str) -> bool: def verify(self, token: str) -> bool:
totp = pyotp.TOTP(self.secret) totp = pyotp.TOTP(self.secret)
return totp.verify(token) return totp.verify(token)
class WebauthnCredential(BaseModel): # pylint: disable=too-few-public-methods class WebauthnCredential(BaseModel, ModelUpdatedMixin): # pylint: disable=too-few-public-methods
"""Webauthn credential model""" """Webauthn credential model"""
id = db.Column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id = db.Column(db.String(length=36), db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) user_id: Mapped[uuid.UUID] = mapped_column(db.Uuid, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
user_handle = db.Column(db.String(64), nullable=False) user_handle: Mapped[str] = mapped_column(db.String(64), nullable=False)
credential_data = db.Column(db.LargeBinary, nullable=False) credential_data: Mapped[bytes] = mapped_column(db.LargeBinary, nullable=False)
name = db.Column(db.String(250)) name: Mapped[str] = mapped_column(db.String(250), nullable=False)
registered = db.Column(db.DateTime, default=datetime.utcnow) registered: Mapped[datetime] = mapped_column(db.DateTime, default=datetime.utcnow, nullable=False)
user = db.relationship('User', back_populates='webauthn_credentials') # user = db.relationship('User', back_populates='webauthn_credentials')
class Group(BaseModel): class Group(BaseModel, ModelUpdatedMixin):
id = db.Column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name = db.Column(db.String(), nullable=False, unique=True) name: Mapped[str] = mapped_column(db.String(), nullable=False, unique=True)

View file

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<p> <p>
The application "{{ client.client_id }}" requested the following scopes: {{ requested_scope }} The application "{{ client.client_name }}" (id: {{ client.client_id }}) requested the following scopes: {{ requested_scope }}
</p> </p>
<p> Allow this app to access that data?</p> <p> Allow this app to access that data?</p>

View file

@ -46,7 +46,7 @@ async def index() -> ResponseReturnValue:
@admin_views.route('/user', methods=['GET']) @admin_views.route('/user', methods=['GET'])
async def users(): async def users() -> ResponseReturnValue:
users = User.query.all() # type: Iterable[User] users = User.query.all() # type: Iterable[User]
return render_template('admin/users.html.j2', users=users) return render_template('admin/users.html.j2', users=users)

View file

@ -21,6 +21,7 @@ from ory_hydra_client.api.o_auth_2 import get_o_auth_2_consent_request, accept_o
from ory_hydra_client import models as ory_hydra_m from ory_hydra_client import models as ory_hydra_m
from ory_hydra_client.models import TheRequestPayloadUsedToAcceptALoginOrConsentRequest, TheRequestPayloadUsedToAcceptAConsentRequest, GenericError from ory_hydra_client.models import TheRequestPayloadUsedToAcceptALoginOrConsentRequest, TheRequestPayloadUsedToAcceptAConsentRequest, GenericError
from typing import Optional from typing import Optional
from uuid import uuid4
from ..model import db, User, SecurityUser from ..model import db, User, SecurityUser
from ..form.auth import ConsentForm, LoginForm, RegistrationForm from ..form.auth import ConsentForm, LoginForm, RegistrationForm
@ -136,7 +137,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.filter_by(username=session['username']).first() # Optional[User] user = User.query.filter_by(username=session['username']).first_or_404()
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\
@ -154,7 +155,7 @@ async def login_auth() -> ResponseReturnValue:
# db.session.add(db_user) # db.session.add(db_user)
# db.session.commit() # db.session.commit()
subject = user.id subject = str(user.id)
user.last_login = datetime.now() user.last_login = datetime.now()
db.session.commit() db.session.commit()
resp = await accept_o_auth_2_login_request.asyncio(_client=hydra_service.hydra_client, resp = await accept_o_auth_2_login_request.asyncio(_client=hydra_service.hydra_client,
@ -170,8 +171,9 @@ async def login_auth() -> ResponseReturnValue:
@auth_views.route('/webauthn/pkcro', methods=['POST']) @auth_views.route('/webauthn/pkcro', methods=['POST'])
def webauthn_pkcro_route(): def webauthn_pkcro_route() -> ResponseReturnValue:
"""login webauthn pkcro route""" """login webauthn pkcro route"""
return '', 404
user = User.query.filter(User.id == session.get('webauthn_login_user_id')).one() #type: User user = User.query.filter(User.id == session.get('webauthn_login_user_id')).one() #type: User
form = ButtonForm() form = ButtonForm()
@ -213,6 +215,7 @@ def sign_up_submit():
form = RegistrationForm() form = RegistrationForm()
if form.validate_on_submit(): if form.validate_on_submit():
user = User() user = User()
user.id = uuid4()
user.username = form.data['username'] user.username = form.data['username']
user.password_hashed = 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']

View file

@ -21,7 +21,7 @@ from urllib.parse import urlencode, parse_qs
from random import SystemRandom from random import SystemRandom
import string import string
from collections.abc import Iterable from collections.abc import Iterable
from typing import Optional, Mapping, Iterator, List from typing import Optional, Mapping, Iterator, List, Any
from ..model import db, User, SecurityUser, Totp, AppToken, WebauthnCredential from ..model import db, User, SecurityUser, Totp, AppToken, WebauthnCredential
from ..form.frontend import ClientCertForm, TOTPForm, \ from ..form.frontend import ClientCertForm, TOTPForm, \
@ -38,11 +38,16 @@ 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__)
def get_current_user() -> User:
user_any: Any = current_user
user: User = user_any
return user
def before_request() -> Optional[ResponseReturnValue]: def before_request() -> Optional[ResponseReturnValue]:
try: try:
resp = oauth2.custom.get('/userinfo') resp = oauth2.custom.get('/userinfo')
if not current_user.is_authenticated or resp.status_code != 200: if not get_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 MissingTokenError: except MissingTokenError:
@ -79,7 +84,7 @@ def client_cert() -> ResponseReturnValue:
client_certs = {} client_certs = {}
for service in lenticular_services.values(): for service in lenticular_services.values():
client_certs[str(service.name)] = \ client_certs[str(service.name)] = \
pki.get_client_certs(current_user, service) pki.get_client_certs(get_current_user(), service)
return render_template( return render_template(
'frontend/client_cert.html.j2', 'frontend/client_cert.html.j2',
@ -91,7 +96,7 @@ def client_cert() -> ResponseReturnValue:
def get_client_cert(service_name, serial_number) -> ResponseReturnValue: def get_client_cert(service_name, serial_number) -> ResponseReturnValue:
service = lenticular_services[service_name] service = lenticular_services[service_name]
cert = pki.get_client_cert( cert = pki.get_client_cert(
current_user, service, serial_number) get_current_user(), service, serial_number)
return jsonify({ return jsonify({
'data': { 'data': {
'pem': cert.pem()} 'pem': cert.pem()}
@ -103,7 +108,7 @@ def get_client_cert(service_name, serial_number) -> ResponseReturnValue:
def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue: def revoke_client_cert(service_name, serial_number) -> ResponseReturnValue:
service = lenticular_services[service_name] service = lenticular_services[service_name]
cert = pki.get_client_cert( cert = pki.get_client_cert(
current_user, service, serial_number) get_current_user(), service, serial_number)
pki.revoke_certificate(cert) pki.revoke_certificate(cert)
return jsonify({}) return jsonify({})
@ -119,7 +124,7 @@ def client_cert_new(service_name) -> ResponseReturnValue:
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 = pki.signing_publickey( cert = pki.signing_publickey(
current_user, get_current_user(),
service, service,
form.data['publickey'], form.data['publickey'],
valid_time=valid_time) valid_time=valid_time)
@ -156,13 +161,15 @@ def app_token_new(service_name: str) -> ResponseReturnValue:
form = AppTokenForm() form = AppTokenForm()
if form.validate_on_submit(): if form.validate_on_submit():
app_token = AppToken.new(service) user_any = get_current_user() # type: Any
user = user_any # type: User
app_token = AppToken.new(user, service, "")
form.populate_obj(app_token) form.populate_obj(app_token)
# check for duplicate names # check for duplicate names
for user_app_token in current_user.app_tokens: for user_app_token in user.app_tokens:
if user_app_token.name == app_token.name: if user_app_token.name == app_token.name:
return 'name already exist', 400 return 'name already exist', 400
current_user.app_tokens.append(app_token) user.app_tokens.append(app_token)
db.session.commit() db.session.commit()
return render_template('frontend/app_token_new_show.html.j2', service=service, app_token=app_token) return render_template('frontend/app_token_new_show.html.j2', service=service, app_token=app_token)
@ -180,7 +187,7 @@ def app_token_delete(service_name: str, app_token_name: str) -> ResponseReturnVa
service = lenticular_services[service_name] service = lenticular_services[service_name]
if form.validate_on_submit(): if form.validate_on_submit():
app_token = current_user.get_token(service, app_token_name) app_token = get_current_user().get_token(service, app_token_name)
if app_token is None: if app_token is None:
return 'not found', 404 return 'not found', 404
db.session.delete(app_token) db.session.delete(app_token)
@ -199,9 +206,9 @@ def totp_new() -> ResponseReturnValue:
form = TOTPForm() form = TOTPForm()
if form.validate_on_submit(): if form.validate_on_submit():
totp = Totp(name=form.data['name'], secret=form.data['secret']) totp = Totp(name=form.data['name'], secret=form.data['secret'], user=get_current_user())
if totp.verify(form.data['token']): if totp.verify(form.data['token']):
current_user.totps.append(totp) get_current_user().totps.append(totp)
db.session.commit() db.session.commit()
return jsonify({ return jsonify({
'status': 'ok'}) 'status': 'ok'})
@ -269,7 +276,7 @@ def random_string(length=32) -> str:
def webauthn_pkcco_route() -> ResponseReturnValue: def webauthn_pkcco_route() -> ResponseReturnValue:
"""get publicKeyCredentialCreationOptions""" """get publicKeyCredentialCreationOptions"""
user = User.query.get(current_user.id) #type: Optional[User] user = User.query.get(get_current_user().id) #type: Optional[User]
if user is None: if user is None:
return 'internal error', 500 return 'internal error', 500
user_handle = random_string() user_handle = random_string()
@ -287,7 +294,7 @@ def webauthn_pkcco_route() -> ResponseReturnValue:
def webauthn_register_route() -> ResponseReturnValue: def webauthn_register_route() -> ResponseReturnValue:
"""register credential for current user""" """register credential for current user"""
user = current_user # type: User user = get_current_user() # type: User
form = WebauthnRegisterForm() form = WebauthnRegisterForm()
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
@ -300,7 +307,7 @@ def webauthn_register_route() -> ResponseReturnValue:
AttestationObject(attestation['attestationObject'])) AttestationObject(attestation['attestationObject']))
db.session.add(WebauthnCredential( db.session.add(WebauthnCredential(
user_id=user.id, user=user,
user_handle=session.pop('webauthn_register_user_handle'), user_handle=session.pop('webauthn_register_user_handle'),
credential_data=cbor.encode(auth_data.credential_data.__dict__), credential_data=cbor.encode(auth_data.credential_data.__dict__),
name=form.name.data)) name=form.name.data))
@ -327,12 +334,12 @@ def password_change_post() -> ResponseReturnValue:
password_old = str(form.data['password_old']) password_old = str(form.data['password_old'])
password_new = str(form.data['password_new']) password_new = str(form.data['password_new'])
if not PasswordAuthProvider.check_auth_internal( if not PasswordAuthProvider.check_auth_internal(
current_user, password_old): get_current_user(), password_old):
return jsonify( return jsonify(
{'errors': {'password_old': 'Old Password is invalid'}}) {'errors': {'password_old': 'Old Password is invalid'}})
current_user.change_password(password_new) get_current_user().change_password(password_new)
logger.info(f"user {current_user.username} changed password") logger.info(f"user {get_current_user().username} changed password")
db.session.commit() db.session.commit()
return jsonify({}) return jsonify({})
return jsonify({'errors': form.errors}) return jsonify({'errors': form.errors})

View file

@ -9,6 +9,7 @@ from werkzeug.wrappers.response import Response as WerkzeugResponse
import logging import logging
from ..model import User, SecurityUser from ..model import User, SecurityUser
from ..hydra import hydra_service
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -90,7 +91,7 @@ def init_login_manager(app: Flask) -> None:
oauth2.register( oauth2.register(
name="custom", name="custom",
client_id=app.config['OAUTH_ID'], client_id=hydra_service.client_id,
client_secret=app.config['OAUTH_SECRET'], client_secret=app.config['OAUTH_SECRET'],
server_metadata_url=f'{base_url}/.well-known/openid-configuration', server_metadata_url=f'{base_url}/.well-known/openid-configuration',
access_token_url=f"{base_url}/oauth2/token", access_token_url=f"{base_url}/oauth2/token",

View file

@ -1,12 +1,31 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
cfg = config.services.lenticular-cloud; cfg = config.services.lenticular-cloud;
username = "lenticular_cloud";
data_folder = "/var/lib/${username}";
python = pkgs.python3; python = pkgs.python3;
in in
{ {
options = with lib.options; { options = with lib.options; {
services.lenticular-cloud ={ services.lenticular-cloud ={
enable = mkEnableOption "lenticluar service enable"; enable = mkEnableOption "lenticluar service enable";
domain = mkOption {
type = lib.types.str;
example = "account.example.com";
};
settings = mkOption {
type = lib.types.attrs;
default = rec {
DOMAIN = cfg.domain;
DATA_FOLDER = data_folder;
PKI_PATH = "${DATA_FOLDER}/pki";
# SQLALCHEMY_DATABASE_URI = "sqlite:////${DATA_FOLDER}/db.sqlite";
SQLALCHEMY_DATABASE_URI = "postgresql://${username}@/${username}?host=/run/postgresql";
HYDRA_ADMIN_URL= "https://${config.services.ory-hydra.admin_domain}";
HYDRA_PUBLIC_URL= "https://${config.services.ory-hydra.public_domain}";
};
};
}; };
}; };
config = { config = {
@ -17,51 +36,81 @@ in
]; ];
users = { users = {
groups.lenticular = { groups."${username}" = {
}; };
users.lenticular = { users."${username}" = {
createHome = true; createHome = true;
home = "/var/lib/lenticular"; home = data_folder;
description = "web server"; description = "web server";
extraGroups = [ extraGroups = [
# "ory-hydra"
]; ];
group = "lenticular"; group = username;
isSystemUser = true; isSystemUser = true;
}; };
}; };
services.postgresql = {
enable = true;
ensureDatabases = [ username ];
ensureUsers = [
{
name = username;
ensurePermissions = {
"DATABASE ${username}" = "All PRIVILEGES";
};
}
];
identMap = ''
# ArbitraryMapName systemUser DBUser
superuser_map ${username} ${username}
'';
};
services.nginx.enable = true;
services.nginx.virtualHosts."${cfg.domain}" = {
addSSL = true;
enableACME = true;
serverName = cfg.domain;
locations."/" = {
recommendedProxySettings = true;
proxyPass = "http://unix:/run/${username}/web.sock";
};
};
users.users.nginx.extraGroups = [ username ];
systemd.services.lenticular-cloud = { systemd.services.lenticular-cloud = {
description = "lenticular account"; description = "lenticular account";
after = [ "network.target" ]; after = [ "network.target" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
requires = [ "ory-hydra.service" ];
enable = cfg.enable; enable = cfg.enable;
environment = let environment = let
python_path = with python.pkgs; makePythonPath [ lenticular-cloud gevent psycopg2]; python_path = with python.pkgs; makePythonPath [ pkgs.lenticular-cloud gevent ];
in { in {
CONFIG_FILE = "/etc/lenticular_cloud/production.conf"; # CONFIG_FILE = "/etc/lenticular_cloud/production.conf";
CONFIG_FILE = pkgs.writeText "lenticular-cloud.json" (builtins.toJSON cfg.settings);
PYTHONPATH = "${python_path}"; PYTHONPATH = "${python_path}";
# PYTHONPATH = "${lenticular-pkg.pythonPath}:${lenticular-pkg}/lib/python3.10/site-packages:${python_path}"; # PYTHONPATH = "${lenticular-pkg.pythonPath}:${lenticular-pkg}/lib/python3.10/site-packages:${python_path}";
}; };
preStart = ''
#cat > ${data_folder}/foobar.conf <<EOF
#SECRET_KEY=""
#EOF
${pkgs.lenticular-cloud}/bin/lenticular_cloud-cli db_upgrade
'';
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
WorkingDirectory = /var/lib/lenticular; WorkingDirectory = data_folder;
#User="lenticular"; #done by gunicorn User = username;
ExecStartPre = pkgs.writeScript "lenticular-cloud-server-init" ''
#!/bin/sh
#cat > /var/lib/lenticular/foobar.conf <<EOF
#SECRET_KEY=""
#EOF
${pkgs.lenticular-cloud}/bin/lenticular_cloud-cli db_upgrade
'';
ExecStart = ''${python.pkgs.gunicorn}/bin/gunicorn lenticular_cloud.wsgi --name lenticular_cloud \ ExecStart = ''${python.pkgs.gunicorn}/bin/gunicorn lenticular_cloud.wsgi --name lenticular_cloud \
-u lenticular \
-g lenticular \
--workers 3 --log-level=info \ --workers 3 --log-level=info \
--bind=unix:/run/lenticular.sock \ --bind=unix:/run/${username}/web.sock \
-k gevent''; -k gevent'';
Restart = "on-failure"; Restart = "on-failure";
RuntimeDirectory = username;
}; };
}; };

View file

@ -4,6 +4,16 @@ let
in { in {
python3 = prev.python3.override { python3 = prev.python3.override {
packageOverrides = final: prev: with final; { packageOverrides = final: prev: with final; {
sqlalchemy = prev.sqlalchemy.overridePythonAttrs (old: rec {
version = "2.0.19";
src = pkgs.fetchFromGitHub {
owner = "sqlalchemy";
repo = "sqlalchemy";
rev = "refs/tags/rel_${lib.replaceStrings [ "." ] [ "_" ] version}";
hash = "sha256-97q04wQVtlV2b6VJHxvnQ9ep76T5umn1KI3hXh6a8kU=";
};
disabledTestPaths = old.disabledTestPaths ++ [ "test/typing" ];
});
urlobject = buildPythonPackage rec { urlobject = buildPythonPackage rec {
pname = "URLObject"; pname = "URLObject";
version = "2.4.3"; version = "2.4.3";
@ -23,7 +33,7 @@ in {
inherit pname version; inherit pname version;
sha256 = "a37dec5c3a21f13966178285d5c10691cd72203dcef8a01db802fef6287e716d"; sha256 = "a37dec5c3a21f13966178285d5c10691cd72203dcef8a01db802fef6287e716d";
}; };
doCheck = false; doCheck = true;
propagatedBuildInputs = [ propagatedBuildInputs = [
requests requests
oauthlib oauthlib
@ -79,58 +89,64 @@ in {
typing-extensions typing-extensions
]; ];
}); });
flask = prev.flask.overridePythonAttrs (old: {
propagatedBuildInputs = old.propagatedBuildInputs ++ flask.optional-dependencies.async;
});
lenticular-cloud = buildPythonPackage {
pname = "lenticular_cloud";
version = "0.3";
src = ./.;
propagatedBuildInputs = [
flask
flask-restful
flask_sqlalchemy
flask_wtf
flask-babel
flask_login
requests
requests_oauthlib
ldap3
#ldap3-orm
pyotp
cryptography
blinker
authlib # as oauth client lib
fido2 # for webauthn
flask_migrate # db migrations
flask-dance
ory-hydra-client
toml
pkgs.nodejs
#node-env
gunicorn
psycopg2
];
testBuildInputs = [
pytest
pytest-mypy
flask_testing
tox
types-dateutil
types-toml
nose
mypy
];
# passthru = {
# inherit python;
# pythonPath = python.pkgs.makePythonPath propagatedBuildInputs;
# };
doCheck = false;
checkInputs = [
pytest
];
};
}; };
}; };
lenticular-cloud = with final.python3.pkgs; buildPythonApplication { lenticular-cloud = final.python3.pkgs.lenticular-cloud;
pname = "lenticular_cloud";
version = "0.2";
src = ./.;
propagatedBuildInputs = [
flask
flask-restful
flask_sqlalchemy
flask_wtf
flask-babel
flask_login
requests
requests_oauthlib
ldap3
#ldap3-orm
pyotp
cryptography
blinker
authlib # as oauth client lib
fido2 # for webauthn
flask_migrate # db migrations
flask-dance
ory-hydra-client
pkgs.nodejs
#node-env
gunicorn
];
testBuildInputs = [
pytest
pytest-mypy
flask_testing
tox
types-dateutil
nose
mypy
];
# passthru = {
# inherit python;
# pythonPath = python.pkgs.makePythonPath propagatedBuildInputs;
# };
doCheck = false;
checkInputs = [
pytest
];
};
} }

0
production.toml Normal file
View file

0
tests/__init__.py Normal file
View file

17
tests/test_json.py Normal file
View file

@ -0,0 +1,17 @@
import unittest
from flask import jsonify
from uuid import uuid4
from lenticular_cloud.app import create_app
from lenticular_cloud.model import User
class TestBasicJsonFunction(unittest.TestCase):
def test_encode(self):
app = create_app()
uuid = uuid4()
with app.app_context():
text = jsonify(uuid)
print(text)