Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 43 additions & 63 deletions apps/backend/src/rhesis/backend/app/crud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,11 @@ def get_test_set(
) -> Optional[models.TestSet]:
"""
Get a test set by its UUID, applying proper visibility filtering and organization scoping.

Raises ``ItemDeletedException`` for a soft-deleted test set.
"""
return (
QueryBuilder(db, models.TestSet)
.with_organization_filter(organization_id) # Add organization filtering
.with_visibility_filter(user_id)
.with_custom_filter(lambda q: q.filter(models.TestSet.id == test_set_id))
.first()
return get_item_detail(
db, models.TestSet, test_set_id, organization_id=organization_id, user_id=user_id
)


Expand Down Expand Up @@ -446,9 +444,14 @@ def get_test_set_by_nano_id_or_slug(
) -> Optional[models.TestSet]:
"""
Get a test set by its nano_id or slug, applying proper visibility filtering.

Raises ``ItemDeletedException`` for a soft-deleted test set.
"""
return (
from rhesis.backend.app.utils.crud_utils import _check_and_raise_if_deleted

item = (
QueryBuilder(db, models.TestSet)
.with_deleted()
.with_related(*_TEST_SET_RELATED_FIELDS)
.with_organization_filter(organization_id)
.with_visibility_filter(user_id)
Expand All @@ -459,6 +462,7 @@ def get_test_set_by_nano_id_or_slug(
)
.first()
)
return _check_and_raise_if_deleted(item, models.TestSet, identifier, False)


def resolve_test_set(
Expand All @@ -467,6 +471,11 @@ def resolve_test_set(
"""
Resolve a test set from any valid identifier (UUID, nano_id, or slug).
Returns None if not found or if there's an error parsing the identifier.

Raises:
ItemDeletedException: If the identifier resolves to a soft-deleted
test set. Not caught here so callers get the same 410 behavior as
a direct ID lookup.
"""
try:
# First try UUID
Expand Down Expand Up @@ -605,16 +614,13 @@ def get_test_set_tests(
def get_test_configuration(
db: Session, test_configuration_id: uuid.UUID, organization_id: str = None, user_id: str = None
) -> Optional[models.TestConfiguration]:
from rhesis.backend.app.utils.crud_utils import _check_and_raise_if_deleted

item = (
QueryBuilder(db, models.TestConfiguration)
.with_deleted()
.with_organization_filter(organization_id)
.with_visibility_filter(user_id)
.filter_by_id(test_configuration_id)
return get_item_detail(
db,
models.TestConfiguration,
test_configuration_id,
organization_id=organization_id,
user_id=user_id,
)
return _check_and_raise_if_deleted(item, models.TestConfiguration, test_configuration_id, False)


def get_test_configurations(
Expand Down Expand Up @@ -1091,18 +1097,14 @@ def get_test_result(
db: Session, test_result_id: uuid.UUID, organization_id: str = None, user_id: str = None
) -> Optional[models.TestResult]:
"""Get test_result with relationships (tags, test, test_run) eagerly loaded."""
from rhesis.backend.app.utils.crud_utils import _check_and_raise_if_deleted

item = (
QueryBuilder(db, models.TestResult)
.with_deleted()
.with_related(*_TEST_RESULT_RELATED_FIELDS)
.with_default_derived_field_loads()
.with_organization_filter(organization_id)
.with_visibility_filter(user_id)
.filter_by_id(test_result_id)
return get_item_detail(
db,
models.TestResult,
test_result_id,
organization_id=organization_id,
user_id=user_id,
related_fields=_TEST_RESULT_RELATED_FIELDS,
)
return _check_and_raise_if_deleted(item, models.TestResult, test_result_id, False)


def get_test_results(
Expand Down Expand Up @@ -1241,17 +1243,14 @@ def get_tool(
db: Session, tool_id: uuid.UUID, organization_id: str, user_id: str = None
) -> Optional[models.Tool]:
"""Get a specific tool by ID with relationships loaded"""
from rhesis.backend.app.utils.crud_utils import _check_and_raise_if_deleted

item = (
QueryBuilder(db, models.Tool)
.with_deleted()
.with_related(*_TOOL_RELATED_FIELDS)
.with_organization_filter(organization_id)
.with_visibility_filter(user_id)
.filter_by_id(tool_id)
return get_item_detail(
db,
models.Tool,
tool_id,
organization_id=organization_id,
user_id=user_id,
related_fields=_TOOL_RELATED_FIELDS,
)
return _check_and_raise_if_deleted(item, models.Tool, tool_id, False)


def get_tools(
Expand All @@ -1277,22 +1276,6 @@ def get_tools(
)


def get_tool_by_provider(
db: Session, organization_id: str, provider_value: str
) -> Optional[models.Tool]:
"""Get organization's tool by provider type_value (e.g., 'notion', 'github')."""
return (
db.query(models.Tool)
.join(models.TypeLookup, models.Tool.tool_provider_type_id == models.TypeLookup.id)
.filter(
models.Tool.organization_id == uuid.UUID(organization_id),
models.TypeLookup.type_value == provider_value,
models.Tool.deleted_at.is_(None), # Exclude soft-deleted tools
)
.first()
)


def create_tool(
db: Session, tool: schemas.ToolCreate, organization_id: str, user_id: str = None
) -> models.Tool:
Expand Down Expand Up @@ -1337,17 +1320,14 @@ def get_architect_session_detail(
user_id: str = None,
) -> Optional[models.ArchitectSession]:
"""Get an architect session with its messages eagerly loaded."""
from rhesis.backend.app.utils.crud_utils import _check_and_raise_if_deleted

item = (
QueryBuilder(db, models.ArchitectSession)
.with_deleted()
.with_related(include(models.ArchitectSession.messages))
.with_organization_filter(organization_id)
.with_visibility_filter(user_id)
.filter_by_id(session_id)
return get_item_detail(
db,
models.ArchitectSession,
session_id,
organization_id=organization_id,
user_id=user_id,
related_fields=(include(models.ArchitectSession.messages),),
)
return _check_and_raise_if_deleted(item, models.ArchitectSession, session_id, False)


def get_architect_sessions(
Expand Down
82 changes: 19 additions & 63 deletions apps/backend/src/rhesis/backend/app/crud/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from rhesis.backend.app.utils.crud_utils import (
create_item,
delete_item,
get_item,
get_item_detail,
get_items_detail,
update_item,
)
Expand Down Expand Up @@ -50,19 +52,16 @@ def get_metric(
) -> Optional[models.Metric]:
"""Get a specific metric by ID with its related objects, including many-to-many relationships.

Deliberately not routed through ``get_item_detail``: a soft-deleted metric must come
back as ``None`` here (tested behavior), not raise ``ItemDeletedException`` -- unlike
every other entity's single-item fetch, callers of this one don't need to tell "not
found" apart from "deleted".
Raises ``ItemDeletedException`` for a soft-deleted metric, same as every other
entity's single-item fetch.
"""
return (
QueryBuilder(db, models.Metric)
.with_related(*_METRIC_RELATED_FIELDS)
.with_default_derived_field_loads()
.with_organization_filter(organization_id)
.with_visibility_filter(user_id)
.with_custom_filter(lambda q: q.filter(models.Metric.id == metric_id))
.first()
return get_item_detail(
db,
models.Metric,
metric_id,
organization_id=organization_id,
user_id=user_id,
related_fields=_METRIC_RELATED_FIELDS,
)


Expand Down Expand Up @@ -269,23 +268,12 @@ def add_requirement_to_metric(
bool: True if the requirement was added, False if it was already associated
"""
# Verify the metric exists AND belongs to the organization (SECURITY CRITICAL)
metric = (
db.query(models.Metric)
.filter(models.Metric.id == metric_id, models.Metric.organization_id == organization_id)
.first()
)
metric = get_item(db, models.Metric, metric_id, organization_id)
if not metric:
raise ValueError(f"Metric with id {metric_id} not found or not accessible")

# Verify the requirement exists AND belongs to the organization (SECURITY CRITICAL)
requirement = (
db.query(models.Requirement)
.filter(
models.Requirement.id == requirement_id,
models.Requirement.organization_id == organization_id,
)
.first()
)
requirement = get_item(db, models.Requirement, requirement_id, organization_id)
if not requirement:
raise ValueError(f"Requirement with id {requirement_id} not found or not accessible")

Expand Down Expand Up @@ -332,23 +320,12 @@ def remove_requirement_from_metric(
bool: True if the requirement was removed, False if it wasn't associated
"""
# Verify the metric exists AND belongs to the organization (SECURITY CRITICAL)
metric = (
db.query(models.Metric)
.filter(models.Metric.id == metric_id, models.Metric.organization_id == organization_id)
.first()
)
metric = get_item(db, models.Metric, metric_id, organization_id)
if not metric:
raise ValueError(f"Metric with id {metric_id} not found or not accessible")

# Verify the requirement exists AND belongs to the organization (SECURITY CRITICAL)
requirement = (
db.query(models.Requirement)
.filter(
models.Requirement.id == requirement_id,
models.Requirement.organization_id == organization_id,
)
.first()
)
requirement = get_item(db, models.Requirement, requirement_id, organization_id)
if not requirement:
raise ValueError(f"Requirement with id {requirement_id} not found or not accessible")

Expand Down Expand Up @@ -392,13 +369,7 @@ def get_metric_requirements(
List of requirements associated with the metric
"""
# Verify the metric exists AND belongs to the organization (SECURITY CRITICAL)
metric = (
db.query(models.Metric)
.filter(
models.Metric.id == metric_id, models.Metric.organization_id == UUID(organization_id)
)
.first()
)
metric = get_item(db, models.Metric, metric_id, organization_id)
if not metric:
raise ValueError(f"Metric with id {metric_id} not found or not accessible")

Expand Down Expand Up @@ -444,14 +415,7 @@ def get_requirement_metrics(
List of metrics associated with the requirement
"""
# Verify the requirement exists AND belongs to the organization (SECURITY CRITICAL)
requirement = (
db.query(models.Requirement)
.filter(
models.Requirement.id == requirement_id,
models.Requirement.organization_id == UUID(organization_id),
)
.first()
)
requirement = get_item(db, models.Requirement, requirement_id, organization_id)
if not requirement:
raise ValueError(f"Requirement with id {requirement_id} not found or not accessible")

Expand Down Expand Up @@ -518,11 +482,7 @@ def add_metric_to_test_set(
raise ValueError(f"Test set with id {test_set_id} not found or not accessible")

# Verify the metric exists AND belongs to the organization (SECURITY CRITICAL)
metric = (
db.query(models.Metric)
.filter(models.Metric.id == metric_id, models.Metric.organization_id == organization_id)
.first()
)
metric = get_item(db, models.Metric, metric_id, organization_id)
if not metric:
raise ValueError(f"Metric with id {metric_id} not found or not accessible")

Expand Down Expand Up @@ -578,11 +538,7 @@ def remove_metric_from_test_set(
raise ValueError(f"Test set with id {test_set_id} not found or not accessible")

# Verify the metric exists AND belongs to the organization (SECURITY CRITICAL)
metric = (
db.query(models.Metric)
.filter(models.Metric.id == metric_id, models.Metric.organization_id == organization_id)
.first()
)
metric = get_item(db, models.Metric, metric_id, organization_id)
if not metric:
raise ValueError(f"Metric with id {metric_id} not found or not accessible")

Expand Down
30 changes: 13 additions & 17 deletions apps/backend/src/rhesis/backend/app/crud/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
stay editable so users can still organize it. Both raise ``ValueError`` with "protected" in
the message; the router turns that into a 403.

``get_model`` re-applies the organization filter by hand on top of the ambient scope filter,
because a leaked model row hands out a stored provider API key. The other functions go
through ``crud_utils``, which applies it for them.
``get_model`` intentionally doesn't follow this repo's usual soft-delete contract -- see
its own docstring.

The block that moved here also held ``test_model_connection``, a stub that never did
anything but return ``True`` -- the real check was never written. It had no callers (the
Expand All @@ -33,7 +32,7 @@
get_items_detail,
update_item,
)
from rhesis.backend.app.utils.query_utils import include
from rhesis.backend.app.utils.query_utils import QueryBuilder, include

# Relationships serialized by schemas.ModelDetail -- provider_type, status.
# owner/assignee: unused, excluded. Public (no leading underscore) since
Expand All @@ -47,20 +46,17 @@
def get_model(
db: Session, model_id: uuid.UUID, organization_id: str = None, user_id: str = None
) -> Optional[models.Model]:
"""Get a specific model by ID with its related objects and organization filtering"""
query = (
db.query(models.Model)
.options(include(models.Model.provider_type))
.filter(models.Model.id == model_id)
)

# Apply organization filtering (SECURITY CRITICAL)
if organization_id:
from uuid import UUID as UUIDType

query = query.filter(models.Model.organization_id == UUIDType(organization_id))
"""Get a specific model by ID with its related objects and organization filtering.

return query.first()
Doesn't call ``get_item_detail`` directly: callers here need a soft-deleted model
to return ``None``, not raise ``ItemDeletedException``, so they can fall back.
"""
return (
QueryBuilder(db, models.Model)
.with_related(include(models.Model.provider_type))
.with_organization_filter(organization_id)
.filter_by_id(model_id)
)


def get_models(
Expand Down
Loading
Loading