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" "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 "" \ --gecos "" \
api-server api-server
RUN mkdir -p /var/run/ansible
USER api-server USER api-server
WORKDIR /opt/api-server WORKDIR /opt/api-server
@ -21,7 +23,10 @@ ENV API_PROJECT_DIR=/var/project \
API_RUNAS=api-server \ API_RUNAS=api-server \
PATH="/home/api-server/.local/bin:${PATH}" PATH="/home/api-server/.local/bin:${PATH}"
VOLUME [ "${API_PROJECT_DIR}" ] VOLUME [ "${API_PROJECT_DIR}" ]
VOLUME [ "/var/run/ansible" ]
WORKDIR ${API_PROJECT_DIR} WORKDIR ${API_PROJECT_DIR}
# switch to root as it must be available # switch to root as it must be available

View File

@ -1,14 +1,59 @@
import asyncio import asyncio
import uuid import uuid
from wsgiref.util import request_uri
import ansible_runner import ansible_runner
import os import os
import sys
from typing import Any, Dict 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.responses import StreamingResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ansible_runner import Runner from ansible_runner import Runner
from dataclasses import dataclass 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): class Link(BaseModel):
@ -29,6 +74,7 @@ class DeploymentLinks(BaseModel):
@dataclass @dataclass
class Deployment: class Deployment:
id: str id: str
args: DeployArgs
runner: Runner | None = None runner: Runner | None = None
@ -50,15 +96,42 @@ class DeploymentDTO(BaseModel):
deployments: Dict[str, Deployment] = {} deployments: Dict[str, Deployment] = {}
app = FastAPI()
def get_deployment(id: str) -> Deployment:
@app.get("/deployment/{id}/logs")
async def deployment_logs(id: str):
if id not in deployments: if id not in deployments:
raise HTTPException(status_code=404, detail="Deployment was not found") 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(): async def stream():
stdout = runner.stdout stdout = runner.stdout
@ -77,34 +150,42 @@ async def deployment_logs(id: str):
return StreamingResponse(stream(), media_type="text/plain") 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}") @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) return DeploymentDTO.from_deployment(deployments[id], request)
@app.post("/deployment") @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()) ident = str(uuid.uuid4())
_, runner = ansible_runner.run_async( _, runner = ansible_runner.run_async(
playbook="deploy.yaml", playbook="deploy.yaml",
ident=ident, ident=ident,
extravars={"services": [args.service], **args.vars}, 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"), project_dir=os.environ.get("API_PROJECT_DIR", "/var/project"),
inventory=args.inventory or os.environ.get("API_INVENTORY"), inventory=args.inventory or os.environ.get("API_INVENTORY"),
settings={"suppress_ansible_output": True}, settings={"suppress_ansible_output": True},
) )
deployment = Deployment(ident, runner) deployment = Deployment(id=ident, runner=runner, args=args)
deployments[ident] = deployment deployments[ident] = deployment
return DeploymentDTO.from_deployment(deployment, request) 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: volumes:
- .:/var/project - .:/var/project
- ./api:/opt/api-server - ./api:/opt/api-server
- ./api/fixtures/users.yaml:/etc/api-server/users.yaml:ro
- ${SSH_AUTH_SOCK}:${SSH_AUTH_SOCK} - ${SSH_AUTH_SOCK}:${SSH_AUTH_SOCK}