mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-10 11:12:13 +02:00
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:
parent
041c31a613
commit
fb4038a969
59 changed files with 3531 additions and 517 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue