281 lines
6.0 KiB
Python
281 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import os
|
|
from tools.sync.wiki_git import expected_branch
|
|
from typing import Any
|
|
|
|
from fastapi import (
|
|
HTTPException,
|
|
Security,
|
|
status,
|
|
)
|
|
from fastapi.security import (
|
|
APIKeyHeader,
|
|
HTTPAuthorizationCredentials,
|
|
HTTPBearer,
|
|
)
|
|
|
|
MIN_SECRET_LENGTH = 32
|
|
|
|
SEARCH_API_KEY_HEADER = "X-API-Key"
|
|
SYNC_API_KEY_HEADER = "X-API-Key"
|
|
|
|
|
|
TRUE_ENV_VALUES = {
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
"on",
|
|
}
|
|
|
|
FALSE_ENV_VALUES = {
|
|
"0",
|
|
"false",
|
|
"no",
|
|
"off",
|
|
}
|
|
|
|
|
|
search_api_key_scheme = APIKeyHeader(
|
|
name=SEARCH_API_KEY_HEADER,
|
|
scheme_name="SearchApiKey",
|
|
auto_error=False,
|
|
description=("API kľúč pre vyhľadávanie v ZP Wiki " "cez hlavičku X-API-Key."),
|
|
)
|
|
|
|
search_bearer_scheme = HTTPBearer(
|
|
scheme_name="SearchBearer",
|
|
auto_error=False,
|
|
description=("Bearer token pre vyhľadávanie v ZP Wiki. " "Používa hodnotu SEARCH_API_KEY."),
|
|
)
|
|
|
|
sync_api_key_scheme = APIKeyHeader(
|
|
name=SYNC_API_KEY_HEADER,
|
|
scheme_name="SyncApiKey",
|
|
auto_error=False,
|
|
description=("API kľúč pre manuálne spustenie " "reindexovania."),
|
|
)
|
|
|
|
|
|
def required_environment_value(
|
|
name: str,
|
|
) -> str:
|
|
value = os.getenv(
|
|
name,
|
|
"",
|
|
).strip()
|
|
|
|
if not value:
|
|
raise RuntimeError("Chýba povinná environment " f"premenná {name}")
|
|
|
|
return value
|
|
|
|
|
|
def validate_secret(
|
|
name: str,
|
|
) -> str:
|
|
value = required_environment_value(name)
|
|
|
|
if len(value) < MIN_SECRET_LENGTH:
|
|
raise RuntimeError(f"{name} musí mať aspoň " f"{MIN_SECRET_LENGTH} znakov")
|
|
|
|
return value
|
|
|
|
|
|
def expected_gitea_repository() -> str:
|
|
value = required_environment_value("EXPECTED_GITEA_REPOSITORY")
|
|
|
|
parts = value.split("/")
|
|
|
|
if len(parts) != 2:
|
|
raise RuntimeError("EXPECTED_GITEA_REPOSITORY musí mať " "tvar vlastník/repozitár")
|
|
|
|
owner, repository = parts
|
|
|
|
if not owner or not repository or any(character.isspace() for character in value):
|
|
raise RuntimeError("EXPECTED_GITEA_REPOSITORY musí mať " "tvar vlastník/repozitár")
|
|
|
|
return value
|
|
|
|
|
|
def webhook_should_pull_git() -> bool:
|
|
value = (
|
|
os.getenv(
|
|
"WEBHOOK_PULL_GIT",
|
|
"false",
|
|
)
|
|
.strip()
|
|
.casefold()
|
|
)
|
|
|
|
if value in TRUE_ENV_VALUES:
|
|
return True
|
|
|
|
if value in FALSE_ENV_VALUES:
|
|
return False
|
|
|
|
raise RuntimeError("WEBHOOK_PULL_GIT musí byť boolean " "hodnota true/false")
|
|
|
|
|
|
def validate_security_configuration() -> None:
|
|
webhook_secret = validate_secret("WEBHOOK_SECRET")
|
|
|
|
sync_api_key = validate_secret("SYNC_API_KEY")
|
|
|
|
search_api_key = validate_secret("SEARCH_API_KEY")
|
|
|
|
# Každá funkcia musí používať vlastný
|
|
# credential. Jeden uniknutý secret tak
|
|
# neposkytne prístup ku všetkým operáciám.
|
|
if (
|
|
len(
|
|
{
|
|
webhook_secret,
|
|
sync_api_key,
|
|
search_api_key,
|
|
}
|
|
)
|
|
!= 3
|
|
):
|
|
raise RuntimeError(
|
|
"WEBHOOK_SECRET, SYNC_API_KEY " "a SEARCH_API_KEY musia byť " "navzájom rozdielne"
|
|
)
|
|
|
|
expected_gitea_repository()
|
|
expected_branch()
|
|
|
|
# Validujeme aj voliteľnú boolean
|
|
# konfiguráciu už pri štarte aplikácie.
|
|
webhook_should_pull_git()
|
|
|
|
|
|
def require_search_api_key(
|
|
api_key: str | None = Security(search_api_key_scheme),
|
|
bearer: HTTPAuthorizationCredentials | None = Security(search_bearer_scheme),
|
|
) -> None:
|
|
expected = validate_secret("SEARCH_API_KEY")
|
|
|
|
supplied_credentials: list[str] = []
|
|
|
|
if api_key:
|
|
supplied_credentials.append(api_key)
|
|
|
|
if bearer is not None and bearer.scheme.casefold() == "bearer" and bearer.credentials:
|
|
supplied_credentials.append(bearer.credentials)
|
|
|
|
valid = any(
|
|
hmac.compare_digest(
|
|
supplied,
|
|
expected,
|
|
)
|
|
for supplied in supplied_credentials
|
|
)
|
|
|
|
if not valid:
|
|
raise HTTPException(
|
|
status_code=(status.HTTP_401_UNAUTHORIZED),
|
|
detail=("Neplatný alebo chýbajúci " "API kľúč"),
|
|
headers={
|
|
"WWW-Authenticate": ("Bearer"),
|
|
},
|
|
)
|
|
|
|
|
|
def require_sync_api_key(
|
|
api_key: str | None = Security(sync_api_key_scheme),
|
|
) -> None:
|
|
expected = validate_secret("SYNC_API_KEY")
|
|
|
|
if not api_key or not hmac.compare_digest(
|
|
api_key,
|
|
expected,
|
|
):
|
|
raise HTTPException(
|
|
status_code=(status.HTTP_401_UNAUTHORIZED),
|
|
detail=("Neplatný alebo chýbajúci " "API kľúč"),
|
|
headers={
|
|
"WWW-Authenticate": ("ApiKey"),
|
|
},
|
|
)
|
|
|
|
|
|
def verify_gitea_signature(
|
|
raw_body: bytes,
|
|
signature: str | None,
|
|
secret: str,
|
|
) -> bool:
|
|
if not signature:
|
|
return False
|
|
|
|
supplied = signature.strip().casefold()
|
|
|
|
if supplied.startswith("sha256="):
|
|
supplied = supplied.removeprefix("sha256=")
|
|
|
|
if len(supplied) != 64:
|
|
return False
|
|
|
|
try:
|
|
int(
|
|
supplied,
|
|
16,
|
|
)
|
|
|
|
except ValueError:
|
|
return False
|
|
|
|
expected = hmac.new(
|
|
secret.encode("utf-8"),
|
|
raw_body,
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
|
|
return hmac.compare_digest(
|
|
expected,
|
|
supplied,
|
|
)
|
|
|
|
|
|
def repository_name_from_payload(
|
|
payload: dict[str, Any],
|
|
) -> str | None:
|
|
repository = payload.get("repository")
|
|
|
|
if not isinstance(
|
|
repository,
|
|
dict,
|
|
):
|
|
return None
|
|
|
|
# Pri webhooku akceptujeme iba
|
|
# jednoznačný Gitea full_name:
|
|
#
|
|
# vlastník/repozitár
|
|
#
|
|
# Samotné "name" nestačí, pretože
|
|
# rovnaký názov môže existovať pod
|
|
# rôznymi vlastníkmi.
|
|
value = repository.get("full_name")
|
|
|
|
if not isinstance(
|
|
value,
|
|
str,
|
|
):
|
|
return None
|
|
|
|
value = value.strip()
|
|
|
|
return value or None
|
|
|
|
|
|
def same_repository(
|
|
actual: str,
|
|
expected: str,
|
|
) -> bool:
|
|
return hmac.compare_digest(
|
|
actual.strip().casefold(),
|
|
expected.strip().casefold(),
|
|
)
|