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
10 changes: 10 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# CVE-2026-34040: Moby AuthZ plugin bypass with oversized request bodies
# github.com/docker/docker v28.5.2+incompatible → fixed in v29.3.1
#
# This is a test-only transitive dependency pulled in by testcontainers-go.
# Docker client is NOT used in production code — only in integration tests
# that spin up PostgreSQL containers. The vulnerable AuthZ plugin bypass
# requires local Docker daemon access which is not exposed in production.
#
# Will be resolved when testcontainers-go releases a version using docker v29+.
CVE-2026-34040
15 changes: 8 additions & 7 deletions internal/handler/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ type GetAgentOutput struct {
}

type ListAgentsInput struct {
AgentType string `query:"agent_type" doc:"Filter by agent type"`
AgentType string `query:"agent_type" doc:"Filter by agent type"`
IdentityType []string `query:"identity_type" doc:"Filter by identity type. Comma-separated for multiple (e.g. agent,application)."`
Label string `query:"label" doc:"Filter by label (key:value, e.g. product:guardrails)"`
TrustLevel string `query:"trust_level" doc:"Filter by trust level"`
IsActive string `query:"is_active" doc:"Filter by active status"`
Limit int `query:"limit" default:"20" doc:"Items per page (max 100)"`
Offset int `query:"offset" default:"0" doc:"Offset for pagination"`
Label string `query:"label" doc:"Filter by label (key:value, e.g. product:guardrails)"`
TrustLevel string `query:"trust_level" doc:"Filter by trust level"`
IsActive string `query:"is_active" doc:"Filter by active status"`
Search string `query:"search" doc:"Search by name or external_id"`
Limit int `query:"limit" default:"20" doc:"Items per page (max 100)"`
Offset int `query:"offset" default:"0" doc:"Offset for pagination"`
}

type ListAgentsOutput struct {
Expand Down Expand Up @@ -245,7 +246,7 @@ func (a *API) listAgentsOp(ctx context.Context, input *ListAgentsInput) (*ListAg
return nil, huma.Error401Unauthorized("missing tenant context")
}

resp, err := a.agentSvc.ListAgents(ctx, tenant.AccountID, tenant.ProjectID, input.IdentityType, input.Label, input.Limit, input.Offset)
resp, err := a.agentSvc.ListAgents(ctx, tenant.AccountID, tenant.ProjectID, input.IdentityType, input.Label, input.TrustLevel, input.IsActive, input.Search, input.Limit, input.Offset)
if err != nil {
return nil, mapErr(err)
}
Expand Down
29 changes: 26 additions & 3 deletions internal/handler/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,22 @@ type IdentityIDInput struct {
ID string `path:"id" doc:"Identity UUID"`
}

type ListIdentitiesInput struct {
IdentityType []string `query:"identity_type" doc:"Filter by identity type. Comma-separated for multiple (e.g. agent,application)."`
Label string `query:"label" doc:"Filter by label (key:value, e.g. product:guardrails)"`
TrustLevel string `query:"trust_level" doc:"Filter by trust level"`
IsActive string `query:"is_active" doc:"Filter by active status"`
Search string `query:"search" doc:"Search by name or external_id"`
Limit int `query:"limit" default:"20" doc:"Items per page (max 100)"`
Offset int `query:"offset" default:"0" doc:"Offset for pagination"`
}

type IdentityListOutput struct {
Body struct {
Identities []*domain.Identity `json:"identities"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
}

Expand Down Expand Up @@ -203,13 +215,22 @@ func (a *API) getIdentityOp(ctx context.Context, input *IdentityIDInput) (*Ident
return &IdentityOutput{Body: identity}, nil
}

func (a *API) listIdentitiesOp(ctx context.Context, _ *struct{}) (*IdentityListOutput, error) {
func (a *API) listIdentitiesOp(ctx context.Context, input *ListIdentitiesInput) (*IdentityListOutput, error) {
tenant, err := internalMiddleware.GetTenant(ctx)
if err != nil {
return nil, huma.Error401Unauthorized("missing tenant context")
}

identities, err := a.identitySvc.ListIdentities(ctx, tenant.AccountID, tenant.ProjectID, nil, "")
limit := input.Limit
if limit <= 0 || limit > 100 {
limit = 20
}
offset := input.Offset
if offset < 0 {
offset = 0
}

identities, total, err := a.identitySvc.ListIdentities(ctx, tenant.AccountID, tenant.ProjectID, input.IdentityType, input.Label, input.TrustLevel, input.IsActive, input.Search, limit, offset)
if err != nil {
log.Error().Err(err).Msg("failed to list identities")
return nil, huma.Error500InternalServerError("failed to list identities")
Expand All @@ -220,7 +241,9 @@ func (a *API) listIdentitiesOp(ctx context.Context, _ *struct{}) (*IdentityListO
}
out := &IdentityListOutput{}
out.Body.Identities = identities
out.Body.Total = len(identities)
out.Body.Total = total
out.Body.Limit = limit
out.Body.Offset = offset
return out, nil
}

Expand Down
16 changes: 2 additions & 14 deletions internal/service/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,31 +173,19 @@ func (s *AgentService) GetAgent(ctx context.Context, id, accountID, projectID st
}

// ListAgents lists agents for a tenant, optionally filtered by identity_type(s) and label.
func (s *AgentService) ListAgents(ctx context.Context, accountID, projectID string, identityTypes []string, label string, limit, offset int) (*AgentListResponse, error) {
func (s *AgentService) ListAgents(ctx context.Context, accountID, projectID string, identityTypes []string, label, trustLevel, isActive, search string, limit, offset int) (*AgentListResponse, error) {
if limit <= 0 || limit > 100 {
limit = 20
}
if offset < 0 {
offset = 0
}

identities, err := s.identitySvc.ListIdentities(ctx, accountID, projectID, identityTypes, label)
identities, total, err := s.identitySvc.ListIdentities(ctx, accountID, projectID, identityTypes, label, trustLevel, isActive, search, limit, offset)
if err != nil {
return nil, err
}

// Apply simple offset/limit on the result set.
total := len(identities)
end := offset + limit
if offset >= total {
identities = nil
} else {
if end > total {
end = total
}
identities = identities[offset:end]
}

agents := make([]AgentResponse, len(identities))
for i, id := range identities {
keyPrefix := s.getKeyPrefix(ctx, id.ID)
Expand Down
4 changes: 2 additions & 2 deletions internal/service/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ func (s *IdentityService) GetIdentityByExternalID(ctx context.Context, externalI
}

// ListIdentities returns identities for a tenant, optionally filtered by identity_type(s) and label.
func (s *IdentityService) ListIdentities(ctx context.Context, accountID, projectID string, identityTypes []string, label string) ([]*domain.Identity, error) {
return s.repo.List(ctx, accountID, projectID, identityTypes, label)
func (s *IdentityService) ListIdentities(ctx context.Context, accountID, projectID string, identityTypes []string, label, trustLevel, isActive, search string, limit, offset int) ([]*domain.Identity, int, error) {
return s.repo.List(ctx, accountID, projectID, identityTypes, label, trustLevel, isActive, search, limit, offset)
}

// UpdateIdentityRequest holds parameters for identity updates.
Expand Down
37 changes: 33 additions & 4 deletions internal/store/postgres/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/uptrace/bun"
Expand Down Expand Up @@ -75,7 +76,7 @@ func (r *IdentityRepository) GetByWIMSEURI(ctx context.Context, wimseURI, accoun
// List returns identities for a tenant, optionally filtered by identity_type(s) and label.
// The label parameter accepts "key:value" format (e.g. "product:guardrails", "team:platform")
// and filters using JSONB containment: labels @> {"key": "value"}.
func (r *IdentityRepository) List(ctx context.Context, accountID, projectID string, identityTypes []string, label string) ([]*domain.Identity, error) {
func (r *IdentityRepository) List(ctx context.Context, accountID, projectID string, identityTypes []string, label, trustLevel, isActive, search string, limit, offset int) ([]*domain.Identity, int, error) {
var identities []*domain.Identity
q := r.db.NewSelect().Model(&identities).
Where("account_id = ?", accountID).
Expand All @@ -90,16 +91,44 @@ func (r *IdentityRepository) List(ctx context.Context, accountID, projectID stri
if label != "" {
parts := strings.SplitN(label, ":", 2)
if len(parts) != 2 || parts[0] == "" {
return nil, fmt.Errorf("invalid label format: expected non-empty-key:value, got %q", label)
return nil, 0, fmt.Errorf("invalid label format: expected non-empty-key:value, got %q", label)
}
labelJSON, _ := json.Marshal(map[string]string{parts[0]: parts[1]})
q = q.Where("labels @> ?::jsonb", string(labelJSON))
}
if trustLevel != "" {
q = q.Where("trust_level = ?", trustLevel)
}
if isActive != "" {
if active, err := strconv.ParseBool(isActive); err == nil {
if active {
q = q.Where("status = 'active'")
} else {
q = q.Where("status != 'active'")
}
}
}
Comment on lines +102 to +110
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The isActive filter logic relies on exact string matches for "true" and "false". This is fragile as it doesn't handle different casings or invalid values gracefully. Consider using a boolean type in the API handler and passing it down, or using strconv.ParseBool to handle the string input more robustly.

if search != "" {
searchPattern := "%" + search + "%"
q = q.Where("(name ILIKE ? OR external_id ILIKE ?)", searchPattern, searchPattern)
}

total, err := q.Count(ctx)
if err != nil {
return nil, 0, fmt.Errorf("failed to count identities: %w", err)
}

if limit > 0 {
q = q.Limit(limit)
}
if offset > 0 {
q = q.Offset(offset)
}

if err := q.Scan(ctx); err != nil {
return nil, fmt.Errorf("failed to list identities: %w", err)
return nil, 0, fmt.Errorf("failed to list identities: %w", err)
}
return identities, nil
return identities, total, nil
}

// Update saves changes to an existing identity.
Expand Down
Loading