api-server: Add user token auth
This commit is contained in:
parent
eca2cdddb1
commit
0142d9789b
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -6,5 +6,8 @@
|
||||
],
|
||||
"https://raw.githubusercontent.com/ansible-community/schemas/main/f/ansible-tasks.json": "tasks/deploy.yml"
|
||||
},
|
||||
"yaml.customTags": ["!vault scalar"]
|
||||
"yaml.customTags": [
|
||||
"!vault scalar"
|
||||
],
|
||||
"python.formatting.provider": "black"
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ RUN adduser \
|
||||
--gecos "" \
|
||||
api-server
|
||||
|
||||
RUN mkdir -p /var/run/ansible
|
||||
|
||||
USER api-server
|
||||
WORKDIR /opt/api-server
|
||||
|
||||
@ -21,7 +23,10 @@ ENV API_PROJECT_DIR=/var/project \
|
||||
API_RUNAS=api-server \
|
||||
PATH="/home/api-server/.local/bin:${PATH}"
|
||||
|
||||
|
||||
VOLUME [ "${API_PROJECT_DIR}" ]
|
||||
VOLUME [ "/var/run/ansible" ]
|
||||
|
||||
WORKDIR ${API_PROJECT_DIR}
|
||||
|
||||
# switch to root as it must be available
|
||||
|
119
api/api.py
119
api/api.py
@ -1,14 +1,59 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from wsgiref.util import request_uri
|
||||
import ansible_runner
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi import FastAPI, HTTPException, Request, Security, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from pydantic import BaseModel, Field
|
||||
from ansible_runner import Runner
|
||||
from dataclasses import dataclass
|
||||
from yaml import safe_load
|
||||
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
bearer_auth = HTTPBearer()
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
token: str
|
||||
allowed_services: list[str]
|
||||
|
||||
|
||||
def load_users(filename) -> dict[str, User]:
|
||||
try:
|
||||
with open(filename, "r") as file:
|
||||
data = safe_load(file)
|
||||
|
||||
if not isinstance(data, list):
|
||||
print(f"{filename} must be list of users", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return {user["token"]: User(**user) for user in data}
|
||||
except FileNotFoundError as e:
|
||||
print(
|
||||
f"File {filename} was not found, please make sure that you provide users file.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
print(e, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
users = load_users("/etc/api-server/users.yaml")
|
||||
|
||||
|
||||
def get_user(bearer_auth: HTTPAuthorizationCredentials = Security(bearer_auth)) -> User:
|
||||
if bearer_auth and bearer_auth.credentials in users:
|
||||
return users[bearer_auth.credentials]
|
||||
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
@ -29,6 +74,7 @@ class DeploymentLinks(BaseModel):
|
||||
@dataclass
|
||||
class Deployment:
|
||||
id: str
|
||||
args: DeployArgs
|
||||
runner: Runner | None = None
|
||||
|
||||
|
||||
@ -50,15 +96,42 @@ class DeploymentDTO(BaseModel):
|
||||
|
||||
deployments: Dict[str, Deployment] = {}
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/deployment/{id}/logs")
|
||||
async def deployment_logs(id: str):
|
||||
def get_deployment(id: str) -> Deployment:
|
||||
if id not in deployments:
|
||||
raise HTTPException(status_code=404, detail="Deployment was not found")
|
||||
|
||||
runner = deployments.get(id).runner
|
||||
return deployments[id]
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
docs_url="/",
|
||||
openapi_url="/swagger.json",
|
||||
description="API for services management",
|
||||
title="Server Management API",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/deployment")
|
||||
async def deployment_list(request: Request, user: User = Depends(get_user)):
|
||||
return [
|
||||
DeploymentDTO.from_deployment(deployment, request)
|
||||
for deployment in deployments.values()
|
||||
if deployment.args.service in user.allowed_services
|
||||
]
|
||||
|
||||
|
||||
@app.get("/deployment/{id}/logs")
|
||||
async def deployment_logs(
|
||||
deployment: Deployment = Depends(get_deployment), user: User = Depends(get_user)
|
||||
):
|
||||
if deployment.args.service not in user.allowed_services:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail=f"This token does not allow to access {deployment.args.service} deployments.",
|
||||
)
|
||||
|
||||
runner = deployment.runner
|
||||
|
||||
async def stream():
|
||||
stdout = runner.stdout
|
||||
@ -77,34 +150,42 @@ async def deployment_logs(id: str):
|
||||
return StreamingResponse(stream(), media_type="text/plain")
|
||||
|
||||
|
||||
@app.get("/deployment")
|
||||
async def deployment_list(request: Request):
|
||||
return [
|
||||
DeploymentDTO.from_deployment(deployment, request)
|
||||
for deployment in deployments.values()
|
||||
]
|
||||
|
||||
|
||||
@app.get("/deployment/{id}")
|
||||
async def deployment(id: str, request: Request):
|
||||
async def deployment(
|
||||
request: Request,
|
||||
deployment: Deployment = Depends(get_deployment),
|
||||
user: User = Depends(get_user),
|
||||
):
|
||||
if deployment.args.service not in user.allowed_services:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail=f"This token does not allow to access {deployment.args.service} deployments.",
|
||||
)
|
||||
|
||||
return DeploymentDTO.from_deployment(deployments[id], request)
|
||||
|
||||
|
||||
@app.post("/deployment")
|
||||
async def deploy(args: DeployArgs, request: Request):
|
||||
async def deploy(args: DeployArgs, request: Request, user: User = Depends(get_user)):
|
||||
if args.service not in user.allowed_services:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail=f"This token does not allow to deploy {args.service} service.",
|
||||
)
|
||||
|
||||
ident = str(uuid.uuid4())
|
||||
|
||||
_, runner = ansible_runner.run_async(
|
||||
playbook="deploy.yaml",
|
||||
ident=ident,
|
||||
extravars={"services": [args.service], **args.vars},
|
||||
private_data_dir="/home/api-server",
|
||||
private_data_dir="/var/run/ansible",
|
||||
project_dir=os.environ.get("API_PROJECT_DIR", "/var/project"),
|
||||
inventory=args.inventory or os.environ.get("API_INVENTORY"),
|
||||
settings={"suppress_ansible_output": True},
|
||||
)
|
||||
|
||||
deployment = Deployment(ident, runner)
|
||||
deployment = Deployment(id=ident, runner=runner, args=args)
|
||||
deployments[ident] = deployment
|
||||
|
||||
return DeploymentDTO.from_deployment(deployment, request)
|
||||
|
1
api/bin/docker-entrypoint.d/02-make-private-directory-writable.sh
Executable file
1
api/bin/docker-entrypoint.d/02-make-private-directory-writable.sh
Executable file
@ -0,0 +1 @@
|
||||
chown $API_RUNAS /var/run/ansible
|
2
api/fixtures/users.yaml
Normal file
2
api/fixtures/users.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
- token: test
|
||||
allowed_services: ["wipe-stg"]
|
@ -12,4 +12,5 @@ services:
|
||||
volumes:
|
||||
- .:/var/project
|
||||
- ./api:/opt/api-server
|
||||
- ./api/fixtures/users.yaml:/etc/api-server/users.yaml:ro
|
||||
- ${SSH_AUTH_SOCK}:${SSH_AUTH_SOCK}
|
||||
|
Loading…
Reference in New Issue
Block a user