From dc868b266877146c8e5540c0024498ebce1c64c1 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Mon, 1 Jun 2026 15:56:14 -0500 Subject: [PATCH 1/4] feat(policy): add search support to ListSubjectMappings Signed-off-by: Chris Reed --- service/integration/subject_mappings_test.go | 230 ++++++++++++++++++ .../policy/db/queries/subject_mappings.sql | 58 ++++- service/policy/db/subject_mappings.go | 9 + service/policy/db/subject_mappings.sql.go | 154 +++++++++--- 4 files changed, 408 insertions(+), 43 deletions(-) diff --git a/service/integration/subject_mappings_test.go b/service/integration/subject_mappings_test.go index bff23910d8..d91ae860e3 100644 --- a/service/integration/subject_mappings_test.go +++ b/service/integration/subject_mappings_test.go @@ -12,6 +12,7 @@ import ( "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/actions" + "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" "github.com/opentdf/platform/service/internal/fixtures" @@ -1040,6 +1041,168 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_NoNamespaceFilter_Return s.True(foundUnnamespaced) } +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByFields_Succeeds() { + suffix := time.Now().UnixNano() + ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{ + fmt.Sprintf("fqn-only-%d", suffix), + fmt.Sprintf("action-holder-%d", suffix), + fmt.Sprintf("selector-holder-%d", suffix), + fmt.Sprintf("external-holder-%d", suffix), + fmt.Sprintf("wildcarda-%d", suffix), + }) + defer s.deleteSearchSubjectMappingNamespace(ns) + + fqnSM := s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".fqn-holder", []string{"fqn-holder@example.com"}, "read") + actionToken := fmt.Sprintf("action-only-%d", suffix) + actionSM := s.createSearchSubjectMapping(ns.GetId(), values[1].GetId(), ".action-holder", []string{"action-holder@example.com"}, actionToken) + selectorToken := fmt.Sprintf(".selector-only-%d", suffix) + selectorSM := s.createSearchSubjectMapping(ns.GetId(), values[2].GetId(), selectorToken, []string{"selector-holder@example.com"}, "read") + externalToken := fmt.Sprintf("external-only-%d@example.com", suffix) + externalSM := s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), ".external-holder", []string{externalToken}, "read") + s.createSearchSubjectMapping(ns.GetId(), values[4].GetId(), ".wildcard-holder", []string{fmt.Sprintf("wildcarda-%d@example.com", suffix)}, "read") + + tests := []struct { + name string + term string + id string + }{ + {name: "attribute value fqn", term: strings.ToUpper(values[0].GetFqn()), id: fqnSM.GetId()}, + {name: "action name", term: strings.ToUpper(actionToken), id: actionSM.GetId()}, + {name: "subject external selector value", term: strings.ToUpper(selectorToken), id: selectorSM.GetId()}, + {name: "subject external values", term: strings.ToUpper(externalToken), id: externalSM.GetId()}, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + list, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: tc.term}, + }) + s.Require().NoError(err) + s.Require().Len(list.GetSubjectMappings(), 1) + s.Equal(tc.id, list.GetSubjectMappings()[0].GetId()) + s.Equal(int32(1), list.GetPagination().GetTotal()) + }) + } + + wildcardSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: fmt.Sprintf("wildcard_-%d", suffix)}, + }) + s.Require().NoError(err) + s.Empty(wildcardSearch.GetSubjectMappings()) + s.Equal(int32(0), wildcardSearch.GetPagination().GetTotal()) + + spacePaddedSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: " " + actionToken + " "}, + }) + s.Require().NoError(err) + s.Empty(spacePaddedSearch.GetSubjectMappings()) + s.Equal(int32(0), spacePaddedSearch.GetPagination().GetTotal()) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchCombinesWithNamespace_Succeeds() { + suffix := time.Now().UnixNano() + searchToken := fmt.Sprintf("namespace-and-search-%d", suffix) + + firstNS, firstValues := s.createSearchSubjectMappingNamespace(suffix, []string{fmt.Sprintf("first-%d", suffix)}) + defer s.deleteSearchSubjectMappingNamespace(firstNS) + secondNS, secondValues := s.createSearchSubjectMappingNamespace(suffix+1, []string{fmt.Sprintf("second-%d", suffix)}) + defer s.deleteSearchSubjectMappingNamespace(secondNS) + + firstSM := s.createSearchSubjectMapping(firstNS.GetId(), firstValues[0].GetId(), ".first", []string{"first@example.com"}, searchToken) + secondSM := s.createSearchSubjectMapping(secondNS.GetId(), secondValues[0].GetId(), ".second", []string{"second@example.com"}, searchToken) + + byFirstNS, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: firstNS.GetId(), + Search: &policy.Search{Term: searchToken}, + }) + s.Require().NoError(err) + s.Require().Len(byFirstNS.GetSubjectMappings(), 1) + s.Equal(firstSM.GetId(), byFirstNS.GetSubjectMappings()[0].GetId()) + s.Equal(int32(1), byFirstNS.GetPagination().GetTotal()) + + bySecondFQN, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceFqn: secondNS.GetFqn(), + Search: &policy.Search{Term: searchToken}, + }) + s.Require().NoError(err) + s.Require().Len(bySecondFQN.GetSubjectMappings(), 1) + s.Equal(secondSM.GetId(), bySecondFQN.GetSubjectMappings()[0].GetId()) + s.Equal(int32(1), bySecondFQN.GetPagination().GetTotal()) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchEmptyQuery_Succeeds() { + suffix := time.Now().UnixNano() + ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{fmt.Sprintf("empty-search-%d", suffix)}) + defer s.deleteSearchSubjectMappingNamespace(ns) + s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".empty-search", []string{"empty-search@example.com"}, "read") + + noSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + }) + s.Require().NoError(err) + emptySearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: ""}, + }) + s.Require().NoError(err) + s.Equal(noSearch.GetPagination().GetTotal(), emptySearch.GetPagination().GetTotal()) + s.Len(emptySearch.GetSubjectMappings(), len(noSearch.GetSubjectMappings())) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchPaginationAppliesAfterFiltering_Succeeds() { + suffix := time.Now().UnixNano() + searchToken := fmt.Sprintf("search-page-%d", suffix) + ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{ + fmt.Sprintf("page-a-%d", suffix), + fmt.Sprintf("page-b-%d", suffix), + fmt.Sprintf("page-c-%d", suffix), + fmt.Sprintf("page-other-%d", suffix), + }) + defer s.deleteSearchSubjectMappingNamespace(ns) + + ids := make([]string, 3) + for i := range ids { + if i > 0 { + time.Sleep(5 * time.Millisecond) + } + ids[i] = s.createSearchSubjectMapping(ns.GetId(), values[i].GetId(), fmt.Sprintf(".page-%d", i), []string{fmt.Sprintf("page-%d@example.com", i)}, searchToken).GetId() + } + s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), ".page-other", []string{"page-other@example.com"}, "read") + + firstPage, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: searchToken}, + Pagination: &policy.PageRequest{Limit: 2}, + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.Require().Len(firstPage.GetSubjectMappings(), 2) + s.Equal(int32(3), firstPage.GetPagination().GetTotal()) + s.Equal(int32(2), firstPage.GetPagination().GetNextOffset()) + s.Equal(ids[0], firstPage.GetSubjectMappings()[0].GetId()) + s.Equal(ids[1], firstPage.GetSubjectMappings()[1].GetId()) + + secondPage, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: searchToken}, + Pagination: &policy.PageRequest{Limit: 2, Offset: 2}, + Sort: []*subjectmapping.SubjectMappingsSort{ + {Field: subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, Direction: policy.SortDirection_SORT_DIRECTION_ASC}, + }, + }) + s.Require().NoError(err) + s.Require().Len(secondPage.GetSubjectMappings(), 1) + s.Equal(int32(3), secondPage.GetPagination().GetTotal()) + s.Equal(int32(2), secondPage.GetPagination().GetCurrentOffset()) + s.Equal(int32(0), secondPage.GetPagination().GetNextOffset()) + s.Equal(ids[2], secondPage.GetSubjectMappings()[0].GetId()) +} + func (s *SubjectMappingsSuite) TestDeleteSubjectMapping() { // create a new subject mapping, delete it, and verify get fails with not found fixtureAttrValID := s.f.GetAttributeValueKey("example.com/attr/attr2/value/value1").ID @@ -3075,6 +3238,73 @@ func (s *SubjectMappingsSuite) createSortTestSubjectConditionSets(prefixes []str return ids } +func (s *SubjectMappingsSuite) createSearchSubjectMappingNamespace(suffix int64, values []string) (*policy.Namespace, []*policy.Value) { + ns, err := s.db.PolicyClient.CreateNamespace(s.ctx, &namespaces.CreateNamespaceRequest{ + Name: fmt.Sprintf("list-sm-search-%d.example", suffix), + }) + s.Require().NoError(err) + s.Require().NotNil(ns) + + attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{ + Name: fmt.Sprintf("list-sm-search-attr-%d", suffix), + NamespaceId: ns.GetId(), + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + }) + s.Require().NoError(err) + s.Require().NotNil(attr) + + createdValues := make([]*policy.Value, len(values)) + for i, value := range values { + created, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attr.GetId(), &attributes.CreateAttributeValueRequest{ + Value: value, + }) + s.Require().NoError(err) + createdValues[i] = created + } + + return ns, createdValues +} + +func (s *SubjectMappingsSuite) createSearchSubjectMapping( + namespaceID string, + attributeValueID string, + selector string, + externalValues []string, + actionName string, +) *policy.SubjectMapping { + created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ + NamespaceId: namespaceID, + AttributeValueId: attributeValueID, + Actions: []*policy.Action{{Name: actionName}}, + NewSubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: selector, + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: externalValues, + }, + }, + }, + }, + }, + }, + }, + }) + s.Require().NoError(err) + s.Require().NotNil(created) + return created +} + +func (s *SubjectMappingsSuite) deleteSearchSubjectMappingNamespace(ns *policy.Namespace) { + _, err := s.db.PolicyClient.UnsafeDeleteNamespace(s.ctx, ns, ns.GetFqn()) + s.Require().NoError(err) +} + // deleteSortTestSubjectMappings cleans up subject mappings created by sort tests. func (s *SubjectMappingsSuite) deleteSortTestSubjectMappings(ids []string) { for _, id := range ids { diff --git a/service/policy/db/queries/subject_mappings.sql b/service/policy/db/queries/subject_mappings.sql index e3db68777d..da00d16370 100644 --- a/service/policy/db/queries/subject_mappings.sql +++ b/service/policy/db/queries/subject_mappings.sql @@ -116,15 +116,58 @@ subject_actions AS ( LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL GROUP BY sma.subject_mapping_id -), counted AS ( - SELECT COUNT(sm.id) AS total +), filtered_subject_mappings AS ( + SELECT DISTINCT sm.id FROM subject_mappings sm + LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id + LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id + LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL WHERE - (sqlc.narg('namespace_id')::uuid IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL) - OR sm.namespace_id = sqlc.narg('namespace_id')::uuid - OR sm_ns_fqns.fqn = sqlc.narg('namespace_fqn')::text + ( + (sqlc.narg('namespace_id')::uuid IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL) + OR sm.namespace_id = sqlc.narg('namespace_id')::uuid + OR sm_ns_fqns.fqn = sqlc.narg('namespace_fqn')::text + ) + AND ( + sqlc.narg('search')::TEXT IS NULL + OR LOWER(fqns.fqn) LIKE sqlc.narg('search')::TEXT ESCAPE '\' + OR EXISTS ( + SELECT 1 + FROM subject_mapping_actions search_sma + JOIN actions search_a ON search_a.id = search_sma.action_id + WHERE search_sma.subject_mapping_id = sm.id + AND LOWER(search_a.name) LIKE sqlc.narg('search')::TEXT ESCAPE '\' + ) + OR EXISTS ( + SELECT 1 + FROM JSONB_ARRAY_ELEMENTS(COALESCE(scs.condition, '[]'::JSONB)) AS ss(subject_set) + CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS( + COALESCE(ss.subject_set->'conditionGroups', ss.subject_set->'condition_groups', '[]'::JSONB) + ) AS cg(condition_group) + CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(COALESCE(cg.condition_group->'conditions', '[]'::JSONB)) AS con(condition) + WHERE + LOWER(con.condition->>'subjectExternalSelectorValue') LIKE sqlc.narg('search')::TEXT ESCAPE '\' + OR LOWER(con.condition->>'subject_external_selector_value') LIKE sqlc.narg('search')::TEXT ESCAPE '\' + OR EXISTS ( + SELECT 1 + FROM JSONB_ARRAY_ELEMENTS_TEXT( + COALESCE( + con.condition->'subjectExternalValues', + con.condition->'subject_external_values', + con.condition->'subjectExternalSelectorValues', + con.condition->'subject_external_selector_values', + '[]'::JSONB + ) + ) AS external_value(value) + WHERE LOWER(external_value.value) LIKE sqlc.narg('search')::TEXT ESCAPE '\' + ) + ) + ) +), counted AS ( + SELECT COUNT(id) AS total + FROM filtered_subject_mappings ) SELECT sm.id, @@ -152,6 +195,7 @@ SELECT END AS namespace, counted.total FROM subject_mappings sm +JOIN filtered_subject_mappings fsm ON fsm.id = sm.id CROSS JOIN counted CROSS JOIN params p LEFT JOIN subject_actions sa ON sm.id = sa.subject_mapping_id @@ -162,10 +206,6 @@ LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL -WHERE - (sqlc.narg('namespace_id')::uuid IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL) - OR sm.namespace_id = sqlc.narg('namespace_id')::uuid - OR sm_ns_fqns.fqn = sqlc.narg('namespace_fqn')::text GROUP BY sm.id, sa.standard_actions, diff --git a/service/policy/db/subject_mappings.go b/service/policy/db/subject_mappings.go index c8ae25d99f..1b2f45ea9f 100644 --- a/service/policy/db/subject_mappings.go +++ b/service/policy/db/subject_mappings.go @@ -361,10 +361,12 @@ func (c PolicyDBClient) ListSubjectMappings(ctx context.Context, r *subjectmappi } sortField, sortDirection := GetSubjectMappingsSortParams(r.GetSort()) + search := pgtypeSubjectMappingSearchPattern(r.GetSearch().GetTerm()) list, err := c.queries.listSubjectMappings(ctx, listSubjectMappingsParams{ NamespaceID: pgtypeUUID(r.GetNamespaceId()), NamespaceFqn: pgtypeText(r.GetNamespaceFqn()), + Search: search, Limit: limit, Offset: offset, SortField: sortField, @@ -440,6 +442,13 @@ func (c PolicyDBClient) ListSubjectMappings(ctx context.Context, r *subjectmappi }, nil } +func pgtypeSubjectMappingSearchPattern(query string) pgtype.Text { + if query == "" { + return pgtype.Text{} + } + return pgtypeText("%" + escapeLikePattern(strings.ToLower(query)) + "%") +} + // Mutates provided fields and returns the updated subject mapping func (c PolicyDBClient) UpdateSubjectMapping(ctx context.Context, r *subjectmapping.UpdateSubjectMappingRequest) (*policy.SubjectMapping, error) { id := r.GetId() diff --git a/service/policy/db/subject_mappings.sql.go b/service/policy/db/subject_mappings.sql.go index b822bbcefb..e51c8e831a 100644 --- a/service/policy/db/subject_mappings.sql.go +++ b/service/policy/db/subject_mappings.sql.go @@ -101,7 +101,8 @@ type createSubjectMappingParams struct { // ) // SELECT id FROM inserted_mapping func (q *Queries) createSubjectMapping(ctx context.Context, arg createSubjectMappingParams) (string, error) { - row := q.db.QueryRow(ctx, createSubjectMapping, + row := q.db.QueryRow( + ctx, createSubjectMapping, arg.AttributeValueID, arg.Metadata, arg.SubjectConditionSetID, @@ -459,7 +460,8 @@ type listSubjectConditionSetsRow struct { // LIMIT $5 // OFFSET $4 func (q *Queries) listSubjectConditionSets(ctx context.Context, arg listSubjectConditionSetsParams) ([]listSubjectConditionSetsRow, error) { - rows, err := q.db.Query(ctx, listSubjectConditionSets, + rows, err := q.db.Query( + ctx, listSubjectConditionSets, arg.NamespaceID, arg.NamespaceFqn, arg.Search, @@ -496,8 +498,8 @@ const listSubjectMappings = `-- name: listSubjectMappings :many WITH params AS ( SELECT - COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, - COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction + COALESCE(NULLIF($3::text, ''), 'created_at') AS resolved_field, + COALESCE(NULLIF($4::text, ''), 'DESC') AS resolved_direction ), subject_actions AS ( SELECT @@ -523,15 +525,58 @@ subject_actions AS ( LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL GROUP BY sma.subject_mapping_id -), counted AS ( - SELECT COUNT(sm.id) AS total +), filtered_subject_mappings AS ( + SELECT DISTINCT sm.id FROM subject_mappings sm + LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id + LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id + LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL WHERE - ($1::uuid IS NULL AND $2::text IS NULL) - OR sm.namespace_id = $1::uuid - OR sm_ns_fqns.fqn = $2::text + ( + ($5::uuid IS NULL AND $6::text IS NULL) + OR sm.namespace_id = $5::uuid + OR sm_ns_fqns.fqn = $6::text + ) + AND ( + $7::TEXT IS NULL + OR LOWER(fqns.fqn) LIKE $7::TEXT ESCAPE '\' + OR EXISTS ( + SELECT 1 + FROM subject_mapping_actions search_sma + JOIN actions search_a ON search_a.id = search_sma.action_id + WHERE search_sma.subject_mapping_id = sm.id + AND LOWER(search_a.name) LIKE $7::TEXT ESCAPE '\' + ) + OR EXISTS ( + SELECT 1 + FROM JSONB_ARRAY_ELEMENTS(COALESCE(scs.condition, '[]'::JSONB)) AS ss(subject_set) + CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS( + COALESCE(ss.subject_set->'conditionGroups', ss.subject_set->'condition_groups', '[]'::JSONB) + ) AS cg(condition_group) + CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(COALESCE(cg.condition_group->'conditions', '[]'::JSONB)) AS con(condition) + WHERE + LOWER(con.condition->>'subjectExternalSelectorValue') LIKE $7::TEXT ESCAPE '\' + OR LOWER(con.condition->>'subject_external_selector_value') LIKE $7::TEXT ESCAPE '\' + OR EXISTS ( + SELECT 1 + FROM JSONB_ARRAY_ELEMENTS_TEXT( + COALESCE( + con.condition->'subjectExternalValues', + con.condition->'subject_external_values', + con.condition->'subjectExternalSelectorValues', + con.condition->'subject_external_selector_values', + '[]'::JSONB + ) + ) AS external_value(value) + WHERE LOWER(external_value.value) LIKE $7::TEXT ESCAPE '\' + ) + ) + ) +), counted AS ( + SELECT COUNT(id) AS total + FROM filtered_subject_mappings ) SELECT sm.id, @@ -559,6 +604,7 @@ SELECT END AS namespace, counted.total FROM subject_mappings sm +JOIN filtered_subject_mappings fsm ON fsm.id = sm.id CROSS JOIN counted CROSS JOIN params p LEFT JOIN subject_actions sa ON sm.id = sa.subject_mapping_id @@ -569,10 +615,6 @@ LEFT JOIN attribute_namespaces scs_ns ON scs_ns.id = scs.namespace_id LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL -WHERE - ($1::uuid IS NULL AND $2::text IS NULL) - OR sm.namespace_id = $1::uuid - OR sm_ns_fqns.fqn = $2::text GROUP BY sm.id, sa.standard_actions, @@ -591,17 +633,18 @@ ORDER BY CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN sm.updated_at END ASC, CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN sm.updated_at END DESC, sm.id ASC -LIMIT $4 -OFFSET $3 +LIMIT $2 +OFFSET $1 ` type listSubjectMappingsParams struct { - NamespaceID pgtype.UUID `json:"namespace_id"` - NamespaceFqn pgtype.Text `json:"namespace_fqn"` Offset int32 `json:"offset_"` Limit int32 `json:"limit_"` SortField string `json:"sort_field"` SortDirection string `json:"sort_direction"` + NamespaceID pgtype.UUID `json:"namespace_id"` + NamespaceFqn pgtype.Text `json:"namespace_fqn"` + Search pgtype.Text `json:"search"` } type listSubjectMappingsRow struct { @@ -621,8 +664,8 @@ type listSubjectMappingsRow struct { // // WITH params AS ( // SELECT -// COALESCE(NULLIF($5::text, ''), 'created_at') AS resolved_field, -// COALESCE(NULLIF($6::text, ''), 'DESC') AS resolved_direction +// COALESCE(NULLIF($3::text, ''), 'created_at') AS resolved_field, +// COALESCE(NULLIF($4::text, ''), 'DESC') AS resolved_direction // ), // subject_actions AS ( // SELECT @@ -648,15 +691,58 @@ type listSubjectMappingsRow struct { // LEFT JOIN attribute_namespaces ans ON ans.id = a.namespace_id // LEFT JOIN attribute_fqns ans_fqns ON ans_fqns.namespace_id = ans.id AND ans_fqns.attribute_id IS NULL AND ans_fqns.value_id IS NULL // GROUP BY sma.subject_mapping_id -// ), counted AS ( -// SELECT COUNT(sm.id) AS total +// ), filtered_subject_mappings AS ( +// SELECT DISTINCT sm.id // FROM subject_mappings sm +// LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id +// LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id +// LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id // LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id // LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL // WHERE -// ($1::uuid IS NULL AND $2::text IS NULL) -// OR sm.namespace_id = $1::uuid -// OR sm_ns_fqns.fqn = $2::text +// ( +// ($5::uuid IS NULL AND $6::text IS NULL) +// OR sm.namespace_id = $5::uuid +// OR sm_ns_fqns.fqn = $6::text +// ) +// AND ( +// $7::TEXT IS NULL +// OR LOWER(fqns.fqn) LIKE $7::TEXT ESCAPE '\' +// OR EXISTS ( +// SELECT 1 +// FROM subject_mapping_actions search_sma +// JOIN actions search_a ON search_a.id = search_sma.action_id +// WHERE search_sma.subject_mapping_id = sm.id +// AND LOWER(search_a.name) LIKE $7::TEXT ESCAPE '\' +// ) +// OR EXISTS ( +// SELECT 1 +// FROM JSONB_ARRAY_ELEMENTS(COALESCE(scs.condition, '[]'::JSONB)) AS ss(subject_set) +// CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS( +// COALESCE(ss.subject_set->'conditionGroups', ss.subject_set->'condition_groups', '[]'::JSONB) +// ) AS cg(condition_group) +// CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(COALESCE(cg.condition_group->'conditions', '[]'::JSONB)) AS con(condition) +// WHERE +// LOWER(con.condition->>'subjectExternalSelectorValue') LIKE $7::TEXT ESCAPE '\' +// OR LOWER(con.condition->>'subject_external_selector_value') LIKE $7::TEXT ESCAPE '\' +// OR EXISTS ( +// SELECT 1 +// FROM JSONB_ARRAY_ELEMENTS_TEXT( +// COALESCE( +// con.condition->'subjectExternalValues', +// con.condition->'subject_external_values', +// con.condition->'subjectExternalSelectorValues', +// con.condition->'subject_external_selector_values', +// '[]'::JSONB +// ) +// ) AS external_value(value) +// WHERE LOWER(external_value.value) LIKE $7::TEXT ESCAPE '\' +// ) +// ) +// ) +// ), counted AS ( +// SELECT COUNT(id) AS total +// FROM filtered_subject_mappings // ) // SELECT // sm.id, @@ -684,6 +770,7 @@ type listSubjectMappingsRow struct { // END AS namespace, // counted.total // FROM subject_mappings sm +// JOIN filtered_subject_mappings fsm ON fsm.id = sm.id // CROSS JOIN counted // CROSS JOIN params p // LEFT JOIN subject_actions sa ON sm.id = sa.subject_mapping_id @@ -694,10 +781,6 @@ type listSubjectMappingsRow struct { // LEFT JOIN attribute_fqns scs_ns_fqns ON scs_ns_fqns.namespace_id = scs_ns.id AND scs_ns_fqns.attribute_id IS NULL AND scs_ns_fqns.value_id IS NULL // LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id // LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL -// WHERE -// ($1::uuid IS NULL AND $2::text IS NULL) -// OR sm.namespace_id = $1::uuid -// OR sm_ns_fqns.fqn = $2::text // GROUP BY // sm.id, // sa.standard_actions, @@ -716,16 +799,18 @@ type listSubjectMappingsRow struct { // CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'ASC' THEN sm.updated_at END ASC, // CASE WHEN p.resolved_field = 'updated_at' AND p.resolved_direction = 'DESC' THEN sm.updated_at END DESC, // sm.id ASC -// LIMIT $4 -// OFFSET $3 +// LIMIT $2 +// OFFSET $1 func (q *Queries) listSubjectMappings(ctx context.Context, arg listSubjectMappingsParams) ([]listSubjectMappingsRow, error) { - rows, err := q.db.Query(ctx, listSubjectMappings, - arg.NamespaceID, - arg.NamespaceFqn, + rows, err := q.db.Query( + ctx, listSubjectMappings, arg.Offset, arg.Limit, arg.SortField, arg.SortDirection, + arg.NamespaceID, + arg.NamespaceFqn, + arg.Search, ) if err != nil { return nil, err @@ -1027,7 +1112,8 @@ type updateSubjectMappingParams struct { // SELECT cnt // FROM update_count func (q *Queries) updateSubjectMapping(ctx context.Context, arg updateSubjectMappingParams) (int64, error) { - result, err := q.db.Exec(ctx, updateSubjectMapping, + result, err := q.db.Exec( + ctx, updateSubjectMapping, arg.Metadata, arg.SubjectConditionSetID, arg.ID, From e5b04c9eaef30025b2782dcf53d160bdba9d4f8f Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Wed, 3 Jun 2026 07:32:49 -0500 Subject: [PATCH 2/4] fix(policy): narrow subject mapping search fields Signed-off-by: Chris Reed --- service/integration/subject_mappings_test.go | 61 +++++++---- .../policy/db/queries/subject_mappings.sql | 45 ++------ service/policy/db/subject_mappings.go | 9 +- service/policy/db/subject_mappings.sql.go | 102 +++++------------- 4 files changed, 76 insertions(+), 141 deletions(-) diff --git a/service/integration/subject_mappings_test.go b/service/integration/subject_mappings_test.go index d91ae860e3..72fd85ccfe 100644 --- a/service/integration/subject_mappings_test.go +++ b/service/integration/subject_mappings_test.go @@ -1041,25 +1041,30 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_NoNamespaceFilter_Return s.True(foundUnnamespaced) } -func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByFields_Succeeds() { +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByAttributeValueFQNAndLabels_Succeeds() { suffix := time.Now().UnixNano() ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{ fmt.Sprintf("fqn-only-%d", suffix), - fmt.Sprintf("action-holder-%d", suffix), - fmt.Sprintf("selector-holder-%d", suffix), - fmt.Sprintf("external-holder-%d", suffix), + fmt.Sprintf("label-holder-%d", suffix), + fmt.Sprintf("label-key-holder-%d", suffix), + fmt.Sprintf("condition-holder-%d", suffix), fmt.Sprintf("wildcarda-%d", suffix), + fmt.Sprintf("unmatched-%d", suffix), }) defer s.deleteSearchSubjectMappingNamespace(ns) - fqnSM := s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".fqn-holder", []string{"fqn-holder@example.com"}, "read") actionToken := fmt.Sprintf("action-only-%d", suffix) - actionSM := s.createSearchSubjectMapping(ns.GetId(), values[1].GetId(), ".action-holder", []string{"action-holder@example.com"}, actionToken) + labelToken := fmt.Sprintf("label-only-%d", suffix) + labelKeyToken := fmt.Sprintf("label-key-only-%d", suffix) selectorToken := fmt.Sprintf(".selector-only-%d", suffix) - selectorSM := s.createSearchSubjectMapping(ns.GetId(), values[2].GetId(), selectorToken, []string{"selector-holder@example.com"}, "read") externalToken := fmt.Sprintf("external-only-%d@example.com", suffix) - externalSM := s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), ".external-holder", []string{externalToken}, "read") - s.createSearchSubjectMapping(ns.GetId(), values[4].GetId(), ".wildcard-holder", []string{fmt.Sprintf("wildcarda-%d@example.com", suffix)}, "read") + + fqnSM := s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".fqn-holder", []string{"fqn-holder@example.com"}, "read", nil) + labelSM := s.createSearchSubjectMapping(ns.GetId(), values[1].GetId(), ".label-holder", []string{"label-holder@example.com"}, actionToken, map[string]string{"search-label": labelToken}) + s.createSearchSubjectMapping(ns.GetId(), values[2].GetId(), ".label-key-holder", []string{"label-key-holder@example.com"}, "read", map[string]string{labelKeyToken: "not-searchable-by-key"}) + s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), selectorToken, []string{externalToken}, "read", nil) + s.createSearchSubjectMapping(ns.GetId(), values[4].GetId(), ".wildcard-holder", []string{fmt.Sprintf("wildcarda-%d@example.com", suffix)}, "read", map[string]string{"wildcard": fmt.Sprintf("wildcarda-%d", suffix)}) + s.createSearchSubjectMapping(ns.GetId(), values[5].GetId(), ".unmatched", []string{"unmatched@example.com"}, "read", map[string]string{"unused": fmt.Sprintf("unused-%d", suffix)}) tests := []struct { name string @@ -1067,9 +1072,7 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByFields_Succeeds( id string }{ {name: "attribute value fqn", term: strings.ToUpper(values[0].GetFqn()), id: fqnSM.GetId()}, - {name: "action name", term: strings.ToUpper(actionToken), id: actionSM.GetId()}, - {name: "subject external selector value", term: strings.ToUpper(selectorToken), id: selectorSM.GetId()}, - {name: "subject external values", term: strings.ToUpper(externalToken), id: externalSM.GetId()}, + {name: "metadata label value", term: strings.ToUpper(labelToken), id: labelSM.GetId()}, } for _, tc := range tests { @@ -1085,6 +1088,21 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByFields_Succeeds( }) } + for _, term := range []string{ + actionToken, + labelKeyToken, + selectorToken, + externalToken, + } { + list, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: term}, + }) + s.Require().NoError(err) + s.Empty(list.GetSubjectMappings()) + s.Equal(int32(0), list.GetPagination().GetTotal()) + } + wildcardSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ NamespaceId: ns.GetId(), Search: &policy.Search{Term: fmt.Sprintf("wildcard_-%d", suffix)}, @@ -1095,11 +1113,12 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByFields_Succeeds( spacePaddedSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ NamespaceId: ns.GetId(), - Search: &policy.Search{Term: " " + actionToken + " "}, + Search: &policy.Search{Term: " " + labelToken + " "}, }) s.Require().NoError(err) - s.Empty(spacePaddedSearch.GetSubjectMappings()) - s.Equal(int32(0), spacePaddedSearch.GetPagination().GetTotal()) + s.Require().Len(spacePaddedSearch.GetSubjectMappings(), 1) + s.Equal(labelSM.GetId(), spacePaddedSearch.GetSubjectMappings()[0].GetId()) + s.Equal(int32(1), spacePaddedSearch.GetPagination().GetTotal()) } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchCombinesWithNamespace_Succeeds() { @@ -1111,8 +1130,8 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchCombinesWithNamesp secondNS, secondValues := s.createSearchSubjectMappingNamespace(suffix+1, []string{fmt.Sprintf("second-%d", suffix)}) defer s.deleteSearchSubjectMappingNamespace(secondNS) - firstSM := s.createSearchSubjectMapping(firstNS.GetId(), firstValues[0].GetId(), ".first", []string{"first@example.com"}, searchToken) - secondSM := s.createSearchSubjectMapping(secondNS.GetId(), secondValues[0].GetId(), ".second", []string{"second@example.com"}, searchToken) + firstSM := s.createSearchSubjectMapping(firstNS.GetId(), firstValues[0].GetId(), ".first", []string{"first@example.com"}, "read", map[string]string{"search": searchToken}) + secondSM := s.createSearchSubjectMapping(secondNS.GetId(), secondValues[0].GetId(), ".second", []string{"second@example.com"}, "read", map[string]string{"search": searchToken}) byFirstNS, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ NamespaceId: firstNS.GetId(), @@ -1137,7 +1156,7 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchEmptyQuery_Succeed suffix := time.Now().UnixNano() ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{fmt.Sprintf("empty-search-%d", suffix)}) defer s.deleteSearchSubjectMappingNamespace(ns) - s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".empty-search", []string{"empty-search@example.com"}, "read") + s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".empty-search", []string{"empty-search@example.com"}, "read", nil) noSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ NamespaceId: ns.GetId(), @@ -1168,9 +1187,9 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchPaginationAppliesA if i > 0 { time.Sleep(5 * time.Millisecond) } - ids[i] = s.createSearchSubjectMapping(ns.GetId(), values[i].GetId(), fmt.Sprintf(".page-%d", i), []string{fmt.Sprintf("page-%d@example.com", i)}, searchToken).GetId() + ids[i] = s.createSearchSubjectMapping(ns.GetId(), values[i].GetId(), fmt.Sprintf(".page-%d", i), []string{fmt.Sprintf("page-%d@example.com", i)}, "read", map[string]string{"search": searchToken}).GetId() } - s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), ".page-other", []string{"page-other@example.com"}, "read") + s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), ".page-other", []string{"page-other@example.com"}, "read", map[string]string{"other": fmt.Sprintf("other-%d", suffix)}) firstPage, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ NamespaceId: ns.GetId(), @@ -3271,11 +3290,13 @@ func (s *SubjectMappingsSuite) createSearchSubjectMapping( selector string, externalValues []string, actionName string, + labels map[string]string, ) *policy.SubjectMapping { created, err := s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{ NamespaceId: namespaceID, AttributeValueId: attributeValueID, Actions: []*policy.Action{{Name: actionName}}, + Metadata: &common.MetadataMutable{Labels: labels}, NewSubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ SubjectSets: []*policy.SubjectSet{ { diff --git a/service/policy/db/queries/subject_mappings.sql b/service/policy/db/queries/subject_mappings.sql index da00d16370..07a2a73e20 100644 --- a/service/policy/db/queries/subject_mappings.sql +++ b/service/policy/db/queries/subject_mappings.sql @@ -121,7 +121,6 @@ subject_actions AS ( FROM subject_mappings sm LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id - LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL WHERE @@ -130,41 +129,17 @@ subject_actions AS ( OR sm.namespace_id = sqlc.narg('namespace_id')::uuid OR sm_ns_fqns.fqn = sqlc.narg('namespace_fqn')::text ) - AND ( - sqlc.narg('search')::TEXT IS NULL - OR LOWER(fqns.fqn) LIKE sqlc.narg('search')::TEXT ESCAPE '\' - OR EXISTS ( - SELECT 1 - FROM subject_mapping_actions search_sma - JOIN actions search_a ON search_a.id = search_sma.action_id - WHERE search_sma.subject_mapping_id = sm.id - AND LOWER(search_a.name) LIKE sqlc.narg('search')::TEXT ESCAPE '\' - ) - OR EXISTS ( - SELECT 1 - FROM JSONB_ARRAY_ELEMENTS(COALESCE(scs.condition, '[]'::JSONB)) AS ss(subject_set) - CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS( - COALESCE(ss.subject_set->'conditionGroups', ss.subject_set->'condition_groups', '[]'::JSONB) - ) AS cg(condition_group) - CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(COALESCE(cg.condition_group->'conditions', '[]'::JSONB)) AS con(condition) - WHERE - LOWER(con.condition->>'subjectExternalSelectorValue') LIKE sqlc.narg('search')::TEXT ESCAPE '\' - OR LOWER(con.condition->>'subject_external_selector_value') LIKE sqlc.narg('search')::TEXT ESCAPE '\' - OR EXISTS ( - SELECT 1 - FROM JSONB_ARRAY_ELEMENTS_TEXT( - COALESCE( - con.condition->'subjectExternalValues', - con.condition->'subject_external_values', - con.condition->'subjectExternalSelectorValues', - con.condition->'subject_external_selector_values', - '[]'::JSONB - ) - ) AS external_value(value) - WHERE LOWER(external_value.value) LIKE sqlc.narg('search')::TEXT ESCAPE '\' - ) + AND CASE + WHEN sqlc.narg('search')::TEXT IS NULL THEN TRUE + ELSE ( + fqns.fqn ILIKE sqlc.narg('search')::TEXT ESCAPE '\' + OR EXISTS ( + SELECT 1 + FROM JSONB_EACH_TEXT(COALESCE(sm.metadata -> 'labels', '{}'::JSONB)) AS label(key, value) + WHERE label.value ILIKE sqlc.narg('search')::TEXT ESCAPE '\' + ) ) - ) + END ), counted AS ( SELECT COUNT(id) AS total FROM filtered_subject_mappings diff --git a/service/policy/db/subject_mappings.go b/service/policy/db/subject_mappings.go index 1b2f45ea9f..04fd2fb45c 100644 --- a/service/policy/db/subject_mappings.go +++ b/service/policy/db/subject_mappings.go @@ -361,7 +361,7 @@ func (c PolicyDBClient) ListSubjectMappings(ctx context.Context, r *subjectmappi } sortField, sortDirection := GetSubjectMappingsSortParams(r.GetSort()) - search := pgtypeSubjectMappingSearchPattern(r.GetSearch().GetTerm()) + search := pgtypeSubstringSearchPattern(r.GetSearch().GetTerm()) list, err := c.queries.listSubjectMappings(ctx, listSubjectMappingsParams{ NamespaceID: pgtypeUUID(r.GetNamespaceId()), @@ -442,13 +442,6 @@ func (c PolicyDBClient) ListSubjectMappings(ctx context.Context, r *subjectmappi }, nil } -func pgtypeSubjectMappingSearchPattern(query string) pgtype.Text { - if query == "" { - return pgtype.Text{} - } - return pgtypeText("%" + escapeLikePattern(strings.ToLower(query)) + "%") -} - // Mutates provided fields and returns the updated subject mapping func (c PolicyDBClient) UpdateSubjectMapping(ctx context.Context, r *subjectmapping.UpdateSubjectMappingRequest) (*policy.SubjectMapping, error) { id := r.GetId() diff --git a/service/policy/db/subject_mappings.sql.go b/service/policy/db/subject_mappings.sql.go index e51c8e831a..a360e16794 100644 --- a/service/policy/db/subject_mappings.sql.go +++ b/service/policy/db/subject_mappings.sql.go @@ -101,8 +101,7 @@ type createSubjectMappingParams struct { // ) // SELECT id FROM inserted_mapping func (q *Queries) createSubjectMapping(ctx context.Context, arg createSubjectMappingParams) (string, error) { - row := q.db.QueryRow( - ctx, createSubjectMapping, + row := q.db.QueryRow(ctx, createSubjectMapping, arg.AttributeValueID, arg.Metadata, arg.SubjectConditionSetID, @@ -460,8 +459,7 @@ type listSubjectConditionSetsRow struct { // LIMIT $5 // OFFSET $4 func (q *Queries) listSubjectConditionSets(ctx context.Context, arg listSubjectConditionSetsParams) ([]listSubjectConditionSetsRow, error) { - rows, err := q.db.Query( - ctx, listSubjectConditionSets, + rows, err := q.db.Query(ctx, listSubjectConditionSets, arg.NamespaceID, arg.NamespaceFqn, arg.Search, @@ -530,7 +528,6 @@ subject_actions AS ( FROM subject_mappings sm LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id - LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL WHERE @@ -539,41 +536,17 @@ subject_actions AS ( OR sm.namespace_id = $5::uuid OR sm_ns_fqns.fqn = $6::text ) - AND ( - $7::TEXT IS NULL - OR LOWER(fqns.fqn) LIKE $7::TEXT ESCAPE '\' - OR EXISTS ( - SELECT 1 - FROM subject_mapping_actions search_sma - JOIN actions search_a ON search_a.id = search_sma.action_id - WHERE search_sma.subject_mapping_id = sm.id - AND LOWER(search_a.name) LIKE $7::TEXT ESCAPE '\' - ) - OR EXISTS ( - SELECT 1 - FROM JSONB_ARRAY_ELEMENTS(COALESCE(scs.condition, '[]'::JSONB)) AS ss(subject_set) - CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS( - COALESCE(ss.subject_set->'conditionGroups', ss.subject_set->'condition_groups', '[]'::JSONB) - ) AS cg(condition_group) - CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(COALESCE(cg.condition_group->'conditions', '[]'::JSONB)) AS con(condition) - WHERE - LOWER(con.condition->>'subjectExternalSelectorValue') LIKE $7::TEXT ESCAPE '\' - OR LOWER(con.condition->>'subject_external_selector_value') LIKE $7::TEXT ESCAPE '\' - OR EXISTS ( - SELECT 1 - FROM JSONB_ARRAY_ELEMENTS_TEXT( - COALESCE( - con.condition->'subjectExternalValues', - con.condition->'subject_external_values', - con.condition->'subjectExternalSelectorValues', - con.condition->'subject_external_selector_values', - '[]'::JSONB - ) - ) AS external_value(value) - WHERE LOWER(external_value.value) LIKE $7::TEXT ESCAPE '\' - ) + AND CASE + WHEN $7::TEXT IS NULL THEN TRUE + ELSE ( + fqns.fqn ILIKE $7::TEXT ESCAPE '\' + OR EXISTS ( + SELECT 1 + FROM JSONB_EACH_TEXT(COALESCE(sm.metadata -> 'labels', '{}'::JSONB)) AS label(key, value) + WHERE label.value ILIKE $7::TEXT ESCAPE '\' + ) ) - ) + END ), counted AS ( SELECT COUNT(id) AS total FROM filtered_subject_mappings @@ -696,7 +669,6 @@ type listSubjectMappingsRow struct { // FROM subject_mappings sm // LEFT JOIN attribute_values av ON sm.attribute_value_id = av.id // LEFT JOIN attribute_fqns fqns ON av.id = fqns.value_id -// LEFT JOIN subject_condition_set scs ON scs.id = sm.subject_condition_set_id // LEFT JOIN attribute_namespaces sm_ns ON sm_ns.id = sm.namespace_id // LEFT JOIN attribute_fqns sm_ns_fqns ON sm_ns_fqns.namespace_id = sm_ns.id AND sm_ns_fqns.attribute_id IS NULL AND sm_ns_fqns.value_id IS NULL // WHERE @@ -705,41 +677,17 @@ type listSubjectMappingsRow struct { // OR sm.namespace_id = $5::uuid // OR sm_ns_fqns.fqn = $6::text // ) -// AND ( -// $7::TEXT IS NULL -// OR LOWER(fqns.fqn) LIKE $7::TEXT ESCAPE '\' -// OR EXISTS ( -// SELECT 1 -// FROM subject_mapping_actions search_sma -// JOIN actions search_a ON search_a.id = search_sma.action_id -// WHERE search_sma.subject_mapping_id = sm.id -// AND LOWER(search_a.name) LIKE $7::TEXT ESCAPE '\' -// ) -// OR EXISTS ( -// SELECT 1 -// FROM JSONB_ARRAY_ELEMENTS(COALESCE(scs.condition, '[]'::JSONB)) AS ss(subject_set) -// CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS( -// COALESCE(ss.subject_set->'conditionGroups', ss.subject_set->'condition_groups', '[]'::JSONB) -// ) AS cg(condition_group) -// CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(COALESCE(cg.condition_group->'conditions', '[]'::JSONB)) AS con(condition) -// WHERE -// LOWER(con.condition->>'subjectExternalSelectorValue') LIKE $7::TEXT ESCAPE '\' -// OR LOWER(con.condition->>'subject_external_selector_value') LIKE $7::TEXT ESCAPE '\' -// OR EXISTS ( -// SELECT 1 -// FROM JSONB_ARRAY_ELEMENTS_TEXT( -// COALESCE( -// con.condition->'subjectExternalValues', -// con.condition->'subject_external_values', -// con.condition->'subjectExternalSelectorValues', -// con.condition->'subject_external_selector_values', -// '[]'::JSONB -// ) -// ) AS external_value(value) -// WHERE LOWER(external_value.value) LIKE $7::TEXT ESCAPE '\' -// ) +// AND CASE +// WHEN $7::TEXT IS NULL THEN TRUE +// ELSE ( +// fqns.fqn ILIKE $7::TEXT ESCAPE '\' +// OR EXISTS ( +// SELECT 1 +// FROM JSONB_EACH_TEXT(COALESCE(sm.metadata -> 'labels', '{}'::JSONB)) AS label(key, value) +// WHERE label.value ILIKE $7::TEXT ESCAPE '\' +// ) // ) -// ) +// END // ), counted AS ( // SELECT COUNT(id) AS total // FROM filtered_subject_mappings @@ -802,8 +750,7 @@ type listSubjectMappingsRow struct { // LIMIT $2 // OFFSET $1 func (q *Queries) listSubjectMappings(ctx context.Context, arg listSubjectMappingsParams) ([]listSubjectMappingsRow, error) { - rows, err := q.db.Query( - ctx, listSubjectMappings, + rows, err := q.db.Query(ctx, listSubjectMappings, arg.Offset, arg.Limit, arg.SortField, @@ -1112,8 +1059,7 @@ type updateSubjectMappingParams struct { // SELECT cnt // FROM update_count func (q *Queries) updateSubjectMapping(ctx context.Context, arg updateSubjectMappingParams) (int64, error) { - result, err := q.db.Exec( - ctx, updateSubjectMapping, + result, err := q.db.Exec(ctx, updateSubjectMapping, arg.Metadata, arg.SubjectConditionSetID, arg.ID, From 1a8d75b0ea3d63425e6d2e3eb4687c9fc2dc8ea9 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Wed, 3 Jun 2026 08:57:00 -0500 Subject: [PATCH 3/4] test(policy): address subject mapping search comments Signed-off-by: Chris Reed --- service/integration/subject_mappings_test.go | 87 +++++++++++++------ .../policy/db/queries/subject_mappings.sql | 2 +- service/policy/db/subject_mappings.sql.go | 4 +- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/service/integration/subject_mappings_test.go b/service/integration/subject_mappings_test.go index 72fd85ccfe..7814752655 100644 --- a/service/integration/subject_mappings_test.go +++ b/service/integration/subject_mappings_test.go @@ -1043,12 +1043,13 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_NoNamespaceFilter_Return func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByAttributeValueFQNAndLabels_Succeeds() { suffix := time.Now().UnixNano() + attrName := fmt.Sprintf("list-sm-search-attr-%d", suffix) + valueToken := fmt.Sprintf("fqn-only-%d", suffix) ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{ - fmt.Sprintf("fqn-only-%d", suffix), + valueToken, fmt.Sprintf("label-holder-%d", suffix), fmt.Sprintf("label-key-holder-%d", suffix), fmt.Sprintf("condition-holder-%d", suffix), - fmt.Sprintf("wildcarda-%d", suffix), fmt.Sprintf("unmatched-%d", suffix), }) defer s.deleteSearchSubjectMappingNamespace(ns) @@ -1060,19 +1061,23 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByAttributeValueFQ externalToken := fmt.Sprintf("external-only-%d@example.com", suffix) fqnSM := s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".fqn-holder", []string{"fqn-holder@example.com"}, "read", nil) - labelSM := s.createSearchSubjectMapping(ns.GetId(), values[1].GetId(), ".label-holder", []string{"label-holder@example.com"}, actionToken, map[string]string{"search-label": labelToken}) - s.createSearchSubjectMapping(ns.GetId(), values[2].GetId(), ".label-key-holder", []string{"label-key-holder@example.com"}, "read", map[string]string{labelKeyToken: "not-searchable-by-key"}) - s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), selectorToken, []string{externalToken}, "read", nil) - s.createSearchSubjectMapping(ns.GetId(), values[4].GetId(), ".wildcard-holder", []string{fmt.Sprintf("wildcarda-%d@example.com", suffix)}, "read", map[string]string{"wildcard": fmt.Sprintf("wildcarda-%d", suffix)}) - s.createSearchSubjectMapping(ns.GetId(), values[5].GetId(), ".unmatched", []string{"unmatched@example.com"}, "read", map[string]string{"unused": fmt.Sprintf("unused-%d", suffix)}) + labelSM := s.createSearchSubjectMapping(ns.GetId(), values[1].GetId(), ".label-holder", []string{"label-holder@example.com"}, actionToken, map[string]string{ + "search-label": labelToken, + "other-label": fmt.Sprintf("other-label-%d", suffix), + }) + labelKeySM := s.createSearchSubjectMapping(ns.GetId(), values[2].GetId(), ".label-key-holder", []string{"label-key-holder@example.com"}, "read", map[string]string{labelKeyToken: "not-searchable-by-key"}) + conditionSM := s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), selectorToken, []string{externalToken}, "read", nil) + unmatchedSM := s.createSearchSubjectMapping(ns.GetId(), values[4].GetId(), ".unmatched", []string{"unmatched@example.com"}, "read", map[string]string{"unused": fmt.Sprintf("unused-%d", suffix)}) tests := []struct { name string term string - id string + ids []string }{ - {name: "attribute value fqn", term: strings.ToUpper(values[0].GetFqn()), id: fqnSM.GetId()}, - {name: "metadata label value", term: strings.ToUpper(labelToken), id: labelSM.GetId()}, + {name: "attribute value fqn", term: strings.ToUpper(values[0].GetFqn()), ids: []string{fqnSM.GetId()}}, + {name: "attribute name in fqn", term: attrName, ids: []string{fqnSM.GetId(), labelSM.GetId(), labelKeySM.GetId(), conditionSM.GetId(), unmatchedSM.GetId()}}, + {name: "attribute value in fqn", term: valueToken, ids: []string{fqnSM.GetId()}}, + {name: "metadata label value among multiple labels", term: strings.ToUpper(labelToken), ids: []string{labelSM.GetId()}}, } for _, tc := range tests { @@ -1082,9 +1087,12 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByAttributeValueFQ Search: &policy.Search{Term: tc.term}, }) s.Require().NoError(err) - s.Require().Len(list.GetSubjectMappings(), 1) - s.Equal(tc.id, list.GetSubjectMappings()[0].GetId()) - s.Equal(int32(1), list.GetPagination().GetTotal()) + gotIDs := make([]string, 0, len(list.GetSubjectMappings())) + for _, sm := range list.GetSubjectMappings() { + gotIDs = append(gotIDs, sm.GetId()) + } + s.ElementsMatch(tc.ids, gotIDs) + s.Equal(int32(len(tc.ids)), list.GetPagination().GetTotal()) }) } @@ -1102,23 +1110,51 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByAttributeValueFQ s.Empty(list.GetSubjectMappings()) s.Equal(int32(0), list.GetPagination().GetTotal()) } +} - wildcardSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ - NamespaceId: ns.GetId(), - Search: &policy.Search{Term: fmt.Sprintf("wildcard_-%d", suffix)}, - }) - s.Require().NoError(err) - s.Empty(wildcardSearch.GetSubjectMappings()) - s.Equal(int32(0), wildcardSearch.GetPagination().GetTotal()) +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchTrimsWhitespace_Succeeds() { + suffix := time.Now().UnixNano() + labelToken := fmt.Sprintf("trimmed-label-%d", suffix) + + ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{fmt.Sprintf("trimmed-%d", suffix)}) + defer s.deleteSearchSubjectMappingNamespace(ns) + + sm := s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".trimmed", []string{"trimmed@example.com"}, "read", map[string]string{"search-label": labelToken}) - spacePaddedSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + list, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ NamespaceId: ns.GetId(), Search: &policy.Search{Term: " " + labelToken + " "}, }) s.Require().NoError(err) - s.Require().Len(spacePaddedSearch.GetSubjectMappings(), 1) - s.Equal(labelSM.GetId(), spacePaddedSearch.GetSubjectMappings()[0].GetId()) - s.Equal(int32(1), spacePaddedSearch.GetPagination().GetTotal()) + s.Require().Len(list.GetSubjectMappings(), 1) + s.Equal(sm.GetId(), list.GetSubjectMappings()[0].GetId()) + s.Equal(int32(1), list.GetPagination().GetTotal()) +} + +func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchEscapesLikeWildcardLiterals_Succeeds() { + suffix := time.Now().UnixNano() + + ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{ + fmt.Sprintf("wildcarda-%d", suffix), + fmt.Sprintf("wildcardb-%d", suffix), + }) + defer s.deleteSearchSubjectMappingNamespace(ns) + + s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".wildcard-a", []string{"wildcard-a@example.com"}, "read", map[string]string{"wildcard": fmt.Sprintf("wildcarda-%d", suffix)}) + s.createSearchSubjectMapping(ns.GetId(), values[1].GetId(), ".wildcard-b", []string{"wildcard-b@example.com"}, "read", map[string]string{"wildcard": fmt.Sprintf("wildcardb-%d", suffix)}) + + for _, term := range []string{ + fmt.Sprintf("wildcard_-%d", suffix), + fmt.Sprintf("wildcard%%-%d", suffix), + } { + list, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ + NamespaceId: ns.GetId(), + Search: &policy.Search{Term: term}, + }) + s.Require().NoError(err) + s.Empty(list.GetSubjectMappings()) + s.Equal(int32(0), list.GetPagination().GetTotal()) + } } func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchCombinesWithNamespace_Succeeds() { @@ -1184,9 +1220,6 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchPaginationAppliesA ids := make([]string, 3) for i := range ids { - if i > 0 { - time.Sleep(5 * time.Millisecond) - } ids[i] = s.createSearchSubjectMapping(ns.GetId(), values[i].GetId(), fmt.Sprintf(".page-%d", i), []string{fmt.Sprintf("page-%d@example.com", i)}, "read", map[string]string{"search": searchToken}).GetId() } s.createSearchSubjectMapping(ns.GetId(), values[3].GetId(), ".page-other", []string{"page-other@example.com"}, "read", map[string]string{"other": fmt.Sprintf("other-%d", suffix)}) diff --git a/service/policy/db/queries/subject_mappings.sql b/service/policy/db/queries/subject_mappings.sql index 07a2a73e20..c82c6b6211 100644 --- a/service/policy/db/queries/subject_mappings.sql +++ b/service/policy/db/queries/subject_mappings.sql @@ -132,7 +132,7 @@ subject_actions AS ( AND CASE WHEN sqlc.narg('search')::TEXT IS NULL THEN TRUE ELSE ( - fqns.fqn ILIKE sqlc.narg('search')::TEXT ESCAPE '\' + fqns.fqn LIKE sqlc.narg('search')::TEXT ESCAPE '\' OR EXISTS ( SELECT 1 FROM JSONB_EACH_TEXT(COALESCE(sm.metadata -> 'labels', '{}'::JSONB)) AS label(key, value) diff --git a/service/policy/db/subject_mappings.sql.go b/service/policy/db/subject_mappings.sql.go index a360e16794..ea5411c3e6 100644 --- a/service/policy/db/subject_mappings.sql.go +++ b/service/policy/db/subject_mappings.sql.go @@ -539,7 +539,7 @@ subject_actions AS ( AND CASE WHEN $7::TEXT IS NULL THEN TRUE ELSE ( - fqns.fqn ILIKE $7::TEXT ESCAPE '\' + fqns.fqn LIKE $7::TEXT ESCAPE '\' OR EXISTS ( SELECT 1 FROM JSONB_EACH_TEXT(COALESCE(sm.metadata -> 'labels', '{}'::JSONB)) AS label(key, value) @@ -680,7 +680,7 @@ type listSubjectMappingsRow struct { // AND CASE // WHEN $7::TEXT IS NULL THEN TRUE // ELSE ( -// fqns.fqn ILIKE $7::TEXT ESCAPE '\' +// fqns.fqn LIKE $7::TEXT ESCAPE '\' // OR EXISTS ( // SELECT 1 // FROM JSONB_EACH_TEXT(COALESCE(sm.metadata -> 'labels', '{}'::JSONB)) AS label(key, value) From 7dacd7fbbac221238c1ca660b93ee1c52a0d3ece Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Wed, 3 Jun 2026 10:02:07 -0500 Subject: [PATCH 4/4] address comments. --- service/integration/subject_mappings_test.go | 31 +++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/service/integration/subject_mappings_test.go b/service/integration/subject_mappings_test.go index 7814752655..ae3c03bfdf 100644 --- a/service/integration/subject_mappings_test.go +++ b/service/integration/subject_mappings_test.go @@ -1052,7 +1052,9 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByAttributeValueFQ fmt.Sprintf("condition-holder-%d", suffix), fmt.Sprintf("unmatched-%d", suffix), }) - defer s.deleteSearchSubjectMappingNamespace(ns) + s.T().Cleanup(func() { + s.deleteSearchSubjectMappingNamespace(ns) + }) actionToken := fmt.Sprintf("action-only-%d", suffix) labelToken := fmt.Sprintf("label-only-%d", suffix) @@ -1096,6 +1098,9 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchByAttributeValueFQ }) } + // ListSubjectMappings search is intentionally limited to attribute value FQNs + // and metadata label values; action names, label keys, and subject condition + // selectors/external values should not affect filtered results. for _, term := range []string{ actionToken, labelKeyToken, @@ -1117,7 +1122,9 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchTrimsWhitespace_Su labelToken := fmt.Sprintf("trimmed-label-%d", suffix) ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{fmt.Sprintf("trimmed-%d", suffix)}) - defer s.deleteSearchSubjectMappingNamespace(ns) + s.T().Cleanup(func() { + s.deleteSearchSubjectMappingNamespace(ns) + }) sm := s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".trimmed", []string{"trimmed@example.com"}, "read", map[string]string{"search-label": labelToken}) @@ -1138,7 +1145,9 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchEscapesLikeWildcar fmt.Sprintf("wildcarda-%d", suffix), fmt.Sprintf("wildcardb-%d", suffix), }) - defer s.deleteSearchSubjectMappingNamespace(ns) + s.T().Cleanup(func() { + s.deleteSearchSubjectMappingNamespace(ns) + }) s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".wildcard-a", []string{"wildcard-a@example.com"}, "read", map[string]string{"wildcard": fmt.Sprintf("wildcarda-%d", suffix)}) s.createSearchSubjectMapping(ns.GetId(), values[1].GetId(), ".wildcard-b", []string{"wildcard-b@example.com"}, "read", map[string]string{"wildcard": fmt.Sprintf("wildcardb-%d", suffix)}) @@ -1162,9 +1171,13 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchCombinesWithNamesp searchToken := fmt.Sprintf("namespace-and-search-%d", suffix) firstNS, firstValues := s.createSearchSubjectMappingNamespace(suffix, []string{fmt.Sprintf("first-%d", suffix)}) - defer s.deleteSearchSubjectMappingNamespace(firstNS) + s.T().Cleanup(func() { + s.deleteSearchSubjectMappingNamespace(firstNS) + }) secondNS, secondValues := s.createSearchSubjectMappingNamespace(suffix+1, []string{fmt.Sprintf("second-%d", suffix)}) - defer s.deleteSearchSubjectMappingNamespace(secondNS) + s.T().Cleanup(func() { + s.deleteSearchSubjectMappingNamespace(secondNS) + }) firstSM := s.createSearchSubjectMapping(firstNS.GetId(), firstValues[0].GetId(), ".first", []string{"first@example.com"}, "read", map[string]string{"search": searchToken}) secondSM := s.createSearchSubjectMapping(secondNS.GetId(), secondValues[0].GetId(), ".second", []string{"second@example.com"}, "read", map[string]string{"search": searchToken}) @@ -1191,7 +1204,9 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchCombinesWithNamesp func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchEmptyQuery_Succeeds() { suffix := time.Now().UnixNano() ns, values := s.createSearchSubjectMappingNamespace(suffix, []string{fmt.Sprintf("empty-search-%d", suffix)}) - defer s.deleteSearchSubjectMappingNamespace(ns) + s.T().Cleanup(func() { + s.deleteSearchSubjectMappingNamespace(ns) + }) s.createSearchSubjectMapping(ns.GetId(), values[0].GetId(), ".empty-search", []string{"empty-search@example.com"}, "read", nil) noSearch, err := s.db.PolicyClient.ListSubjectMappings(s.ctx, &subjectmapping.ListSubjectMappingsRequest{ @@ -1216,7 +1231,9 @@ func (s *SubjectMappingsSuite) Test_ListSubjectMappings_SearchPaginationAppliesA fmt.Sprintf("page-c-%d", suffix), fmt.Sprintf("page-other-%d", suffix), }) - defer s.deleteSearchSubjectMappingNamespace(ns) + s.T().Cleanup(func() { + s.deleteSearchSubjectMappingNamespace(ns) + }) ids := make([]string, 3) for i := range ids {