Compare commits
No commits in common. "3d2abbc39bc7a089bed6624933666ee23fd506e6" and "799b2f7585c97adea0a5ed8dc00b26cb39a5561a" have entirely different histories.
3d2abbc39b
...
799b2f7585
7 changed files with 62 additions and 45 deletions
|
|
@ -29,16 +29,14 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
production_mode: bool = False
|
production_mode: bool = False
|
||||||
data_dir: Path = Path("./tmp")
|
data_dir: Path = Path("./tmp")
|
||||||
api_v1_prefix: str = "api/v1"
|
|
||||||
openapi_url: str = "/openapi.json"
|
openapi_url: str = "/openapi.json"
|
||||||
docs_url: str | None = "/docs"
|
docs_url: str | None = "/docs"
|
||||||
redoc_url: str | None = "/redoc"
|
redoc_url: str | None = "/redoc"
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
@property
|
|
||||||
@functools.lru_cache
|
@functools.lru_cache
|
||||||
def _(cls) -> Settings:
|
def get() -> Settings:
|
||||||
return cls()
|
return Settings()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_file(self) -> Path:
|
def config_file(self) -> Path:
|
||||||
|
|
@ -63,7 +61,7 @@ class DBConfig(BaseModel):
|
||||||
user: str | None = None
|
user: str | None = None
|
||||||
password: str | None = None
|
password: str | None = None
|
||||||
host: str | None = None
|
host: str | None = None
|
||||||
database: str | None = Settings._.data_dir.joinpath("vpn.db")
|
database: str | None = Settings.get().data_dir.joinpath("vpn.db")
|
||||||
|
|
||||||
mysql_driver: str = "pymysql"
|
mysql_driver: str = "pymysql"
|
||||||
mysql_args: list[str] = ["charset=utf8mb4"]
|
mysql_args: list[str] = ["charset=utf8mb4"]
|
||||||
|
|
@ -203,7 +201,7 @@ class Config(BaseModel):
|
||||||
return cls.__singleton
|
return cls.__singleton
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(Settings._.config_file, "r") as config_file:
|
with open(Settings.get().config_file, "r") as config_file:
|
||||||
cls.__singleton = Config.parse_obj(json.load(config_file))
|
cls.__singleton = Config.parse_obj(json.load(config_file))
|
||||||
return cls.__singleton
|
return cls.__singleton
|
||||||
|
|
||||||
|
|
@ -224,5 +222,5 @@ class Config(BaseModel):
|
||||||
Save configuration to config file
|
Save configuration to config file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with open(Settings._.config_file, "w") as config_file:
|
with open(Settings.get().config_file, "w") as config_file:
|
||||||
config_file.write(self.json(indent=2))
|
config_file.write(self.json(indent=2))
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ class User(UserBase, table=True):
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(self)
|
db.refresh(self)
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Delete this user from the database.
|
Delete this user from the database.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ from .config import Config, Settings
|
||||||
from .db import Connection, User
|
from .db import Connection, User
|
||||||
from .routers import main_router
|
from .routers import main_router
|
||||||
|
|
||||||
|
settings = Settings.get()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="kiwi-vpn API",
|
title="kiwi-vpn API",
|
||||||
description="This API enables the `kiwi-vpn` service.",
|
description="This API enables the `kiwi-vpn` service.",
|
||||||
|
|
@ -27,12 +30,12 @@ app = FastAPI(
|
||||||
"name": "MIT License",
|
"name": "MIT License",
|
||||||
"url": "https://opensource.org/licenses/mit-license.php",
|
"url": "https://opensource.org/licenses/mit-license.php",
|
||||||
},
|
},
|
||||||
openapi_url=Settings._.openapi_url,
|
openapi_url=settings.openapi_url,
|
||||||
docs_url=Settings._.docs_url if not Settings._.production_mode else None,
|
docs_url=settings.docs_url if not settings.production_mode else None,
|
||||||
redoc_url=Settings._.redoc_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, prefix=f"/{Settings._.api_v1_prefix}")
|
app.include_router(main_router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import admin, user
|
from . import admin
|
||||||
|
|
||||||
main_router = APIRouter()
|
# from . import user
|
||||||
|
|
||||||
|
main_router = APIRouter(prefix="/api/v1")
|
||||||
|
|
||||||
main_router.include_router(admin.router)
|
main_router.include_router(admin.router)
|
||||||
main_router.include_router(user.router)
|
# main_router.include_router(user.router)
|
||||||
|
|
||||||
__all__ = ["main_router"]
|
__all__ = ["main_router"]
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,10 @@ Common dependencies for routers.
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
|
||||||
from ..config import Config, Settings
|
from ..config import Config
|
||||||
from ..db import Capability, User
|
from ..db import Capability, User
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="user/authenticate")
|
||||||
tokenUrl=f"{Settings._.api_v1_prefix}/user/authenticate"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Responses:
|
class Responses:
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ async def create_initial_admin(
|
||||||
)
|
)
|
||||||
async def set_config(
|
async def set_config(
|
||||||
config: Config,
|
config: Config,
|
||||||
_: User = Depends(get_current_user_if_admin),
|
_: User | None = Depends(get_current_user_if_admin),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
PUT ./config: Edit `kiwi-vpn` main config.
|
PUT ./config: Edit `kiwi-vpn` main config.
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..db import Capability, User, UserCreate, UserRead
|
from ..db import Connection
|
||||||
|
from ..db.schemata import User, UserCapability, UserCreate
|
||||||
from ._common import Responses, get_current_user, get_current_user_if_admin
|
from ._common import Responses, get_current_user, get_current_user_if_admin
|
||||||
|
|
||||||
router = APIRouter(prefix="/user", tags=["user"])
|
router = APIRouter(prefix="/user", tags=["user"])
|
||||||
|
|
@ -26,6 +28,7 @@ class Token(BaseModel):
|
||||||
async def login(
|
async def login(
|
||||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||||
current_config: Config | None = Depends(Config.load),
|
current_config: Config | None = Depends(Config.load),
|
||||||
|
db: Session | None = Depends(Connection.get),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
POST ./authenticate: Authenticate a user. Issues a bearer token.
|
POST ./authenticate: Authenticate a user. Issues a bearer token.
|
||||||
|
|
@ -36,10 +39,12 @@ async def login(
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# try logging in
|
# try logging in
|
||||||
if not (user := User.authenticate(
|
user = User(name=form_data.username)
|
||||||
name=form_data.username,
|
if not user.authenticate(
|
||||||
|
db=db,
|
||||||
password=form_data.password,
|
password=form_data.password,
|
||||||
)):
|
crypt_context=current_config.crypto.crypt_context,
|
||||||
|
):
|
||||||
# authentication failed
|
# authentication failed
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -52,7 +57,7 @@ async def login(
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/current", response_model=UserRead)
|
@router.get("/current", response_model=User)
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
current_user: User | None = Depends(get_current_user),
|
current_user: User | None = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
@ -76,14 +81,20 @@ async def get_current_user(
|
||||||
)
|
)
|
||||||
async def add_user(
|
async def add_user(
|
||||||
user: UserCreate,
|
user: UserCreate,
|
||||||
|
current_config: Config | None = Depends(Config.load),
|
||||||
_: User = Depends(get_current_user_if_admin),
|
_: User = Depends(get_current_user_if_admin),
|
||||||
|
db: Session | None = Depends(Connection.get),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
POST ./: Create a new user in the database.
|
POST ./: Create a new user in the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# actually create the new user
|
# actually create the new user
|
||||||
new_user = User.create(**user.dict())
|
new_user = User.create(
|
||||||
|
db=db,
|
||||||
|
user=user,
|
||||||
|
crypt_context=current_config.crypto.crypt_context,
|
||||||
|
)
|
||||||
|
|
||||||
# fail if creation was unsuccessful
|
# fail if creation was unsuccessful
|
||||||
if new_user is None:
|
if new_user is None:
|
||||||
|
|
@ -107,21 +118,22 @@ async def add_user(
|
||||||
async def remove_user(
|
async def remove_user(
|
||||||
user_name: str,
|
user_name: str,
|
||||||
_: User = Depends(get_current_user_if_admin),
|
_: User = Depends(get_current_user_if_admin),
|
||||||
|
db: Session | None = Depends(Connection.get),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
DELETE ./{user_name}: Remove a user from the database.
|
DELETE ./{user_name}: Remove a user from the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# get the user
|
# get the user
|
||||||
user = User.get(user_name)
|
user = User.from_db(
|
||||||
|
db=db,
|
||||||
|
name=user_name,
|
||||||
|
)
|
||||||
|
|
||||||
# fail if user not found
|
# fail if deletion was unsuccessful
|
||||||
if user is None:
|
if user is None or not user.delete(db):
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# delete user
|
|
||||||
user.delete()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/{user_name}/capabilities",
|
"/{user_name}/capabilities",
|
||||||
|
|
@ -134,21 +146,22 @@ async def remove_user(
|
||||||
)
|
)
|
||||||
async def extend_capabilities(
|
async def extend_capabilities(
|
||||||
user_name: str,
|
user_name: str,
|
||||||
capabilities: list[Capability],
|
capabilities: list[UserCapability],
|
||||||
_: User = Depends(get_current_user_if_admin),
|
_: User = Depends(get_current_user_if_admin),
|
||||||
|
db: Session | None = Depends(Connection.get),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
POST ./{user_name}/capabilities: Add capabilities to a user.
|
POST ./{user_name}/capabilities: Add capabilities to a user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# get and change the user
|
# get and change the user
|
||||||
user = User.get(user_name)
|
user = User.from_db(
|
||||||
|
db=db,
|
||||||
user.set_capabilities(
|
name=user_name,
|
||||||
user.get_capabilities() | set(capabilities)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
user.update()
|
user.capabilities.extend(capabilities)
|
||||||
|
user.update(db)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
|
|
@ -162,18 +175,21 @@ async def extend_capabilities(
|
||||||
)
|
)
|
||||||
async def remove_capabilities(
|
async def remove_capabilities(
|
||||||
user_name: str,
|
user_name: str,
|
||||||
capabilities: list[Capability],
|
capabilities: list[UserCapability],
|
||||||
_: User = Depends(get_current_user_if_admin),
|
_: User = Depends(get_current_user_if_admin),
|
||||||
|
db: Session | None = Depends(Connection.get),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
DELETE ./{user_name}/capabilities: Remove capabilities from a user.
|
DELETE ./{user_name}/capabilities: Remove capabilities from a user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# get and change the user
|
# get and change the user
|
||||||
user = User.get(user_name)
|
user = User.from_db(
|
||||||
|
db=db,
|
||||||
user.set_capabilities(
|
name=user_name,
|
||||||
user.get_capabilities() - set(capabilities)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
user.update()
|
for capability in capabilities:
|
||||||
|
user.capabilities.remove(capability)
|
||||||
|
|
||||||
|
user.update(db)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue