mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +02:00
refactor: route authorization through auth context
This commit is contained in:
parent
630880bf7a
commit
7e8d26fa81
4 changed files with 105 additions and 47 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue