Compare commits
6 commits
673c91bc33
...
437bc570e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 437bc570e4 | |||
| ac6e506486 | |||
| c4ad9a8e40 | |||
| 814a19d864 | |||
| fba1bf6514 | |||
| 70df7a47db |
6 changed files with 146 additions and 21 deletions
|
|
@ -18,16 +18,16 @@ class User(ORMBaseModel):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
name = Column(String, primary_key=True, index=True)
|
name = Column(String, primary_key=True, index=True)
|
||||||
password = Column(String)
|
password = Column(String, nullable=False)
|
||||||
|
|
||||||
capabilities: list[UserCapability] = relationship(
|
capabilities: list[UserCapability] = relationship(
|
||||||
"UserCapability", lazy="joined", cascade="all, delete-orphan"
|
"UserCapability", lazy="joined", cascade="all, delete-orphan"
|
||||||
)
|
)
|
||||||
certificates: list[Certificate] = relationship(
|
certificates: list[Certificate] = relationship(
|
||||||
"Certificate", lazy="select"
|
"Certificate", lazy="select", back_populates="owner"
|
||||||
)
|
)
|
||||||
distinguished_names: list[DistinguishedName] = relationship(
|
distinguished_names: list[DistinguishedName] = relationship(
|
||||||
"DistinguishedName", lazy="select"
|
"DistinguishedName", lazy="select", back_populates="owner"
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -60,14 +60,18 @@ class DistinguishedName(ORMBaseModel):
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
owner_name = Column(String, ForeignKey("users.name"))
|
owner_name = Column(String, ForeignKey("users.name"))
|
||||||
cn_only = Column(Boolean, default=True)
|
cn_only = Column(Boolean, default=True, nullable=False)
|
||||||
country = Column(String(2))
|
country = Column(String(2))
|
||||||
state = Column(String)
|
state = Column(String)
|
||||||
city = Column(String)
|
city = Column(String)
|
||||||
organization = Column(String)
|
organization = Column(String)
|
||||||
organizational_unit = Column(String)
|
organizational_unit = Column(String)
|
||||||
email = Column(String)
|
email = Column(String)
|
||||||
common_name = Column(String)
|
common_name = Column(String, nullable=False)
|
||||||
|
|
||||||
|
owner: User = relationship(
|
||||||
|
"User", lazy="joined", back_populates="distinguished_names"
|
||||||
|
)
|
||||||
|
|
||||||
UniqueConstraint(
|
UniqueConstraint(
|
||||||
country,
|
country,
|
||||||
|
|
@ -86,7 +90,17 @@ class Certificate(ORMBaseModel):
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
owner_name = Column(String, ForeignKey("users.name"))
|
owner_name = Column(String, ForeignKey("users.name"))
|
||||||
dn_id = Column(Integer, ForeignKey("distinguished_names.id"))
|
dn_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("distinguished_names.id"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
expiry = Column(DateTime, default=datetime.datetime.now)
|
expiry = Column(DateTime, default=datetime.datetime.now)
|
||||||
|
|
||||||
distinguished_name = relationship("DistinguishedName", lazy="joined")
|
distinguished_name: DistinguishedName = relationship(
|
||||||
|
"DistinguishedName", lazy="joined"
|
||||||
|
)
|
||||||
|
|
||||||
|
owner: User = relationship(
|
||||||
|
"User", lazy="joined", back_populates="certificates"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from enum import Enum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, Field, constr, validator
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
@ -23,12 +23,12 @@ from . import models
|
||||||
|
|
||||||
class DistinguishedNameBase(BaseModel):
|
class DistinguishedNameBase(BaseModel):
|
||||||
cn_only: bool
|
cn_only: bool
|
||||||
country: str
|
country: constr(max_length=2) | None
|
||||||
state: str
|
state: str | None
|
||||||
city: str
|
city: str | None
|
||||||
organization: str
|
organization: str | None
|
||||||
organizational_unit: str
|
organizational_unit: str | None
|
||||||
email: str
|
email: str | None
|
||||||
common_name: str
|
common_name: str
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -40,6 +40,45 @@ class DistinguishedName(DistinguishedNameBase):
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
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
|
||||||
|
|
||||||
##########
|
##########
|
||||||
# table: certificates
|
# table: certificates
|
||||||
##########
|
##########
|
||||||
|
|
@ -67,6 +106,9 @@ class Certificate(CertificateBase):
|
||||||
class UserCapability(Enum):
|
class UserCapability(Enum):
|
||||||
admin = "admin"
|
admin = "admin"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_value(cls, value) -> UserCapability:
|
def from_value(cls, value) -> UserCapability:
|
||||||
"""
|
"""
|
||||||
|
|
@ -100,8 +142,14 @@ class UserCreate(UserBase):
|
||||||
|
|
||||||
class User(UserBase):
|
class User(UserBase):
|
||||||
capabilities: list[UserCapability] = []
|
capabilities: list[UserCapability] = []
|
||||||
distinguished_names: list[DistinguishedName] = []
|
|
||||||
certificates: list[Certificate] = []
|
distinguished_names: list[DistinguishedName] = Field(
|
||||||
|
default=[], repr=False
|
||||||
|
)
|
||||||
|
|
||||||
|
certificates: list[Certificate] = Field(
|
||||||
|
default=[], repr=False
|
||||||
|
)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from fastapi import FastAPI
|
||||||
from .config import Config, Settings
|
from .config import Config, Settings
|
||||||
from .db import Connection
|
from .db import Connection
|
||||||
from .db.schemas import User
|
from .db.schemas import User
|
||||||
from .routers import admin, user
|
from .routers import admin, dn, user
|
||||||
|
|
||||||
settings = Settings.get()
|
settings = Settings.get()
|
||||||
|
|
||||||
|
|
@ -39,6 +39,7 @@ api = FastAPI(
|
||||||
|
|
||||||
api.include_router(admin.router)
|
api.include_router(admin.router)
|
||||||
api.include_router(user.router)
|
api.include_router(user.router)
|
||||||
|
api.include_router(dn.router)
|
||||||
|
|
||||||
app.mount("/api", api)
|
app.mount("/api", api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ class Responses:
|
||||||
"description": "Must be admin",
|
"description": "Must be admin",
|
||||||
"content": None,
|
"content": None,
|
||||||
}
|
}
|
||||||
|
NEEDS_ADMIN_OR_SELF = {
|
||||||
|
"description": "Must be the requested user",
|
||||||
|
"content": None,
|
||||||
|
}
|
||||||
ENTRY_EXISTS = {
|
ENTRY_EXISTS = {
|
||||||
"description": "Entry exists in database",
|
"description": "Entry exists in database",
|
||||||
"content": None,
|
"content": None,
|
||||||
|
|
|
||||||
58
api/kiwi_vpn_api/routers/dn.py
Normal file
58
api/kiwi_vpn_api/routers/dn.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""
|
||||||
|
/dn endpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from ..db import Connection
|
||||||
|
from ..db.schemas 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
|
||||||
|
|
@ -69,7 +69,7 @@ async def get_current_user(
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/",
|
"",
|
||||||
responses={
|
responses={
|
||||||
status.HTTP_200_OK: Responses.OK,
|
status.HTTP_200_OK: Responses.OK,
|
||||||
status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED,
|
status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED,
|
||||||
|
|
@ -111,7 +111,7 @@ async def add_user(
|
||||||
status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED,
|
status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED,
|
||||||
status.HTTP_401_UNAUTHORIZED: Responses.NEEDS_USER,
|
status.HTTP_401_UNAUTHORIZED: Responses.NEEDS_USER,
|
||||||
status.HTTP_403_FORBIDDEN: Responses.NEEDS_ADMIN,
|
status.HTTP_403_FORBIDDEN: Responses.NEEDS_ADMIN,
|
||||||
status.HTTP_409_CONFLICT: Responses.ENTRY_DOESNT_EXIST,
|
status.HTTP_404_NOT_FOUND: Responses.ENTRY_DOESNT_EXIST,
|
||||||
},
|
},
|
||||||
response_model=User,
|
response_model=User,
|
||||||
)
|
)
|
||||||
|
|
@ -131,8 +131,8 @@ async def remove_user(
|
||||||
)
|
)
|
||||||
|
|
||||||
# fail if deletion was unsuccessful
|
# fail if deletion was unsuccessful
|
||||||
if not user.delete():
|
if user is None or not user.delete(db):
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue