From 12f9074d4193d1d8b742f07deb74ca52269b990d Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Mon, 13 Jul 2026 15:36:46 -0700 Subject: [PATCH 1/3] test(policy): cover namespaced subject mapping validation Signed-off-by: jakedoublev --- .../subjectmapping/subject_mapping_test.go | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/service/policy/subjectmapping/subject_mapping_test.go b/service/policy/subjectmapping/subject_mapping_test.go index 5917a050a4..421aac3d1c 100644 --- a/service/policy/subjectmapping/subject_mapping_test.go +++ b/service/policy/subjectmapping/subject_mapping_test.go @@ -4,8 +4,11 @@ import ( "testing" "buf.build/go/protovalidate" + "connectrpc.com/connect" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" + "github.com/opentdf/platform/service/logger" + policyconfig "github.com/opentdf/platform/service/policy/config" "github.com/stretchr/testify/require" ) @@ -37,6 +40,62 @@ var validActions = []*policy.Action{ }, } +func newNamespacedSubjectMappingService() SubjectMappingService { + return SubjectMappingService{ + logger: logger.CreateTestLogger(), + config: &policyconfig.Config{ + NamespacedPolicy: true, + }, + } +} + +func validSubjectConditionSetCreate() *subjectmapping.SubjectConditionSetCreate { + return &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + Conditions: []*policy.Condition{ + { + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalSelectorValue: ".some_field", + SubjectExternalValues: []string{"some_value"}, + }, + }, + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR, + }, + }, + }, + }, + } +} + +func Test_CreateSubjectMapping_NamespacedPolicyRequiresNamespace(t *testing.T) { + service := newNamespacedSubjectMappingService() + + _, err := service.CreateSubjectMapping(t.Context(), connect.NewRequest(&subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: fakeID, + Actions: validActions, + NewSubjectConditionSet: validSubjectConditionSetCreate(), + })) + + require.Error(t, err) + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) + require.Contains(t, err.Error(), "either namespace_id or namespace_fqn must be provided") +} + +func Test_CreateSubjectConditionSet_NamespacedPolicyRequiresNamespace(t *testing.T) { + service := newNamespacedSubjectMappingService() + + _, err := service.CreateSubjectConditionSet(t.Context(), connect.NewRequest(&subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: validSubjectConditionSetCreate(), + })) + + require.Error(t, err) + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) + require.Contains(t, err.Error(), "either namespace_id or namespace_fqn must be provided") +} + func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testing.T) { testCases := []struct { name string From d6013c679d80d09ac4a18f2b52fd4bf736c5c2c4 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 14 Jul 2026 12:58:26 -0700 Subject: [PATCH 2/3] test(policy): cover namespaced create validation Signed-off-by: jakedoublev --- service/policy/actions/actions_test.go | 18 ++++++++++++++++++ .../registered_resources_test.go | 18 ++++++++++++++++++ .../resourcemapping/resource_mapping_test.go | 19 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/service/policy/actions/actions_test.go b/service/policy/actions/actions_test.go index fd5daf39ef..8f35935f10 100644 --- a/service/policy/actions/actions_test.go +++ b/service/policy/actions/actions_test.go @@ -5,9 +5,12 @@ import ( "testing" "buf.build/go/protovalidate" + "connectrpc.com/connect" "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/service/logger" + policyconfig "github.com/opentdf/platform/service/policy/config" "github.com/stretchr/testify/suite" ) @@ -58,6 +61,21 @@ func TestActionServiceProtos(t *testing.T) { suite.Run(t, new(ActionSuite)) } +func (s *ActionSuite) Test_CreateAction_NamespacedPolicyRequiresNamespace() { + service := &ActionService{ + logger: logger.CreateTestLogger(), + config: &policyconfig.Config{NamespacedPolicy: true}, + } + + _, err := service.CreateAction(s.T().Context(), connect.NewRequest(&actions.CreateActionRequest{ + Name: "valid_name", + })) + + s.Require().Error(err) + s.Require().Equal(connect.CodeInvalidArgument, connect.CodeOf(err)) + s.Require().Contains(err.Error(), "either namespace_id or namespace_fqn must be provided") +} + func (s *ActionSuite) Test_CreateActionRequest_Fails() { for _, name := range actionNamesInvalidFormat { s.Run(name, func() { diff --git a/service/policy/registeredresources/registered_resources_test.go b/service/policy/registeredresources/registered_resources_test.go index 78ace16fc8..67f5a316a7 100644 --- a/service/policy/registeredresources/registered_resources_test.go +++ b/service/policy/registeredresources/registered_resources_test.go @@ -5,8 +5,11 @@ import ( "testing" "buf.build/go/protovalidate" + "connectrpc.com/connect" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/service/logger" + policyconfig "github.com/opentdf/platform/service/policy/config" "github.com/stretchr/testify/suite" ) @@ -27,6 +30,21 @@ func TestRegisteredResourcesServiceProtos(t *testing.T) { suite.Run(t, new(RegisteredResourcesSuite)) } +func (s *RegisteredResourcesSuite) TestCreateRegisteredResource_NamespacedPolicyRequiresNamespace() { + service := &RegisteredResourcesService{ + logger: logger.CreateTestLogger(), + config: &policyconfig.Config{NamespacedPolicy: true}, + } + + _, err := service.CreateRegisteredResource(s.T().Context(), connect.NewRequest(®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + })) + + s.Require().Error(err) + s.Require().Equal(connect.CodeInvalidArgument, connect.CodeOf(err)) + s.Require().Contains(err.Error(), "namespace is required: provide either namespace_id or namespace_fqn") +} + const ( validName = "name" validValue = "value" diff --git a/service/policy/resourcemapping/resource_mapping_test.go b/service/policy/resourcemapping/resource_mapping_test.go index efbaebc43e..d6f8748831 100644 --- a/service/policy/resourcemapping/resource_mapping_test.go +++ b/service/policy/resourcemapping/resource_mapping_test.go @@ -4,7 +4,10 @@ import ( "testing" "buf.build/go/protovalidate" + "connectrpc.com/connect" "github.com/opentdf/platform/protocol/go/policy/resourcemapping" + "github.com/opentdf/platform/service/logger" + policyconfig "github.com/opentdf/platform/service/policy/config" "github.com/stretchr/testify/require" ) @@ -60,6 +63,22 @@ func getValidator() protovalidate.Validator { return v } +func Test_CreateResourceMapping_NamespacedPolicyRequiresNamespace(t *testing.T) { + service := ResourceMappingService{ + logger: logger.CreateTestLogger(), + config: &policyconfig.Config{NamespacedPolicy: true}, + } + + _, err := service.CreateResourceMapping(t.Context(), connect.NewRequest(&resourcemapping.CreateResourceMappingRequest{ + AttributeValueId: validUUID, + Terms: []string{"term1"}, + })) + + require.Error(t, err) + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) + require.Contains(t, err.Error(), "namespace is required: provide either namespace_id, namespace_fqn, or group_id") +} + func getMaxTerms() []string { maxTerms := make([]string, 1000) for i := range maxTerms { From 5ad1b60e31e32f3ff7c9852c8ef8abb698a53dc2 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 14 Jul 2026 13:12:13 -0700 Subject: [PATCH 3/3] test(policy): assert namespaced validation errors Signed-off-by: jakedoublev --- service/policy/actions/actions.go | 4 +++- service/policy/actions/actions_test.go | 2 +- service/policy/registeredresources/registered_resources.go | 4 +++- .../policy/registeredresources/registered_resources_test.go | 2 +- service/policy/resourcemapping/resource_mapping.go | 4 +++- service/policy/resourcemapping/resource_mapping_test.go | 2 +- service/policy/subjectmapping/subject_mapping.go | 6 ++++-- service/policy/subjectmapping/subject_mapping_test.go | 4 ++-- 8 files changed, 18 insertions(+), 10 deletions(-) diff --git a/service/policy/actions/actions.go b/service/policy/actions/actions.go index 6c4f5ca5e9..8fda83a8ab 100644 --- a/service/policy/actions/actions.go +++ b/service/policy/actions/actions.go @@ -37,6 +37,8 @@ type ActionService struct { config *policyconfig.Config } +var errNamespacedPolicyNamespaceRequired = errors.New("either namespace_id or namespace_fqn must be provided") + func OnConfigUpdate(actionsSvc *ActionService) serviceregistry.OnConfigUpdateHook { return func(_ context.Context, cfg config.ServiceConfig) error { sharedCfg, err := policyconfig.GetSharedPolicyConfig(cfg) @@ -114,7 +116,7 @@ func (a *ActionService) ListActions(ctx context.Context, req *connect.Request[ac func (a *ActionService) CreateAction(ctx context.Context, req *connect.Request[actions.CreateActionRequest]) (*connect.Response[actions.CreateActionResponse], error) { a.logger.DebugContext(ctx, "creating action", slog.String("name", req.Msg.GetName())) if a.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("either namespace_id or namespace_fqn must be provided")) + return nil, connect.NewError(connect.CodeInvalidArgument, errNamespacedPolicyNamespaceRequired) } auditParams := audit.PolicyEventParams{ diff --git a/service/policy/actions/actions_test.go b/service/policy/actions/actions_test.go index 8f35935f10..fc19b80e3a 100644 --- a/service/policy/actions/actions_test.go +++ b/service/policy/actions/actions_test.go @@ -73,7 +73,7 @@ func (s *ActionSuite) Test_CreateAction_NamespacedPolicyRequiresNamespace() { s.Require().Error(err) s.Require().Equal(connect.CodeInvalidArgument, connect.CodeOf(err)) - s.Require().Contains(err.Error(), "either namespace_id or namespace_fqn must be provided") + s.Require().ErrorIs(err, errNamespacedPolicyNamespaceRequired) } func (s *ActionSuite) Test_CreateActionRequest_Fails() { diff --git a/service/policy/registeredresources/registered_resources.go b/service/policy/registeredresources/registered_resources.go index 6fc98fbd8b..3b30dcfff8 100644 --- a/service/policy/registeredresources/registered_resources.go +++ b/service/policy/registeredresources/registered_resources.go @@ -25,6 +25,8 @@ type RegisteredResourcesService struct { //nolint:revive // RegisteredResourcesS config *policyconfig.Config } +var errNamespacedPolicyNamespaceRequired = errors.New("namespace is required: provide either namespace_id or namespace_fqn") + func OnConfigUpdate(s *RegisteredResourcesService) serviceregistry.OnConfigUpdateHook { return func(_ context.Context, cfg config.ServiceConfig) error { sharedCfg, err := policyconfig.GetSharedPolicyConfig(cfg) @@ -98,7 +100,7 @@ func (s *RegisteredResourcesService) CreateRegisteredResource(ctx context.Contex // --- BEGIN namespace enforcement (remove when enforce_namespace flag is phased out) --- if s.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("namespace is required: provide either namespace_id or namespace_fqn")) + return nil, connect.NewError(connect.CodeInvalidArgument, errNamespacedPolicyNamespaceRequired) } // --- END namespace enforcement --- diff --git a/service/policy/registeredresources/registered_resources_test.go b/service/policy/registeredresources/registered_resources_test.go index 67f5a316a7..2a1fd577cf 100644 --- a/service/policy/registeredresources/registered_resources_test.go +++ b/service/policy/registeredresources/registered_resources_test.go @@ -42,7 +42,7 @@ func (s *RegisteredResourcesSuite) TestCreateRegisteredResource_NamespacedPolicy s.Require().Error(err) s.Require().Equal(connect.CodeInvalidArgument, connect.CodeOf(err)) - s.Require().Contains(err.Error(), "namespace is required: provide either namespace_id or namespace_fqn") + s.Require().ErrorIs(err, errNamespacedPolicyNamespaceRequired) } const ( diff --git a/service/policy/resourcemapping/resource_mapping.go b/service/policy/resourcemapping/resource_mapping.go index 41d7334ca6..b171b6c11d 100644 --- a/service/policy/resourcemapping/resource_mapping.go +++ b/service/policy/resourcemapping/resource_mapping.go @@ -25,6 +25,8 @@ type ResourceMappingService struct { //nolint:revive // ResourceMappingService i config *policyconfig.Config } +var errNamespacedPolicyNamespaceRequired = errors.New("namespace is required: provide either namespace_id, namespace_fqn, or group_id") + func OnConfigUpdate(rmSvc *ResourceMappingService) serviceregistry.OnConfigUpdateHook { return func(_ context.Context, cfg config.ServiceConfig) error { sharedCfg, err := policyconfig.GetSharedPolicyConfig(cfg) @@ -262,7 +264,7 @@ func (s ResourceMappingService) CreateResourceMapping(ctx context.Context, // --- BEGIN namespace enforcement (remove when namespaced_policy flag is phased out) --- // A group implies a namespace, so a mapping assigned to a group satisfies the requirement. if s.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" && req.Msg.GetGroupId() == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("namespace is required: provide either namespace_id, namespace_fqn, or group_id")) + return nil, connect.NewError(connect.CodeInvalidArgument, errNamespacedPolicyNamespaceRequired) } // --- END namespace enforcement --- diff --git a/service/policy/resourcemapping/resource_mapping_test.go b/service/policy/resourcemapping/resource_mapping_test.go index d6f8748831..eba3383556 100644 --- a/service/policy/resourcemapping/resource_mapping_test.go +++ b/service/policy/resourcemapping/resource_mapping_test.go @@ -76,7 +76,7 @@ func Test_CreateResourceMapping_NamespacedPolicyRequiresNamespace(t *testing.T) require.Error(t, err) require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) - require.Contains(t, err.Error(), "namespace is required: provide either namespace_id, namespace_fqn, or group_id") + require.ErrorIs(t, err, errNamespacedPolicyNamespaceRequired) } func getMaxTerms() []string { diff --git a/service/policy/subjectmapping/subject_mapping.go b/service/policy/subjectmapping/subject_mapping.go index 554a3c3ab3..da9c3e180d 100644 --- a/service/policy/subjectmapping/subject_mapping.go +++ b/service/policy/subjectmapping/subject_mapping.go @@ -25,6 +25,8 @@ type SubjectMappingService struct { //nolint:revive // SubjectMappingService is config *policyconfig.Config } +var errNamespacedPolicyNamespaceRequired = errors.New("either namespace_id or namespace_fqn must be provided") + func OnConfigUpdate(smSvc *SubjectMappingService) serviceregistry.OnConfigUpdateHook { return func(_ context.Context, cfg config.ServiceConfig) error { sharedCfg, err := policyconfig.GetSharedPolicyConfig(cfg) @@ -85,7 +87,7 @@ func (s SubjectMappingService) CreateSubjectMapping(ctx context.Context, rsp := &sm.CreateSubjectMappingResponse{} s.logger.DebugContext(ctx, "creating subject mapping") if s.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("either namespace_id or namespace_fqn must be provided")) + return nil, connect.NewError(connect.CodeInvalidArgument, errNamespacedPolicyNamespaceRequired) } auditParams := audit.PolicyEventParams{ @@ -266,7 +268,7 @@ func (s SubjectMappingService) CreateSubjectConditionSet(ctx context.Context, rsp := &sm.CreateSubjectConditionSetResponse{} s.logger.DebugContext(ctx, "creating subject condition set", slog.Any("subject_condition_set", req.Msg)) if s.config.NamespacedPolicy && req.Msg.GetNamespaceId() == "" && req.Msg.GetNamespaceFqn() == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("either namespace_id or namespace_fqn must be provided")) + return nil, connect.NewError(connect.CodeInvalidArgument, errNamespacedPolicyNamespaceRequired) } auditParams := audit.PolicyEventParams{ diff --git a/service/policy/subjectmapping/subject_mapping_test.go b/service/policy/subjectmapping/subject_mapping_test.go index 421aac3d1c..6d99bd9fb5 100644 --- a/service/policy/subjectmapping/subject_mapping_test.go +++ b/service/policy/subjectmapping/subject_mapping_test.go @@ -81,7 +81,7 @@ func Test_CreateSubjectMapping_NamespacedPolicyRequiresNamespace(t *testing.T) { require.Error(t, err) require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) - require.Contains(t, err.Error(), "either namespace_id or namespace_fqn must be provided") + require.ErrorIs(t, err, errNamespacedPolicyNamespaceRequired) } func Test_CreateSubjectConditionSet_NamespacedPolicyRequiresNamespace(t *testing.T) { @@ -93,7 +93,7 @@ func Test_CreateSubjectConditionSet_NamespacedPolicyRequiresNamespace(t *testing require.Error(t, err) require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) - require.Contains(t, err.Error(), "either namespace_id or namespace_fqn must be provided") + require.ErrorIs(t, err, errNamespacedPolicyNamespaceRequired) } func Test_CreateSubjectMappingRequest_InvalidSubjectConditionSet_Fails(t *testing.T) {