api-server: Add user token auth

This commit is contained in:
Kacper Donat 2022-11-16 20:23:15 +01:00
parent eca2cdddb1
commit 0142d9789b
6 changed files with 113 additions and 20 deletions

View File

@ -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"
}

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1 @@
chown $API_RUNAS /var/run/ansible

2
api/fixtures/users.yaml Normal file
View File

@ -0,0 +1,2 @@
- token: test
allowed_services: ["wipe-stg"]

View File

@ -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}