Compare commits
No commits in common. "23a806e325818f4228f57f7e0806602529c6d6c1" and "d9552cbf42138a4d88fd668f1844dfd9b59ec66f" have entirely different histories.
23a806e325
...
d9552cbf42
8 changed files with 53 additions and 92 deletions
2
api/.vscode/launch.json
vendored
2
api/.vscode/launch.json
vendored
|
|
@ -5,7 +5,7 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Main App",
|
||||
"name": "Python: Modul",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "kiwi_vpn_api.main",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue