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