refactor: route authorization through auth context

This commit is contained in:
Anish Sarkar 2026-06-19 20:27:28 +05:30
parent 630880bf7a
commit 7e8d26fa81
4 changed files with 105 additions and 47 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.context import AuthContext
from app.db import ExternalChatBinding, Permission, User
from app.gateway.bindings import suspend_binding
from app.observability.metrics import record_gateway_auth_invariant_failure
@ -39,11 +40,13 @@ async def assert_authorization_invariant(
if user is None:
await _fail(session, binding, "owner_missing")
auth = AuthContext.system(user, source="gateway")
try:
await check_search_space_access(session, user, binding.search_space_id)
await check_search_space_access(session, auth, binding.search_space_id)
await check_permission(
session,
user,
auth,
binding.search_space_id,
Permission.CHATS_CREATE.value,
"External chat owner no longer has permission to chat in this search space",

View file

@ -30,6 +30,7 @@ from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.context import AuthContext
from app.agents.chat.multi_agent_chat.shared.feature_flags import get_flags
from app.db import (
AgentPermissionRule,
@ -39,7 +40,7 @@ from app.db import (
User,
get_async_session,
)
from app.users import current_active_user
from app.users import get_auth_context
from app.utils.rbac import check_permission
logger = logging.getLogger(__name__)
@ -133,15 +134,16 @@ def _to_read(row: AgentPermissionRule) -> AgentPermissionRuleRead:
async def _ensure_search_space_membership_admin(
session: AsyncSession, user: User, search_space_id: int
session: AsyncSession, auth: AuthContext, search_space_id: int
) -> None:
user = auth.user
"""Curating agent rules == "settings" administration on the space."""
space = await session.get(SearchSpace, search_space_id)
if space is None:
raise HTTPException(status_code=404, detail="Search space not found.")
await check_permission(
session,
user,
auth,
search_space_id,
Permission.SETTINGS_UPDATE.value,
"You don't have permission to manage agent permission rules in this space.",
@ -160,8 +162,9 @@ async def _ensure_search_space_membership_admin(
async def list_rules(
search_space_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
) -> list[AgentPermissionRuleRead]:
user = auth.user
_flag_guard()
await _ensure_search_space_membership_admin(session, user, search_space_id)
@ -183,8 +186,9 @@ async def create_rule(
search_space_id: int,
payload: AgentPermissionRuleCreate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
) -> AgentPermissionRuleRead:
user = auth.user
_flag_guard()
await _ensure_search_space_membership_admin(session, user, search_space_id)
@ -232,8 +236,9 @@ async def update_rule(
rule_id: int,
payload: AgentPermissionRuleUpdate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
) -> AgentPermissionRuleRead:
user = auth.user
_flag_guard()
await _ensure_search_space_membership_admin(session, user, search_space_id)
@ -266,8 +271,9 @@ async def delete_rule(
search_space_id: int,
rule_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
) -> None:
user = auth.user
_flag_guard()
await _ensure_search_space_membership_admin(session, user, search_space_id)

View file

@ -18,6 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from app.auth.context import AuthContext
from app.db import (
Permission,
SearchSpace,
@ -43,7 +44,7 @@ from app.schemas import (
RoleUpdate,
UserSearchSpaceAccess,
)
from app.users import current_active_user
from app.users import get_auth_context
from app.utils.rbac import (
check_permission,
check_search_space_access,
@ -107,6 +108,8 @@ PERMISSION_DESCRIPTIONS = {
"settings:view": "View search space settings",
"settings:update": "Modify search space settings",
"settings:delete": "Delete the entire search space",
# API access
"api_access:manage": "Enable or disable programmatic API access for a search space",
# Automations
"automations:create": "Create automations from chat or JSON",
"automations:read": "View automations, their triggers, and run history",
@ -120,8 +123,9 @@ PERMISSION_DESCRIPTIONS = {
@router.get("/permissions", response_model=PermissionsListResponse)
async def list_all_permissions(
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
List all available permissions that can be assigned to roles.
"""
@ -156,8 +160,9 @@ async def create_role(
search_space_id: int,
role_data: RoleCreate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Create a new custom role in a search space.
Requires ROLES_CREATE permission.
@ -165,7 +170,7 @@ async def create_role(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.ROLES_CREATE.value,
"You don't have permission to create roles",
@ -237,8 +242,9 @@ async def create_role(
async def list_roles(
search_space_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
List all roles in a search space.
Requires ROLES_READ permission.
@ -246,7 +252,7 @@ async def list_roles(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.ROLES_READ.value,
"You don't have permission to view roles",
@ -275,8 +281,9 @@ async def get_role(
search_space_id: int,
role_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Get a specific role by ID.
Requires ROLES_READ permission.
@ -284,7 +291,7 @@ async def get_role(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.ROLES_READ.value,
"You don't have permission to view roles",
@ -320,8 +327,9 @@ async def update_role(
role_id: int,
role_update: RoleUpdate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Update a role.
Requires ROLES_UPDATE permission.
@ -330,7 +338,7 @@ async def update_role(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.ROLES_UPDATE.value,
"You don't have permission to update roles",
@ -417,8 +425,9 @@ async def delete_role(
search_space_id: int,
role_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Delete a custom role.
Requires ROLES_DELETE permission.
@ -427,7 +436,7 @@ async def delete_role(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.ROLES_DELETE.value,
"You don't have permission to delete roles",
@ -474,8 +483,9 @@ async def delete_role(
async def list_members(
search_space_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
List all members of a search space.
Requires MEMBERS_VIEW permission.
@ -483,7 +493,7 @@ async def list_members(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.MEMBERS_VIEW.value,
"You don't have permission to view members",
@ -539,8 +549,9 @@ async def update_member_role(
membership_id: int,
membership_update: MembershipUpdate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Update a member's role.
Requires MEMBERS_MANAGE_ROLES permission.
@ -549,7 +560,7 @@ async def update_member_role(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.MEMBERS_MANAGE_ROLES.value,
"You don't have permission to manage member roles",
@ -629,8 +640,9 @@ async def update_member_role(
async def leave_search_space(
search_space_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Leave a search space (remove own membership).
Owners cannot leave their search space.
@ -675,8 +687,9 @@ async def remove_member(
search_space_id: int,
membership_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Remove a member from a search space.
Requires MEMBERS_REMOVE permission.
@ -685,7 +698,7 @@ async def remove_member(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.MEMBERS_REMOVE.value,
"You don't have permission to remove members",
@ -733,8 +746,9 @@ async def create_invite(
search_space_id: int,
invite_data: InviteCreate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Create a new invite link for a search space.
Requires MEMBERS_INVITE permission.
@ -742,7 +756,7 @@ async def create_invite(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.MEMBERS_INVITE.value,
"You don't have permission to create invites",
@ -798,8 +812,9 @@ async def create_invite(
async def list_invites(
search_space_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
List all invites for a search space.
Requires MEMBERS_INVITE permission.
@ -807,7 +822,7 @@ async def list_invites(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.MEMBERS_INVITE.value,
"You don't have permission to view invites",
@ -837,8 +852,9 @@ async def update_invite(
invite_id: int,
invite_update: InviteUpdate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Update an invite.
Requires MEMBERS_INVITE permission.
@ -846,7 +862,7 @@ async def update_invite(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.MEMBERS_INVITE.value,
"You don't have permission to update invites",
@ -903,8 +919,9 @@ async def revoke_invite(
search_space_id: int,
invite_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Revoke (delete) an invite.
Requires MEMBERS_INVITE permission.
@ -912,7 +929,7 @@ async def revoke_invite(
try:
await check_permission(
session,
user,
auth,
search_space_id,
Permission.MEMBERS_INVITE.value,
"You don't have permission to revoke invites",
@ -1022,8 +1039,9 @@ async def get_invite_info(
async def accept_invite(
request: InviteAcceptRequest,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Accept an invite and join a search space.
"""
@ -1120,13 +1138,14 @@ async def accept_invite(
async def get_my_access(
search_space_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Get the current user's access info for a search space.
"""
try:
membership = await check_search_space_access(session, user, search_space_id)
membership = await check_search_space_access(session, auth, search_space_id)
# Get search space name
result = await session.execute(

View file

@ -11,12 +11,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from app.auth.context import AuthContext
from app.db import (
Permission,
SearchSpace,
SearchSpaceMembership,
SearchSpaceRole,
User,
has_permission,
)
@ -80,9 +80,33 @@ async def get_user_permissions(
return []
async def _enforce_api_access_gate(
session: AsyncSession,
auth: AuthContext,
search_space_id: int,
search_space: SearchSpace | None = None,
) -> SearchSpace:
if search_space is None:
result = await session.execute(
select(SearchSpace).filter(SearchSpace.id == search_space_id)
)
search_space = result.scalars().first()
if not search_space:
raise HTTPException(status_code=404, detail="Search space not found")
if auth.is_gated and not search_space.api_access_enabled:
raise HTTPException(
status_code=403,
detail="API access is not enabled for this search space.",
)
return search_space
async def check_permission(
session: AsyncSession,
user: User,
auth: AuthContext,
search_space_id: int,
required_permission: str,
error_message: str = "You don't have permission to perform this action",
@ -104,7 +128,7 @@ async def check_permission(
Raises:
HTTPException: If user doesn't have access or permission
"""
membership = await get_user_membership(session, user.id, search_space_id)
membership = await get_user_membership(session, auth.user.id, search_space_id)
if not membership:
raise HTTPException(
@ -123,12 +147,14 @@ async def check_permission(
if not has_permission(permissions, required_permission):
raise HTTPException(status_code=403, detail=error_message)
await _enforce_api_access_gate(session, auth, search_space_id)
return membership
async def check_search_space_access(
session: AsyncSession,
user: User,
auth: AuthContext,
search_space_id: int,
) -> SearchSpaceMembership:
"""
@ -146,7 +172,7 @@ async def check_search_space_access(
Raises:
HTTPException: If user doesn't have access
"""
membership = await get_user_membership(session, user.id, search_space_id)
membership = await get_user_membership(session, auth.user.id, search_space_id)
if not membership:
raise HTTPException(
@ -154,6 +180,8 @@ async def check_search_space_access(
detail="You don't have access to this search space",
)
await _enforce_api_access_gate(session, auth, search_space_id)
return membership
@ -179,7 +207,7 @@ async def is_search_space_owner(
async def get_search_space_with_access_check(
session: AsyncSession,
user: User,
auth: AuthContext,
search_space_id: int,
required_permission: str | None = None,
) -> tuple[SearchSpace, SearchSpaceMembership]:
@ -210,10 +238,12 @@ async def get_search_space_with_access_check(
# Check access
if required_permission:
membership = await check_permission(
session, user, search_space_id, required_permission
session, auth, search_space_id, required_permission
)
else:
membership = await check_search_space_access(session, user, search_space_id)
membership = await check_search_space_access(session, auth, search_space_id)
await _enforce_api_access_gate(session, auth, search_space_id, search_space)
return search_space, membership