Compare commits

..

No commits in common. "23a806e325818f4228f57f7e0806602529c6d6c1" and "d9552cbf42138a4d88fd668f1844dfd9b59ec66f" have entirely different histories.

8 changed files with 53 additions and 92 deletions

View file

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Main App",
"name": "Python: Modul",
"type": "python",
"request": "launch",
"module": "kiwi_vpn_api.main",

View file

@ -227,7 +227,6 @@ class CryptoConfig(BaseModel):
schemes: list[str] = ["bcrypt"]
# pki settings
ca_password: str | None
cert_algo: CertificateAlgo | None
expiry_days: int | None

View file

@ -1,7 +1,3 @@
"""
Python interface to EasyRSA CA.
"""
import subprocess
from datetime import datetime
from pathlib import Path
@ -9,32 +5,20 @@ from pathlib import Path
from OpenSSL import crypto
from passlib import pwd
from .config import Config, Settings
class EasyRSA:
"""
Represents an EasyRSA PKI.
"""
__directory: Path | None
__ca_password: str | None
@property
def pki_directory(self) -> Path:
return Settings._.data_dir.joinpath("pki")
def __init__(self, directory: Path) -> None:
self.__directory = directory
@property
def ca_password(self) -> str:
config = Config._
def set_ca_password(self, password: str | None = None) -> None:
if password is None:
password = pwd.genword(length=32, charset="ascii_62")
if (ca_password := config.crypto.ca_password) is None:
ca_password = pwd.genword(
length=32,
charset="ascii_62",
)
config.crypto.ca_password = ca_password
config.save()
return config.crypto.ca_password
self.__ca_password = password
print(self.__ca_password)
def __easyrsa(
self,
@ -43,7 +27,7 @@ class EasyRSA:
return subprocess.run(
[
"easyrsa", "--batch",
f"--pki-dir={self.pki_directory}",
f"--pki-dir={self.__directory}",
*easyrsa_args,
],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
@ -58,7 +42,7 @@ class EasyRSA:
self.__easyrsa(*easyrsa_args)
with open(
self.pki_directory.joinpath(cert_filename), "r"
self.__directory.joinpath(cert_filename), "r"
) as cert_file:
return crypto.load_certificate(
crypto.FILETYPE_PEM, cert_file.read()
@ -69,14 +53,14 @@ class EasyRSA:
def build_ca(
self,
days: int = 365 * 50,
cn: str = "kiwi-vpn-ca"
) -> crypto.X509:
config = Config._
cert = self.__build_cert(
Path("ca.crt"),
f"--passout=pass:{self.ca_password}",
f"--passin=pass:{self.ca_password}",
f"--passout=pass:{self.__ca_password}",
f"--passin=pass:{self.__ca_password}",
# "--dn-mode=org",
# "--req-c=EX",
@ -86,8 +70,8 @@ class EasyRSA:
# "--req-ou=EXAMPLE",
# "--req-email=EXAMPLE",
f"--req-cn={config.server_name}",
f"--days={config.crypto.expiry_days}",
f"--req-cn={cn}",
f"--days={days}",
# "--use-algo=ed",
# "--curve=ed25519",
@ -95,21 +79,20 @@ class EasyRSA:
"build-ca",
)
# self.__easyrsa("gen-dh")
self.__easyrsa("gen-dh")
return cert
def issue(
self,
days: int = 365 * 50,
cn: str = "kiwi-vpn-client",
cert_type: str = "client"
) -> crypto.X509:
config = Config._
return self.__build_cert(
Path(f"issued/{cn}.crt"),
f"--passin=pass:{self.ca_password}",
f"--days={config.crypto.expiry_days}",
f"--passin=pass:{self.__ca_password}",
f"--days={days}",
f"build-{cert_type}-full",
cn,
@ -118,10 +101,11 @@ class EasyRSA:
if __name__ == "__main__":
easy_rsa = EasyRSA()
easy_rsa = EasyRSA(Path("tmp/easyrsa"))
easy_rsa.init_pki()
easy_rsa.set_ca_password()
ca = easy_rsa.build_ca()
ca = easy_rsa.build_ca(cn="kiwi-vpn-ca")
server = easy_rsa.issue(cert_type="server", cn="kiwi-vpn-server")
client = easy_rsa.issue(cert_type="client", cn="kiwi-vpn-client")

View file

@ -7,12 +7,11 @@ This file: Main API router definition.
from fastapi import APIRouter
from ..config import Settings
from . import admin, device, service, user
from . import admin, device, user
main_router = APIRouter(prefix=f"/{Settings._.api_v1_prefix}")
main_router = APIRouter()
main_router.include_router(admin.router)
main_router.include_router(service.router)
main_router.include_router(admin.router, prefix=f"/{Settings._.api_v1_prefix}")
main_router.include_router(device.router)
main_router.include_router(user.router)

View file

@ -2,6 +2,7 @@
Common dependencies for routers.
"""
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
@ -49,28 +50,18 @@ class Responses:
}
async def get_current_config(
current_config: Config | None = Depends(Config.load),
) -> Config:
"""
Get the current configuration if it exists.
"""
# fail if not configured
if current_config is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
return current_config
async def get_current_user(
token: str = Depends(oauth2_scheme),
current_config: Config = Depends(get_current_config),
current_config: Config | None = Depends(Config.load),
) -> User:
"""
Get the currently logged-in user if it exists.
"""
# can't connect to an unconfigured database
if current_config is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
username = await current_config.jwt.decode_token(token)
# fail if not requested by a user
@ -83,18 +74,22 @@ async def get_current_user(
async def get_user_by_name(
user_name: str,
_: Config = Depends(get_current_config),
current_config: Config | None = Depends(Config.load),
) -> User | None:
"""
Get a user by name.
"""
# can't connect to an unconfigured database
if current_config is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
return User.get(user_name)
async def get_device_by_id(
device_id: int,
current_config: Config = Depends(get_current_config),
current_config: Config | None = Depends(Config.load),
) -> Device | None:
# can't connect to an unconfigured database

View file

@ -2,12 +2,13 @@
/admin endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import select
from ..config import Config
from ..db import Connection, TagValue, User, UserCreate
from ._common import Responses, get_current_config, get_current_user
from ._common import Responses, get_current_user
router = APIRouter(prefix="/admin", tags=["admin"])
@ -46,12 +47,16 @@ async def initial_configure(
)
async def create_initial_admin(
admin_user: UserCreate,
current_config: Config = Depends(get_current_config),
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)
# fail if any user exists
with Connection.session as db:
if db.exec(select(User).limit(1)).first() is not None:

View file

@ -1,24 +0,0 @@
"""
/service endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from ..config import Config
from ._common import Responses, get_current_config
router = APIRouter(prefix="/service", tags=["service"])
@router.put(
"/pki/init",
responses={
status.HTTP_200_OK: Responses.OK,
status.HTTP_400_BAD_REQUEST: Responses.NOT_INSTALLED,
status.HTTP_403_FORBIDDEN: Responses.NEEDS_PERMISSION,
},
)
async def init_pki(
_: Config = Depends(get_current_config),
) -> None:
pass

View file

@ -8,8 +8,7 @@ from pydantic import BaseModel
from ..config import Config
from ..db import TagValue, User, UserCreate, UserRead
from ._common import (Responses, get_current_config, get_current_user,
get_user_by_name)
from ._common import Responses, get_current_user, get_user_by_name
router = APIRouter(prefix="/user", tags=["user"])
@ -26,12 +25,16 @@ class Token(BaseModel):
@router.post("/authenticate", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
current_config: Config = Depends(get_current_config),
current_config: Config | None = Depends(Config.load),
):
"""
POST ./authenticate: Authenticate a user. Issues a bearer token.
"""
# fail if not installed
if current_config is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
# try logging in
if (user := User.authenticate(
name=form_data.username,