Skip to content
Merged

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

228 changes: 228 additions & 0 deletions service/integration/registered_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,234 @@ func (s *RegisteredResourcesSuite) Test_SameNamespaceEnforcement_SameNamespace_S
s.Require().Len(resVal.GetActionAttributeValues(), 1)
}

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResourceValue_WithNamespacedCustomActionName_Succeeds() {
nsID := s.getNamespaceID("example.com")

res, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, &registeredresources.CreateRegisteredResourceRequest{
NamespaceId: nsID,
Name: fmt.Sprintf("test_rr_custom_action_name_%d", time.Now().UnixNano()),
})
s.Require().NoError(err)
s.NotNil(res)

customActionName := fmt.Sprintf("rr_custom_action_%d", time.Now().UnixNano())
customAction, err := s.db.PolicyClient.CreateAction(s.ctx, &pbActions.CreateActionRequest{
Name: customActionName,
NamespaceId: nsID,
})
s.Require().NoError(err)
s.NotNil(customAction)

resVal, err := s.db.PolicyClient.CreateRegisteredResourceValue(s.ctx, &registeredresources.CreateRegisteredResourceValueRequest{
ResourceId: res.GetId(),
Value: fmt.Sprintf("test_rr_custom_action_name_value_%d", time.Now().UnixNano()),
ActionAttributeValues: []*registeredresources.ActionAttributeValue{
{
ActionIdentifier: &registeredresources.ActionAttributeValue_ActionName{
ActionName: customActionName,
},
AttributeValueIdentifier: &registeredresources.ActionAttributeValue_AttributeValueFqn{
AttributeValueFqn: "https://example.com/attr/attr1/value/value1",
},
},
},
})
s.Require().NoError(err)
s.NotNil(resVal)
s.Require().Len(resVal.GetActionAttributeValues(), 1)
s.Equal(customAction.GetId(), resVal.GetActionAttributeValues()[0].GetAction().GetId())
s.Equal(customActionName, resVal.GetActionAttributeValues()[0].GetAction().GetName())
}

// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ namespace-optional tests │
// │ Remove this section when enforce_namespace flag is phased out │
// └─────────────────────────────────────────────────────────────────────────────┘

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_Succeeds() {
req := &registeredresources.CreateRegisteredResourceRequest{
Name: "test_create_no_ns",
}

created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req)
s.Require().NoError(err)
s.NotNil(created)
s.NotEmpty(created.GetId())
s.Equal("test_create_no_ns", created.GetName())
s.Nil(created.GetNamespace())
}

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_WithValues_Succeeds() {
req := &registeredresources.CreateRegisteredResourceRequest{
Name: "test_create_no_ns_vals",
Values: []string{"val1", "val2"},
}

created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req)
s.Require().NoError(err)
s.NotNil(created)
s.Nil(created.GetNamespace())
s.Require().Len(created.GetValues(), 2)
}

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_GetByID_Succeeds() {
req := &registeredresources.CreateRegisteredResourceRequest{
Name: "test_no_ns_get_by_id",
}

created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req)
s.Require().NoError(err)
s.NotNil(created)

got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, &registeredresources.GetRegisteredResourceRequest{
Identifier: &registeredresources.GetRegisteredResourceRequest_Id{
Id: created.GetId(),
},
})
s.Require().NoError(err)
s.NotNil(got)
s.Equal(created.GetId(), got.GetId())
s.Nil(got.GetNamespace())
}

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_GetByName_Succeeds() {
name := "test_no_ns_get_by_name"
req := &registeredresources.CreateRegisteredResourceRequest{
Name: name,
}

created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req)
s.Require().NoError(err)
s.NotNil(created)

got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, &registeredresources.GetRegisteredResourceRequest{
Identifier: &registeredresources.GetRegisteredResourceRequest_Name{
Name: name,
},
})
s.Require().NoError(err)
s.NotNil(got)
s.Equal(created.GetId(), got.GetId())
s.Nil(got.GetNamespace())
}

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_DuplicateName_Fails() {
name := "test_no_ns_dup"
req := &registeredresources.CreateRegisteredResourceRequest{
Name: name,
}

created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req)
s.Require().NoError(err)
s.NotNil(created)

_, err = s.db.PolicyClient.CreateRegisteredResource(s.ctx, req)
s.Require().Error(err)
s.Require().ErrorIs(err, db.ErrUniqueConstraintViolation)
}

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_WithoutNamespace_ListIncluded_Succeeds() {
name := "test_no_ns_list"
req := &registeredresources.CreateRegisteredResourceRequest{
Name: name,
}

created, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, req)
s.Require().NoError(err)
s.NotNil(created)

