feat(auth): implement Keycloak authentication with RBAC and pagination
Major changes: - Add Keycloak integration via token introspection endpoint - Implement RBAC system with roles: admin, user, guest - Add role-based permissions for post operations - Add pagination support (default limit: 10) to list endpoints - Add published_only filter with admin-only override for unpublished posts Security improvements: - Remove hardcoded default secrets (SECRET_KEY, KEYCLOAK_CLIENT_SECRET) - Update .env.example with proper security placeholders - Add comprehensive RBAC unit tests Infrastructure: - Add httpx dependency for HTTP client - Add KeycloakAuthClient with token caching (TTL: 60s) - Add role-based dependencies (RequireAdmin, RequireUser, etc.) - Update DI container with Keycloak provider Endpoints updated: - GET /posts: filter by published status (admin can see all) - Add pagination params (limit, offset) to list endpoints - Enforce RBAC on post operations Tests: - Add 16 auth infrastructure tests - Add 13 RBAC role tests - Update existing tests for new required settings Breaking changes: - SECRET_KEY and KEYCLOAK_CLIENT_SECRET now required (no defaults)
This commit is contained in:
@@ -6,8 +6,11 @@ from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.application.dtos import CreatePostDTO, UpdatePostDTO
|
||||
from app.domain.exceptions import ForbiddenException
|
||||
from app.domain.roles import Permission, has_permission
|
||||
from app.presentation.api.deps import (
|
||||
CreatePostDep,
|
||||
CurrentRoleDep,
|
||||
CurrentUserDep,
|
||||
DeletePostDep,
|
||||
GetPostDep,
|
||||
@@ -50,11 +53,38 @@ async def create_post(
|
||||
@router.get(
|
||||
"",
|
||||
response_model=PostListResponseSchema,
|
||||
summary="List all posts",
|
||||
summary="List posts",
|
||||
)
|
||||
async def list_posts(use_case: ListPostsDep) -> PostListResponseSchema:
|
||||
"""Get all blog posts."""
|
||||
results = await use_case.all_posts()
|
||||
async def list_posts(
|
||||
use_case: ListPostsDep,
|
||||
role: CurrentRoleDep,
|
||||
include_unpublished: bool = False,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> PostListResponseSchema:
|
||||
"""Get blog posts with optional filtering and pagination.
|
||||
|
||||
Args:
|
||||
include_unpublished: If True, returns all posts including drafts.
|
||||
Only admins can use this parameter.
|
||||
limit: Maximum number of posts to return (default: 10, max: 100).
|
||||
offset: Number of posts to skip (default: 0).
|
||||
|
||||
Raises:
|
||||
ForbiddenException: If non-admin tries to include unpublished posts.
|
||||
"""
|
||||
# Clamp limit to reasonable range
|
||||
limit = max(1, min(limit, 100))
|
||||
offset = max(0, offset)
|
||||
|
||||
# Check permissions for unpublished posts
|
||||
if include_unpublished:
|
||||
if not has_permission(role, Permission.POST_READ_UNPUBLISHED):
|
||||
raise ForbiddenException("Only admins can view unpublished posts")
|
||||
results = await use_case.all_posts()
|
||||
else:
|
||||
results = await use_case.published_posts(limit=limit, offset=offset)
|
||||
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user