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"
|
"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 "" \
|
--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
|
||||||
|
119
api/api.py
119
api/api.py
@ -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)
|
||||||
|
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:
|
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}
|
||||||
|
Loading…
Reference in New Issue
Block a user