Skip to content

Search for a group named "null"#2230

Open
ctron wants to merge 33 commits intoguacsec:mainfrom
ctron:feature/search_null_1
Open

Search for a group named "null"#2230
ctron wants to merge 33 commits intoguacsec:mainfrom
ctron:feature/search_null_1

Conversation

@ctron
Copy link
Contributor

@ctron ctron commented Feb 4, 2026

@jcrossley3 I need your help with this, how can a user search for a group named null?

Summary by Sourcery

Introduce SBOM grouping support with a new hierarchical group model, API endpoints, and storage, including validation, permissions, and documentation.

New Features:

  • Add SBOM group entities, assignments, and database migrations to persist hierarchical SBOM group metadata.
  • Expose REST API endpoints to create, read, update, delete, and list SBOM groups with filtering, pagination, and totals.
  • Support querying SBOM groups by parent and by literal "null" group names via the existing q-style search semantics.

Bug Fixes:

  • Correct database error handling to detect unique-violation and read-only errors for both query and exec operations.

Enhancements:

  • Extend error handling with structured BadRequest and Conflict variants and a dedicated RevisionNotFound error mapped to HTTP responses.
  • Add max group name length configuration and validation, and general label conversion and pagination helper improvements.
  • Define new RBAC permissions and default OAuth scope mappings for SBOM group operations.
  • Refactor shared If-Match / ETag revision extraction into a common helper and reuse in endpoints.

Documentation:

  • Document the SBOM grouping design and API surface in a new architecture decision record (ADR).

Tests:

  • Add an extensive SBOM group endpoint test suite covering creation, validation, uniqueness, hierarchy, cycle detection, deletion semantics, and q-style listing, including handling of literal "null" names.

ctron added 30 commits February 4, 2026 10:55
Assisted-by: Claude Code
Also, restructure some things.
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 4, 2026

Reviewer's Guide

Introduce SBOM group management: a new hierarchical grouping model for SBOMs with full CRUD REST API, validations, permissions, database schema/migrations, and extensive tests, including support for querying groups by name (including the literal string "null") via the existing q-filter mechanism.

Sequence diagram for SBOM group update with revision and cycle validation

sequenceDiagram
    actor Client
    participant Api as SbomGroupEndpoints
    participant Service as SbomGroupService
    participant DB as Database

    Client->>Api: PUT /v2/group/sbom/{id}\nIf-Match: ETag\nGroupRequest
    Api->>Api: extract_revision(IfMatch)
    Api->>DB: begin()
    DB-->>Api: Transaction
    Api->>Service: update(id, revision, group, tx)
    Service->>Service: validate_group_name_or_fail(group.name)
    Service->>Service: parse_parent_group(group.parent)
    alt parent is Some
        Service->>Service: validate_no_cycle(id, parent_id, tx)
        Service-->>Service: ok or Conflict
    end
    Service->>DB: query_by_revision(id, revision).update_many(...)
    DB-->>Service: rows_affected or unique_violation
    alt unique_violation
        Service-->>Api: Error::Conflict
        Api->>DB: rollback()
        DB-->>Api: ok
        Api-->>Client: 409 Conflict
    else rows_affected == 0
        Service->>DB: query_by_revision(id, None).count()
        DB-->>Service: count
        alt count == 0
            Service-->>Api: Error::NotFound
            Api->>DB: rollback()
            DB-->>Api: ok
            Api-->>Client: 404 Not Found
        else count > 0
            Service-->>Api: Error::RevisionNotFound
            Api->>DB: rollback()
            DB-->>Api: ok
            Api-->>Client: 412 Precondition Failed
        end
    else success
        Service-->>Api: Ok
        Api->>DB: commit()
        DB-->>Api: ok
        Api-->>Client: 204 No Content
    end
Loading

ER diagram for SBOM groups and assignments

