mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 18:02:41 +02:00
✨ — Add application API (fix #86)
This commit is contained in:
parent
a601fccad3
commit
32d11c5598
6 changed files with 234 additions and 17 deletions
|
@ -8,6 +8,7 @@
|
||||||
- 🔒️ — Logging out now invalidate tokens
|
- 🔒️ — Logging out now invalidate tokens
|
||||||
- 📝 — Improve OpenAPI doc
|
- 📝 — Improve OpenAPI doc
|
||||||
- 🤕 — Fix order of tasks sent to agent
|
- 🤕 — Fix order of tasks sent to agent
|
||||||
|
- ✨ — Add application API (fix #86)
|
||||||
|
|
||||||
## 0.9.0
|
## 0.9.0
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
|
|
||||||
|
|
||||||
class NotAuthenticatedException(Exception):
|
class NotAuthenticatedException(Exception):
|
||||||
|
@ -10,6 +10,16 @@ def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
|
||||||
"""
|
"""
|
||||||
Redirect the user to the login page if not logged in
|
Redirect the user to the login page if not logged in
|
||||||
"""
|
"""
|
||||||
|
print(request.headers.get("accept", ""))
|
||||||
|
print("application/json" in request.headers.get("accept", ""))
|
||||||
|
if "application/json" in request.headers.get("accept", ""):
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"access": "denied",
|
||||||
|
"msg": "You are not authenticated or your token has expired",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
response = RedirectResponse(
|
response = RedirectResponse(
|
||||||
url=request.url_for("login_view").include_query_params(msg="not-authenticated")
|
url=request.url_for("login_view").include_query_params(msg="not-authenticated")
|
||||||
)
|
)
|
||||||
|
|
|
@ -70,6 +70,7 @@ def get_application() -> FastAPI:
|
||||||
return await queries.get_user(appli.state.db, user)
|
return await queries.get_user(appli.state.db, user)
|
||||||
|
|
||||||
appli.include_router(routes.api, prefix="/api")
|
appli.include_router(routes.api, prefix="/api")
|
||||||
|
appli.include_router(routes.api_app, prefix="/api/app")
|
||||||
appli.include_router(routes.views)
|
appli.include_router(routes.views)
|
||||||
|
|
||||||
static_dir = Path(__file__).resolve().parent / "static"
|
static_dir = Path(__file__).resolve().parent / "static"
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
from .api import route as api
|
from .api import route as api
|
||||||
|
from .api_app import route as api_app
|
||||||
from .views import route as views
|
from .views import route as views
|
||||||
|
|
|
@ -85,22 +85,6 @@ async def create_results( # pylint: disable-msg=too-many-positional-arguments
|
||||||
return {"result_ids": [r.id for r in db_results]}
|
return {"result_ids": [r.id for r in db_results]}
|
||||||
|
|
||||||
|
|
||||||
@route.post(
|
|
||||||
"/reschedule/all",
|
|
||||||
responses={
|
|
||||||
200: {
|
|
||||||
"content": {
|
|
||||||
"application/json": {"example": {"msg": "Non OK tasks reschuled"}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
async def reschedule_all(request: Request, db: Session = Depends(get_db)):
|
|
||||||
"""Reschedule checks of all non OK tasks ASAP"""
|
|
||||||
await queries.reschedule_all(db)
|
|
||||||
return {"msg": "Non OK tasks reschuled"}
|
|
||||||
|
|
||||||
|
|
||||||
@route.get(
|
@route.get(
|
||||||
"/stats",
|
"/stats",
|
||||||
responses={
|
responses={
|
||||||
|
|
220
argos/server/routes/api_app.py
Normal file
220
argos/server/routes/api_app.py
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
"""Web interface for humans"""
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Annotated, Dict, Literal
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Form, Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from argos import VERSION
|
||||||
|
from argos.schemas import Config
|
||||||
|
from argos.server import queries
|
||||||
|
from argos.server.models import Result, User
|
||||||
|
from argos.server.routes.dependencies import (
|
||||||
|
create_user_token,
|
||||||
|
get_config,
|
||||||
|
get_db,
|
||||||
|
get_manager,
|
||||||
|
good_user_credentials,
|
||||||
|
)
|
||||||
|
|
||||||
|
route = APIRouter()
|
||||||
|
|
||||||
|
current_dir = Path(__file__).resolve().parent
|
||||||
|
templates = Jinja2Templates(directory=current_dir / ".." / "templates")
|
||||||
|
SEVERITY_LEVELS = {"ok": 1, "warning": 2, "critical": 3, "unknown": 4}
|
||||||
|
|
||||||
|
|
||||||
|
class Version(BaseModel):
|
||||||
|
argos_monitoring: str
|
||||||
|
logged_in: bool
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{"argos_monitoring": VERSION, "logged_in": True},
|
||||||
|
{"argos_monitoring": VERSION, "logged_in": False},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@route.get("/version", response_model=Version)
|
||||||
|
async def version(
|
||||||
|
request: Request,
|
||||||
|
):
|
||||||
|
"""Return a JSON object containing argos version
|
||||||
|
and indicates if you are logged in"""
|
||||||
|
token = request.cookies.get("access-token")
|
||||||
|
if token is not None and token != "":
|
||||||
|
manager = request.app.state.manager
|
||||||
|
user = await manager.get_current_user(token)
|
||||||
|
return Version(argos_monitoring=VERSION, logged_in=user is not None)
|
||||||
|
|
||||||
|
return Version(argos_monitoring=VERSION, logged_in=False)
|
||||||
|
|
||||||
|
|
||||||
|
class GrantedLogin(BaseModel):
|
||||||
|
access: Literal["granted"]
|
||||||
|
msg: str
|
||||||
|
cookie_name: Literal["access-token"] | None
|
||||||
|
cookie: str | None
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"access": "granted",
|
||||||
|
"msg": "Login successful",
|
||||||
|
"cookie_name": "access-token",
|
||||||
|
"cookie": "foobarbaz",
|
||||||
|
},
|
||||||
|
{"access": "granted", "msg": "No authentication needed"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeniedLogin(BaseModel):
|
||||||
|
access: Literal["deniel"]
|
||||||
|
msg: str
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{"access": "denied", "msg": "Sorry, invalid username or bad password."},
|
||||||
|
{
|
||||||
|
"access": "denied",
|
||||||
|
"msg": "Sorry, invalid username or bad password. "
|
||||||
|
"Or the LDAP server is unreachable (see logs to verify).",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@route.post(
|
||||||
|
"/login", response_model=GrantedLogin, responses={401: {"model": DeniedLogin}}
|
||||||
|
)
|
||||||
|
async def login(
|
||||||
|
request: Request,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
data: OAuth2PasswordRequestForm = Depends(),
|
||||||
|
rememberme: Annotated[str | None, Form()] = None,
|
||||||
|
config: Config = Depends(get_config),
|
||||||
|
):
|
||||||
|
"""If the login is successful, sends a token to be included as cookie
|
||||||
|
in requests needing authentication.
|
||||||
|
"""
|
||||||
|
if config.general.unauthenticated_access == "all":
|
||||||
|
return {"access": "granted", "msg": "No authentication needed"}
|
||||||
|
|
||||||
|
good_credentials = await good_user_credentials(
|
||||||
|
config, request, data.username, data.password
|
||||||
|
)
|
||||||
|
if config.general.ldap is not None:
|
||||||
|
if not good_credentials:
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"access": "denied",
|
||||||
|
"msg": "Sorry, invalid username or bad password. "
|
||||||
|
"Or the LDAP server is unreachable (see logs to verify).",
|
||||||
|
},
|
||||||
|
status_code=401,
|
||||||
|
)
|
||||||
|
elif not good_credentials:
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"access": "denied",
|
||||||
|
"msg": "Sorry, invalid username or bad password.",
|
||||||
|
},
|
||||||
|
status_code=401,
|
||||||
|
)
|
||||||
|
|
||||||
|
manager = request.app.state.manager
|
||||||
|
token = await create_user_token(manager, config.general, data.username, rememberme)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"access": "granted",
|
||||||
|
"msg": "Login successful",
|
||||||
|
"cookie_name": manager.cookie_name,
|
||||||
|
"cookie": token["token"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SuccessLogout(BaseModel):
|
||||||
|
logout: Literal["success"]
|
||||||
|
msg: str
|
||||||
|
|
||||||
|
|
||||||
|
@route.get("/logout", response_model=SuccessLogout)
|
||||||
|
async def logout(
|
||||||
|
request: Request,
|
||||||
|
config: Config = Depends(get_config),
|
||||||
|
user: User | None = Depends(get_manager),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Discard the token used for the request, so logging out the user"""
|
||||||
|
if config.general.unauthenticated_access == "all":
|
||||||
|
return {"logout": "error", "msg": "No authentication needed"}
|
||||||
|
|
||||||
|
await queries.block_token(db, request)
|
||||||
|
|
||||||
|
response = JSONResponse(content={"logout": "success", "msg": "logout successful"})
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class SeverityCounts(BaseModel):
|
||||||
|
severities: Dict[Literal["ok", "warning", "critical", "unknown"], int]
|
||||||
|
agents: int
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"json_schema_extra": {
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"severities": {"ok": 10, "warning": 0, "critical": 2, "unknown": 0},
|
||||||
|
"agents": 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@route.get("/", response_model=SeverityCounts)
|
||||||
|
async def get_severity_counts(
|
||||||
|
request: Request,
|
||||||
|
user: User | None = Depends(get_manager),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Shows the number of results per severity and number of known agents"""
|
||||||
|
counts_dict = await queries.get_severity_counts(db)
|
||||||
|
|
||||||
|
agents = db.query(Result.agent_id).distinct().all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"severities": counts_dict,
|
||||||
|
"agents": len(agents),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@route.post(
|
||||||
|
"/reschedule/all",
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"content": {
|
||||||
|
"application/json": {"example": {"msg": "Non OK tasks reschuled"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def reschedule_all(
|
||||||
|
request: Request,
|
||||||
|
user: User | None = Depends(get_manager),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Reschedule checks of all non OK tasks ASAP"""
|
||||||
|
await queries.reschedule_all(db)
|
||||||
|
return {"msg": "Non OK tasks reschuled"}
|
Loading…
Reference in a new issue