From 1ed3b587c74fc69416c7b8fe7ff5481b1c39479a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Fri, 25 Mar 2022 15:50:45 +0000 Subject: [PATCH 01/47] quark --- api/kiwi_vpn_api/db/models.py | 88 ++++++++++------------------------ api/kiwi_vpn_api/db/schemas.py | 30 ++++++++++++ api/kiwi_vpn_api/routers/dn.py | 30 ++++++++++++ api/plan.md | 26 ++++++++++ 4 files changed, 111 insertions(+), 63 deletions(-) create mode 100644 api/plan.md diff --git a/api/kiwi_vpn_api/db/models.py b/api/kiwi_vpn_api/db/models.py index f9b6afa..d67f4aa 100644 --- a/api/kiwi_vpn_api/db/models.py +++ b/api/kiwi_vpn_api/db/models.py @@ -14,34 +14,6 @@ from sqlalchemy.orm import Session, relationship ORMBaseModel = declarative_base() -class User(ORMBaseModel): - __tablename__ = "users" - - name = Column(String, primary_key=True, index=True) - password = Column(String, nullable=False) - - capabilities: list[UserCapability] = relationship( - "UserCapability", lazy="joined", cascade="all, delete-orphan" - ) - certificates: list[Certificate] = relationship( - "Certificate", lazy="select", back_populates="owner" - ) - distinguished_names: list[DistinguishedName] = relationship( - "DistinguishedName", lazy="select", back_populates="owner" - ) - - @classmethod - def load(cls, db: Session, name: str) -> User | None: - """ - Load user from database by name. - """ - - return (db - .query(User) - .filter(User.name == name) - .first()) - - class UserCapability(ORMBaseModel): __tablename__ = "user_capabilities" @@ -54,53 +26,43 @@ class UserCapability(ORMBaseModel): capability = Column(String, primary_key=True) -class DistinguishedName(ORMBaseModel): - __tablename__ = "distinguished_names" +class User(ORMBaseModel): + __tablename__ = "users" - id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String, primary_key=True, index=True) + password = Column(String, nullable=False) - owner_name = Column(String, ForeignKey("users.name")) - cn_only = Column(Boolean, default=True, nullable=False) country = Column(String(2)) state = Column(String) city = Column(String) organization = Column(String) organizational_unit = Column(String) + email = Column(String) - common_name = Column(String, nullable=False) + + capabilities: list[UserCapability] = relationship( + "UserCapability", lazy="joined", cascade="all, delete-orphan" + ) + devices: list[Device] = relationship( + "Device", lazy="select", back_populates="owner" + ) + + +class Device(ORMBaseModel): + __tablename__ = "devices" + + id = Column(Integer, primary_key=True, autoincrement=True) + + owner_name = Column(String, ForeignKey("users.name")) + name = Column(String) + type = Column(String) + expiry = Column(DateTime, default=datetime.datetime.now) owner: User = relationship( "User", lazy="joined", back_populates="distinguished_names" ) UniqueConstraint( - country, - state, - city, - organization, - organizational_unit, - email, - common_name, - ) - - -class Certificate(ORMBaseModel): - __tablename__ = "certificates" - - id = Column(Integer, primary_key=True, autoincrement=True) - - owner_name = Column(String, ForeignKey("users.name")) - dn_id = Column( - Integer, - ForeignKey("distinguished_names.id"), - nullable=False, - ) - expiry = Column(DateTime, default=datetime.datetime.now) - - distinguished_name: DistinguishedName = relationship( - "DistinguishedName", lazy="joined" - ) - - owner: User = relationship( - "User", lazy="joined", back_populates="certificates" + owner_name, + name, ) diff --git a/api/kiwi_vpn_api/db/schemas.py b/api/kiwi_vpn_api/db/schemas.py index 132aab1..c20d226 100644 --- a/api/kiwi_vpn_api/db/schemas.py +++ b/api/kiwi_vpn_api/db/schemas.py @@ -79,6 +79,36 @@ class DistinguishedName(DistinguishedNameBase): # distinguished name already existed pass + def delete( + self, + db: Session, + ) -> bool: + """ + Delete this distinguished name from the database. + """ + + db_dn = models.DistinguishedName(**dict(self)) + db.refresh(db_dn) + + # .load( + # db=db, + # country=self.country, + # state=self.state, + # city=self.city, + # organization=self.organization, + # organizational_unit=self.organizational_unit, + # email=self.email, + # common_name=self.common_name, + # ) + + if db_dn is None: + # nonexistent user + return False + + db.delete(db_dn) + db.commit() + return True + ########## # table: certificates ########## diff --git a/api/kiwi_vpn_api/routers/dn.py b/api/kiwi_vpn_api/routers/dn.py index ae1eae2..bdebb9c 100644 --- a/api/kiwi_vpn_api/routers/dn.py +++ b/api/kiwi_vpn_api/routers/dn.py @@ -56,3 +56,33 @@ async def add_distinguished_name( # return the created user on success return new_dn + + +# @router.delete( +# "", +# responses={ +# status.HTTP_200_OK: Responses.OK, +# status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED, +# status.HTTP_401_UNAUTHORIZED: Responses.NEEDS_USER, +# status.HTTP_403_FORBIDDEN: Responses.NEEDS_ADMIN, +# status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST, +# }, +# ) +# async def remove_distinguished_name( +# user_name: str, +# _: User = Depends(get_current_user_if_admin), +# db: Session | None = Depends(Connection.get), +# ): +# """ +# DELETE ./{user_name}: Remove a user from the database. +# """ + +# # get the user +# user = User.from_db( +# db=db, +# name=user_name, +# ) + +# # fail if deletion was unsuccessful +# if user is None or not user.delete(db): +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) diff --git a/api/plan.md b/api/plan.md new file mode 100644 index 0000000..fc15cf3 --- /dev/null +++ b/api/plan.md @@ -0,0 +1,26 @@ +## Server props +- default DN parts: country, state, city, org, OU +- "customizable" flags for DN parts +- flag: use client-to-client +- force cipher, tls-cipher, auth params +- server name +- default certification length +- default certificate algo + +## User props +- username +- password +- custom DN parts: country, state, city, org, OU +- email + +## User caps +- admin: administrator +- login: can log into the web interface +- certify: can certify own devices without approval +- renew: can renew certificates for own devices + +## Device props +- name +- type (icon) +- is active +- certified until From 02225cdf09ae3ec4d1f38d93b3efaf4d6de54313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Fri, 25 Mar 2022 23:03:56 +0000 Subject: [PATCH 02/47] plan models + schemas --- api/kiwi_vpn_api/db/models.py | 8 +- api/kiwi_vpn_api/db/schemas.py | 258 +++++++++++++++------------------ api/plan.md | 5 +- 3 files changed, 124 insertions(+), 147 deletions(-) diff --git a/api/kiwi_vpn_api/db/models.py b/api/kiwi_vpn_api/db/models.py index d67f4aa..06200ba 100644 --- a/api/kiwi_vpn_api/db/models.py +++ b/api/kiwi_vpn_api/db/models.py @@ -4,12 +4,10 @@ SQLAlchemy representation of database contents. from __future__ import annotations -import datetime - -from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String, +from sqlalchemy import (Column, DateTime, ForeignKey, Integer, String, UniqueConstraint) from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Session, relationship +from sqlalchemy.orm import relationship ORMBaseModel = declarative_base() @@ -56,7 +54,7 @@ class Device(ORMBaseModel): owner_name = Column(String, ForeignKey("users.name")) name = Column(String) type = Column(String) - expiry = Column(DateTime, default=datetime.datetime.now) + expiry = Column(DateTime) owner: User = relationship( "User", lazy="joined", back_populates="distinguished_names" diff --git a/api/kiwi_vpn_api/db/schemas.py b/api/kiwi_vpn_api/db/schemas.py index c20d226..7fe003f 100644 --- a/api/kiwi_vpn_api/db/schemas.py +++ b/api/kiwi_vpn_api/db/schemas.py @@ -10,124 +10,12 @@ from enum import Enum from typing import Any from passlib.context import CryptContext -from pydantic import BaseModel, Field, constr, validator +from pydantic import BaseModel, Field, validator from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from . import models -########## -# table: distinguished_names -########## - - -class DistinguishedNameBase(BaseModel): - cn_only: bool - country: constr(max_length=2) | None - state: str | None - city: str | None - organization: str | None - organizational_unit: str | None - email: str | None - common_name: str - - -class DistinguishedNameCreate(DistinguishedNameBase): - pass - - -class DistinguishedName(DistinguishedNameBase): - class Config: - orm_mode = True - - @classmethod - def create( - cls, - db: Session, - dn: DistinguishedNameCreate, - owner: User, - ) -> User | None: - """ - Create a new distinguished name in the database. - """ - - try: - db_owner = models.User.load( - db=db, - name=owner.name, - ) - - dn = models.DistinguishedName( - cn_only=dn.cn_only, - country=dn.country, - state=dn.state, - city=dn.city, - organization=dn.organization, - organizational_unit=dn.organizational_unit, - email=dn.email, - common_name=dn.common_name, - owner=db_owner, - ) - - db.add(dn) - db.commit() - db.refresh(dn) - - return cls.from_orm(dn) - - except IntegrityError: - # distinguished name already existed - pass - - def delete( - self, - db: Session, - ) -> bool: - """ - Delete this distinguished name from the database. - """ - - db_dn = models.DistinguishedName(**dict(self)) - db.refresh(db_dn) - - # .load( - # db=db, - # country=self.country, - # state=self.state, - # city=self.city, - # organization=self.organization, - # organizational_unit=self.organizational_unit, - # email=self.email, - # common_name=self.common_name, - # ) - - if db_dn is None: - # nonexistent user - return False - - db.delete(db_dn) - db.commit() - return True - -########## -# table: certificates -########## - - -class CertificateBase(BaseModel): - expiry: datetime - - -class CertificateCreate(CertificateBase): - pass - - -class Certificate(CertificateBase): - distinguished_name: DistinguishedName - - class Config: - orm_mode = True - ########## # table: user_capabilities ########## @@ -135,6 +23,9 @@ class Certificate(CertificateBase): class UserCapability(Enum): admin = "admin" + login = "login" + issue = "issue" + renew = "renew" def __repr__(self) -> str: return self.value @@ -157,6 +48,13 @@ class UserCapability(Enum): # create from string representation return cls(str(value)) + @property + def model(self) -> models.UserCapability: + return models.UserCapability( + capability=self.value, + ) + + ########## # table: users ########## @@ -165,19 +63,23 @@ class UserCapability(Enum): class UserBase(BaseModel): name: str + country: str + state: str + city: str + organization: str + organizational_unit: str + + email: str + + capabilities: list[UserCapability] = [] + class UserCreate(UserBase): password: str class User(UserBase): - capabilities: list[UserCapability] = [] - - distinguished_names: list[DistinguishedName] = Field( - default=[], repr=False - ) - - certificates: list[Certificate] = Field( + devices: list[Device] = Field( default=[], repr=False ) @@ -206,8 +108,8 @@ class User(UserBase): Load user from database by name. """ - if (db_user := models.User.load(db, name)) is None: - return None + db_user = models.User(name=name) + db.refresh(db_user) return cls.from_orm(db_user) @@ -223,17 +125,20 @@ class User(UserBase): """ try: - user = models.User( + db_user = models.User( name=user.name, password=crypt_context.hash(user.password), - capabilities=[], + capabilities=[ + capability.model + for capability in user.capabilities + ], ) - db.add(user) + db.add(db_user) db.commit() - db.refresh(user) + db.refresh(db_user) - return cls.from_orm(user) + return cls.from_orm(db_user) except IntegrityError: # user already existed @@ -252,7 +157,10 @@ class User(UserBase): Authenticate with name/password against users in database. """ - if (db_user := models.User.load(db, self.name)) is None: + db_user = models.User(name=self.name) + db.refresh(db_user) + + if db_user is None: # nonexistent user, fake doing password verification crypt_context.dummy_verify() return False @@ -273,18 +181,16 @@ class User(UserBase): Update this user in the database. """ - old_dbuser = models.User.load(db, self.name) - old_user = self.from_orm(old_dbuser) + db_user = models.User(name=self.name) + db.refresh(db_user) - for capability in self.capabilities: - if capability not in old_user.capabilities: - old_dbuser.capabilities.append( - models.UserCapability(capability=capability.value) - ) + for capability in db_user.capabilities: + db.delete(capability) - for capability in old_dbuser.capabilities: - if UserCapability.from_value(capability) not in self.capabilities: - db.delete(capability) + db_user.capabilities = [ + capability.model + for capability in self.capabilities + ] db.commit() @@ -296,10 +202,84 @@ class User(UserBase): Delete this user from the database. """ - if (db_user := models.User.load(db, self.name)) is None: + db_user = models.User(name=self.name) + db.refresh(db_user) + + if db_user is None: # nonexistent user return False db.delete(db_user) db.commit() return True + + +########## +# table: devices +########## + + +class DeviceBase(BaseModel): + name: str + type: str + expiry: datetime + + +class DeviceCreate(DeviceBase): + owner_name: str + + +class Device(DeviceBase): + class Config: + orm_mode = True + + @classmethod + def create( + cls, + db: Session, + device: DeviceCreate, + ) -> Device | None: + """ + Create a new device in the database. + """ + + try: + db_device = models.Device( + owner_name=device.owner_name, + + name=device.name, + type=device.type, + expiry=device.expiry, + ) + + db.add(db_device) + db.commit() + db.refresh(db_device) + + return cls.from_orm(db_device) + + except IntegrityError: + # device already existed + pass + + def delete( + self, + db: Session, + ) -> bool: + """ + Delete this device from the database. + """ + + db_device = models.Device( + # owner_name= + name=self.name, + ) + db.refresh(db_device) + + if db_device is None: + # nonexistent device + return False + + db.delete(db_device) + db.commit() + return True diff --git a/api/plan.md b/api/plan.md index fc15cf3..0822af0 100644 --- a/api/plan.md +++ b/api/plan.md @@ -16,11 +16,10 @@ ## User caps - admin: administrator - login: can log into the web interface -- certify: can certify own devices without approval +- issue: can certify own devices without approval - renew: can renew certificates for own devices ## Device props - name - type (icon) -- is active -- certified until +- expiry From c47fa5a89b707cba142e4a6461b56bd0e7a809d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Fri, 25 Mar 2022 23:54:19 +0000 Subject: [PATCH 03/47] split db.schemas -> db.schemata package --- api/kiwi_vpn_api/db/__init__.py | 4 +- api/kiwi_vpn_api/db/schemata/__init__.py | 6 + api/kiwi_vpn_api/db/schemata/device.py | 79 ++++++++++ .../db/{schemas.py => schemata/user.py} | 139 ++---------------- .../db/schemata/user_capability.py | 43 ++++++ api/kiwi_vpn_api/main.py | 2 +- api/kiwi_vpn_api/routers/_common.py | 2 +- api/kiwi_vpn_api/routers/admin.py | 2 +- api/kiwi_vpn_api/routers/dn.py | 2 +- api/kiwi_vpn_api/routers/user.py | 2 +- 10 files changed, 149 insertions(+), 132 deletions(-) create mode 100644 api/kiwi_vpn_api/db/schemata/__init__.py create mode 100644 api/kiwi_vpn_api/db/schemata/device.py rename api/kiwi_vpn_api/db/{schemas.py => schemata/user.py} (58%) create mode 100644 api/kiwi_vpn_api/db/schemata/user_capability.py diff --git a/api/kiwi_vpn_api/db/__init__.py b/api/kiwi_vpn_api/db/__init__.py index 0e8041b..80b582e 100644 --- a/api/kiwi_vpn_api/db/__init__.py +++ b/api/kiwi_vpn_api/db/__init__.py @@ -1,4 +1,4 @@ -from . import models, schemas +from . import models, schemata from .connection import Connection -__all__ = ["Connection", "models", "schemas"] +__all__ = ["Connection", "models", "schemata"] diff --git a/api/kiwi_vpn_api/db/schemata/__init__.py b/api/kiwi_vpn_api/db/schemata/__init__.py new file mode 100644 index 0000000..5a991aa --- /dev/null +++ b/api/kiwi_vpn_api/db/schemata/__init__.py @@ -0,0 +1,6 @@ +from .device import Device, DeviceBase, DeviceCreate +from .user import User, UserBase, UserCreate +from .user_capability import UserCapability + +__all__ = ["Device", "DeviceBase", "DeviceCreate", + "User", "UserBase", "UserCreate", "UserCapability"] diff --git a/api/kiwi_vpn_api/db/schemata/device.py b/api/kiwi_vpn_api/db/schemata/device.py new file mode 100644 index 0000000..8e47f7e --- /dev/null +++ b/api/kiwi_vpn_api/db/schemata/device.py @@ -0,0 +1,79 @@ +""" +Pydantic representation of database contents. +""" + +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session + +from .. import models + + +class DeviceBase(BaseModel): + name: str + type: str + expiry: datetime + + +class DeviceCreate(DeviceBase): + owner_name: str + + +class Device(DeviceBase): + class Config: + orm_mode = True + + @classmethod + def create( + cls, + db: Session, + device: DeviceCreate, + ) -> Device | None: + """ + Create a new device in the database. + """ + + try: + db_device = models.Device( + owner_name=device.owner_name, + + name=device.name, + type=device.type, + expiry=device.expiry, + ) + + db.add(db_device) + db.commit() + db.refresh(db_device) + + return cls.from_orm(db_device) + + except IntegrityError: + # device already existed + return None + + def delete( + self, + db: Session, + ) -> bool: + """ + Delete this device from the database. + """ + + db_device = models.Device( + # owner_name= + name=self.name, + ) + db.refresh(db_device) + + if db_device is None: + # nonexistent device + return False + + db.delete(db_device) + db.commit() + return True diff --git a/api/kiwi_vpn_api/db/schemas.py b/api/kiwi_vpn_api/db/schemata/user.py similarity index 58% rename from api/kiwi_vpn_api/db/schemas.py rename to api/kiwi_vpn_api/db/schemata/user.py index 7fe003f..5789f05 100644 --- a/api/kiwi_vpn_api/db/schemas.py +++ b/api/kiwi_vpn_api/db/schemata/user.py @@ -2,62 +2,18 @@ Pydantic representation of database contents. """ - from __future__ import annotations -from datetime import datetime -from enum import Enum from typing import Any from passlib.context import CryptContext from pydantic import BaseModel, Field, validator -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError, InvalidRequestError from sqlalchemy.orm import Session -from . import models - -########## -# table: user_capabilities -########## - - -class UserCapability(Enum): - admin = "admin" - login = "login" - issue = "issue" - renew = "renew" - - def __repr__(self) -> str: - return self.value - - @classmethod - def from_value(cls, value) -> UserCapability: - """ - Create UserCapability from various formats - """ - - if isinstance(value, cls): - # value is already a UserCapability, use that - return value - - elif isinstance(value, models.UserCapability): - # create from db format - return cls(value.capability) - - else: - # create from string representation - return cls(str(value)) - - @property - def model(self) -> models.UserCapability: - return models.UserCapability( - capability=self.value, - ) - - -########## -# table: users -########## +from .. import models +from .device import Device +from .user_capability import UserCapability class UserBase(BaseModel): @@ -71,14 +27,14 @@ class UserBase(BaseModel): email: str - capabilities: list[UserCapability] = [] - class UserCreate(UserBase): password: str class User(UserBase): + capabilities: list[UserCapability] = [] + devices: list[Device] = Field( default=[], repr=False ) @@ -108,10 +64,14 @@ class User(UserBase): Load user from database by name. """ - db_user = models.User(name=name) - db.refresh(db_user) + try: + db_user = models.User(name=name) + db.refresh(db_user) - return cls.from_orm(db_user) + return cls.from_orm(db_user) + + except InvalidRequestError: + return None @classmethod def create( @@ -142,7 +102,7 @@ class User(UserBase): except IntegrityError: # user already existed - pass + return None def is_admin(self) -> bool: return UserCapability.admin in self.capabilities @@ -212,74 +172,3 @@ class User(UserBase): db.delete(db_user) db.commit() return True - - -########## -# table: devices -########## - - -class DeviceBase(BaseModel): - name: str - type: str - expiry: datetime - - -class DeviceCreate(DeviceBase): - owner_name: str - - -class Device(DeviceBase): - class Config: - orm_mode = True - - @classmethod - def create( - cls, - db: Session, - device: DeviceCreate, - ) -> Device | None: - """ - Create a new device in the database. - """ - - try: - db_device = models.Device( - owner_name=device.owner_name, - - name=device.name, - type=device.type, - expiry=device.expiry, - ) - - db.add(db_device) - db.commit() - db.refresh(db_device) - - return cls.from_orm(db_device) - - except IntegrityError: - # device already existed - pass - - def delete( - self, - db: Session, - ) -> bool: - """ - Delete this device from the database. - """ - - db_device = models.Device( - # owner_name= - name=self.name, - ) - db.refresh(db_device) - - if db_device is None: - # nonexistent device - return False - - db.delete(db_device) - db.commit() - return True diff --git a/api/kiwi_vpn_api/db/schemata/user_capability.py b/api/kiwi_vpn_api/db/schemata/user_capability.py new file mode 100644 index 0000000..28e1870 --- /dev/null +++ b/api/kiwi_vpn_api/db/schemata/user_capability.py @@ -0,0 +1,43 @@ +""" +Pydantic representation of database contents. +""" + +from __future__ import annotations + +from enum import Enum + +from .. import models + + +class UserCapability(Enum): + admin = "admin" + login = "login" + issue = "issue" + renew = "renew" + + def __repr__(self) -> str: + return self.value + + @classmethod + def from_value(cls, value) -> UserCapability: + """ + Create UserCapability from various formats + """ + + if isinstance(value, cls): + # value is already a UserCapability, use that + return value + + elif isinstance(value, models.UserCapability): + # create from db format + return cls(value.capability) + + else: + # create from string representation + return cls(str(value)) + + @property + def model(self) -> models.UserCapability: + return models.UserCapability( + capability=self.value, + ) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 82c2fa8..9ed5b77 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -14,7 +14,7 @@ from fastapi import FastAPI from .config import Config, Settings from .db import Connection -from .db.schemas import User +from .db.schemata import User from .routers import main_router settings = Settings.get() diff --git a/api/kiwi_vpn_api/routers/_common.py b/api/kiwi_vpn_api/routers/_common.py index f9424bc..d7f3969 100644 --- a/api/kiwi_vpn_api/routers/_common.py +++ b/api/kiwi_vpn_api/routers/_common.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import Session from ..config import Config from ..db import Connection -from ..db.schemas import User +from ..db.schemata import User oauth2_scheme = OAuth2PasswordBearer(tokenUrl="user/authenticate") diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index f3e6daf..9422eff 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from ..config import Config from ..db import Connection -from ..db.schemas import User, UserCapability, UserCreate +from ..db.schemata import User, UserCapability, UserCreate from ._common import Responses, get_current_user router = APIRouter(prefix="/admin", tags=["admin"]) diff --git a/api/kiwi_vpn_api/routers/dn.py b/api/kiwi_vpn_api/routers/dn.py index bdebb9c..80ed087 100644 --- a/api/kiwi_vpn_api/routers/dn.py +++ b/api/kiwi_vpn_api/routers/dn.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from ..db import Connection -from ..db.schemas import DistinguishedName, DistinguishedNameCreate, User +from ..db.schemata import DistinguishedName, DistinguishedNameCreate, User from ._common import Responses, get_current_user_if_admin_or_self router = APIRouter(prefix="/dn") diff --git a/api/kiwi_vpn_api/routers/user.py b/api/kiwi_vpn_api/routers/user.py index 63e2b22..627c5fc 100644 --- a/api/kiwi_vpn_api/routers/user.py +++ b/api/kiwi_vpn_api/routers/user.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import Session from ..config import Config from ..db import Connection -from ..db.schemas import User, UserCapability, UserCreate +from ..db.schemata import User, UserCapability, UserCreate from ._common import Responses, get_current_user, get_current_user_if_admin router = APIRouter(prefix="/user", tags=["user"]) From 557bceed1f120d0863e1da718da22d68d74290c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Fri, 25 Mar 2022 23:56:57 +0000 Subject: [PATCH 04/47] legacy --- api/kiwi_vpn_api/db/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/db/models.py b/api/kiwi_vpn_api/db/models.py index 06200ba..1097400 100644 --- a/api/kiwi_vpn_api/db/models.py +++ b/api/kiwi_vpn_api/db/models.py @@ -57,7 +57,7 @@ class Device(ORMBaseModel): expiry = Column(DateTime) owner: User = relationship( - "User", lazy="joined", back_populates="distinguished_names" + "User", lazy="joined", back_populates="devices" ) UniqueConstraint( From 94fbab278cd8949a45073e338d96550d18f7dcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Sat, 26 Mar 2022 01:49:47 +0000 Subject: [PATCH 05/47] add models.User.from_db() --- api/kiwi_vpn_api/db/models.py | 22 ++++++++++++-- api/kiwi_vpn_api/db/schemata/user.py | 44 +++++++++++----------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/api/kiwi_vpn_api/db/models.py b/api/kiwi_vpn_api/db/models.py index 1097400..fc41d6f 100644 --- a/api/kiwi_vpn_api/db/models.py +++ b/api/kiwi_vpn_api/db/models.py @@ -7,7 +7,7 @@ from __future__ import annotations from sqlalchemy import (Column, DateTime, ForeignKey, Integer, String, UniqueConstraint) from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import Session, relationship ORMBaseModel = declarative_base() @@ -29,6 +29,7 @@ class User(ORMBaseModel): name = Column(String, primary_key=True, index=True) password = Column(String, nullable=False) + email = Column(String) country = Column(String(2)) state = Column(String) @@ -36,8 +37,6 @@ class User(ORMBaseModel): organization = Column(String) organizational_unit = Column(String) - email = Column(String) - capabilities: list[UserCapability] = relationship( "UserCapability", lazy="joined", cascade="all, delete-orphan" ) @@ -45,6 +44,23 @@ class User(ORMBaseModel): "Device", lazy="select", back_populates="owner" ) + @classmethod + def from_db( + cls, + db: Session, + name: str, + ) -> User | None: + """ + Load user from database by name. + """ + + return ( + db + .query(cls) + .filter(cls.name == name) + .first() + ) + class Device(ORMBaseModel): __tablename__ = "devices" diff --git a/api/kiwi_vpn_api/db/schemata/user.py b/api/kiwi_vpn_api/db/schemata/user.py index 5789f05..b31e31e 100644 --- a/api/kiwi_vpn_api/db/schemata/user.py +++ b/api/kiwi_vpn_api/db/schemata/user.py @@ -8,7 +8,7 @@ from typing import Any from passlib.context import CryptContext from pydantic import BaseModel, Field, validator -from sqlalchemy.exc import IntegrityError, InvalidRequestError +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from .. import models @@ -18,23 +18,22 @@ from .user_capability import UserCapability class UserBase(BaseModel): name: str - - country: str - state: str - city: str - organization: str - organizational_unit: str - email: str + capabilities: list[UserCapability] = [] + + country: str | None = Field(default=None, repr=False) + state: str | None = Field(default=None, repr=False) + city: str | None = Field(default=None, repr=False) + organization: str | None = Field(default=None, repr=False) + organizational_unit: str | None = Field(default=None, repr=False) + class UserCreate(UserBase): password: str class User(UserBase): - capabilities: list[UserCapability] = [] - devices: list[Device] = Field( default=[], repr=False ) @@ -64,15 +63,11 @@ class User(UserBase): Load user from database by name. """ - try: - db_user = models.User(name=name) - db.refresh(db_user) - - return cls.from_orm(db_user) - - except InvalidRequestError: + if (db_user := models.User.from_db(db, name)) is None: return None + return cls.from_orm(db_user) + @classmethod def create( cls, @@ -88,6 +83,7 @@ class User(UserBase): db_user = models.User( name=user.name, password=crypt_context.hash(user.password), + email=user.email, capabilities=[ capability.model for capability in user.capabilities @@ -117,10 +113,7 @@ class User(UserBase): Authenticate with name/password against users in database. """ - db_user = models.User(name=self.name) - db.refresh(db_user) - - if db_user is None: + if (db_user := models.User.from_db(db, self.name)) is None: # nonexistent user, fake doing password verification crypt_context.dummy_verify() return False @@ -141,8 +134,8 @@ class User(UserBase): Update this user in the database. """ - db_user = models.User(name=self.name) - db.refresh(db_user) + if (db_user := models.User.from_db(db, self.name)) is None: + return None for capability in db_user.capabilities: db.delete(capability) @@ -162,10 +155,7 @@ class User(UserBase): Delete this user from the database. """ - db_user = models.User(name=self.name) - db.refresh(db_user) - - if db_user is None: + if (db_user := models.User.from_db(db, self.name)) is None: # nonexistent user return False From b5e9323026ab8aeacc56f0794ecfc22390bb0d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Sat, 26 Mar 2022 16:05:36 +0000 Subject: [PATCH 06/47] git close diff on operation --- api/.vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/.vscode/settings.json b/api/.vscode/settings.json index cd32962..2607a64 100644 --- a/api/.vscode/settings.json +++ b/api/.vscode/settings.json @@ -11,5 +11,6 @@ "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": true - } + }, + "git.closeDiffOnOperation": true } \ No newline at end of file From e7030eb521d61dc74226e3252b4f9c002bc6f7cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Sun, 27 Mar 2022 01:17:48 +0000 Subject: [PATCH 07/47] "user" table with sqlmodel --- api/kiwi_vpn_api/config.py | 20 +++++++ api/kiwi_vpn_api/db_new/__init__.py | 0 api/kiwi_vpn_api/db_new/connection.py | 30 +++++++++++ api/kiwi_vpn_api/db_new/user.py | 77 +++++++++++++++++++++++++++ api/kiwi_vpn_api/main.py | 11 ++++ api/poetry.lock | 34 +++++++++++- api/pyproject.toml | 1 + 7 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 api/kiwi_vpn_api/db_new/__init__.py create mode 100644 api/kiwi_vpn_api/db_new/connection.py create mode 100644 api/kiwi_vpn_api/db_new/user.py diff --git a/api/kiwi_vpn_api/config.py b/api/kiwi_vpn_api/config.py index 4b2cf24..5c8672f 100644 --- a/api/kiwi_vpn_api/config.py +++ b/api/kiwi_vpn_api/config.py @@ -180,6 +180,13 @@ class CryptoConfig(BaseModel): schemes: list[str] = ["bcrypt"] + @property + def crypt_context_sync(self) -> CryptContext: + return CryptContext( + schemes=self.schemes, + deprecated="auto", + ) + @property async def crypt_context(self) -> CryptContext: return CryptContext( @@ -197,6 +204,19 @@ class Config(BaseModel): jwt: JWTConfig = Field(default_factory=JWTConfig) crypto: CryptoConfig = Field(default_factory=CryptoConfig) + @staticmethod + def load_sync() -> Config | None: + """ + Load configuration from config file + """ + + try: + with open(Settings.get().config_file, "r") as config_file: + return Config.parse_obj(json.load(config_file)) + + except FileNotFoundError: + return None + @staticmethod async def load() -> Config | None: """ diff --git a/api/kiwi_vpn_api/db_new/__init__.py b/api/kiwi_vpn_api/db_new/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/kiwi_vpn_api/db_new/connection.py b/api/kiwi_vpn_api/db_new/connection.py new file mode 100644 index 0000000..ff9341a --- /dev/null +++ b/api/kiwi_vpn_api/db_new/connection.py @@ -0,0 +1,30 @@ +from sqlmodel import Session, SQLModel, create_engine + + +class Connection: + """ + Namespace for the database connection. + """ + + engine = None + + @classmethod + def connect(cls, connection_url: str) -> None: + """ + Connect ORM to a database engine. + """ + + cls.engine = create_engine(connection_url) + SQLModel.metadata.create_all(cls.engine) + + @classmethod + @property + def session(cls) -> Session | None: + """ + Create an ORM session using a context manager. + """ + + if cls.engine is None: + return None + + return Session(cls.engine) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py new file mode 100644 index 0000000..935d9a7 --- /dev/null +++ b/api/kiwi_vpn_api/db_new/user.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import root_validator +from sqlalchemy.exc import IntegrityError +from sqlmodel import Field, SQLModel + +from ..config import Config +from .connection import Connection + + +class UserBase(SQLModel): + name: str = Field(primary_key=True) + email: str + + country: str | None = Field(default=None) + state: str | None = Field(default=None) + city: str | None = Field(default=None) + organization: str | None = Field(default=None) + organizational_unit: str | None = Field(default=None) + + +class User(UserBase, table=True): + password: str + + @classmethod + def create(cls, **kwargs) -> User | None: + """ + Create a new user in the database. + """ + + try: + with Connection.session as db: + user = User.from_orm(UserCreate(**kwargs)) + + db.add(user) + db.commit() + db.refresh(user) + + return user + + except IntegrityError: + # user already existed + return None + + @classmethod + def get(cls, name: str) -> User | None: + with Connection.session as db: + return db.get(cls, name) + + +class UserCreate(UserBase): + password: str | None = Field(default=None) + password_clear: str | None = Field(default=None) + + @root_validator + @classmethod + def hash_password(cls, values: dict[str, Any]) -> dict[str, Any]: + if (values.get("password")) is not None: + # password is set + return values + + if (password_clear := values.get("password_clear")) is None: + raise ValueError("No password to hash") + + if (current_config := Config.load_sync()) is None: + raise ValueError("Not configured") + + values["password"] = current_config.crypto.crypt_context_sync.hash( + password_clear) + + return values + + +class UserRead(UserBase): + pass diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 9ed5b77..70d6915 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -15,6 +15,7 @@ from fastapi import FastAPI from .config import Config, Settings from .db import Connection from .db.schemata import User +from .db_new import connection, user from .routers import main_router settings = Settings.get() @@ -51,6 +52,16 @@ async def on_startup() -> None: print(User.from_db(db, "admin")) print(User.from_db(db, "nonexistent")) + connection.Connection.connect("sqlite:///tmp/v2.db") + + user.User.create( + name="Uwe", + password_clear="ulf", + email="uwe@feh.de", + ) + + print(user.User.get("Uwe")) + def main() -> None: uvicorn.run( diff --git a/api/poetry.lock b/api/poetry.lock index 3f5e84d..b778921 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -428,6 +428,30 @@ postgresql_psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql (<1)", "pymysql"] sqlcipher = ["sqlcipher3-binary"] +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a21" +description = "Typing Stubs for SQLAlchemy 1.4" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=3.7.4" + +[[package]] +name = "sqlmodel" +version = "0.0.6" +description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." +category = "main" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +pydantic = ">=1.8.2,<2.0.0" +SQLAlchemy = ">=1.4.17,<1.5.0" +sqlalchemy2-stubs = "*" + [[package]] name = "starlette" version = "0.17.1" @@ -477,7 +501,7 @@ standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "p [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "432d2933102f8a0091cec1b5484944a0211ca74c5dc9b65877d99d7bd160e4bb" +content-hash = "a580f9fe4c68667e4cbdf385ac11d5c7a2925e3c990b7164faa922ec8b6f9555" [metadata.files] anyio = [ @@ -834,6 +858,14 @@ sqlalchemy = [ {file = "SQLAlchemy-1.4.32-cp39-cp39-win_amd64.whl", hash = "sha256:b3f1d9b3aa09ab9adc7f8c4b40fc3e081eb903054c9a6f9ae1633fe15ae503b4"}, {file = "SQLAlchemy-1.4.32.tar.gz", hash = "sha256:6fdd2dc5931daab778c2b65b03df6ae68376e028a3098eb624d0909d999885bc"}, ] +sqlalchemy2-stubs = [ + {file = "sqlalchemy2-stubs-0.0.2a21.tar.gz", hash = "sha256:207e3d8a36fc032d325f4eec89e0c6760efe81d07e978513d8c9b14f108dcd0c"}, + {file = "sqlalchemy2_stubs-0.0.2a21-py3-none-any.whl", hash = "sha256:bd4a3d5ca7ff9d01b2245e1b26304d6b2ec4daf43a01faf40db9e09245679433"}, +] +sqlmodel = [ + {file = "sqlmodel-0.0.6-py3-none-any.whl", hash = "sha256:c5fd8719e09da348cd32ce2a5b6a44f289d3029fa8f1c9818229b6f34f1201b4"}, + {file = "sqlmodel-0.0.6.tar.gz", hash = "sha256:3b4f966b9671b24d85529d274e6c4dbc7753b468e35d2d6a40bd75cad1f66813"}, +] starlette = [ {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, diff --git a/api/pyproject.toml b/api/pyproject.toml index a105d62..a9a7855 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -13,6 +13,7 @@ python-jose = {extras = ["cryptography"], version = "^3.3.0"} passlib = {extras = ["argon2", "bcrypt"], version = "^1.7.4"} SQLAlchemy = "^1.4.32" pyOpenSSL = "^22.0.0" +sqlmodel = "^0.0.6" [tool.poetry.dev-dependencies] pytest = "^7.1.0" From ae3627cbe0e055693a2bb7241d9f6378003aac84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Sun, 27 Mar 2022 01:22:28 +0000 Subject: [PATCH 08/47] use "cls" --- api/kiwi_vpn_api/db_new/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index 935d9a7..623640d 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -32,7 +32,7 @@ class User(UserBase, table=True): try: with Connection.session as db: - user = User.from_orm(UserCreate(**kwargs)) + user = cls.from_orm(UserCreate(**kwargs)) db.add(user) db.commit() From 9625336df9f6305b8ea062ea4dbe1d4ee5342d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Sun, 27 Mar 2022 13:47:18 +0000 Subject: [PATCH 09/47] User CRUD --- api/kiwi_vpn_api/db_new/user.py | 44 +++++++++++++++++++++++++++++++++ api/kiwi_vpn_api/main.py | 3 +++ 2 files changed, 47 insertions(+) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index 623640d..45235e6 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -46,10 +46,54 @@ class User(UserBase, table=True): @classmethod def get(cls, name: str) -> User | None: + """ + Load user from database by name. + """ + with Connection.session as db: return db.get(cls, name) + @classmethod + def authenticate( + cls, + name: str, + password: str, + ) -> User | None: + """ + Authenticate with name/password against users in database. + """ + crypt_context = Config.load_sync().crypto.crypt_context_sync + + if (user := cls.get(name)) is None: + # nonexistent user, fake doing password verification + crypt_context.dummy_verify() + return None + + if not crypt_context.verify(password, user.password): + # password hash mismatch + return None + + return user + + def update(self) -> None: + """ + Update this user in the database. + """ + + with Connection.session as db: + db.add(self) + db.commit() + db.refresh(self) + + def delete(self) -> bool: + """ + Delete this user from the database. + """ + + with Connection.session as db: + db.delete(self) + db.commit() class UserCreate(UserBase): password: str | None = Field(default=None) password_clear: str | None = Field(default=None) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 70d6915..335d058 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -61,6 +61,9 @@ async def on_startup() -> None: ) print(user.User.get("Uwe")) + print(user.User.authenticate("Uwe", "uwe")) + + uwe = user.User.authenticate("Uwe", "ulf") def main() -> None: From e2f916debcd669d38213ad7fdbbbaa65013e61ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Sun, 27 Mar 2022 13:47:38 +0000 Subject: [PATCH 10/47] Capabilities --- api/kiwi_vpn_api/db_new/capabilities.py | 32 ++++++++++++++++++ api/kiwi_vpn_api/db_new/user.py | 44 +++++++++++++++++++++++-- api/kiwi_vpn_api/main.py | 10 +++++- 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 api/kiwi_vpn_api/db_new/capabilities.py diff --git a/api/kiwi_vpn_api/db_new/capabilities.py b/api/kiwi_vpn_api/db_new/capabilities.py new file mode 100644 index 0000000..7d92064 --- /dev/null +++ b/api/kiwi_vpn_api/db_new/capabilities.py @@ -0,0 +1,32 @@ +from enum import Enum +from typing import TYPE_CHECKING + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .user import User + + +class Capability(Enum): + admin = "admin" + login = "login" + issue = "issue" + renew = "renew" + + +class UserCapabilityBase(SQLModel): + user_name: str = Field(primary_key=True, foreign_key="user.name") + capability_name: str = Field(primary_key=True) + + @property + def _(self) -> Capability: + return Capability(self.capability_name) + + def __repr__(self) -> str: + return self.capability_name + + +class UserCapability(UserCapabilityBase, table=True): + user: "User" = Relationship( + back_populates="capabilities" + ) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index 45235e6..3d3eea7 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import Any +from typing import Any, Sequence from pydantic import root_validator from sqlalchemy.exc import IntegrityError -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel from ..config import Config +from .capabilities import Capability, UserCapability from .connection import Connection @@ -24,6 +25,14 @@ class UserBase(SQLModel): class User(UserBase, table=True): password: str + capabilities: list[UserCapability] = Relationship( + back_populates="user", + sa_relationship_kwargs={ + "lazy": "joined", + "cascade": "all, delete-orphan", + }, + ) + @classmethod def create(cls, **kwargs) -> User | None: """ @@ -94,6 +103,37 @@ class User(UserBase, table=True): with Connection.session as db: db.delete(self) db.commit() + + def extend_capabilities(self, capabilities: Sequence[Capability]) -> None: + """ + Extend this user's capabilities + """ + + for capability in capabilities: + user_capability = UserCapability( + user_name=self.name, + capability_name=capability.value, + ) + + if user_capability not in self.capabilities: + self.capabilities.append(user_capability) + + def remove_capabilities(self, capabilities: Sequence[Capability]) -> None: + """ + Remove from this user's capabilities + """ + + for capability in capabilities: + try: + self.capabilities.remove(UserCapability( + user_name=self.name, + capability_name=capability.value, + )) + + except ValueError: + pass + + class UserCreate(UserBase): password: str | None = Field(default=None) password_clear: str | None = Field(default=None) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 335d058..074e5b5 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -15,7 +15,7 @@ from fastapi import FastAPI from .config import Config, Settings from .db import Connection from .db.schemata import User -from .db_new import connection, user +from .db_new import capabilities, connection, user from .routers import main_router settings = Settings.get() @@ -65,6 +65,14 @@ async def on_startup() -> None: uwe = user.User.authenticate("Uwe", "ulf") + uwe.extend_capabilities([capabilities.Capability.admin]) + uwe.update() + print(uwe) + + uwe.remove_capabilities([capabilities.Capability.admin]) + uwe.update() + print(uwe) + def main() -> None: uvicorn.run( From 730c7ab966785bc9d1d6261f51e30e8e6653a408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:43:28 +0000 Subject: [PATCH 11/47] add devices --- api/kiwi_vpn_api/db_new/device.py | 75 +++++++++++++++++++++++++++++++ api/kiwi_vpn_api/db_new/user.py | 5 +++ 2 files changed, 80 insertions(+) create mode 100644 api/kiwi_vpn_api/db_new/device.py diff --git a/api/kiwi_vpn_api/db_new/device.py b/api/kiwi_vpn_api/db_new/device.py new file mode 100644 index 0000000..c493ebb --- /dev/null +++ b/api/kiwi_vpn_api/db_new/device.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from datetime import datetime +from typing import TYPE_CHECKING + +from sqlalchemy.exc import IntegrityError +from sqlmodel import Field, Relationship, SQLModel, UniqueConstraint + +from .connection import Connection + +if TYPE_CHECKING: + from .user import User + + +class DeviceBase(SQLModel): + name: str + type: str + expiry: datetime | None + + +class DeviceCreate(DeviceBase): + owner_name: str | None + + +class Device(DeviceBase, table=True, ): + __table_args__ = (UniqueConstraint( + "owner_name", + "name", + ),) + + id: int | None = Field(primary_key=True) + owner_name: str | None = Field(foreign_key="user.name") + + owner: User = Relationship( + back_populates="devices", + ) + + @classmethod + def create(cls, **kwargs) -> Device | None: + """ + Create a new device in the database. + """ + + try: + with Connection.session as db: + device = cls.from_orm(DeviceCreate(**kwargs)) + + db.add(device) + db.commit() + db.refresh(device) + + return device + + except IntegrityError: + # device already existed + return None + + def update(self) -> None: + """ + Update this device in the database. + """ + + with Connection.session as db: + db.add(self) + db.commit() + db.refresh(self) + + def delete(self) -> bool: + """ + Delete this device from the database. + """ + + with Connection.session as db: + db.delete(self) + db.commit() diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index 3d3eea7..7d0ec57 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -9,6 +9,7 @@ from sqlmodel import Field, Relationship, SQLModel from ..config import Config from .capabilities import Capability, UserCapability from .connection import Connection +from .device import Device class UserBase(SQLModel): @@ -33,6 +34,10 @@ class User(UserBase, table=True): }, ) + devices: list[Device] = Relationship( + back_populates="owner", + ) + @classmethod def create(cls, **kwargs) -> User | None: """ From 04a5798258068b180726d909a3b5112a3c0a3416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:48:44 +0000 Subject: [PATCH 12/47] capabilities rework --- api/kiwi_vpn_api/db_new/capabilities.py | 8 ++++-- api/kiwi_vpn_api/db_new/user.py | 37 ++++++++----------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/api/kiwi_vpn_api/db_new/capabilities.py b/api/kiwi_vpn_api/db_new/capabilities.py index 7d92064..9ed8aba 100644 --- a/api/kiwi_vpn_api/db_new/capabilities.py +++ b/api/kiwi_vpn_api/db_new/capabilities.py @@ -13,9 +13,11 @@ class Capability(Enum): issue = "issue" renew = "renew" + def __repr__(self) -> str: + return self.value + class UserCapabilityBase(SQLModel): - user_name: str = Field(primary_key=True, foreign_key="user.name") capability_name: str = Field(primary_key=True) @property @@ -27,6 +29,8 @@ class UserCapabilityBase(SQLModel): class UserCapability(UserCapabilityBase, table=True): + user_name: str = Field(primary_key=True, foreign_key="user.name") + user: "User" = Relationship( - back_populates="capabilities" + back_populates="capabilities", ) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index 7d0ec57..cb2fe59 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Sequence +from typing import Any from pydantic import root_validator from sqlalchemy.exc import IntegrityError @@ -109,34 +109,19 @@ class User(UserBase, table=True): db.delete(self) db.commit() - def extend_capabilities(self, capabilities: Sequence[Capability]) -> None: - """ - Extend this user's capabilities - """ + def get_capabilities(self) -> set[Capability]: + return set( + capability._ + for capability in self.capabilities + ) - for capability in capabilities: - user_capability = UserCapability( + def set_capabilities(self, capabilities: set[Capability]) -> None: + self.capabilities = [ + UserCapability( user_name=self.name, capability_name=capability.value, - ) - - if user_capability not in self.capabilities: - self.capabilities.append(user_capability) - - def remove_capabilities(self, capabilities: Sequence[Capability]) -> None: - """ - Remove from this user's capabilities - """ - - for capability in capabilities: - try: - self.capabilities.remove(UserCapability( - user_name=self.name, - capability_name=capability.value, - )) - - except ValueError: - pass + ) for capability in capabilities + ] class UserCreate(UserBase): From 24ade65bb0e5435e54c8eedad6cff123c0391ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:48:59 +0000 Subject: [PATCH 13/47] db_new interface --- api/kiwi_vpn_api/db_new/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/kiwi_vpn_api/db_new/__init__.py b/api/kiwi_vpn_api/db_new/__init__.py index e69de29..3dd1119 100644 --- a/api/kiwi_vpn_api/db_new/__init__.py +++ b/api/kiwi_vpn_api/db_new/__init__.py @@ -0,0 +1,7 @@ +from .capabilities import Capability +from .connection import Connection +from .device import Device, DeviceBase, DeviceCreate +from .user import User, UserBase, UserCreate, UserRead + +__all__ = ["Capability", "Connection", "Device", "DeviceBase", "DeviceCreate", + "User", "UserBase", "UserCreate", "UserRead"] From c7f93d468e3de658f0c4282ea81f88f2485c4efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:50:00 +0000 Subject: [PATCH 14/47] rename --- api/kiwi_vpn_api/db_new/__init__.py | 2 +- api/kiwi_vpn_api/db_new/{capabilities.py => capability.py} | 0 api/kiwi_vpn_api/db_new/user.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename api/kiwi_vpn_api/db_new/{capabilities.py => capability.py} (100%) diff --git a/api/kiwi_vpn_api/db_new/__init__.py b/api/kiwi_vpn_api/db_new/__init__.py index 3dd1119..f418393 100644 --- a/api/kiwi_vpn_api/db_new/__init__.py +++ b/api/kiwi_vpn_api/db_new/__init__.py @@ -1,4 +1,4 @@ -from .capabilities import Capability +from .capability import Capability from .connection import Connection from .device import Device, DeviceBase, DeviceCreate from .user import User, UserBase, UserCreate, UserRead diff --git a/api/kiwi_vpn_api/db_new/capabilities.py b/api/kiwi_vpn_api/db_new/capability.py similarity index 100% rename from api/kiwi_vpn_api/db_new/capabilities.py rename to api/kiwi_vpn_api/db_new/capability.py diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index cb2fe59..fbb6083 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -7,7 +7,7 @@ from sqlalchemy.exc import IntegrityError from sqlmodel import Field, Relationship, SQLModel from ..config import Config -from .capabilities import Capability, UserCapability +from .capability import Capability, UserCapability from .connection import Connection from .device import Device From bc3f7984f5a049852da7a208fa4a93abc559ee34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:50:21 +0000 Subject: [PATCH 15/47] some fun with db_new --- api/kiwi_vpn_api/main.py | 56 +++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 074e5b5..a4a171d 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -14,8 +14,10 @@ from fastapi import FastAPI from .config import Config, Settings from .db import Connection -from .db.schemata import User -from .db_new import capabilities, connection, user +# from .db.schemata import User +from .db_new import Capability +from .db_new import Connection as Connection_new +from .db_new import Device, User from .routers import main_router settings = Settings.get() @@ -47,31 +49,43 @@ async def on_startup() -> None: # connect to database Connection.connect(await current_config.db.db_engine) - # some testing - with Connection.use() as db: - print(User.from_db(db, "admin")) - print(User.from_db(db, "nonexistent")) + # # some testing + # with Connection.use() as db: + # print(User.from_db(db, "admin")) + # print(User.from_db(db, "nonexistent")) - connection.Connection.connect("sqlite:///tmp/v2.db") + Connection_new.connect("sqlite:///tmp/v2.db") - user.User.create( - name="Uwe", - password_clear="ulf", - email="uwe@feh.de", - ) + User.create( + name="Uwe", + password_clear="ulf", + email="uwe@feh.de", + ) - print(user.User.get("Uwe")) - print(user.User.authenticate("Uwe", "uwe")) + print(User.get(name="Uwe")) + print(User.authenticate("Uwe", "uwe")) - uwe = user.User.authenticate("Uwe", "ulf") + uwe = User.authenticate("Uwe", "ulf") - uwe.extend_capabilities([capabilities.Capability.admin]) - uwe.update() - print(uwe) + uwe.set_capabilities([Capability.admin]) + uwe.update() + print(uwe.get_capabilities()) - uwe.remove_capabilities([capabilities.Capability.admin]) - uwe.update() - print(uwe) + uwe.set_capabilities([]) + uwe.update() + print(uwe.get_capabilities()) + + with Connection_new.session as db: + db.add(uwe) + print(uwe.devices) + + ipad = Device.create( + owner_name="Uwe", + name="iPad", + type="tablet", + ) + # ipad = Device. + print(ipad) def main() -> None: From 89069c9d0f5872371e2abad59eebcfd9a9d59b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:55:18 +0000 Subject: [PATCH 16/47] typo --- api/kiwi_vpn_api/db_new/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/db_new/device.py b/api/kiwi_vpn_api/db_new/device.py index c493ebb..1357ab3 100644 --- a/api/kiwi_vpn_api/db_new/device.py +++ b/api/kiwi_vpn_api/db_new/device.py @@ -22,7 +22,7 @@ class DeviceCreate(DeviceBase): owner_name: str | None -class Device(DeviceBase, table=True, ): +class Device(DeviceBase, table=True): __table_args__ = (UniqueConstraint( "owner_name", "name", From 396359ceff2faf0f74eabaf8702455b4da21ffab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:59:53 +0000 Subject: [PATCH 17/47] message to future me --- api/kiwi_vpn_api/db_new/device.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/kiwi_vpn_api/db_new/device.py b/api/kiwi_vpn_api/db_new/device.py index 1357ab3..f260cbc 100644 --- a/api/kiwi_vpn_api/db_new/device.py +++ b/api/kiwi_vpn_api/db_new/device.py @@ -31,6 +31,8 @@ class Device(DeviceBase, table=True): id: int | None = Field(primary_key=True) owner_name: str | None = Field(foreign_key="user.name") + # no idea, but "User" (in quotes) doesn't work here + # might be a future problem? owner: User = Relationship( back_populates="devices", ) From 22e1ef7bf4cd8e03899bf688195a8c1fc26a26fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 01:00:07 +0000 Subject: [PATCH 18/47] don't require email --- api/kiwi_vpn_api/db_new/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index fbb6083..b8cd6ec 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -14,7 +14,7 @@ from .device import Device class UserBase(SQLModel): name: str = Field(primary_key=True) - email: str + email: str | None = Field(default=None) country: str | None = Field(default=None) state: str | None = Field(default=None) From d41cd9b15be674379070d2621453d8dfad6f81fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 01:28:49 +0000 Subject: [PATCH 19/47] check if user can do --- api/kiwi_vpn_api/db_new/user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db_new/user.py index b8cd6ec..2e53964 100644 --- a/api/kiwi_vpn_api/db_new/user.py +++ b/api/kiwi_vpn_api/db_new/user.py @@ -109,6 +109,9 @@ class User(UserBase, table=True): db.delete(self) db.commit() + def can(self, capability: Capability) -> bool: + return capability in self.get_capabilities() + def get_capabilities(self) -> set[Capability]: return set( capability._ From 6012998ecf70d9ad5e6c86c57270342952de0a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 01:30:37 +0000 Subject: [PATCH 20/47] don't play with new db --- api/kiwi_vpn_api/main.py | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index a4a171d..4730d7e 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -13,11 +13,8 @@ import uvicorn from fastapi import FastAPI from .config import Config, Settings -from .db import Connection # from .db.schemata import User -from .db_new import Capability -from .db_new import Connection as Connection_new -from .db_new import Device, User +from .db_new import Connection from .routers import main_router settings = Settings.get() @@ -47,46 +44,13 @@ async def on_startup() -> None: # check if configured if (current_config := await Config.load()) is not None: # connect to database - Connection.connect(await current_config.db.db_engine) + Connection.connect("sqlite:///tmp/v2.db") # # some testing # with Connection.use() as db: # print(User.from_db(db, "admin")) # print(User.from_db(db, "nonexistent")) - Connection_new.connect("sqlite:///tmp/v2.db") - - User.create( - name="Uwe", - password_clear="ulf", - email="uwe@feh.de", - ) - - print(User.get(name="Uwe")) - print(User.authenticate("Uwe", "uwe")) - - uwe = User.authenticate("Uwe", "ulf") - - uwe.set_capabilities([Capability.admin]) - uwe.update() - print(uwe.get_capabilities()) - - uwe.set_capabilities([]) - uwe.update() - print(uwe.get_capabilities()) - - with Connection_new.session as db: - db.add(uwe) - print(uwe.devices) - - ipad = Device.create( - owner_name="Uwe", - name="iPad", - type="tablet", - ) - # ipad = Device. - print(ipad) - def main() -> None: uvicorn.run( From 0619f00f6afd3c34b920d8ad2c40faf4b82289e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 01:31:37 +0000 Subject: [PATCH 21/47] rework common and admin router for new db --- api/kiwi_vpn_api/routers/__init__.py | 6 +- api/kiwi_vpn_api/routers/_common.py | 16 ++--- api/kiwi_vpn_api/routers/admin.py | 33 ++++------- api/kiwi_vpn_api/routers/dn.py | 88 ---------------------------- 4 files changed, 20 insertions(+), 123 deletions(-) delete mode 100644 api/kiwi_vpn_api/routers/dn.py diff --git a/api/kiwi_vpn_api/routers/__init__.py b/api/kiwi_vpn_api/routers/__init__.py index 22bb142..d80b610 100644 --- a/api/kiwi_vpn_api/routers/__init__.py +++ b/api/kiwi_vpn_api/routers/__init__.py @@ -1,10 +1,12 @@ from fastapi import APIRouter -from . import admin, user +from . import admin + +# from . import user main_router = APIRouter(prefix="/api/v1") main_router.include_router(admin.router) -main_router.include_router(user.router) +# main_router.include_router(user.router) __all__ = ["main_router"] diff --git a/api/kiwi_vpn_api/routers/_common.py b/api/kiwi_vpn_api/routers/_common.py index d7f3969..7420ce0 100644 --- a/api/kiwi_vpn_api/routers/_common.py +++ b/api/kiwi_vpn_api/routers/_common.py @@ -5,11 +5,9 @@ Common dependencies for routers. from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.orm import Session from ..config import Config -from ..db import Connection -from ..db.schemata import User +from ..db_new import Capability, User oauth2_scheme = OAuth2PasswordBearer(tokenUrl="user/authenticate") @@ -56,7 +54,6 @@ class Responses: async def get_current_user( token: str = Depends(oauth2_scheme), - db: Session | None = Depends(Connection.get), current_config: Config | None = Depends(Config.load), ) -> User | None: """ @@ -68,13 +65,11 @@ async def get_current_user( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) username = await current_config.jwt.decode_token(token) - user = User.from_db(db, username) - return user + return User.get(username) async def get_current_user_if_exists( - current_config: Config | None = Depends(Config.load), current_user: User | None = Depends(get_current_user), ) -> User: """ @@ -89,7 +84,6 @@ async def get_current_user_if_exists( async def get_current_user_if_admin( - current_config: Config | None = Depends(Config.load), current_user: User = Depends(get_current_user_if_exists), ) -> User: """ @@ -97,7 +91,7 @@ async def get_current_user_if_admin( """ # fail if not requested by an admin - if not current_user.is_admin(): + if not current_user.can(Capability.admin): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) return current_user @@ -105,7 +99,6 @@ async def get_current_user_if_admin( async def get_current_user_if_admin_or_self( user_name: str, - current_config: Config | None = Depends(Config.load), current_user: User = Depends(get_current_user_if_exists), ) -> User: """ @@ -116,7 +109,8 @@ async def get_current_user_if_admin_or_self( """ # fail if not requested by an admin or self - if not (current_user.is_admin() or current_user.name == user_name): + if not (current_user.can(Capability.admin) + or current_user.name == user_name): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) return current_user diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 9422eff..6b6f3fa 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -6,9 +6,8 @@ from fastapi import APIRouter, Depends, HTTPException, status from ..config import Config -from ..db import Connection -from ..db.schemata import User, UserCapability, UserCreate -from ._common import Responses, get_current_user +from ..db_new import Capability, Connection, User, UserCreate +from ._common import Responses, get_current_user_if_admin router = APIRouter(prefix="/admin", tags=["admin"]) @@ -22,7 +21,7 @@ router = APIRouter(prefix="/admin", tags=["admin"]) ) async def install( config: Config, - admin_user: UserCreate, + # admin_user: UserCreate, current_config: Config | None = Depends(Config.load), ): """ @@ -35,18 +34,13 @@ async def install( # create config file, connect to database await config.save() - Connection.connect(await config.db.db_engine) + Connection.connect("sqlite:///tmp/v2.db") - # create an administrative user - with Connection.use() as db: - new_user = User.create( - db=db, - user=admin_user, - crypt_context=await config.crypto.crypt_context, - ) - - new_user.capabilities.append(UserCapability.admin) - new_user.update(db) + # # create an administrative user + # new_user = User.create(**admin_user) + # assert new_user is not None + # new_user.set_capabilities([Capability.login, Capability.admin]) + # new_user.update() @router.put( @@ -61,7 +55,7 @@ async def install( async def set_config( new_config: Config, current_config: Config | None = Depends(Config.load), - current_user: User | None = Depends(get_current_user), + _: User | None = Depends(get_current_user_if_admin), ): """ PUT ./config: Edit `kiwi-vpn` main config. @@ -71,11 +65,6 @@ async def set_config( if current_config is None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) - # fail if not requested by an admin - if (current_user is None - or UserCapability.admin not in current_user.capabilities): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - # update config file, reconnect to database await new_config.save() - Connection.connect(await new_config.db.db_engine) + Connection.connect("sqlite:///tmp/v2.db") diff --git a/api/kiwi_vpn_api/routers/dn.py b/api/kiwi_vpn_api/routers/dn.py deleted file mode 100644 index 80ed087..0000000 --- a/api/kiwi_vpn_api/routers/dn.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -/dn endpoints. -""" - - -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session - -from ..db import Connection -from ..db.schemata import DistinguishedName, DistinguishedNameCreate, User -from ._common import Responses, get_current_user_if_admin_or_self - -router = APIRouter(prefix="/dn") - - -@router.post( - "", - responses={ - status.HTTP_200_OK: Responses.OK, - status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED, - status.HTTP_401_UNAUTHORIZED: Responses.NEEDS_USER, - status.HTTP_403_FORBIDDEN: Responses.NEEDS_ADMIN, - status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST, - status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS, - }, -) -async def add_distinguished_name( - user_name: str, - distinguished_name: DistinguishedNameCreate, - _: User = Depends(get_current_user_if_admin_or_self), - db: Session | None = Depends(Connection.get), -): - """ - POST ./: Create a new distinguished name in the database. - """ - - owner = User.from_db( - db=db, - name=user_name, - ) - - # fail if owner doesn't exist - if owner is None: - raise HTTPException(status_code=status.HTTP_409_CONFLICT) - - # actually create the new user - new_dn = DistinguishedName.create( - db=db, - dn=distinguished_name, - owner=owner, - ) - - # fail if creation was unsuccessful - if new_dn is None: - raise HTTPException(status_code=status.HTTP_409_CONFLICT) - - # return the created user on success - return new_dn - - -# @router.delete( -# "", -# responses={ -# status.HTTP_200_OK: Responses.OK, -# status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED, -# status.HTTP_401_UNAUTHORIZED: Responses.NEEDS_USER, -# status.HTTP_403_FORBIDDEN: Responses.NEEDS_ADMIN, -# status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST, -# }, -# ) -# async def remove_distinguished_name( -# user_name: str, -# _: User = Depends(get_current_user_if_admin), -# db: Session | None = Depends(Connection.get), -# ): -# """ -# DELETE ./{user_name}: Remove a user from the database. -# """ - -# # get the user -# user = User.from_db( -# db=db, -# name=user_name, -# ) - -# # fail if deletion was unsuccessful -# if user is None or not user.delete(db): -# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) From b4a74aca5ffae50336f981e6b7296b8c4c75989a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 01:34:15 +0000 Subject: [PATCH 22/47] remove old db --- api/kiwi_vpn_api/db/__init__.py | 4 - api/kiwi_vpn_api/db/connection.py | 75 -------- api/kiwi_vpn_api/db/models.py | 82 --------- api/kiwi_vpn_api/db/schemata/__init__.py | 6 - api/kiwi_vpn_api/db/schemata/device.py | 79 --------- api/kiwi_vpn_api/db/schemata/user.py | 164 ------------------ .../db/schemata/user_capability.py | 43 ----- 7 files changed, 453 deletions(-) delete mode 100644 api/kiwi_vpn_api/db/__init__.py delete mode 100644 api/kiwi_vpn_api/db/connection.py delete mode 100644 api/kiwi_vpn_api/db/models.py delete mode 100644 api/kiwi_vpn_api/db/schemata/__init__.py delete mode 100644 api/kiwi_vpn_api/db/schemata/device.py delete mode 100644 api/kiwi_vpn_api/db/schemata/user.py delete mode 100644 api/kiwi_vpn_api/db/schemata/user_capability.py diff --git a/api/kiwi_vpn_api/db/__init__.py b/api/kiwi_vpn_api/db/__init__.py deleted file mode 100644 index 80b582e..0000000 --- a/api/kiwi_vpn_api/db/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import models, schemata -from .connection import Connection - -__all__ = ["Connection", "models", "schemata"] diff --git a/api/kiwi_vpn_api/db/connection.py b/api/kiwi_vpn_api/db/connection.py deleted file mode 100644 index 97592e1..0000000 --- a/api/kiwi_vpn_api/db/connection.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Utilities for handling SQLAlchemy database connections. -""" - -from typing import Generator - -from sqlalchemy.engine import Engine -from sqlalchemy.orm import Session, sessionmaker - -from .models import ORMBaseModel - - -class SessionManager: - """ - Simple context manager for an ORM session. - """ - - __session: Session - - def __init__(self, session: Session) -> None: - self.__session = session - - def __enter__(self) -> Session: - return self.__session - - def __exit__(self, *args) -> None: - self.__session.close() - - -class Connection: - """ - Namespace for the database connection. - """ - - engine: Engine | None = None - session_local: sessionmaker | None = None - - @classmethod - def connect(cls, engine: Engine) -> None: - """ - Connect ORM to a database engine. - """ - - cls.engine = engine - cls.session_local = sessionmaker( - autocommit=False, autoflush=False, bind=engine, - ) - ORMBaseModel.metadata.create_all(bind=engine) - - @classmethod - def use(cls) -> SessionManager | None: - """ - Create an ORM session using a context manager. - """ - - if cls.session_local is None: - return None - - return SessionManager(cls.session_local()) - - @classmethod - async def get(cls) -> Generator[Session | None, None, None]: - """ - Create an ORM session using a FastAPI compatible async generator. - """ - - if cls.session_local is None: - yield None - - else: - db = cls.session_local() - try: - yield db - finally: - db.close() diff --git a/api/kiwi_vpn_api/db/models.py b/api/kiwi_vpn_api/db/models.py deleted file mode 100644 index fc41d6f..0000000 --- a/api/kiwi_vpn_api/db/models.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -SQLAlchemy representation of database contents. -""" - -from __future__ import annotations - -from sqlalchemy import (Column, DateTime, ForeignKey, Integer, String, - UniqueConstraint) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Session, relationship - -ORMBaseModel = declarative_base() - - -class UserCapability(ORMBaseModel): - __tablename__ = "user_capabilities" - - user_name = Column( - String, - ForeignKey("users.name"), - primary_key=True, - index=True, - ) - capability = Column(String, primary_key=True) - - -class User(ORMBaseModel): - __tablename__ = "users" - - name = Column(String, primary_key=True, index=True) - password = Column(String, nullable=False) - email = Column(String) - - country = Column(String(2)) - state = Column(String) - city = Column(String) - organization = Column(String) - organizational_unit = Column(String) - - capabilities: list[UserCapability] = relationship( - "UserCapability", lazy="joined", cascade="all, delete-orphan" - ) - devices: list[Device] = relationship( - "Device", lazy="select", back_populates="owner" - ) - - @classmethod - def from_db( - cls, - db: Session, - name: str, - ) -> User | None: - """ - Load user from database by name. - """ - - return ( - db - .query(cls) - .filter(cls.name == name) - .first() - ) - - -class Device(ORMBaseModel): - __tablename__ = "devices" - - id = Column(Integer, primary_key=True, autoincrement=True) - - owner_name = Column(String, ForeignKey("users.name")) - name = Column(String) - type = Column(String) - expiry = Column(DateTime) - - owner: User = relationship( - "User", lazy="joined", back_populates="devices" - ) - - UniqueConstraint( - owner_name, - name, - ) diff --git a/api/kiwi_vpn_api/db/schemata/__init__.py b/api/kiwi_vpn_api/db/schemata/__init__.py deleted file mode 100644 index 5a991aa..0000000 --- a/api/kiwi_vpn_api/db/schemata/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .device import Device, DeviceBase, DeviceCreate -from .user import User, UserBase, UserCreate -from .user_capability import UserCapability - -__all__ = ["Device", "DeviceBase", "DeviceCreate", - "User", "UserBase", "UserCreate", "UserCapability"] diff --git a/api/kiwi_vpn_api/db/schemata/device.py b/api/kiwi_vpn_api/db/schemata/device.py deleted file mode 100644 index 8e47f7e..0000000 --- a/api/kiwi_vpn_api/db/schemata/device.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Pydantic representation of database contents. -""" - -from __future__ import annotations - -from datetime import datetime - -from pydantic import BaseModel -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session - -from .. import models - - -class DeviceBase(BaseModel): - name: str - type: str - expiry: datetime - - -class DeviceCreate(DeviceBase): - owner_name: str - - -class Device(DeviceBase): - class Config: - orm_mode = True - - @classmethod - def create( - cls, - db: Session, - device: DeviceCreate, - ) -> Device | None: - """ - Create a new device in the database. - """ - - try: - db_device = models.Device( - owner_name=device.owner_name, - - name=device.name, - type=device.type, - expiry=device.expiry, - ) - - db.add(db_device) - db.commit() - db.refresh(db_device) - - return cls.from_orm(db_device) - - except IntegrityError: - # device already existed - return None - - def delete( - self, - db: Session, - ) -> bool: - """ - Delete this device from the database. - """ - - db_device = models.Device( - # owner_name= - name=self.name, - ) - db.refresh(db_device) - - if db_device is None: - # nonexistent device - return False - - db.delete(db_device) - db.commit() - return True diff --git a/api/kiwi_vpn_api/db/schemata/user.py b/api/kiwi_vpn_api/db/schemata/user.py deleted file mode 100644 index b31e31e..0000000 --- a/api/kiwi_vpn_api/db/schemata/user.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -Pydantic representation of database contents. -""" - -from __future__ import annotations - -from typing import Any - -from passlib.context import CryptContext -from pydantic import BaseModel, Field, validator -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session - -from .. import models -from .device import Device -from .user_capability import UserCapability - - -class UserBase(BaseModel): - name: str - email: str - - capabilities: list[UserCapability] = [] - - country: str | None = Field(default=None, repr=False) - state: str | None = Field(default=None, repr=False) - city: str | None = Field(default=None, repr=False) - organization: str | None = Field(default=None, repr=False) - organizational_unit: str | None = Field(default=None, repr=False) - - -class UserCreate(UserBase): - password: str - - -class User(UserBase): - devices: list[Device] = Field( - default=[], repr=False - ) - - class Config: - orm_mode = True - - @validator("capabilities", pre=True) - @classmethod - def unify_capabilities(cls, value: list[Any]) -> list[UserCapability]: - """ - Import the capabilities from various formats - """ - - return [ - UserCapability.from_value(capability) - for capability in value - ] - - @classmethod - def from_db( - cls, - db: Session, - name: str, - ) -> User | None: - """ - Load user from database by name. - """ - - if (db_user := models.User.from_db(db, name)) is None: - return None - - return cls.from_orm(db_user) - - @classmethod - def create( - cls, - db: Session, - user: UserCreate, - crypt_context: CryptContext, - ) -> User | None: - """ - Create a new user in the database. - """ - - try: - db_user = models.User( - name=user.name, - password=crypt_context.hash(user.password), - email=user.email, - capabilities=[ - capability.model - for capability in user.capabilities - ], - ) - - db.add(db_user) - db.commit() - db.refresh(db_user) - - return cls.from_orm(db_user) - - except IntegrityError: - # user already existed - return None - - def is_admin(self) -> bool: - return UserCapability.admin in self.capabilities - - def authenticate( - self, - db: Session, - password: str, - crypt_context: CryptContext, - ) -> User | None: - """ - Authenticate with name/password against users in database. - """ - - if (db_user := models.User.from_db(db, self.name)) is None: - # nonexistent user, fake doing password verification - crypt_context.dummy_verify() - return False - - if not crypt_context.verify(password, db_user.password): - # password hash mismatch - return False - - self.from_orm(db_user) - - return True - - def update( - self, - db: Session, - ) -> None: - """ - Update this user in the database. - """ - - if (db_user := models.User.from_db(db, self.name)) is None: - return None - - for capability in db_user.capabilities: - db.delete(capability) - - db_user.capabilities = [ - capability.model - for capability in self.capabilities - ] - - db.commit() - - def delete( - self, - db: Session, - ) -> bool: - """ - Delete this user from the database. - """ - - if (db_user := models.User.from_db(db, self.name)) is None: - # nonexistent user - return False - - db.delete(db_user) - db.commit() - return True diff --git a/api/kiwi_vpn_api/db/schemata/user_capability.py b/api/kiwi_vpn_api/db/schemata/user_capability.py deleted file mode 100644 index 28e1870..0000000 --- a/api/kiwi_vpn_api/db/schemata/user_capability.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Pydantic representation of database contents. -""" - -from __future__ import annotations - -from enum import Enum - -from .. import models - - -class UserCapability(Enum): - admin = "admin" - login = "login" - issue = "issue" - renew = "renew" - - def __repr__(self) -> str: - return self.value - - @classmethod - def from_value(cls, value) -> UserCapability: - """ - Create UserCapability from various formats - """ - - if isinstance(value, cls): - # value is already a UserCapability, use that - return value - - elif isinstance(value, models.UserCapability): - # create from db format - return cls(value.capability) - - else: - # create from string representation - return cls(str(value)) - - @property - def model(self) -> models.UserCapability: - return models.UserCapability( - capability=self.value, - ) From 12432286bf7ffabd7ac3a4f6e8854a129b7d1747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 01:34:52 +0000 Subject: [PATCH 23/47] rename db_new -> db --- api/kiwi_vpn_api/{db_new => db}/__init__.py | 0 api/kiwi_vpn_api/{db_new => db}/capability.py | 0 api/kiwi_vpn_api/{db_new => db}/connection.py | 0 api/kiwi_vpn_api/{db_new => db}/device.py | 0 api/kiwi_vpn_api/{db_new => db}/user.py | 0 api/kiwi_vpn_api/main.py | 2 +- api/kiwi_vpn_api/routers/_common.py | 2 +- api/kiwi_vpn_api/routers/admin.py | 2 +- 8 files changed, 3 insertions(+), 3 deletions(-) rename api/kiwi_vpn_api/{db_new => db}/__init__.py (100%) rename api/kiwi_vpn_api/{db_new => db}/capability.py (100%) rename api/kiwi_vpn_api/{db_new => db}/connection.py (100%) rename api/kiwi_vpn_api/{db_new => db}/device.py (100%) rename api/kiwi_vpn_api/{db_new => db}/user.py (100%) diff --git a/api/kiwi_vpn_api/db_new/__init__.py b/api/kiwi_vpn_api/db/__init__.py similarity index 100% rename from api/kiwi_vpn_api/db_new/__init__.py rename to api/kiwi_vpn_api/db/__init__.py diff --git a/api/kiwi_vpn_api/db_new/capability.py b/api/kiwi_vpn_api/db/capability.py similarity index 100% rename from api/kiwi_vpn_api/db_new/capability.py rename to api/kiwi_vpn_api/db/capability.py diff --git a/api/kiwi_vpn_api/db_new/connection.py b/api/kiwi_vpn_api/db/connection.py similarity index 100% rename from api/kiwi_vpn_api/db_new/connection.py rename to api/kiwi_vpn_api/db/connection.py diff --git a/api/kiwi_vpn_api/db_new/device.py b/api/kiwi_vpn_api/db/device.py similarity index 100% rename from api/kiwi_vpn_api/db_new/device.py rename to api/kiwi_vpn_api/db/device.py diff --git a/api/kiwi_vpn_api/db_new/user.py b/api/kiwi_vpn_api/db/user.py similarity index 100% rename from api/kiwi_vpn_api/db_new/user.py rename to api/kiwi_vpn_api/db/user.py diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 4730d7e..887f52e 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -14,7 +14,7 @@ from fastapi import FastAPI from .config import Config, Settings # from .db.schemata import User -from .db_new import Connection +from .db import Connection from .routers import main_router settings = Settings.get() diff --git a/api/kiwi_vpn_api/routers/_common.py b/api/kiwi_vpn_api/routers/_common.py index 7420ce0..295e404 100644 --- a/api/kiwi_vpn_api/routers/_common.py +++ b/api/kiwi_vpn_api/routers/_common.py @@ -7,7 +7,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from ..config import Config -from ..db_new import Capability, User +from ..db import Capability, User oauth2_scheme = OAuth2PasswordBearer(tokenUrl="user/authenticate") diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 6b6f3fa..866f6a4 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from ..config import Config -from ..db_new import Capability, Connection, User, UserCreate +from ..db import Capability, Connection, User, UserCreate from ._common import Responses, get_current_user_if_admin router = APIRouter(prefix="/admin", tags=["admin"]) From ae8894f5ccd3a4a05a0761a09f0df5474472c185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 01:52:56 +0000 Subject: [PATCH 24/47] repaired admin routes --- api/kiwi_vpn_api/main.py | 2 +- api/kiwi_vpn_api/routers/admin.py | 48 +++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 887f52e..e560590 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -44,7 +44,7 @@ async def on_startup() -> None: # check if configured if (current_config := await Config.load()) is not None: # connect to database - Connection.connect("sqlite:///tmp/v2.db") + Connection.connect("sqlite:///tmp/vpn.db") # # some testing # with Connection.use() as db: diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 866f6a4..e856ea9 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status +from sqlmodel import select from ..config import Config from ..db import Capability, Connection, User, UserCreate @@ -13,34 +14,57 @@ router = APIRouter(prefix="/admin", tags=["admin"]) @router.put( - "/install", + "/install/config", responses={ status.HTTP_200_OK: Responses.OK, status.HTTP_400_BAD_REQUEST: Responses.INSTALLED, }, ) -async def install( +async def initial_configure( config: Config, - # admin_user: UserCreate, current_config: Config | None = Depends(Config.load), ): """ - PUT ./install: Install `kiwi-vpn`. + PUT ./install/config: Configure `kiwi-vpn`. """ - # fail if already installed + # fail if already configured if current_config is not None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) # create config file, connect to database await config.save() - Connection.connect("sqlite:///tmp/v2.db") + Connection.connect("sqlite:///tmp/vpn.db") - # # create an administrative user - # new_user = User.create(**admin_user) - # assert new_user is not None - # new_user.set_capabilities([Capability.login, Capability.admin]) - # new_user.update() + +@router.put( + "/install/admin", + responses={ + status.HTTP_200_OK: Responses.OK, + status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED, + status.HTTP_409_CONFLICT: Responses.ENTRY_EXISTS, + }, +) +async def create_initial_admin( + admin_user: UserCreate, + current_config: Config | None = Depends(Config.load), +): + """ + PUT ./install/admin: Create the first administrative user. + """ + + # fail if not configured + if current_config is None: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + + with Connection.session as db: + if db.exec(select(User)).first() is not None: + raise HTTPException(status_code=status.HTTP_409_CONFLICT) + + # create an administrative user + new_user = User.create(**admin_user.dict()) + new_user.set_capabilities([Capability.login, Capability.admin]) + new_user.update() @router.put( @@ -67,4 +91,4 @@ async def set_config( # update config file, reconnect to database await new_config.save() - Connection.connect("sqlite:///tmp/v2.db") + Connection.connect("sqlite:///tmp/vpn.db") From 19dd5aaee743c41a10010b10675b58884ebda4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:00:58 +0000 Subject: [PATCH 25/47] use correct database URI --- api/kiwi_vpn_api/config.py | 20 ++++++-------------- api/kiwi_vpn_api/main.py | 2 +- api/kiwi_vpn_api/routers/admin.py | 4 ++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/api/kiwi_vpn_api/config.py b/api/kiwi_vpn_api/config.py index 5c8672f..095b6c4 100644 --- a/api/kiwi_vpn_api/config.py +++ b/api/kiwi_vpn_api/config.py @@ -20,8 +20,6 @@ from jose import JWTError, jwt from jose.constants import ALGORITHMS from passlib.context import CryptContext from pydantic import BaseModel, BaseSettings, Field, validator -from sqlalchemy import create_engine -from sqlalchemy.engine import Engine class Settings(BaseSettings): @@ -69,17 +67,14 @@ class DBConfig(BaseModel): mysql_args: list[str] = ["charset=utf8mb4"] @property - async def db_engine(self) -> Engine: + def uri(self) -> str: """ - Construct an SQLAlchemy engine + Construct a database connection string """ if self.type is DBType.sqlite: # SQLite backend - return create_engine( - f"sqlite:///{self.database}", - connect_args={"check_same_thread": False}, - ) + return f"sqlite:///{self.database}" elif self.type is DBType.mysql: # MySQL backend @@ -88,12 +83,9 @@ class DBConfig(BaseModel): else: args_str = "" - return create_engine( - f"mysql+{self.mysql_driver}://" - f"{self.user}:{self.password}@{self.host}" - f"/{self.database}{args_str}", - pool_recycle=3600, - ) + return (f"mysql+{self.mysql_driver}://" + f"{self.user}:{self.password}@{self.host}" + f"/{self.database}{args_str}") class JWTConfig(BaseModel): diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index e560590..71870fb 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -44,7 +44,7 @@ async def on_startup() -> None: # check if configured if (current_config := await Config.load()) is not None: # connect to database - Connection.connect("sqlite:///tmp/vpn.db") + Connection.connect(current_config.db.uri) # # some testing # with Connection.use() as db: diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index e856ea9..ce03583 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -34,7 +34,7 @@ async def initial_configure( # create config file, connect to database await config.save() - Connection.connect("sqlite:///tmp/vpn.db") + Connection.connect(current_config.db.uri) @router.put( @@ -91,4 +91,4 @@ async def set_config( # update config file, reconnect to database await new_config.save() - Connection.connect("sqlite:///tmp/vpn.db") + Connection.connect(current_config.db.uri) From ce9ea61da54073a71717b710c27b09596669f325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:02:12 +0000 Subject: [PATCH 26/47] some testing stuff --- api/kiwi_vpn_api/main.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 71870fb..4503d1a 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -13,8 +13,7 @@ import uvicorn from fastapi import FastAPI from .config import Config, Settings -# from .db.schemata import User -from .db import Connection +from .db import Connection, User from .routers import main_router settings = Settings.get() @@ -46,10 +45,9 @@ async def on_startup() -> None: # connect to database Connection.connect(current_config.db.uri) - # # some testing - # with Connection.use() as db: - # print(User.from_db(db, "admin")) - # print(User.from_db(db, "nonexistent")) + # some testing + print(User.get("admin")) + print(User.get("nonexistent")) def main() -> None: From b7179e7cfc4cc73e8527187f824b77e5791adaed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:03:31 +0000 Subject: [PATCH 27/47] limit query --- api/kiwi_vpn_api/routers/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index ce03583..99dfdeb 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -58,7 +58,7 @@ async def create_initial_admin( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) with Connection.session as db: - if db.exec(select(User)).first() is not None: + if db.exec(select(User).limit(1)).first() is not None: raise HTTPException(status_code=status.HTTP_409_CONFLICT) # create an administrative user From 3d83ddb6cc2838c5c68129ce727256d9f619cab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:15:42 +0000 Subject: [PATCH 28/47] Config.load_sync -> Config._ --- api/kiwi_vpn_api/config.py | 15 +++++++++++---- api/kiwi_vpn_api/db/user.py | 4 ++-- api/kiwi_vpn_api/main.py | 2 +- api/kiwi_vpn_api/routers/admin.py | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/api/kiwi_vpn_api/config.py b/api/kiwi_vpn_api/config.py index 095b6c4..b90c103 100644 --- a/api/kiwi_vpn_api/config.py +++ b/api/kiwi_vpn_api/config.py @@ -196,15 +196,22 @@ class Config(BaseModel): jwt: JWTConfig = Field(default_factory=JWTConfig) crypto: CryptoConfig = Field(default_factory=CryptoConfig) - @staticmethod - def load_sync() -> Config | None: + __singleton: Config | None = None + + @classmethod + @property + def _(cls) -> Config | None: """ Load configuration from config file """ + if cls.__singleton is not None: + return cls.__singleton + try: with open(Settings.get().config_file, "r") as config_file: - return Config.parse_obj(json.load(config_file)) + cls.__singleton = Config.parse_obj(json.load(config_file)) + return cls.__singleton except FileNotFoundError: return None @@ -222,7 +229,7 @@ class Config(BaseModel): except FileNotFoundError: return None - async def save(self) -> None: + def save(self) -> None: """ Save configuration to config file """ diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index 2e53964..f13ccc1 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -77,7 +77,7 @@ class User(UserBase, table=True): Authenticate with name/password against users in database. """ - crypt_context = Config.load_sync().crypto.crypt_context_sync + crypt_context = Config._.crypto.crypt_context_sync if (user := cls.get(name)) is None: # nonexistent user, fake doing password verification @@ -141,7 +141,7 @@ class UserCreate(UserBase): if (password_clear := values.get("password_clear")) is None: raise ValueError("No password to hash") - if (current_config := Config.load_sync()) is None: + if (current_config := Config._) is None: raise ValueError("Not configured") values["password"] = current_config.crypto.crypt_context_sync.hash( diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 4503d1a..594d948 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -41,7 +41,7 @@ app.include_router(main_router) @app.on_event("startup") async def on_startup() -> None: # check if configured - if (current_config := await Config.load()) is not None: + if (current_config := Config._) is not None: # connect to database Connection.connect(current_config.db.uri) diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 99dfdeb..64c7307 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -33,7 +33,7 @@ async def initial_configure( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) # create config file, connect to database - await config.save() + config.save() Connection.connect(current_config.db.uri) @@ -90,5 +90,5 @@ async def set_config( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) # update config file, reconnect to database - await new_config.save() + new_config.save() Connection.connect(current_config.db.uri) From ca955d11044af05e3e9c22b95c47c643ede798df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:23:00 +0000 Subject: [PATCH 29/47] fix for crypt_context and load() --- api/kiwi_vpn_api/config.py | 26 +++++++------------------- api/kiwi_vpn_api/db/user.py | 4 ++-- api/kiwi_vpn_api/routers/user.py | 4 ++-- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/api/kiwi_vpn_api/config.py b/api/kiwi_vpn_api/config.py index b90c103..531b69a 100644 --- a/api/kiwi_vpn_api/config.py +++ b/api/kiwi_vpn_api/config.py @@ -173,14 +173,7 @@ class CryptoConfig(BaseModel): schemes: list[str] = ["bcrypt"] @property - def crypt_context_sync(self) -> CryptContext: - return CryptContext( - schemes=self.schemes, - deprecated="auto", - ) - - @property - async def crypt_context(self) -> CryptContext: + def crypt_context(self) -> CryptContext: return CryptContext( schemes=self.schemes, deprecated="auto", @@ -199,8 +192,7 @@ class Config(BaseModel): __singleton: Config | None = None @classmethod - @property - def _(cls) -> Config | None: + def load(cls) -> Config | None: """ Load configuration from config file """ @@ -216,18 +208,14 @@ class Config(BaseModel): except FileNotFoundError: return None - @staticmethod - async def load() -> Config | None: + @classmethod + @property + def _(cls) -> Config | None: """ - Load configuration from config file + Shorthand for load() """ - try: - with open(Settings.get().config_file, "r") as config_file: - return Config.parse_obj(json.load(config_file)) - - except FileNotFoundError: - return None + return cls.load() def save(self) -> None: """ diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index f13ccc1..3b462c9 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -77,7 +77,7 @@ class User(UserBase, table=True): Authenticate with name/password against users in database. """ - crypt_context = Config._.crypto.crypt_context_sync + crypt_context = Config._.crypto.crypt_context if (user := cls.get(name)) is None: # nonexistent user, fake doing password verification @@ -144,7 +144,7 @@ class UserCreate(UserBase): if (current_config := Config._) is None: raise ValueError("Not configured") - values["password"] = current_config.crypto.crypt_context_sync.hash( + values["password"] = current_config.crypto.crypt_context.hash( password_clear) return values diff --git a/api/kiwi_vpn_api/routers/user.py b/api/kiwi_vpn_api/routers/user.py index 627c5fc..2deace2 100644 --- a/api/kiwi_vpn_api/routers/user.py +++ b/api/kiwi_vpn_api/routers/user.py @@ -43,7 +43,7 @@ async def login( if not user.authenticate( db=db, password=form_data.password, - crypt_context=await current_config.crypto.crypt_context, + crypt_context=current_config.crypto.crypt_context, ): # authentication failed raise HTTPException( @@ -93,7 +93,7 @@ async def add_user( new_user = User.create( db=db, user=user, - crypt_context=await current_config.crypto.crypt_context, + crypt_context=current_config.crypto.crypt_context, ) # fail if creation was unsuccessful From 6b6da69bb42b81979b9ee92a725d6ea8ac413d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:27:14 +0000 Subject: [PATCH 30/47] app not a string --- api/kiwi_vpn_api/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 82c2fa8..9da2440 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -54,7 +54,7 @@ async def on_startup() -> None: def main() -> None: uvicorn.run( - "kiwi_vpn_api.main:app", + app=app, host="0.0.0.0", port=8000, reload=True, From d406f1538252368e637e4991271b4b179ab88d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:27:14 +0000 Subject: [PATCH 31/47] app not a string --- api/kiwi_vpn_api/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 82c2fa8..9da2440 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -54,7 +54,7 @@ async def on_startup() -> None: def main() -> None: uvicorn.run( - "kiwi_vpn_api.main:app", + app=app, host="0.0.0.0", port=8000, reload=True, From 71ac02e5d770ebebefbd0e79e260dcb66432ec70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:33:56 +0000 Subject: [PATCH 32/47] actually, app must be a string! --- api/kiwi_vpn_api/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index 9da2440..3cca498 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -54,7 +54,7 @@ async def on_startup() -> None: def main() -> None: uvicorn.run( - app=app, + app="kiwi_vpn_api.main:app", host="0.0.0.0", port=8000, reload=True, From 799b2f7585c97adea0a5ed8dc00b26cb39a5561a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:38:52 +0000 Subject: [PATCH 33/47] simplify set_config --- api/kiwi_vpn_api/routers/admin.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 64c7307..f0ee109 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -77,18 +77,13 @@ async def create_initial_admin( }, ) async def set_config( - new_config: Config, - current_config: Config | None = Depends(Config.load), + config: Config, _: User | None = Depends(get_current_user_if_admin), ): """ PUT ./config: Edit `kiwi-vpn` main config. """ - # fail if not installed - if current_config is None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) - # update config file, reconnect to database - new_config.save() - Connection.connect(current_config.db.uri) + config.save() + Connection.connect(config.db.uri) From aa8563995e6765c1a0370375e52fefe69ff2ade4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:04:19 +0000 Subject: [PATCH 34/47] Settings.get() -> Settings._ --- api/kiwi_vpn_api/config.py | 14 ++++++++------ api/kiwi_vpn_api/main.py | 11 ++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/api/kiwi_vpn_api/config.py b/api/kiwi_vpn_api/config.py index 531b69a..f577172 100644 --- a/api/kiwi_vpn_api/config.py +++ b/api/kiwi_vpn_api/config.py @@ -29,14 +29,16 @@ class Settings(BaseSettings): production_mode: bool = False data_dir: Path = Path("./tmp") + api_v1_prefix: str = "api/v1" openapi_url: str = "/openapi.json" docs_url: str | None = "/docs" redoc_url: str | None = "/redoc" - @staticmethod + @classmethod + @property @functools.lru_cache - def get() -> Settings: - return Settings() + def _(cls) -> Settings: + return cls() @property def config_file(self) -> Path: @@ -61,7 +63,7 @@ class DBConfig(BaseModel): user: str | None = None password: str | None = None host: str | None = None - database: str | None = Settings.get().data_dir.joinpath("vpn.db") + database: str | None = Settings._.data_dir.joinpath("vpn.db") mysql_driver: str = "pymysql" mysql_args: list[str] = ["charset=utf8mb4"] @@ -201,7 +203,7 @@ class Config(BaseModel): return cls.__singleton try: - with open(Settings.get().config_file, "r") as config_file: + with open(Settings._.config_file, "r") as config_file: cls.__singleton = Config.parse_obj(json.load(config_file)) return cls.__singleton @@ -222,5 +224,5 @@ class Config(BaseModel): Save configuration to config file """ - with open(Settings.get().config_file, "w") as config_file: + with open(Settings._.config_file, "w") as config_file: config_file.write(self.json(indent=2)) diff --git a/api/kiwi_vpn_api/main.py b/api/kiwi_vpn_api/main.py index c033959..86ae4f2 100755 --- a/api/kiwi_vpn_api/main.py +++ b/api/kiwi_vpn_api/main.py @@ -16,9 +16,6 @@ from .config import Config, Settings from .db import Connection, User from .routers import main_router -settings = Settings.get() - - app = FastAPI( title="kiwi-vpn API", description="This API enables the `kiwi-vpn` service.", @@ -30,12 +27,12 @@ app = FastAPI( "name": "MIT License", "url": "https://opensource.org/licenses/mit-license.php", }, - openapi_url=settings.openapi_url, - docs_url=settings.docs_url if not settings.production_mode else None, - redoc_url=settings.redoc_url if not settings.production_mode else None, + openapi_url=Settings._.openapi_url, + docs_url=Settings._.docs_url if not Settings._.production_mode else None, + redoc_url=Settings._.redoc_url if not Settings._.production_mode else None, ) -app.include_router(main_router) +app.include_router(main_router, prefix=f"/{Settings._.api_v1_prefix}") @app.on_event("startup") From a5783a0c40b479a3bf41e35e9dca616530f7b68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:04:49 +0000 Subject: [PATCH 35/47] fix: typing --- api/kiwi_vpn_api/routers/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index f0ee109..36b3332 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -78,7 +78,7 @@ async def create_initial_admin( ) async def set_config( config: Config, - _: User | None = Depends(get_current_user_if_admin), + _: User = Depends(get_current_user_if_admin), ): """ PUT ./config: Edit `kiwi-vpn` main config. From 77b40cb836d82233b9ee74eb540deb6f8dca109f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:18:00 +0000 Subject: [PATCH 36/47] User.delete return value --- api/kiwi_vpn_api/db/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index 3b462c9..f40253b 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -100,7 +100,7 @@ class User(UserBase, table=True): db.commit() db.refresh(self) - def delete(self) -> bool: + def delete(self) -> None: """ Delete this user from the database. """ From 3d2abbc39bc7a089bed6624933666ee23fd506e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:18:19 +0000 Subject: [PATCH 37/47] fix user router --- api/kiwi_vpn_api/routers/__init__.py | 8 ++-- api/kiwi_vpn_api/routers/_common.py | 6 ++- api/kiwi_vpn_api/routers/user.py | 64 +++++++++++----------------- 3 files changed, 31 insertions(+), 47 deletions(-) diff --git a/api/kiwi_vpn_api/routers/__init__.py b/api/kiwi_vpn_api/routers/__init__.py index d80b610..1d19caa 100644 --- a/api/kiwi_vpn_api/routers/__init__.py +++ b/api/kiwi_vpn_api/routers/__init__.py @@ -1,12 +1,10 @@ from fastapi import APIRouter -from . import admin +from . import admin, user -# from . import user - -main_router = APIRouter(prefix="/api/v1") +main_router = APIRouter() main_router.include_router(admin.router) -# main_router.include_router(user.router) +main_router.include_router(user.router) __all__ = ["main_router"] diff --git a/api/kiwi_vpn_api/routers/_common.py b/api/kiwi_vpn_api/routers/_common.py index 295e404..dd49d41 100644 --- a/api/kiwi_vpn_api/routers/_common.py +++ b/api/kiwi_vpn_api/routers/_common.py @@ -6,10 +6,12 @@ Common dependencies for routers. from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer -from ..config import Config +from ..config import Config, Settings from ..db import Capability, User -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="user/authenticate") +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl=f"{Settings._.api_v1_prefix}/user/authenticate" +) class Responses: diff --git a/api/kiwi_vpn_api/routers/user.py b/api/kiwi_vpn_api/routers/user.py index 2deace2..abf8529 100644 --- a/api/kiwi_vpn_api/routers/user.py +++ b/api/kiwi_vpn_api/routers/user.py @@ -5,11 +5,9 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel -from sqlalchemy.orm import Session from ..config import Config -from ..db import Connection -from ..db.schemata import User, UserCapability, UserCreate +from ..db import Capability, User, UserCreate, UserRead from ._common import Responses, get_current_user, get_current_user_if_admin router = APIRouter(prefix="/user", tags=["user"]) @@ -28,7 +26,6 @@ class Token(BaseModel): async def login( form_data: OAuth2PasswordRequestForm = Depends(), current_config: Config | None = Depends(Config.load), - db: Session | None = Depends(Connection.get), ): """ POST ./authenticate: Authenticate a user. Issues a bearer token. @@ -39,12 +36,10 @@ async def login( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) # try logging in - user = User(name=form_data.username) - if not user.authenticate( - db=db, + if not (user := User.authenticate( + name=form_data.username, password=form_data.password, - crypt_context=current_config.crypto.crypt_context, - ): + )): # authentication failed raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -57,7 +52,7 @@ async def login( return {"access_token": access_token, "token_type": "bearer"} -@router.get("/current", response_model=User) +@router.get("/current", response_model=UserRead) async def get_current_user( current_user: User | None = Depends(get_current_user), ): @@ -81,20 +76,14 @@ async def get_current_user( ) async def add_user( user: UserCreate, - current_config: Config | None = Depends(Config.load), _: User = Depends(get_current_user_if_admin), - db: Session | None = Depends(Connection.get), ): """ POST ./: Create a new user in the database. """ # actually create the new user - new_user = User.create( - db=db, - user=user, - crypt_context=current_config.crypto.crypt_context, - ) + new_user = User.create(**user.dict()) # fail if creation was unsuccessful if new_user is None: @@ -118,22 +107,21 @@ async def add_user( async def remove_user( user_name: str, _: User = Depends(get_current_user_if_admin), - db: Session | None = Depends(Connection.get), ): """ DELETE ./{user_name}: Remove a user from the database. """ # get the user - user = User.from_db( - db=db, - name=user_name, - ) + user = User.get(user_name) - # fail if deletion was unsuccessful - if user is None or not user.delete(db): + # fail if user not found + if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + # delete user + user.delete() + @router.post( "/{user_name}/capabilities", @@ -146,22 +134,21 @@ async def remove_user( ) async def extend_capabilities( user_name: str, - capabilities: list[UserCapability], + capabilities: list[Capability], _: User = Depends(get_current_user_if_admin), - db: Session | None = Depends(Connection.get), ): """ POST ./{user_name}/capabilities: Add capabilities to a user. """ # get and change the user - user = User.from_db( - db=db, - name=user_name, + user = User.get(user_name) + + user.set_capabilities( + user.get_capabilities() | set(capabilities) ) - user.capabilities.extend(capabilities) - user.update(db) + user.update() @router.delete( @@ -175,21 +162,18 @@ async def extend_capabilities( ) async def remove_capabilities( user_name: str, - capabilities: list[UserCapability], + capabilities: list[Capability], _: User = Depends(get_current_user_if_admin), - db: Session | None = Depends(Connection.get), ): """ DELETE ./{user_name}/capabilities: Remove capabilities from a user. """ # get and change the user - user = User.from_db( - db=db, - name=user_name, + user = User.get(user_name) + + user.set_capabilities( + user.get_capabilities() - set(capabilities) ) - for capability in capabilities: - user.capabilities.remove(capability) - - user.update(db) + user.update() From 1b24861e48812075b1800c948542d37c4db90fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:58:40 +0000 Subject: [PATCH 38/47] Docstrings --- api/kiwi_vpn_api/db/__init__.py | 6 +- api/kiwi_vpn_api/db/connection.py | 6 +- api/kiwi_vpn_api/db/device.py | 24 +++++ api/kiwi_vpn_api/db/user.py | 98 +++++++++++++------ .../db/{capability.py => user_capability.py} | 20 ++++ api/kiwi_vpn_api/routers/__init__.py | 6 ++ 6 files changed, 127 insertions(+), 33 deletions(-) rename api/kiwi_vpn_api/db/{capability.py => user_capability.py} (71%) diff --git a/api/kiwi_vpn_api/db/__init__.py b/api/kiwi_vpn_api/db/__init__.py index f418393..099af9b 100644 --- a/api/kiwi_vpn_api/db/__init__.py +++ b/api/kiwi_vpn_api/db/__init__.py @@ -1,7 +1,11 @@ -from .capability import Capability +""" +Package `db`: ORM and schemas for database content. +""" + from .connection import Connection from .device import Device, DeviceBase, DeviceCreate from .user import User, UserBase, UserCreate, UserRead +from .user_capability import Capability __all__ = ["Capability", "Connection", "Device", "DeviceBase", "DeviceCreate", "User", "UserBase", "UserCreate", "UserRead"] diff --git a/api/kiwi_vpn_api/db/connection.py b/api/kiwi_vpn_api/db/connection.py index ff9341a..ed5d4c2 100644 --- a/api/kiwi_vpn_api/db/connection.py +++ b/api/kiwi_vpn_api/db/connection.py @@ -1,9 +1,13 @@ +""" +Database connection management +""" + from sqlmodel import Session, SQLModel, create_engine class Connection: """ - Namespace for the database connection. + Namespace for the database connection """ engine = None diff --git a/api/kiwi_vpn_api/db/device.py b/api/kiwi_vpn_api/db/device.py index f260cbc..2f9e3e1 100644 --- a/api/kiwi_vpn_api/db/device.py +++ b/api/kiwi_vpn_api/db/device.py @@ -1,3 +1,7 @@ +""" +Python representation of `device` table. +""" + from __future__ import annotations from datetime import datetime @@ -13,16 +17,36 @@ if TYPE_CHECKING: class DeviceBase(SQLModel): + """ + Common to all representations of devices + """ + name: str type: str expiry: datetime | None class DeviceCreate(DeviceBase): + """ + Representation of a newly created device + """ + + owner_name: str | None + + +class DeviceRead(DeviceBase): + """ + Representation of a device read via the API + """ + owner_name: str | None class Device(DeviceBase, table=True): + """ + Representation of device table + """ + __table_args__ = (UniqueConstraint( "owner_name", "name", diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index f40253b..cc5e079 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -1,3 +1,7 @@ +""" +Python representation of `user` table. +""" + from __future__ import annotations from typing import Any @@ -7,12 +11,16 @@ from sqlalchemy.exc import IntegrityError from sqlmodel import Field, Relationship, SQLModel from ..config import Config -from .capability import Capability, UserCapability from .connection import Connection from .device import Device +from .user_capability import Capability, UserCapability class UserBase(SQLModel): + """ + Common to all representations of users + """ + name: str = Field(primary_key=True) email: str | None = Field(default=None) @@ -23,7 +31,50 @@ class UserBase(SQLModel): organizational_unit: str | None = Field(default=None) +class UserCreate(UserBase): + """ + Representation of a newly created user + """ + + password: str | None = Field(default=None) + password_clear: str | None = Field(default=None) + + @root_validator + @classmethod + def hash_password(cls, values: dict[str, Any]) -> dict[str, Any]: + """ + Ensure the `password` value of this user gets set. + """ + + if (values.get("password")) is not None: + # password is set + return values + + if (password_clear := values.get("password_clear")) is None: + raise ValueError("No password to hash") + + if (current_config := Config._) is None: + raise ValueError("Not configured") + + values["password"] = current_config.crypto.crypt_context.hash( + password_clear) + + return values + + +class UserRead(UserBase): + """ + Representation of a user read via the API + """ + + pass + + class User(UserBase, table=True): + """ + Representation of user table + """ + password: str capabilities: list[UserCapability] = Relationship( @@ -109,46 +160,31 @@ class User(UserBase, table=True): db.delete(self) db.commit() - def can(self, capability: Capability) -> bool: - return capability in self.get_capabilities() - def get_capabilities(self) -> set[Capability]: + """ + Return the capabilities of this user. + """ + return set( capability._ for capability in self.capabilities ) + def can(self, capability: Capability) -> bool: + """ + Check if this user has a capability. + """ + + return capability in self.get_capabilities() + def set_capabilities(self, capabilities: set[Capability]) -> None: + """ + Change the capabilities of this user. + """ + self.capabilities = [ UserCapability( user_name=self.name, capability_name=capability.value, ) for capability in capabilities ] - - -class UserCreate(UserBase): - password: str | None = Field(default=None) - password_clear: str | None = Field(default=None) - - @root_validator - @classmethod - def hash_password(cls, values: dict[str, Any]) -> dict[str, Any]: - if (values.get("password")) is not None: - # password is set - return values - - if (password_clear := values.get("password_clear")) is None: - raise ValueError("No password to hash") - - if (current_config := Config._) is None: - raise ValueError("Not configured") - - values["password"] = current_config.crypto.crypt_context.hash( - password_clear) - - return values - - -class UserRead(UserBase): - pass diff --git a/api/kiwi_vpn_api/db/capability.py b/api/kiwi_vpn_api/db/user_capability.py similarity index 71% rename from api/kiwi_vpn_api/db/capability.py rename to api/kiwi_vpn_api/db/user_capability.py index 9ed8aba..7b98eb7 100644 --- a/api/kiwi_vpn_api/db/capability.py +++ b/api/kiwi_vpn_api/db/user_capability.py @@ -1,3 +1,7 @@ +""" +Python representation of `usercapability` table. +""" + from enum import Enum from typing import TYPE_CHECKING @@ -8,6 +12,10 @@ if TYPE_CHECKING: class Capability(Enum): + """ + Allowed values for capabilities + """ + admin = "admin" login = "login" issue = "issue" @@ -18,10 +26,18 @@ class Capability(Enum): class UserCapabilityBase(SQLModel): + """ + Common to all representations of capabilities + """ + capability_name: str = Field(primary_key=True) @property def _(self) -> Capability: + """ + Transform into a `Capability`. + """ + return Capability(self.capability_name) def __repr__(self) -> str: @@ -29,6 +45,10 @@ class UserCapabilityBase(SQLModel): class UserCapability(UserCapabilityBase, table=True): + """ + Representation of usercapability table + """ + user_name: str = Field(primary_key=True, foreign_key="user.name") user: "User" = Relationship( diff --git a/api/kiwi_vpn_api/routers/__init__.py b/api/kiwi_vpn_api/routers/__init__.py index 1d19caa..0398ff7 100644 --- a/api/kiwi_vpn_api/routers/__init__.py +++ b/api/kiwi_vpn_api/routers/__init__.py @@ -1,3 +1,9 @@ +""" +Package `routers`: Each module contains the path operations for their prefixes. + +This file: Main API router definition. +""" + from fastapi import APIRouter from . import admin, user From 270de7f87c12d2c14c46d14bb6b99edbac39baf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:59:27 +0000 Subject: [PATCH 39/47] Settings.config_file_name --- api/kiwi_vpn_api/config.py | 3 ++- api/kiwi_vpn_api/db/connection.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/kiwi_vpn_api/config.py b/api/kiwi_vpn_api/config.py index f577172..2ba228a 100644 --- a/api/kiwi_vpn_api/config.py +++ b/api/kiwi_vpn_api/config.py @@ -29,6 +29,7 @@ class Settings(BaseSettings): production_mode: bool = False data_dir: Path = Path("./tmp") + config_file_name: Path = Path("config.json") api_v1_prefix: str = "api/v1" openapi_url: str = "/openapi.json" docs_url: str | None = "/docs" @@ -42,7 +43,7 @@ class Settings(BaseSettings): @property def config_file(self) -> Path: - return self.data_dir.joinpath("config.json") + return self.data_dir.joinpath(self.config_file_name) class DBType(Enum): diff --git a/api/kiwi_vpn_api/db/connection.py b/api/kiwi_vpn_api/db/connection.py index ed5d4c2..5826daf 100644 --- a/api/kiwi_vpn_api/db/connection.py +++ b/api/kiwi_vpn_api/db/connection.py @@ -1,5 +1,5 @@ """ -Database connection management +Database connection management. """ from sqlmodel import Session, SQLModel, create_engine From 5b623e885c38f338df7a7b3bc1bb402d866e5a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:26:47 +0000 Subject: [PATCH 40/47] table names --- api/kiwi_vpn_api/db/device.py | 8 +++++--- api/kiwi_vpn_api/db/user.py | 6 ++++-- api/kiwi_vpn_api/db/user_capability.py | 8 +++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/api/kiwi_vpn_api/db/device.py b/api/kiwi_vpn_api/db/device.py index 2f9e3e1..449b64a 100644 --- a/api/kiwi_vpn_api/db/device.py +++ b/api/kiwi_vpn_api/db/device.py @@ -1,5 +1,5 @@ """ -Python representation of `device` table. +Python representation of `devices` table. """ from __future__ import annotations @@ -44,16 +44,18 @@ class DeviceRead(DeviceBase): class Device(DeviceBase, table=True): """ - Representation of device table + Representation of `devices` table """ + __tablename__ = "devices" + __table_args__ = (UniqueConstraint( "owner_name", "name", ),) id: int | None = Field(primary_key=True) - owner_name: str | None = Field(foreign_key="user.name") + owner_name: str | None = Field(foreign_key="users.name") # no idea, but "User" (in quotes) doesn't work here # might be a future problem? diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index cc5e079..71d3269 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -1,5 +1,5 @@ """ -Python representation of `user` table. +Python representation of `users` table. """ from __future__ import annotations @@ -72,9 +72,11 @@ class UserRead(UserBase): class User(UserBase, table=True): """ - Representation of user table + Representation of `users` table """ + __tablename__ = "users" + password: str capabilities: list[UserCapability] = Relationship( diff --git a/api/kiwi_vpn_api/db/user_capability.py b/api/kiwi_vpn_api/db/user_capability.py index 7b98eb7..9fd3cb1 100644 --- a/api/kiwi_vpn_api/db/user_capability.py +++ b/api/kiwi_vpn_api/db/user_capability.py @@ -1,5 +1,5 @@ """ -Python representation of `usercapability` table. +Python representation of `user_capabilities` table. """ from enum import Enum @@ -46,10 +46,12 @@ class UserCapabilityBase(SQLModel): class UserCapability(UserCapabilityBase, table=True): """ - Representation of usercapability table + Representation of `user_capabilities` table """ - user_name: str = Field(primary_key=True, foreign_key="user.name") + __tablename__ = "user_capabilities" + + user_name: str = Field(primary_key=True, foreign_key="users.name") user: "User" = Relationship( back_populates="capabilities", From 499c97a28a6b387b28817f6bf46084389d1f37d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:41:49 +0000 Subject: [PATCH 41/47] Capability -> UserCapabilityType --- api/kiwi_vpn_api/db/__init__.py | 15 ++++++++++++--- api/kiwi_vpn_api/db/user.py | 8 ++++---- api/kiwi_vpn_api/db/user_capability.py | 6 +++--- api/kiwi_vpn_api/routers/_common.py | 6 +++--- api/kiwi_vpn_api/routers/admin.py | 7 +++++-- api/kiwi_vpn_api/routers/user.py | 6 +++--- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/api/kiwi_vpn_api/db/__init__.py b/api/kiwi_vpn_api/db/__init__.py index 099af9b..aa0cb3c 100644 --- a/api/kiwi_vpn_api/db/__init__.py +++ b/api/kiwi_vpn_api/db/__init__.py @@ -5,7 +5,16 @@ Package `db`: ORM and schemas for database content. from .connection import Connection from .device import Device, DeviceBase, DeviceCreate from .user import User, UserBase, UserCreate, UserRead -from .user_capability import Capability +from .user_capability import UserCapabilityType -__all__ = ["Capability", "Connection", "Device", "DeviceBase", "DeviceCreate", - "User", "UserBase", "UserCreate", "UserRead"] +__all__ = [ + "Connection", + "Device", + "DeviceBase", + "DeviceCreate", + "User", + "UserBase", + "UserCreate", + "UserRead", + "UserCapabilityType", +] diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index 71d3269..2058cd6 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -13,7 +13,7 @@ from sqlmodel import Field, Relationship, SQLModel from ..config import Config from .connection import Connection from .device import Device -from .user_capability import Capability, UserCapability +from .user_capability import UserCapabilityType, UserCapability class UserBase(SQLModel): @@ -162,7 +162,7 @@ class User(UserBase, table=True): db.delete(self) db.commit() - def get_capabilities(self) -> set[Capability]: + def get_capabilities(self) -> set[UserCapabilityType]: """ Return the capabilities of this user. """ @@ -172,14 +172,14 @@ class User(UserBase, table=True): for capability in self.capabilities ) - def can(self, capability: Capability) -> bool: + def can(self, capability: UserCapabilityType) -> bool: """ Check if this user has a capability. """ return capability in self.get_capabilities() - def set_capabilities(self, capabilities: set[Capability]) -> None: + def set_capabilities(self, capabilities: set[UserCapabilityType]) -> None: """ Change the capabilities of this user. """ diff --git a/api/kiwi_vpn_api/db/user_capability.py b/api/kiwi_vpn_api/db/user_capability.py index 9fd3cb1..e41a0e8 100644 --- a/api/kiwi_vpn_api/db/user_capability.py +++ b/api/kiwi_vpn_api/db/user_capability.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from .user import User -class Capability(Enum): +class UserCapabilityType(Enum): """ Allowed values for capabilities """ @@ -33,12 +33,12 @@ class UserCapabilityBase(SQLModel): capability_name: str = Field(primary_key=True) @property - def _(self) -> Capability: + def _(self) -> UserCapabilityType: """ Transform into a `Capability`. """ - return Capability(self.capability_name) + return UserCapabilityType(self.capability_name) def __repr__(self) -> str: return self.capability_name diff --git a/api/kiwi_vpn_api/routers/_common.py b/api/kiwi_vpn_api/routers/_common.py index dd49d41..3bb0768 100644 --- a/api/kiwi_vpn_api/routers/_common.py +++ b/api/kiwi_vpn_api/routers/_common.py @@ -7,7 +7,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from ..config import Config, Settings -from ..db import Capability, User +from ..db import UserCapabilityType, User oauth2_scheme = OAuth2PasswordBearer( tokenUrl=f"{Settings._.api_v1_prefix}/user/authenticate" @@ -93,7 +93,7 @@ async def get_current_user_if_admin( """ # fail if not requested by an admin - if not current_user.can(Capability.admin): + if not current_user.can(UserCapabilityType.admin): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) return current_user @@ -111,7 +111,7 @@ async def get_current_user_if_admin_or_self( """ # fail if not requested by an admin or self - if not (current_user.can(Capability.admin) + if not (current_user.can(UserCapabilityType.admin) or current_user.name == user_name): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 36b3332..ea4187d 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel import select from ..config import Config -from ..db import Capability, Connection, User, UserCreate +from ..db import Connection, User, UserCapabilityType, UserCreate from ._common import Responses, get_current_user_if_admin router = APIRouter(prefix="/admin", tags=["admin"]) @@ -63,7 +63,10 @@ async def create_initial_admin( # create an administrative user new_user = User.create(**admin_user.dict()) - new_user.set_capabilities([Capability.login, Capability.admin]) + new_user.set_capabilities([ + UserCapabilityType.login, + UserCapabilityType.admin, + ]) new_user.update() diff --git a/api/kiwi_vpn_api/routers/user.py b/api/kiwi_vpn_api/routers/user.py index abf8529..6815e23 100644 --- a/api/kiwi_vpn_api/routers/user.py +++ b/api/kiwi_vpn_api/routers/user.py @@ -7,7 +7,7 @@ from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel from ..config import Config -from ..db import Capability, User, UserCreate, UserRead +from ..db import UserCapabilityType, User, UserCreate, UserRead from ._common import Responses, get_current_user, get_current_user_if_admin router = APIRouter(prefix="/user", tags=["user"]) @@ -134,7 +134,7 @@ async def remove_user( ) async def extend_capabilities( user_name: str, - capabilities: list[Capability], + capabilities: list[UserCapabilityType], _: User = Depends(get_current_user_if_admin), ): """ @@ -162,7 +162,7 @@ async def extend_capabilities( ) async def remove_capabilities( user_name: str, - capabilities: list[Capability], + capabilities: list[UserCapabilityType], _: User = Depends(get_current_user_if_admin), ): """ From 21b85d7cfa50060da166c98b165cc9376ebe4454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:41:56 +0000 Subject: [PATCH 42/47] formatting --- api/kiwi_vpn_api/routers/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/kiwi_vpn_api/routers/__init__.py b/api/kiwi_vpn_api/routers/__init__.py index 0398ff7..6cb693f 100644 --- a/api/kiwi_vpn_api/routers/__init__.py +++ b/api/kiwi_vpn_api/routers/__init__.py @@ -13,4 +13,6 @@ main_router = APIRouter() main_router.include_router(admin.router) main_router.include_router(user.router) -__all__ = ["main_router"] +__all__ = [ + "main_router", +] From 7dbd25b89432a4745593ad8ab60d63d3a4aa5780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:54:39 +0000 Subject: [PATCH 43/47] rollback tablename settings --- api/kiwi_vpn_api/db/device.py | 8 +++----- api/kiwi_vpn_api/db/user.py | 6 ++---- api/kiwi_vpn_api/db/user_capability.py | 8 +++----- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/api/kiwi_vpn_api/db/device.py b/api/kiwi_vpn_api/db/device.py index 449b64a..ff791e0 100644 --- a/api/kiwi_vpn_api/db/device.py +++ b/api/kiwi_vpn_api/db/device.py @@ -1,5 +1,5 @@ """ -Python representation of `devices` table. +Python representation of `device` table. """ from __future__ import annotations @@ -44,18 +44,16 @@ class DeviceRead(DeviceBase): class Device(DeviceBase, table=True): """ - Representation of `devices` table + Representation of `device` table """ - __tablename__ = "devices" - __table_args__ = (UniqueConstraint( "owner_name", "name", ),) id: int | None = Field(primary_key=True) - owner_name: str | None = Field(foreign_key="users.name") + owner_name: str | None = Field(foreign_key="user.name") # no idea, but "User" (in quotes) doesn't work here # might be a future problem? diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index 2058cd6..d535a6f 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -1,5 +1,5 @@ """ -Python representation of `users` table. +Python representation of `user` table. """ from __future__ import annotations @@ -72,11 +72,9 @@ class UserRead(UserBase): class User(UserBase, table=True): """ - Representation of `users` table + Representation of `user` table """ - __tablename__ = "users" - password: str capabilities: list[UserCapability] = Relationship( diff --git a/api/kiwi_vpn_api/db/user_capability.py b/api/kiwi_vpn_api/db/user_capability.py index e41a0e8..479fec4 100644 --- a/api/kiwi_vpn_api/db/user_capability.py +++ b/api/kiwi_vpn_api/db/user_capability.py @@ -1,5 +1,5 @@ """ -Python representation of `user_capabilities` table. +Python representation of `user_capability` table. """ from enum import Enum @@ -46,12 +46,10 @@ class UserCapabilityBase(SQLModel): class UserCapability(UserCapabilityBase, table=True): """ - Representation of `user_capabilities` table + Representation of `user_capability` table """ - __tablename__ = "user_capabilities" - - user_name: str = Field(primary_key=True, foreign_key="users.name") + user_name: str = Field(primary_key=True, foreign_key="user.name") user: "User" = Relationship( back_populates="capabilities", From a465dba92e6abd22af96fefb9389313a3c0983af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 22:07:12 +0000 Subject: [PATCH 44/47] side effects --- api/kiwi_vpn_api/db/user.py | 2 +- api/kiwi_vpn_api/routers/_common.py | 2 +- api/kiwi_vpn_api/routers/user.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index d535a6f..45af422 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -13,7 +13,7 @@ from sqlmodel import Field, Relationship, SQLModel from ..config import Config from .connection import Connection from .device import Device -from .user_capability import UserCapabilityType, UserCapability +from .user_capability import UserCapability, UserCapabilityType class UserBase(SQLModel): diff --git a/api/kiwi_vpn_api/routers/_common.py b/api/kiwi_vpn_api/routers/_common.py index 3bb0768..ba8c496 100644 --- a/api/kiwi_vpn_api/routers/_common.py +++ b/api/kiwi_vpn_api/routers/_common.py @@ -7,7 +7,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from ..config import Config, Settings -from ..db import UserCapabilityType, User +from ..db import User, UserCapabilityType oauth2_scheme = OAuth2PasswordBearer( tokenUrl=f"{Settings._.api_v1_prefix}/user/authenticate" diff --git a/api/kiwi_vpn_api/routers/user.py b/api/kiwi_vpn_api/routers/user.py index 6815e23..dce2c08 100644 --- a/api/kiwi_vpn_api/routers/user.py +++ b/api/kiwi_vpn_api/routers/user.py @@ -7,7 +7,7 @@ from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel from ..config import Config -from ..db import UserCapabilityType, User, UserCreate, UserRead +from ..db import User, UserCapabilityType, UserCreate, UserRead from ._common import Responses, get_current_user, get_current_user_if_admin router = APIRouter(prefix="/user", tags=["user"]) From 6254daa51d54c0d7aa10c6c5b2fac1e08face037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 22:17:31 +0000 Subject: [PATCH 45/47] check: user can login, "admin" can do everything --- api/kiwi_vpn_api/db/user.py | 17 +++++++++++++---- api/kiwi_vpn_api/routers/admin.py | 5 +---- api/kiwi_vpn_api/routers/user.py | 5 +++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/api/kiwi_vpn_api/db/user.py b/api/kiwi_vpn_api/db/user.py index 45af422..5e1a4bb 100644 --- a/api/kiwi_vpn_api/db/user.py +++ b/api/kiwi_vpn_api/db/user.py @@ -4,7 +4,7 @@ Python representation of `user` table. from __future__ import annotations -from typing import Any +from typing import Any, Sequence from pydantic import root_validator from sqlalchemy.exc import IntegrityError @@ -170,14 +170,23 @@ class User(UserBase, table=True): for capability in self.capabilities ) - def can(self, capability: UserCapabilityType) -> bool: + def can( + self, + capability: UserCapabilityType, + ) -> bool: """ Check if this user has a capability. """ - return capability in self.get_capabilities() + return ( + capability in self.get_capabilities() + or UserCapabilityType.admin in self.get_capabilities() + ) - def set_capabilities(self, capabilities: set[UserCapabilityType]) -> None: + def set_capabilities( + self, + capabilities: Sequence[UserCapabilityType], + ) -> None: """ Change the capabilities of this user. """ diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index ea4187d..14eeacf 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -63,10 +63,7 @@ async def create_initial_admin( # create an administrative user new_user = User.create(**admin_user.dict()) - new_user.set_capabilities([ - UserCapabilityType.login, - UserCapabilityType.admin, - ]) + new_user.set_capabilities((UserCapabilityType.admin)) new_user.update() diff --git a/api/kiwi_vpn_api/routers/user.py b/api/kiwi_vpn_api/routers/user.py index dce2c08..ad69d26 100644 --- a/api/kiwi_vpn_api/routers/user.py +++ b/api/kiwi_vpn_api/routers/user.py @@ -47,6 +47,10 @@ async def login( headers={"WWW-Authenticate": "Bearer"}, ) + if not user.can(UserCapabilityType.login): + # user cannot login + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + # authentication succeeded access_token = await current_config.jwt.create_token(user.name) return {"access_token": access_token, "token_type": "bearer"} @@ -84,6 +88,7 @@ async def add_user( # actually create the new user new_user = User.create(**user.dict()) + new_user.set_capabilities((UserCapabilityType.login)) # fail if creation was unsuccessful if new_user is None: From dbbe7a8c35083bbcd63f8309b584c8dca8059fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 22:20:25 +0000 Subject: [PATCH 46/47] syntax error --- api/kiwi_vpn_api/routers/admin.py | 2 +- api/kiwi_vpn_api/routers/user.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 14eeacf..758b100 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -63,7 +63,7 @@ async def create_initial_admin( # create an administrative user new_user = User.create(**admin_user.dict()) - new_user.set_capabilities((UserCapabilityType.admin)) + new_user.set_capabilities([UserCapabilityType.admin]) new_user.update() diff --git a/api/kiwi_vpn_api/routers/user.py b/api/kiwi_vpn_api/routers/user.py index ad69d26..1db468c 100644 --- a/api/kiwi_vpn_api/routers/user.py +++ b/api/kiwi_vpn_api/routers/user.py @@ -88,7 +88,7 @@ async def add_user( # actually create the new user new_user = User.create(**user.dict()) - new_user.set_capabilities((UserCapabilityType.login)) + new_user.set_capabilities([UserCapabilityType.login]) # fail if creation was unsuccessful if new_user is None: @@ -149,9 +149,7 @@ async def extend_capabilities( # get and change the user user = User.get(user_name) - user.set_capabilities( - user.get_capabilities() | set(capabilities) - ) + user.set_capabilities(user.get_capabilities() | set(capabilities)) user.update() @@ -177,8 +175,6 @@ async def remove_capabilities( # get and change the user user = User.get(user_name) - user.set_capabilities( - user.get_capabilities() - set(capabilities) - ) + user.set_capabilities(user.get_capabilities() - set(capabilities)) user.update() From 567b863742acbbbeb82ce85a7ca08b21371fd327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 28 Mar 2022 22:25:37 +0000 Subject: [PATCH 47/47] comment --- api/kiwi_vpn_api/routers/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/kiwi_vpn_api/routers/admin.py b/api/kiwi_vpn_api/routers/admin.py index 758b100..ca0ad6e 100644 --- a/api/kiwi_vpn_api/routers/admin.py +++ b/api/kiwi_vpn_api/routers/admin.py @@ -57,6 +57,7 @@ async def create_initial_admin( if current_config is None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + # fail if any user exists with Connection.session as db: if db.exec(select(User).limit(1)).first() is not None: raise HTTPException(status_code=status.HTTP_409_CONFLICT)