Backend fixed and features
This commit is contained in:
@ -2,39 +2,86 @@
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from typing import Annotated, Literal
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from sshecret_admin.auth import Token, authenticate_user_async, create_access_token
|
||||
from sshecret_admin.auth import (
|
||||
Token,
|
||||
authenticate_user_async,
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
)
|
||||
from sshecret_admin.core.dependencies import AdminDependencies
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RefreshTokenForm(BaseModel):
|
||||
"""The refresh token form data."""
|
||||
|
||||
grant_type: Literal["refresh_token"]
|
||||
refresh_token: str
|
||||
|
||||
|
||||
def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
"""Create auth router."""
|
||||
app = APIRouter()
|
||||
|
||||
@app.post("/token")
|
||||
async def login_for_access_token(
|
||||
|
||||
session: Annotated[AsyncSession, Depends(dependencies.get_async_session)],
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
) -> Token:
|
||||
"""Login user and generate token."""
|
||||
user = await authenticate_user_async(session, form_data.username, form_data.password)
|
||||
user = await authenticate_user_async(
|
||||
session, form_data.username, form_data.password
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token_data: dict[str, str] = {"sub": user.username}
|
||||
access_token = create_access_token(
|
||||
dependencies.settings,
|
||||
data={"sub": user.username},
|
||||
data=token_data,
|
||||
)
|
||||
refresh_token = create_refresh_token(dependencies.settings, data=token_data)
|
||||
|
||||
return Token(
|
||||
access_token=access_token, refresh_token=refresh_token, token_type="bearer"
|
||||
)
|
||||
|
||||
@app.post("/refresh")
|
||||
async def refresh_token(
|
||||
form_data: Annotated[RefreshTokenForm, Form()],
|
||||
) -> Token:
|
||||
"""Refresh access token."""
|
||||
LOG.info("Refresh token data: %r", form_data)
|
||||
claims = decode_token(dependencies.settings, form_data.refresh_token)
|
||||
if not claims:
|
||||
LOG.info("Could not decode claims")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token_data: dict[str, str] = {"sub": claims.sub}
|
||||
access_token = create_access_token(
|
||||
dependencies.settings,
|
||||
data=token_data,
|
||||
)
|
||||
refresh_token = create_refresh_token(dependencies.settings, data=token_data)
|
||||
|
||||
return Token(
|
||||
access_token=access_token, refresh_token=refresh_token, token_type="bearer"
|
||||
)
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
|
||||
return app
|
||||
|
||||
@ -1,24 +1,52 @@
|
||||
"""Client-related endpoints factory."""
|
||||
"""Client-related endpoints factory.
|
||||
|
||||
# TODO: Settle on name/keyspec pattern
|
||||
"""
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
|
||||
import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
||||
from sshecret.backend import Client
|
||||
from sshecret.backend import Client, ClientFilter
|
||||
from sshecret_admin.core.dependencies import AdminDependencies
|
||||
from sshecret_admin.services import AdminBackend
|
||||
from sshecret_admin.services.models import (
|
||||
ClientCreate,
|
||||
ClientListParams,
|
||||
UpdateKeyModel,
|
||||
UpdateKeyResponse,
|
||||
UpdatePoliciesRequest,
|
||||
)
|
||||
|
||||
from sshecret.backend.models import ClientQueryResult, ClientReference, FilterType
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def query_filter_to_client_filter(query_filter: ClientListParams) -> ClientFilter:
|
||||
"""Convert query filter to client filter."""
|
||||
client_filter = ClientFilter(
|
||||
limit=query_filter.limit,
|
||||
offset=query_filter.offset,
|
||||
order_by=query_filter.order_by,
|
||||
order_reverse=query_filter.order_reverse,
|
||||
)
|
||||
if client_id := query_filter.id:
|
||||
client_filter.id = str(client_id)
|
||||
|
||||
if match_name := query_filter.name:
|
||||
client_filter.name = match_name
|
||||
elif match_name_like := query_filter.name__like:
|
||||
client_filter.name = match_name_like
|
||||
client_filter.filter_name = FilterType.LIKE
|
||||
elif match_name_contains := query_filter.name__contains:
|
||||
client_filter.name = match_name_contains
|
||||
client_filter.filter_name = FilterType.CONTAINS
|
||||
return client_filter
|
||||
|
||||
|
||||
def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
"""Create clients router."""
|
||||
app = APIRouter(dependencies=[Depends(dependencies.get_current_active_user)])
|
||||
@ -31,6 +59,23 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
clients = await admin.get_clients()
|
||||
return clients
|
||||
|
||||
@app.get("/clients/terse/")
|
||||
async def get_clients_terse(
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> list[ClientReference]:
|
||||
"""Get a list of client ids and names."""
|
||||
return await admin.get_clients_terse()
|
||||
|
||||
@app.get("/query/clients/")
|
||||
async def query_clients(
|
||||
filter_query: Annotated[ClientListParams, Query()],
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> ClientQueryResult:
|
||||
"""Query clients."""
|
||||
client_filter = query_filter_to_client_filter(filter_query)
|
||||
clients = await admin.query_clients(client_filter)
|
||||
return clients
|
||||
|
||||
@app.post("/clients/")
|
||||
async def create_client(
|
||||
new_client: ClientCreate,
|
||||
@ -41,26 +86,66 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
if new_client.sources:
|
||||
sources = [str(source) for source in new_client.sources]
|
||||
client = await admin.create_client(
|
||||
new_client.name, new_client.public_key, sources=sources
|
||||
name=new_client.name,
|
||||
public_key=new_client.public_key,
|
||||
description=new_client.description,
|
||||
sources=sources,
|
||||
)
|
||||
return client
|
||||
|
||||
@app.delete("/clients/{name}")
|
||||
@app.get("/clients/{id}")
|
||||
async def get_client(
|
||||
id: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Get a client."""
|
||||
client = await admin.get_client(("id", id))
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
@app.put("/clients/{id}")
|
||||
async def update_client(
|
||||
id: str,
|
||||
updated: ClientCreate,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Update a client."""
|
||||
client = await admin.get_client(("id", id))
|
||||
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
)
|
||||
update_fields = {
|
||||
"description": updated.description,
|
||||
"public_key": updated.public_key,
|
||||
"policies": updated.sources,
|
||||
}
|
||||
new_client = client.model_copy(update=update_fields)
|
||||
|
||||
result = await admin.update_client(new_client)
|
||||
return result
|
||||
|
||||
@app.delete("/clients/{id}")
|
||||
async def delete_client(
|
||||
name: str,
|
||||
id: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Delete a client."""
|
||||
await admin.delete_client(name)
|
||||
await admin.delete_client(("id", id))
|
||||
|
||||
@app.delete("/clients/{name}/secrets/{secret_name}")
|
||||
@app.delete("/clients/{id}/secrets/{secret_name}")
|
||||
async def delete_secret_from_client(
|
||||
name: str,
|
||||
id: str,
|
||||
secret_name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Delete a secret from a client."""
|
||||
client = await admin.get_client(name)
|
||||
client = await admin.get_client(("id", id))
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
@ -70,16 +155,16 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
LOG.debug("Client does not have requested secret. No action to perform.")
|
||||
return None
|
||||
|
||||
await admin.delete_client_secret(name, secret_name)
|
||||
await admin.delete_client_secret(("id", id), secret_name)
|
||||
|
||||
@app.put("/clients/{name}/policies")
|
||||
@app.put("/clients/{id}/policies")
|
||||
async def update_client_policies(
|
||||
name: str,
|
||||
id: str,
|
||||
updated: UpdatePoliciesRequest,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Update the client access policies."""
|
||||
client = await admin.get_client(name)
|
||||
client = await admin.get_client(("id", id))
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
@ -88,16 +173,16 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
LOG.debug("Old policies: %r. New: %r", client.policies, updated.sources)
|
||||
|
||||
addresses: list[str] = [str(source) for source in updated.sources]
|
||||
await admin.update_client_sources(name, addresses)
|
||||
client = await admin.get_client(name)
|
||||
await admin.update_client_sources(("id", id), addresses)
|
||||
client = await admin.get_client(("id", id))
|
||||
|
||||
assert client is not None, "Critical: The client disappeared after update!"
|
||||
|
||||
return client
|
||||
|
||||
@app.put("/clients/{name}/public-key")
|
||||
@app.put("/clients/{id}/public-key")
|
||||
async def update_client_public_key(
|
||||
name: str,
|
||||
id: str,
|
||||
updated: UpdateKeyModel,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> UpdateKeyResponse:
|
||||
@ -107,18 +192,20 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
be resolved first, and re-encrypted using the new key.
|
||||
"""
|
||||
# Let's first ensure that the key is actually updated.
|
||||
updated_secrets = await admin.update_client_public_key(name, updated.public_key)
|
||||
updated_secrets = await admin.update_client_public_key(
|
||||
("id", id), updated.public_key
|
||||
)
|
||||
return UpdateKeyResponse(
|
||||
public_key=updated.public_key, updated_secrets=updated_secrets
|
||||
)
|
||||
|
||||
@app.put("/clients/{name}/secrets/{secret_name}")
|
||||
@app.put("/clients/{id}/secrets/{secret_name}")
|
||||
async def add_secret_to_client(
|
||||
name: str,
|
||||
id: str,
|
||||
secret_name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Add secret to a client."""
|
||||
await admin.create_client_secret(name, secret_name)
|
||||
await admin.create_client_secret(("id", id), secret_name)
|
||||
|
||||
return app
|
||||
|
||||
@ -27,7 +27,7 @@ API_VERSION = "v1"
|
||||
def create_router(dependencies: BaseDependencies) -> APIRouter:
|
||||
"""Create clients router."""
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token", refreshUrl="/api/v1/refresh")
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_scheme)],
|
||||
|
||||
Reference in New Issue
Block a user