list, err := s.db.PolicyClient.ListRegisteredResources(s.ctx, &registeredresources.ListRegisteredResourcesRequest{})
Comment thread
c-r33d marked this conversation as resolved.
s.Require().NoError(err)
s.NotNil(list)

// Also create a namespaced resource to verify both types appear in unfiltered list
namespacedRes, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, &registeredresources.CreateRegisteredResourceRequest{
NamespaceId: s.getNamespaceID("example.com"),
Name: "test_ns_list",
})
s.Require().NoError(err)
s.NotNil(namespacedRes)

list, err = s.db.PolicyClient.ListRegisteredResources(s.ctx, &registeredresources.ListRegisteredResourcesRequest{})
s.Require().NoError(err)

foundNoNS := false
foundNamespaced := false
for _, r := range list.GetResources() {
if r.GetId() == created.GetId() {
foundNoNS = true
s.Nil(r.GetNamespace())
}
if r.GetId() == namespacedRes.GetId() {
foundNamespaced = true
s.NotNil(r.GetNamespace())
}
}
s.True(foundNoNS, "no-namespace resource should appear in unfiltered list")
s.True(foundNamespaced, "namespaced resource should also appear in unfiltered list")
}

func (s *RegisteredResourcesSuite) Test_CreateRegisteredResource_SameName_NoNamespaceAndNamespaced_Succeeds() {
name := "test_same_name_no_ns_and_ns"

// Create non-namespaced resource
created1, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, &registeredresources.CreateRegisteredResourceRequest{
Name: name,
})
s.Require().NoError(err)
s.NotNil(created1)
s.Nil(created1.GetNamespace())

// Create namespaced resource with the same name
nsID := s.getNamespaceID("example.com")
created2, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, &registeredresources.CreateRegisteredResourceRequest{
NamespaceId: nsID,
Name: name,
})
s.Require().NoError(err)
s.NotNil(created2)
s.NotNil(created2.GetNamespace())
s.Equal(nsID, created2.GetNamespace().GetId())

// Both should exist with different IDs
s.NotEqual(created1.GetId(), created2.GetId())
}

func (s *RegisteredResourcesSuite) Test_GetRegisteredResource_ByName_Ambiguous_ReturnsNonNamespaced() {
name := "test_ambiguous_name_lookup"

// Create non-namespaced resource
createdNoNS, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, &registeredresources.CreateRegisteredResourceRequest{
Name: name,
})
s.Require().NoError(err)
s.NotNil(createdNoNS)

// Create namespaced resource with the same name
nsID := s.getNamespaceID("example.com")
createdWithNS, err := s.db.PolicyClient.CreateRegisteredResource(s.ctx, &registeredresources.CreateRegisteredResourceRequest{
NamespaceId: nsID,
Name: name,
})
s.Require().NoError(err)
s.NotNil(createdWithNS)

// Get by name only (no namespace filter) — should deterministically return the non-namespaced one
got, err := s.db.PolicyClient.GetRegisteredResource(s.ctx, &registeredresources.GetRegisteredResourceRequest{
Identifier: &registeredresources.GetRegisteredResourceRequest_Name{
Name: name,
},
})
s.Require().NoError(err)
s.NotNil(got)
s.Equal(createdNoNS.GetId(), got.GetId())
s.Nil(got.GetNamespace())
}
Comment thread
alkalescent marked this conversation as resolved.

// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ end namespace-optional tests │
// └─────────────────────────────────────────────────────────────────────────────┘