erDiagram
    Sbom {
        uuid sbom_id PK
    }
    SbomGroup {
        uuid id PK
        option_uuid parent FK
        string name
        uuid revision
        jsonb labels
    }
    SbomGroupAssignment {
        uuid sbom_id FK
        uuid group_id FK
    }

    SbomGroup ||--o| SbomGroup : parent
    Sbom ||--o{ SbomGroupAssignment : has_assignments
    SbomGroup ||--o{ SbomGroupAssignment : has_assignments

    Sbom ||--o{ SbomGroup : belongs_to_via_assignment
Loading

Class diagram for SBOM group service and models

classDiagram
    class SbomGroupService {
        +usize max_group_name_length
        +new(max_group_name_length usize) SbomGroupService
        +list(options ListOptions, paginated Paginated, query Query, db ConnectionTrait) Result~PaginatedResults_GroupDetails_, Error~
        +create(group GroupRequest, db ConnectionTrait) Result~Revisioned_string_, Error~
        +delete(id str, expected_revision Option_str_, db ConnectionTrait) Result~bool, Error~
        +update(id str, revision Option_str_, group GroupRequest, db ConnectionTrait) Result~void, Error~
        +read(id str, db ConnectionTrait) Result~Option_Revisioned_Group__, Error~
        -resolve_totals(ids Uuid[], db ConnectionTrait, query Selector_SelectGetableTuple_Uuid_i64__ ) Result~u64[], Error~
        -resolve_total_groups(ids Uuid[], db ConnectionTrait) Result~u64[], Error~
        -resolve_total_sboms(ids Uuid[], db ConnectionTrait) Result~u64[], Error~
        -resolve_parents(ids Uuid[], db ConnectionTrait) Result~Vec_Vec_string__, Error~
        -update_columns(id str, revision Option_str_, updates Vec_Tuple2_sbom_group_Column_SimpleExpr__, db ConnectionTrait) Result~void, Error~
        -validate_group_name(name str) Vec_Cow_static_str__
        -validate_group_name_or_fail(name str) Result~void, Error~
        -validate_no_cycle(group_id str, parent_id str, db ConnectionTrait) Result~void, Error~
    }

    class ListOptions {
        +bool totals
        +bool parents
    }

    class Group {
        +string id
        +Option_string_ parent
        +string name
        +Labels labels
    }

    class GroupDetails {
        +Group group
        +Option_u64_ number_of_groups
        +Option_u64_ number_of_sboms
        +Option_Vec_string__ parents
    }

    class GroupRequest {
        +string name
        +Option_string_ parent
        +Labels labels
    }

    class Error {
        <<enum>>
        +BadRequest(Cow_static_str_, Option_Cow_static_str_)
        +Conflict(Cow_static_str_)
        +RevisionNotFound
        +bad_request(message Into_Cow_static_str_, details Option_Into_Cow_static_str_) Error
    }

    class sbom_group_Model {
        +Uuid id
        +Option_Uuid_ parent
        +string name
        +Uuid revision
        +Labels labels
    }

    class sbom_group_assignment_Model {
        +Uuid sbom_id
        +Uuid group_id
    }

    class Paginated {
        +i64 offset
        +i64 limit
    }

    class PaginatedResults_GroupDetails_ {
        +Vec_GroupDetails_ items
        +u64 total
    }

    class Revisioned_T_ {
        +T value
        +string revision
    }

    class Query {
    }

    class ConnectionTrait {
        <<trait>>
    }

    GroupDetails --> Group : deref
    GroupRequest ..> Group : mutable_properties_of

    SbomGroupService --> ListOptions
    SbomGroupService --> Group
    SbomGroupService --> GroupDetails
    SbomGroupService --> GroupRequest
    SbomGroupService --> Error
    SbomGroupService --> Paginated
    SbomGroupService --> PaginatedResults_GroupDetails_
    SbomGroupService --> Revisioned_T_
    SbomGroupService --> Query
    SbomGroupService --> ConnectionTrait

    Group ..> sbom_group_Model : From

    sbom_group_Model --> Labels
    Group --> Labels
    GroupRequest --> Labels
Loading

File-Level Changes

Change Details Files
Add SBOM group API endpoints with list, create, read, update, and delete operations, wired into configuration and OpenAPI, including q-parameter semantics for searching (e.g., matching literal "null" names).
  • Define /api/v2/group/sbom and /api/v2/group/sbom/{id} endpoints in openapi.yaml with detailed q and sort parameter docs explaining how to match literal "null" using the LIKE operator
  • Implement sbom_group endpoint handlers (list, create, read, update, delete) backed by SbomGroupService, including ETag-based revision handling via If-Match and integration into module configuration
  • Expose max_group_name_length configuration through CLI/env, thread it into fundamental::Config, and pass it to the sbom_group service.
openapi.yaml
modules/fundamental/src/sbom_group/endpoints/mod.rs
modules/fundamental/src/endpoints.rs
server/src/profile/api.rs
Introduce SBOM group domain model, service layer, and validation logic, including hierarchical parent relationships, cycle detection, counts, and parent chain resolution.
  • Define Group, GroupDetails, and GroupRequest DTOs for API requests/responses, mapping to sbom_group entity models
  • Implement SbomGroupService with list/create/read/update/delete operations, group name validation rules, parent parsing, conflict handling, and revision-based updates/deletes
  • Add recursive SQL-based logic to compute parent chains and counts of child groups and assigned SBOMs, and helper to detect parent cycles when updating parents.
  • Add focused unit tests for group name validation.
modules/fundamental/src/sbom_group/model.rs
modules/fundamental/src/sbom_group/service.rs
Extend persistence layer with SBOM group and assignment tables, entities, relationships, and migration.
  • Add migration m0002020_add_sbom_group to create sbom_group and sbom_group_assignment tables, including unique (parent,name) constraint, indexes, labels GIN index, and FK relationships with sbom
  • Register the new migration in the Migrator, and add sbom_group/sbom_group_assignment entity modules wired into the entity lib
  • Define sbom_group and sbom_group_assignment SeaORM entities, including relations between SBOMs and groups in both directions, and relate sbom entity to sbom_group via the assignment table.
  • Extend DatabaseErrors.is_duplicate/is_read_only to also treat Exec errors from sqlx::Error::Database as DB constraint violations.
migration/src/m0002020_add_sbom_group.rs
migration/src/lib.rs
entity/src/sbom_group.rs
entity/src/sbom_group_assignment.rs
entity/src/sbom.rs
common/src/db/mod.rs
entity/src/lib.rs
Enhance error handling to support richer bad request/conflict responses and revision errors.
  • Change Error::BadRequest to carry a message and optional details (using Cow) and add Error::Conflict and Error::RevisionNotFound variants
  • Provide Error::bad_request helper for constructing BadRequest with optional details
  • Update ResponseError impl to serialize BadRequest with details, map Conflict to HTTP 409, and RevisionNotFound to HTTP 412 Precondition Failed with structured error body.
modules/fundamental/src/error.rs
Introduce common If-Match revision extraction helper and reuse it across endpoints.
  • Add common::endpoints::extract_revision to normalize extracting revision from IfMatch headers
  • Refactor importer delete endpoint and new sbom_group delete/update endpoints to use extract_revision instead of hand-rolled logic.
common/src/endpoints.rs
common/src/lib.rs
modules/importer/src/endpoints.rs
modules/fundamental/src/sbom_group/endpoints/mod.rs
Add SBOM group-specific permissions and default scope mappings for auth.
  • Define CreateSbomGroup, ReadSbomGroup, UpdateSbomGroup, DeleteSbomGroup permissions in permission.rs
  • Map the new sbomGroup permissions into the default auth scope mappings for create/read/update/delete roles.
common/auth/src/permission.rs
common/auth/src/authenticator/default.rs
Add comprehensive integration tests for SBOM group endpoints and supporting test utilities.
  • Create an extensive sbom_group endpoints test module exercising create/read/update/delete, If-Match behaviors, name validation, uniqueness constraints, parent cycles, and q-based list filtering (including groups named "null")
  • Introduce helper builders (Create/Update), GroupResponse struct, hierarchy fixtures, and utilities to create tree structures and expected results for PaginatedResults
  • Move the q-escaping helper into test-context crate so it can be reused by tests.
  • Update fundamental test config construction to include max_group_name_length and add rstest dev-dependency for fundamental tests.
modules/fundamental/src/sbom_group/endpoints/test.rs
test-context/src/q.rs
test-context/src/lib.rs
modules/fundamental/tests/limit.rs
modules/fundamental/Cargo.toml
Document the SBOM group design and clean up minor related code.
  • Add ADR 00013 describing the SBOM groups data model, API surface, semantics (including querying by name, path, and group assignments), and constraints (name validation, cycles, uniqueness)
  • Tweak TODO.md to add placeholders for group tests and mark other items as done.
  • Remove an unused escape_q helper from analysis test module, now provided by test-context, and minor plumbing changes (e.g., exporting common::endpoints).
docs/adrs/00013-sbom-groups.md
TODO.md
modules/analysis/src/test/mod.rs
common/src/lib.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In the OpenAPI spec for /api/v2/group/sbom/{id}, the id parameter is declared both as a query and a path parameter; this duplication is confusing and the query parameter should be removed so the API definition matches the actual path-based routing.
  • In SbomGroupService::validate_no_cycle, the recursive CTE uses a hard-coded DatabaseBackend::Postgres and raw SQL; consider using db.get_database_backend() and a FromQueryResult-based query to avoid backend assumptions and keep the DB access consistent with the rest of the codebase.
  • The group name validation error for invalid characters currently ends with a trailing comma ("name contains invalid characters, "); tightening this message (e.g., removing the comma or clarifying the allowed character set) would make client-side error handling and display clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the OpenAPI spec for `/api/v2/group/sbom/{id}`, the `id` parameter is declared both as a query and a path parameter; this duplication is confusing and the query parameter should be removed so the API definition matches the actual path-based routing.
- In `SbomGroupService::validate_no_cycle`, the recursive CTE uses a hard-coded `DatabaseBackend::Postgres` and raw SQL; consider using `db.get_database_backend()` and a `FromQueryResult`-based query to avoid backend assumptions and keep the DB access consistent with the rest of the codebase.
- The group name validation error for invalid characters currently ends with a trailing comma (`"name contains invalid characters, "`); tightening this message (e.g., removing the comma or clarifying the allowed character set) would make client-side error handling and display clearer.

## Individual Comments

### Comment 1
<location> `modules/fundamental/src/sbom_group/service.rs:362-366` </location>
<code_context>
+            has_cycle: bool,
+        }
+
+        let result = CycleCheck::find_by_statement(sea_orm::Statement::from_sql_and_values(
+            sea_orm::DatabaseBackend::Postgres,
+            sql,
+            vec![parent_id.into(), group_id.into()],
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Cycle check uses a hard-coded Postgres backend instead of the actual connection backend.

`validate_no_cycle` builds the statement with `DatabaseBackend::Postgres` instead of using the backend from `db` (e.g. `db.get_database_backend()`). This will fail on non-Postgres connections and is inconsistent with other queries here. Please derive the backend from `db` so this remains backend-agnostic and consistent.

```suggestion
        let result = CycleCheck::find_by_statement(sea_orm::Statement::from_sql_and_values(
            db.get_database_backend(),
            sql,
            vec![parent_id.into(), group_id.into()],
        ))
```
</issue_to_address>

### Comment 2
<location> `modules/fundamental/src/sbom_group/endpoints/test.rs:862-865` </location>
<code_context>
+}
+
+/// Test using an invalid parent ID
+#[ignore = "Caused by the q implementation"]
+#[test_context(TrustifyContext)]
+#[test_log::test(actix_web::test)]
+pub async fn list_groups_with_invalid_parent(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
+    let app = caller(ctx).await?;
+
</code_context>

<issue_to_address>
**suggestion (testing):** Clarify or adjust the ignored `list_groups_with_invalid_parent` test

This test is ignored due to a `q` limitation, which makes it easy to forget to re-enable. Either update the assertions to match the current, documented behavior so it can run now (and adjust later when `q` is fixed), or add a short comment linking to an issue/ADR that describes the intended future behavior and when this test should be restored. This will help avoid accumulating permanently ignored tests.

Suggested implementation:

```rust
//// Test using an invalid parent ID
///
/// This test exercises the behavior when an invalid parent ID is provided. It is
/// currently ignored due to a limitation in the `q` implementation when handling
/// invalid parent IDs. Once that limitation is fixed, this test should be re-enabled.
/// See tracking issue / ADR: <INSERT_ISSUE_URL_OR_ADR_REFERENCE_HERE>.
#[ignore = "Blocked by `q`'s handling of invalid parent IDs; see tracking issue in comment above"]

```

1. Replace `<INSERT_ISSUE_URL_OR_ADR_REFERENCE_HERE>` with the actual issue or ADR link that tracks the `q` limitation and future behavior.
2. When the `q` limitation is resolved, remove the `#[ignore(...)]` attribute and update the comment if necessary to reflect the final, intended behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +362 to +366
let result = CycleCheck::find_by_statement(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![parent_id.into(), group_id.into()],
))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Cycle check uses a hard-coded Postgres backend instead of the actual connection backend.

validate_no_cycle builds the statement with DatabaseBackend::Postgres instead of using the backend from db (e.g. db.get_database_backend()). This will fail on non-Postgres connections and is inconsistent with other queries here. Please derive the backend from db so this remains backend-agnostic and consistent.

Suggested change
let result = CycleCheck::find_by_statement(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![parent_id.into(), group_id.into()],
))
let result = CycleCheck::find_by_statement(sea_orm::Statement::from_sql_and_values(
db.get_database_backend(),
sql,
vec![parent_id.into(), group_id.into()],
))

Comment on lines +862 to +865
#[ignore = "Caused by the q implementation"]
#[test_context(TrustifyContext)]
#[test_log::test(actix_web::test)]
pub async fn list_groups_with_invalid_parent(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Clarify or adjust the ignored list_groups_with_invalid_parent test

This test is ignored due to a q limitation, which makes it easy to forget to re-enable. Either update the assertions to match the current, documented behavior so it can run now (and adjust later when q is fixed), or add a short comment linking to an issue/ADR that describes the intended future behavior and when this test should be restored. This will help avoid accumulating permanently ignored tests.

Suggested implementation:

//// Test using an invalid parent ID
///
/// This test exercises the behavior when an invalid parent ID is provided. It is
/// currently ignored due to a limitation in the `q` implementation when handling
/// invalid parent IDs. Once that limitation is fixed, this test should be re-enabled.
/// See tracking issue / ADR: <INSERT_ISSUE_URL_OR_ADR_REFERENCE_HERE>.
#[ignore = "Blocked by `q`'s handling of invalid parent IDs; see tracking issue in comment above"]
  1. Replace <INSERT_ISSUE_URL_OR_ADR_REFERENCE_HERE> with the actual issue or ADR link that tracks the q limitation and future behavior.
  2. When the q limitation is resolved, remove the #[ignore(...)] attribute and update the comment if necessary to reflect the final, intended behavior.

@jcrossley3
Copy link
Contributor

jcrossley3 commented Feb 4, 2026

@jcrossley3 I need your help with this, how can a user search for a group named null?

At present, the literal string "null" is used for querying NULL's in the db, e.g. name=null. To search for the literal string, the best we can do is a LIKE query, e.g. name~null, but that's obviously going to pick up other groups with "null" in their name.

If it's a high priority requirement, we'd need to redesign the q= syntax to accommodate it, or it may be that the 'q' parameter isn't a good fit for the group endpoints.

jcrossley3 added a commit to jcrossley3/trustify that referenced this pull request Feb 4, 2026
Relates guacsec#2230

BREAKING-CHANGE: Querying for NULL fields is now achieved using an
ASCII NUL value, percent-encoded as %00, instead of the literal string
"null".
@ctron
Copy link
Contributor Author

ctron commented Feb 6, 2026

@jcrossley3 I need your help with this, how can a user search for a group named null?

At present, the literal string "null" is used for querying NULL's in the db, e.g. name=null. To search for the literal string, the best we can do is a LIKE query, e.g. name~null, but that's obviously going to pick up other groups with "null" in their name.

If it's a high priority requirement, we'd need to redesign the q= syntax to accommodate it, or it may be that the 'q' parameter isn't a good fit for the group endpoints.

I think having to explain the in the docs that you're not supposed to name a group null looks a bit … weird.

Creating a new query syntax for the group endpoint is equally strange.

So I think this issue should be addressed.

@jcrossley3
Copy link
Contributor

So I think this issue should be addressed.

Is the title of #2233 not clear enough? I requested your review 2 days ago.

@ctron
Copy link
Contributor Author

ctron commented Feb 6, 2026

So I think this issue should be addressed.

Is the title of #2233 not clear enough? I requested your review 2 days ago.

Just commented on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants