Skip to content

?uid= query parameter is silently ignored — server returns all resources unfiltered #7

@Sam-Bolling

Description

@Sam-Bolling

Finding

QueryParams.BuildFromRequest() parses the id query parameter but never reads uid, so all collection list endpoints silently ignore ?uid= and return unfiltered results.

Review Source: Live integration testing against the Go CSAPI server during OSHConnect-Python publisher fleet migration to the Go server.
Severity: P1-Critical
Category: API Design
Ownership: Upstream (SomethingCreativeStudios/connected-systems-go)


Problem Statement

The OGC 23-001 specification (§7.3) defines uid as a standard query parameter for filtering resources by their globally unique identifier. The Go server's base QueryParams struct parses id and maps it to both id and unique_identifier columns in the database, but it never reads the uid query parameter at all.

Affected code — internal/model/query_params/query_params.go:

// QueryParams.BuildFromRequest() — only reads "id", never "uid"
func (QueryParams) BuildFromRequest(r *http.Request) *QueryParams {
    params := &QueryParams{
        Limit:  10,
        Offset: 0,
    }
    // ...
    if ids := r.URL.Query().Get("id"); ids != "" {
        params.IDs = strings.Split(ids, ",")
    }
    // ← No handling of r.URL.Query().Get("uid")
    return params
}

Affected code — internal/repository/deployment_repository.go (representative of all repos):

func (r *DeploymentRepository) applyFilters(query *gorm.DB, params *queryparams.DeploymentsQueryParams, parentId *string) *gorm.DB {
    if len(params.IDs) > 0 {
        query = query.Where("id IN ? OR unique_identifier IN ?", params.IDs, params.IDs)
    }
    // The IDs field is the only one used for identity filtering.
    // Since "uid" is never parsed into IDs (or any other field),
    // ?uid=urn:example:sensor:001 is silently discarded.
}

Scenario:

# Server has 37 systems registered
# Filter by UID — should return only one
GET /systems?uid=urn:os4csapi:system:nws:KDEN:v1

# Expected: 1 system returned
# Actual: all 37 systems returned (first 10 due to default limit)

Impact: Any client using ?uid= to look up resources by their globally unique identifier gets the entire (paginated) collection back instead of the matching resource. This is a spec-compliance failure that affects all resource types: systems, datastreams, deployments, procedures, sampling features, properties, control streams, observations, and commands.

The OSHConnect-Python publisher bootstrap helpers rely on ?uid= for idempotent resource creation (check-before-create). The workaround was to fetch all resources with ?limit=1000 and filter client-side:

# Workaround in bootstrap_helpers.py
def find_by_uid(session, url, target_uid):
    resp = session.get(f"{url}?limit=1000")  # Can't rely on ?uid= filter
    for item in resp.json().get("items", resp.json().get("features", [])):
        uid = item.get("properties", {}).get("uid") or item.get("uid", "")
        if uid == target_uid:
            return item
    return None

Ownership Verification

This code is pre-existing in the upstream SomethingCreativeStudios/connected-systems-go repository. The OS4CSAPI fork is currently synced with upstream (main branch is up to date).

Conclusion: This code is upstream.

Files to Modify

File Action Est. Lines Purpose
internal/model/query_params/query_params.go Modify ~10 Add UIDs []string field and parse uid query parameter in BuildFromRequest()
internal/repository/system_repository.go Modify ~5 Add unique_identifier IN ? clause for UIDs in applyFilters()
internal/repository/deployment_repository.go Modify ~5 Same — add UID filtering
internal/repository/datastream_repository.go Modify ~5 Same — add UID filtering
All other *_repository.go files with applyFilters Modify ~5 each Same pattern across all resource types
e2e/ Modify ~30 Add E2E test for ?uid= filtering

Proposed Solutions

Option A: Add UIDs field to QueryParams (Recommended)

// internal/model/query_params/query_params.go
type QueryParams struct {
    IDs    []string
    UIDs   []string // NEW: parsed from ?uid= parameter
    Q      []string
    Limit  int
    Offset int
}

func (QueryParams) BuildFromRequest(r *http.Request) *QueryParams {
    params := &QueryParams{
        Limit:  10,
        Offset: 0,
    }
    // ... existing code ...
    if ids := r.URL.Query().Get("id"); ids != "" {
        params.IDs = strings.Split(ids, ",")
    }
    // NEW: parse uid parameter
    if uids := r.URL.Query().Get("uid"); uids != "" {
        params.UIDs = strings.Split(uids, ",")
    }
    return params
}

// In each repository's applyFilters():
if len(params.UIDs) > 0 {
    query = query.Where("unique_identifier IN ?", params.UIDs)
}

Pros: Minimal diff; follows existing pattern for id; spec-compliant; backward compatible
Cons: Requires touching every repository's applyFilters()
Effort: Small | Risk: Low

Option B: Map uid into existing IDs field

Since applyFilters() already does WHERE id IN ? OR unique_identifier IN ? for the IDs field, simply also parse uid into IDs:

if uids := r.URL.Query().Get("uid"); uids != "" {
    params.IDs = append(params.IDs, strings.Split(uids, ",")...)
}

Pros: Zero changes to repositories; single-line addition in BuildFromRequest()
Cons: Conflates two semantically different parameters; could return unexpected results if a uid value happens to match a local id
Effort: Small | Risk: Low

Scope — What NOT to Touch

  • ❌ Do NOT modify files outside the "Files to Modify" table above
  • ❌ Do NOT refactor adjacent code that isn't part of this finding
  • ❌ Do NOT change public API signatures unless the finding specifically requires it
  • ❌ Do NOT add uid filtering to single-resource GET endpoints (they use path-based ID lookup)
  • ❌ Do NOT implement uid as a path parameter — the spec defines it as a query parameter on list endpoints

Acceptance Criteria

  • GET /systems?uid=urn:test:system:001 returns only the system with that UID
  • GET /datastreams?uid=urn:test:ds:001 returns only the matching datastream
  • GET /deployments?uid=urn:test:dep:001 returns only the matching deployment
  • Comma-separated UIDs work: ?uid=urn:a,urn:b returns both matching resources
  • When no resource matches the UID, an empty collection is returned (not an error)
  • Existing ?id= filtering still works unchanged
  • Existing tests still pass (make test)
  • E2E test added for ?uid= filtering

Dependencies

Blocked by: Nothing
Blocks: Nothing
Related: #9 — Default pagination limit of 10 (compounds this problem — even ?id= returns only first 10 matches); ogc-client-CSAPI_2#167 — Client library default limit (client-side companion fix)


Operational Constraints

⚠️ MANDATORY: Before starting work on this issue, review docs/governance/AI_OPERATIONAL_CONSTRAINTS.md if available.

Key constraints:

  • Precedence: OGC specifications → AI Collaboration Agreement → This issue description → Existing code → Conversational context
  • No scope expansion: Fix the finding, nothing more
  • Minimal diffs: Prefer the smallest change that satisfies the acceptance criteria
  • Ask when unclear: If intent is ambiguous, stop and ask for clarification
  • Respect ownership: This code is upstream — coordinate with the maintainer if contributing back

Ownership-Specific Constraints

If Upstream:

  • Track the issue for potential future contribution or discussion with the maintainer
  • If the fix is trivial and clearly beneficial, note in the issue that it could be offered as a separate upstream PR

References

# Document What It Provides
1 internal/model/query_params/query_params.goBuildFromRequest() Root cause — uid param never parsed
2 internal/repository/deployment_repository.goapplyFilters() Representative filter logic showing IDs used but no UIDs
3 OGC 23-001 §7.3 — Query parameters Spec defines uid as a standard list filter parameter
4 OSHConnect-Python publishers/bootstrap_helpers.pyfind_by_uid() Real-world workaround demonstrating impact

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions