fix: fix org scoped access for resources (#517)

* fix: fix org scoped access for resources

* Fix auth and config validation regressions

* fix: track org config validation timestamp

* fix: backfill org model configuration v2 from legacy user rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: align config tests with org-level v2 resolution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: helm example values tweaks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abhishek 2026-07-09 23:04:33 +05:30 committed by GitHub
parent 041c31a613
commit fb4038a969
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 3531 additions and 517 deletions

View file

@ -205,11 +205,8 @@ class OrganizationConfigurationModel(Base):
key = Column(String, nullable=False)
value = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(UTC),
onupdate=lambda: datetime.now(UTC),
)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
last_validated_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
organization = relationship("OrganizationModel", back_populates="configurations")

View file

@ -118,7 +118,10 @@ class OrganizationClient(BaseDBClient):
select(
exists().where(
(organization_users_association.c.user_id == user_id)
& (organization_users_association.c.organization_id == organization_id)
& (
organization_users_association.c.organization_id
== organization_id
)
)
)
)

View file

@ -1,3 +1,4 @@
from datetime import UTC, datetime
from typing import Any, Dict, List, Optional
from sqlalchemy.future import select
@ -33,10 +34,15 @@ class OrganizationConfigurationClient(BaseDBClient):
return result.scalars().all()
async def upsert_configuration(
self, organization_id: int, key: str, value: Any
self,
organization_id: int,
key: str,
value: Any,
last_validated_at: datetime | None = None,
) -> OrganizationConfigurationModel:
"""Create or update a configuration for an organization."""
async with self.async_session() as session:
now = datetime.now(UTC)
# First try to get existing configuration
result = await session.execute(
select(OrganizationConfigurationModel).where(
@ -49,12 +55,16 @@ class OrganizationConfigurationClient(BaseDBClient):
if config:
# Update existing configuration
config.value = value
config.updated_at = now
config.last_validated_at = last_validated_at
else:
# Create new configuration
config = OrganizationConfigurationModel(
organization_id=organization_id,
key=key,
value=value,
updated_at=now,
last_validated_at=last_validated_at,
)
session.add(config)
@ -66,6 +76,30 @@ class OrganizationConfigurationClient(BaseDBClient):
await session.refresh(config)
return config
async def mark_configuration_validated(
self, organization_id: int, key: str
) -> Optional[OrganizationConfigurationModel]:
"""Update the validation timestamp for an existing organization configuration."""
async with self.async_session() as session:
result = await session.execute(
select(OrganizationConfigurationModel).where(
OrganizationConfigurationModel.organization_id == organization_id,
OrganizationConfigurationModel.key == key,
)
)
config = result.scalars().first()
if not config:
return None
config.last_validated_at = datetime.now(UTC)
try:
await session.commit()
except Exception as e:
await session.rollback()
raise e
await session.refresh(config)
return config
async def delete_configuration(self, organization_id: int, key: str) -> bool:
"""Delete a configuration for an organization."""
async with self.async_session() as session:

View file

@ -13,13 +13,10 @@ from api.db.models import (
OrganizationConfigurationModel,
OrganizationModel,
OrganizationUsageCycleModel,
UserConfigurationModel,
UserModel,
WorkflowModel,
WorkflowRunModel,
)
from api.enums import OrganizationConfigurationKey, UserConfigurationKey
from api.schemas.ai_model_configuration import EffectiveAIModelConfiguration
from api.enums import OrganizationConfigurationKey
from api.utils.recording_artifacts import get_recording_storage_key
@ -139,9 +136,8 @@ class OrganizationUsageClient(BaseDBClient):
query = (
select(WorkflowRunModel)
.join(WorkflowModel, WorkflowRunModel.workflow_id == WorkflowModel.id)
.join(UserModel, WorkflowModel.user_id == UserModel.id)
.where(
UserModel.selected_organization_id == organization_id,
WorkflowModel.organization_id == organization_id,
WorkflowRunModel.usage_info.isnot(None),
)
.order_by(WorkflowRunModel.created_at.desc())
@ -277,9 +273,8 @@ class OrganizationUsageClient(BaseDBClient):
WorkflowRunModel.public_access_token,
)
.join(WorkflowModel, WorkflowRunModel.workflow_id == WorkflowModel.id)
.join(UserModel, WorkflowModel.user_id == UserModel.id)
.where(
UserModel.selected_organization_id == organization_id,
WorkflowModel.organization_id == organization_id,
WorkflowRunModel.usage_info.isnot(None),
)
.order_by(WorkflowRunModel.created_at.desc())
@ -316,7 +311,6 @@ class OrganizationUsageClient(BaseDBClient):
start_date: datetime,
end_date: datetime,
price_per_second_usd: float,
user_id: Optional[int] = None,
) -> dict:
"""Get daily usage breakdown for an organization with pricing."""
@ -344,22 +338,6 @@ class OrganizationUsageClient(BaseDBClient):
if pref_obj and pref_obj.value:
user_timezone = pref_obj.value.get("timezone") or user_timezone
if user_id:
config_result = await session.execute(
select(UserConfigurationModel).where(
UserConfigurationModel.user_id == user_id,
UserConfigurationModel.key
== UserConfigurationKey.MODEL_CONFIGURATION.value,
)
)
config_obj = config_result.scalar_one_or_none()
if config_obj and config_obj.configuration:
effective_config = EffectiveAIModelConfiguration.model_validate(
config_obj.configuration
)
if effective_config.timezone and user_timezone == "UTC":
user_timezone = effective_config.timezone
# Validate timezone string
try:
# Test if timezone is valid
@ -382,9 +360,8 @@ class OrganizationUsageClient(BaseDBClient):
func.count(WorkflowRunModel.id).label("call_count"),
)
.join(WorkflowModel, WorkflowModel.id == WorkflowRunModel.workflow_id)
.join(UserModel, UserModel.id == WorkflowModel.user_id)
.where(
UserModel.selected_organization_id == organization_id,
WorkflowModel.organization_id == organization_id,
WorkflowRunModel.created_at >= start_date,
WorkflowRunModel.created_at <= end_date,
WorkflowRunModel.is_completed == True,

View file

@ -606,11 +606,9 @@ class WorkflowClient(BaseDBClient):
"""Get workflows by IDs for a specific organization"""
async with self.async_session() as session:
result = await session.execute(
select(WorkflowModel)
.join(WorkflowModel.user)
.where(
select(WorkflowModel).where(
WorkflowModel.id.in_(workflow_ids),
WorkflowModel.user.has(selected_organization_id=organization_id),
WorkflowModel.organization_id == organization_id,
)
)
return result.scalars().all()

View file

@ -40,14 +40,14 @@ class WorkflowRunClient(BaseDBClient):
workflow_query = (
select(WorkflowModel)
.options(joinedload(WorkflowModel.user))
.where(
WorkflowModel.id == workflow_id, WorkflowModel.user_id == user_id
)
.where(WorkflowModel.id == workflow_id)
)
if organization_id is not None:
workflow_query = workflow_query.where(
WorkflowModel.organization_id == organization_id
)
elif user_id is not None:
workflow_query = workflow_query.where(WorkflowModel.user_id == user_id)
workflow = await session.execute(workflow_query)
workflow = workflow.scalars().first()