Complete admin package restructuring
This commit is contained in:
@ -0,0 +1,5 @@
|
||||
"""Admin REST API."""
|
||||
|
||||
from .router import create_router as create_api_router
|
||||
|
||||
__all__ = ["create_api_router"]
|
||||
@ -0,0 +1 @@
|
||||
"""API Endpoints."""
|
||||
@ -0,0 +1,39 @@
|
||||
"""Authentication related endpoints factory."""
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session
|
||||
|
||||
from sshecret_admin.auth import Token, authenticate_user, create_access_token
|
||||
from sshecret_admin.core.dependencies import AdminDependencies
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
"""Create auth router."""
|
||||
app = APIRouter()
|
||||
|
||||
@app.post("/token")
|
||||
async def login_for_access_token(
|
||||
session: Annotated[Session, Depends(dependencies.get_db_session)],
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
) -> Token:
|
||||
"""Login user and generate token."""
|
||||
user = authenticate_user(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"},
|
||||
)
|
||||
access_token = create_access_token(
|
||||
dependencies.settings,
|
||||
data={"sub": user.username},
|
||||
)
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
|
||||
|
||||
return app
|
||||
@ -0,0 +1,124 @@
|
||||
"""Client-related endpoints factory."""
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
|
||||
import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from sshecret.backend import Client
|
||||
from sshecret_admin.core.dependencies import AdminDependencies
|
||||
from sshecret_admin.services import AdminBackend
|
||||
from sshecret_admin.services.models import (
|
||||
ClientCreate,
|
||||
UpdateKeyModel,
|
||||
UpdateKeyResponse,
|
||||
UpdatePoliciesRequest,
|
||||
)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
"""Create clients router."""
|
||||
app = APIRouter()
|
||||
|
||||
@app.get("/clients/")
|
||||
async def get_clients(
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)]
|
||||
) -> list[Client]:
|
||||
"""Get clients."""
|
||||
clients = await admin.get_clients()
|
||||
return clients
|
||||
|
||||
@app.post("/clients/")
|
||||
async def create_client(
|
||||
new_client: ClientCreate,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Create a new client."""
|
||||
sources: list[str] | None = None
|
||||
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
|
||||
)
|
||||
return client
|
||||
|
||||
@app.delete("/clients/{name}")
|
||||
async def delete_client(
|
||||
name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Delete a client."""
|
||||
await admin.delete_client(name)
|
||||
|
||||
@app.delete("/clients/{name}/secrets/{secret_name}")
|
||||
async def delete_secret_from_client(
|
||||
name: 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)
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
)
|
||||
|
||||
if secret_name not in client.secrets:
|
||||
LOG.debug("Client does not have requested secret. No action to perform.")
|
||||
return None
|
||||
|
||||
await admin.delete_client_secret(name, secret_name)
|
||||
|
||||
@app.put("/clients/{name}/policies")
|
||||
async def update_client_policies(
|
||||
name: str,
|
||||
updated: UpdatePoliciesRequest,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Update the client access policies."""
|
||||
client = await admin.get_client(name)
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
assert client is not None, "Critical: The client disappeared after update!"
|
||||
|
||||
return client
|
||||
|
||||
@app.put("/clients/{name}/public-key")
|
||||
async def update_client_public_key(
|
||||
name: str,
|
||||
updated: UpdateKeyModel,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> UpdateKeyResponse:
|
||||
"""Update client public key.
|
||||
|
||||
Updating the public key will invalidate the current secrets, so these well
|
||||
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)
|
||||
return UpdateKeyResponse(
|
||||
public_key=updated.public_key, updated_secrets=updated_secrets
|
||||
)
|
||||
|
||||
@app.put("/clients/{name}/secrets/{secret_name}")
|
||||
async def add_secret_to_client(
|
||||
name: 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)
|
||||
|
||||
return app
|
||||
@ -0,0 +1,70 @@
|
||||
"""Secrets related endpoints factory."""
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from sshecret.backend.models import Secret
|
||||
from sshecret_admin.core.dependencies import AdminDependencies
|
||||
from sshecret_admin.services import AdminBackend
|
||||
from sshecret_admin.services.models import (
|
||||
SecretCreate,
|
||||
SecretUpdate,
|
||||
SecretView,
|
||||
)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
"""Create secrets router."""
|
||||
app = APIRouter()
|
||||
|
||||
@app.get("/secrets/")
|
||||
async def get_secret_names(
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)]
|
||||
) -> list[Secret]:
|
||||
"""Get Secret Names."""
|
||||
return await admin.get_secrets()
|
||||
|
||||
@app.post("/secrets/")
|
||||
async def add_secret(
|
||||
secret: SecretCreate,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Create a secret."""
|
||||
await admin.add_secret(secret.name, secret.get_secret(), secret.clients)
|
||||
|
||||
@app.get("/secrets/{name}")
|
||||
async def get_secret(
|
||||
name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> SecretView:
|
||||
"""Get a secret."""
|
||||
secret_view = await admin.get_secret(name)
|
||||
|
||||
if not secret_view:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found."
|
||||
)
|
||||
return secret_view
|
||||
|
||||
@app.put("/secrets/{name}")
|
||||
async def update_secret(
|
||||
name: str,
|
||||
value: SecretUpdate,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
new_value = value.get_secret()
|
||||
await admin.update_secret(name, new_value)
|
||||
|
||||
@app.delete("/secrets/{name}")
|
||||
async def delete_secret(
|
||||
name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Delete secret."""
|
||||
await admin.delete_secret(name)
|
||||
|
||||
return app
|
||||
78
packages/sshecret-admin/src/sshecret_admin/api/router.py
Normal file
78
packages/sshecret-admin/src/sshecret_admin/api/router.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Main API Router."""
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from sshecret_admin.services.admin_backend import AdminBackend
|
||||
from sshecret_admin.core.dependencies import BaseDependencies, AdminDependencies
|
||||
from sshecret_admin.auth import PasswordDB, User, decode_token
|
||||
|
||||
from .endpoints import auth, clients, secrets
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
API_VERSION = "v1"
|
||||
|
||||
|
||||
def create_router(dependencies: BaseDependencies) -> APIRouter:
|
||||
"""Create clients router."""
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_scheme)],
|
||||
session: Annotated[Session, Depends(dependencies.get_db_session)],
|
||||
) -> User:
|
||||
"""Get current user from token."""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
token_data = decode_token(dependencies.settings, token)
|
||||
if not token_data:
|
||||
raise credentials_exception
|
||||
|
||||
user = session.exec(
|
||||
select(User).where(User.username == token_data.username)
|
||||
).first()
|
||||
if not user:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
) -> User:
|
||||
"""Get current active user."""
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive or disabled user")
|
||||
return current_user
|
||||
|
||||
async def get_admin_backend(session: Annotated[Session, Depends(dependencies.get_db_session)]):
|
||||
"""Get admin backend API."""
|
||||
password_db = session.exec(select(PasswordDB).where(PasswordDB.id == 1)).first()
|
||||
if not password_db:
|
||||
raise HTTPException(
|
||||
500, detail="Error: The password manager has not yet been set up."
|
||||
)
|
||||
admin = AdminBackend(dependencies.settings, password_db.encrypted_password)
|
||||
yield admin
|
||||
|
||||
app = APIRouter(
|
||||
prefix=f"/api/{API_VERSION}", dependencies=[Depends(get_current_active_user)]
|
||||
)
|
||||
|
||||
endpoint_deps = AdminDependencies.create(dependencies, get_admin_backend)
|
||||
|
||||
app.include_router(auth.create_router(endpoint_deps))
|
||||
app.include_router(clients.create_router(endpoint_deps))
|
||||
app.include_router(secrets.create_router(endpoint_deps))
|
||||
|
||||
return app
|
||||
Reference in New Issue
Block a user