func (s *RegisteredResourcesSuite) getNamespaceID(key string) string {
ns := s.f.GetNamespaceKey(key)
return ns.ID
Expand Down
22 changes: 14 additions & 8 deletions service/policy/db/queries/registered_resources.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,23 @@ WITH inserted AS (
LEFT JOIN attribute_fqns fqns ON fqns.fqn = sqlc.narg('namespace_fqn')::text AND sqlc.narg('namespace_id')::text IS NULL
WHERE
(sqlc.narg('namespace_id')::text IS NOT NULL AND direct.direct_namespace_id IS NOT NULL) OR
(sqlc.narg('namespace_fqn')::text IS NOT NULL AND fqns.namespace_id IS NOT NULL)
(sqlc.narg('namespace_fqn')::text IS NOT NULL AND fqns.namespace_id IS NOT NULL) OR
(sqlc.narg('namespace_id')::text IS NULL AND sqlc.narg('namespace_fqn')::text IS NULL)
RETURNING id, namespace_id, name, metadata
)
SELECT
i.id,
i.name,
i.metadata,
JSON_BUILD_OBJECT(
'id', n.id,
'name', n.name,
'fqn', fqns.fqn
) as namespace
CASE WHEN n.id IS NOT NULL THEN
JSON_BUILD_OBJECT(
'id', n.id,
'name', n.name,
'fqn', fqns.fqn
)
ELSE NULL END as namespace
FROM inserted i
JOIN attribute_namespaces n ON i.namespace_id = n.id
LEFT JOIN attribute_namespaces n ON i.namespace_id = n.id
LEFT JOIN attribute_fqns fqns ON fqns.namespace_id = n.id AND fqns.attribute_id IS NULL AND fqns.value_id IS NULL;

-- name: getRegisteredResource :one
Expand Down Expand Up @@ -59,7 +62,10 @@ WHERE
(sqlc.narg('name')::text IS NULL OR r.name = sqlc.narg('name')::text) AND
(sqlc.narg('namespace_id')::uuid IS NULL OR r.namespace_id = sqlc.narg('namespace_id')::uuid) AND
(sqlc.narg('namespace_fqn')::text IS NULL OR ns_fqns.fqn = sqlc.narg('namespace_fqn')::text)
GROUP BY r.id, n.id, ns_fqns.fqn;
GROUP BY r.id, n.id, ns_fqns.fqn
-- prefer non-namespaced over namespaced results (to support legacy behavior)
ORDER BY r.namespace_id NULLS FIRST
LIMIT 1;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

-- name: listRegisteredResources :many
WITH counted AS (
Expand Down
26 changes: 18 additions & 8 deletions service/policy/db/registered_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package db
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"

"github.com/jackc/pgx/v5/pgtype"
"github.com/opentdf/platform/lib/identifier"
"github.com/opentdf/platform/protocol/go/common"
"github.com/opentdf/platform/protocol/go/policy"
Expand Down Expand Up @@ -58,7 +60,7 @@ func unmarshalRegisteredResourceActionAttributeValuesProto(actionAttrValuesJSON
}

// hydrateNamespaceFromInterface converts a nullable namespace interface{} (from CASE WHEN SQL)
// to a *policy.Namespace. Returns an empty Namespace if the namespace is NULL (legacy RRs without namespace).
// to a *policy.Namespace. Returns nil if the namespace is NULL (legacy RRs without namespace).
func hydrateNamespaceFromInterface(nsRaw interface{}) (*policy.Namespace, error) {
if nsRaw == nil {
return nil, nil //nolint:nilnil // nil namespace is valid for legacy RRs without namespace
Expand Down Expand Up @@ -115,9 +117,9 @@ func (c PolicyDBClient) CreateRegisteredResource(ctx context.Context, r *registe
return nil, db.WrapIfKnownInvalidQueryErr(err)
}

namespace := &policy.Namespace{}
if err := unmarshalNamespace(row.Namespace, namespace); err != nil {
return nil, fmt.Errorf("failed to unmarshal registered resource namespace: %w", err)
// Validate namespace JSON is parseable (actual namespace retrieved via GetRegisteredResource below)
if _, err := hydrateNamespaceFromInterface(row.Namespace); err != nil {
return nil, err
}
Comment thread
alkalescent marked this conversation as resolved.

for _, v := range r.GetValues() {
Expand Down Expand Up @@ -584,11 +586,19 @@ func (c PolicyDBClient) createRegisteredResourceActionAttributeValues(ctx contex
case *registeredresources.ActionAttributeValue_ActionId:
actionID = ident.ActionId
case *registeredresources.ActionAttributeValue_ActionName:
a, err := c.queries.getAction(ctx, getActionParams{
Name: pgtypeText(strings.ToLower(ident.ActionName)),
})
actionParams := getActionParams{
Name: pgtypeText(strings.ToLower(ident.ActionName)),
NamespaceID: nsUUID,
}
a, err := c.queries.getAction(ctx, actionParams)
err = db.WrapIfKnownInvalidQueryErr(err)
if err != nil && nsUUID.Valid && errors.Is(err, db.ErrNotFound) {
// Fall back to standard (non-namespaced) action lookup only on not-found
actionParams.NamespaceID = pgtype.UUID{}
a, err = c.queries.getAction(ctx, actionParams)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if err != nil {
return db.WrapIfKnownInvalidQueryErr(err)
return err
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
actionID = a.ID
default:
Expand Down
Loading
Loading