update
add nix flake make a restart
This commit is contained in:
parent
536668d8b9
commit
eee18c1785
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@ node_modules
|
||||||
/dist
|
/dist
|
||||||
build
|
build
|
||||||
result
|
result
|
||||||
|
nixos.qcow2
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
44
flake.lock
44
flake.lock
|
@ -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",
|
||||||
|
|
30
flake.nix
30
flake.nix
|
@ -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"];
|
||||||
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
|
@ -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__":
|
||||||
|
|
|
@ -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
|
||||||
|
# }
|
||||||
|
# }
|
|
@ -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:
|
||||||
|
|
63
lenticular_cloud/migrations/versions/a74320a5d7a1_init.py
Normal file
63
lenticular_cloud/migrations/versions/a74320a5d7a1_init.py
Normal 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 ###
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
|
@ -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),
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
85
module.nix
85
module.nix
|
@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
122
overlay.nix
122
overlay.nix
|
@ -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
0
production.toml
Normal file
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
17
tests/test_json.py
Normal file
17
tests/test_json.py
Normal 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)
|
Loading…
Reference in a new issue