Skip to content
Open
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
50 changes: 42 additions & 8 deletions service/authorization/v2/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
)

const (
attributesCacheKey = "attributes_cache_key"
subjectMappingsCacheKey = "subject_mappings_cache_key"
registeredResourcesCacheKey = "registered_resources_cache_key"
obligationsCacheKey = "obligations_cache_key"
attributesCacheKey = "attributes_cache_key"
subjectMappingsCacheKey = "subject_mappings_cache_key"
dynamicValueMappingsCacheKey = "dynamic_value_mappings_cache_key"
registeredResourcesCacheKey = "registered_resources_cache_key"
obligationsCacheKey = "obligations_cache_key"
)

var (
Expand Down Expand Up @@ -60,10 +61,11 @@ type EntitlementPolicyCache struct {
// The EntitlementPolicy struct holds all the cached entitlement policy, as generics allow one
// data type per service cache instance.
type EntitlementPolicy struct {
Attributes []*policy.Attribute
SubjectMappings []*policy.SubjectMapping
RegisteredResources []*policy.RegisteredResource
Obligations []*policy.Obligation
Attributes []*policy.Attribute
SubjectMappings []*policy.SubjectMapping
DynamicValueMappings []*policy.DynamicValueMapping
RegisteredResources []*policy.RegisteredResource
Obligations []*policy.Obligation
}

// NewEntitlementPolicyCache holds a platform-provided cache client and manages a periodic refresh of
Expand Down Expand Up @@ -178,6 +180,10 @@ func (c *EntitlementPolicyCache) Refresh(ctx context.Context) error {
if err != nil {
return err
}
dynamicValueMappings, err := c.retriever.ListAllDynamicValueMappings(ctx)
if err != nil {
return err
}
registeredResources, err := c.retriever.ListAllRegisteredResources(ctx)
if err != nil {
return err
Expand All @@ -200,6 +206,12 @@ func (c *EntitlementPolicyCache) Refresh(ctx context.Context) error {
return errors.Join(ErrFailedToSet, err)
}

err = c.cacheClient.Set(ctx, dynamicValueMappingsCacheKey, dynamicValueMappings, authzCacheTags)
if err != nil {
c.isCacheFilled = false
return errors.Join(ErrFailedToSet, err)
}

err = c.cacheClient.Set(ctx, registeredResourcesCacheKey, registeredResources, authzCacheTags)
if err != nil {
c.isCacheFilled = false
Expand Down Expand Up @@ -270,6 +282,28 @@ func (c *EntitlementPolicyCache) ListAllSubjectMappings(ctx context.Context) ([]
return subjectMappings, nil
}

// ListAllDynamicValueMappings returns the cached dynamic value entitlement mappings, or none on a cache miss
func (c *EntitlementPolicyCache) ListAllDynamicValueMappings(ctx context.Context) ([]*policy.DynamicValueMapping, error) {
var (
mappings []*policy.DynamicValueMapping
ok bool
)

cached, err := c.cacheClient.Get(ctx, dynamicValueMappingsCacheKey)
if err != nil {
if errors.Is(err, cache.ErrCacheMiss) {
return mappings, nil
}
return nil, fmt.Errorf("%w, dynamic value mappings: %w", ErrFailedToGet, err)
}

mappings, ok = cached.([]*policy.DynamicValueMapping)
if !ok {
return nil, fmt.Errorf("%w: %T", ErrCachedTypeNotExpected, cached)
}
return mappings, nil
}

// ListAllRegisteredResources returns the cached registered resources, or none in the event of a cache miss
func (c *EntitlementPolicyCache) ListAllRegisteredResources(ctx context.Context) ([]*policy.RegisteredResource, error) {
var (
Expand Down
282 changes: 282 additions & 0 deletions service/integration/dynamic_value_mappings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package integration

import (
"context"
"log/slog"
"testing"

"github.com/opentdf/platform/protocol/go/policy"
"github.com/opentdf/platform/protocol/go/policy/attributes"
"github.com/opentdf/platform/protocol/go/policy/dynamicvaluemapping"
"github.com/opentdf/platform/protocol/go/policy/subjectmapping"
"github.com/opentdf/platform/protocol/go/policy/unsafe"
"github.com/opentdf/platform/service/internal/fixtures"
policydb "github.com/opentdf/platform/service/policy/db"
"github.com/stretchr/testify/suite"
)

type DynamicValueMappingsSuite struct {
suite.Suite
f fixtures.Fixtures
db fixtures.DBInterface
//nolint:containedctx // Only used for test suite
ctx context.Context
}

func (s *DynamicValueMappingsSuite) SetupSuite() {
slog.Info("setting up db.DynamicValueMappings test suite")
s.ctx = context.Background()
c := *Config
c.DB.Schema = "test_opentdf_dynamic_value_mappings"
s.db = fixtures.NewDBInterface(s.ctx, c)
s.f = fixtures.NewFixture(s.db)
s.f.Provision(s.ctx)
}

func (s *DynamicValueMappingsSuite) TearDownSuite() {
slog.Info("tearing down db.DynamicValueMappings test suite")
s.f.TearDown(s.ctx)
}

func TestDynamicValueMappingsSuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping dynamic_value_mappings integration tests")
}
suite.Run(t, new(DynamicValueMappingsSuite))
}

func (s *DynamicValueMappingsSuite) TestCreateAndGet() {
attr := s.createDefinition("dvem_create_ok", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)

created, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".patientAssignments[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().NoError(err)
s.Require().NotEmpty(created.GetId())

got, err := s.db.PolicyClient.GetDynamicValueMapping(s.ctx, created.GetId())
s.Require().NoError(err)
s.Equal(attr.GetId(), got.GetAttributeDefinition().GetId())
s.Equal(".patientAssignments[]", got.GetValueResolver().GetSubjectExternalSelectorValue())
s.Equal(policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS, got.GetValueResolver().GetComparison())
s.Len(got.GetActions(), 1)
s.Nil(got.GetSubjectConditionSet(), "optional static pre-gate omitted")
}

func (s *DynamicValueMappingsSuite) TestCreateWithStaticGate() {
attr := s.createDefinition("dvem_create_gate", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)

created, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".patientAssignments[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
NewSubjectConditionSet: s.sampleSCSCreate(),
})
s.Require().NoError(err)

got, err := s.db.PolicyClient.GetDynamicValueMapping(s.ctx, created.GetId())
s.Require().NoError(err)
s.Require().NotNil(got.GetSubjectConditionSet(), "static pre-gate should be hydrated")
s.NotEmpty(got.GetSubjectConditionSet().GetSubjectSets())
}

func (s *DynamicValueMappingsSuite) TestRejectsHierarchyDefinition() {
attr := s.createDefinition("dvem_hierarchy", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY)

_, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".x[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().Error(err, "HIERARCHY definitions must be rejected")
}

func (s *DynamicValueMappingsSuite) TestNoCoexistence_SubjectMappingThenDynamic() {
attr := s.createDefinition("dvem_coexist_fwd", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)
val, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attr.GetId(), &attributes.CreateAttributeValueRequest{Value: "v1"})
s.Require().NoError(err)

_, err = s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{
AttributeValueId: val.GetId(),
Actions: []*policy.Action{s.readAction()},
NewSubjectConditionSet: s.sampleSCSCreate(),
})
s.Require().NoError(err)

// definition now has a value-level subject mapping; a dynamic mapping must be rejected
_, err = s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".x[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().Error(err, "dynamic mapping must not coexist with value-level subject mappings")
}

func (s *DynamicValueMappingsSuite) TestNoCoexistence_DynamicThenSubjectMapping() {
attr := s.createDefinition("dvem_coexist_rev", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)

_, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".x[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().NoError(err)

val, err := s.db.PolicyClient.CreateAttributeValue(s.ctx, attr.GetId(), &attributes.CreateAttributeValueRequest{Value: "v1"})
s.Require().NoError(err)

// definition now has a dynamic mapping; a value-level subject mapping must be rejected
_, err = s.db.PolicyClient.CreateSubjectMapping(s.ctx, &subjectmapping.CreateSubjectMappingRequest{
AttributeValueId: val.GetId(),
Actions: []*policy.Action{s.readAction()},
NewSubjectConditionSet: s.sampleSCSCreate(),
})
s.Require().Error(err, "value-level subject mapping must not coexist with a dynamic mapping")
}

func (s *DynamicValueMappingsSuite) TestRejectsRuleChangeToHierarchy() {
attr := s.createDefinition("dvem_rule_guard", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)

_, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".x[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().NoError(err)

_, err = s.db.PolicyClient.UnsafeUpdateAttribute(s.ctx, &unsafe.UnsafeUpdateAttributeRequest{
Id: attr.GetId(),
Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY,
})
s.Require().Error(err, "changing the rule to HIERARCHY must be rejected when a dynamic mapping exists")
}

func (s *DynamicValueMappingsSuite) TestUpdateAndDelete() {
attr := s.createDefinition("dvem_update_delete", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF)

created, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".patientAssignments[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().NoError(err)

updated, err := s.db.PolicyClient.UpdateDynamicValueMapping(s.ctx, &dynamicvaluemapping.UpdateDynamicValueMappingRequest{
Id: created.GetId(),
ValueResolver: s.resolver(".accounts[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_CONTAINS),
})
s.Require().NoError(err)
s.Equal(".accounts[]", updated.GetValueResolver().GetSubjectExternalSelectorValue())
s.Equal(policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_CONTAINS, updated.GetValueResolver().GetComparison())

_, err = s.db.PolicyClient.DeleteDynamicValueMapping(s.ctx, created.GetId())
s.Require().NoError(err)

_, err = s.db.PolicyClient.GetDynamicValueMapping(s.ctx, created.GetId())
s.Require().Error(err, "mapping should be gone after delete")
}

func (s *DynamicValueMappingsSuite) TestListByDefinition() {
attr := s.createDefinition("dvem_list", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)
_, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(".patientAssignments[]", policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().NoError(err)

resp, err := s.db.PolicyClient.ListDynamicValueMappings(s.ctx, &dynamicvaluemapping.ListDynamicValueMappingsRequest{
AttributeDefinitionId: attr.GetId(),
})
s.Require().NoError(err)
s.Require().Len(resp.GetDynamicValueMappings(), 1)
s.Equal(attr.GetId(), resp.GetDynamicValueMappings()[0].GetAttributeDefinition().GetId())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (s *DynamicValueMappingsSuite) TestListByDefinition_Pagination() {
attr := s.createDefinition("dvem_list_page", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)
for _, selector := range []string{".a[]", ".b[]", ".c[]"} {
_, err := s.db.PolicyClient.CreateDynamicValueMapping(s.ctx, &dynamicvaluemapping.CreateDynamicValueMappingRequest{
AttributeDefinitionId: attr.GetId(),
ValueResolver: s.resolver(selector, policy.ConditionComparisonOperatorEnum_CONDITION_COMPARISON_OPERATOR_ENUM_EQUALS),
Actions: []*policy.Action{s.readAction()},
})
s.Require().NoError(err)
}

// first page: limit 2 of 3 -> next offset points past the page
first, err := s.db.PolicyClient.ListDynamicValueMappings(s.ctx, &dynamicvaluemapping.ListDynamicValueMappingsRequest{
AttributeDefinitionId: attr.GetId(),
Pagination: &policy.PageRequest{Limit: 2},
})
s.Require().NoError(err)
s.Len(first.GetDynamicValueMappings(), 2)
s.Equal(int32(3), first.GetPagination().GetTotal())
s.Equal(int32(2), first.GetPagination().GetNextOffset())

// track ids to assert the pages partition the corpus (no overlap, no gaps)
seen := map[string]struct{}{}
for _, m := range first.GetDynamicValueMappings() {
seen[m.GetId()] = struct{}{}
}
s.Len(seen, 2, "first page should contain two distinct mappings")

// second page: remaining item, no further pages
second, err := s.db.PolicyClient.ListDynamicValueMappings(s.ctx, &dynamicvaluemapping.ListDynamicValueMappingsRequest{
AttributeDefinitionId: attr.GetId(),
Pagination: &policy.PageRequest{Limit: 2, Offset: 2},
})
s.Require().NoError(err)
s.Len(second.GetDynamicValueMappings(), 1)
s.Equal(int32(3), second.GetPagination().GetTotal())
s.Equal(int32(0), second.GetPagination().GetNextOffset())
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for _, m := range second.GetDynamicValueMappings() {
_, overlap := seen[m.GetId()]
s.False(overlap, "page 2 must not repeat an item from page 1")
seen[m.GetId()] = struct{}{}
}
s.Len(seen, 3, "combined pages should cover all created mappings exactly once")
}

// createDefinition makes a fresh attribute under the example.com namespace with no values
// or subject mappings, so each test controls its own coexistence state.
func (s *DynamicValueMappingsSuite) createDefinition(name string, rule policy.AttributeRuleTypeEnum) *policy.Attribute {
nsID := s.f.GetNamespaceKey("example.com").ID
attr, err := s.db.PolicyClient.CreateAttribute(s.ctx, &attributes.CreateAttributeRequest{
Name: name,
NamespaceId: nsID,
Rule: rule,
})
s.Require().NoError(err)
s.Require().NotNil(attr)
return attr
}

func (s *DynamicValueMappingsSuite) readAction() *policy.Action {
return s.f.GetStandardAction(policydb.ActionRead.String())
}

func (s *DynamicValueMappingsSuite) resolver(selector string, comparison policy.ConditionComparisonOperatorEnum) *policy.DynamicValueResolver {
return &policy.DynamicValueResolver{
SubjectExternalSelectorValue: selector,
Comparison: comparison,
}
}

func (s *DynamicValueMappingsSuite) sampleSCSCreate() *subjectmapping.SubjectConditionSetCreate {
return &subjectmapping.SubjectConditionSetCreate{
SubjectSets: []*policy.SubjectSet{{
ConditionGroups: []*policy.ConditionGroup{{
BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND,
Conditions: []*policy.Condition{{
SubjectExternalSelectorValue: ".department",
Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN,
SubjectExternalValues: []string{"cardiology"},
}},
}},
}},
}
}
Loading
Loading