Skip to content
Draft
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
4 changes: 3 additions & 1 deletion service/internal/access/v2/pdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
attrs "github.com/opentdf/platform/protocol/go/policy/attributes"
"github.com/opentdf/platform/service/internal/subjectmappingbuiltin"
"github.com/opentdf/platform/service/logger"
"google.golang.org/protobuf/proto"
)

// Decision represents the overall access decision for an entity.
Expand Down Expand Up @@ -140,7 +141,7 @@ func NewPolicyDecisionPoint(
// Not every value may have a subject mapping and be entitleable, but a lookup must still be possible
for _, value := range attr.GetValues() {
mapped := &attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue{
Value: value,
Value: proto.CloneOf(value),
Attribute: attr,
}
allEntitleableAttributesByValueFQN[value.GetFqn()] = mapped
Expand Down Expand Up @@ -181,6 +182,7 @@ func NewPolicyDecisionPoint(
if err != nil {
return nil, fmt.Errorf("failed to get attribute definition: %w", err)
}
mappedValue = proto.CloneOf(mappedValue)
mappedValue.SubjectMappings = []*policy.SubjectMapping{sm}
mapped := &attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue{
Value: mappedValue,
Expand Down
52 changes: 52 additions & 0 deletions service/internal/access/v2/pdp_immutable_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package access

import (
"log/slog"
"sync"
"testing"

"github.com/opentdf/platform/protocol/go/policy"
"github.com/opentdf/platform/service/logger"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)

func TestPolicyConstructionDoesNotMutateSharedValues(t *testing.T) {
const definitionFQN = "https://scale.example/attr/department"
value := &policy.Value{Fqn: definitionFQN + "/value/engineering"}
attr := &policy.Attribute{Fqn: definitionFQN, Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, Values: []*policy.Value{value}}
// The second mapping exercises values absent from the definition's value list.
mappings := []*policy.SubjectMapping{
{Id: "existing", AttributeValue: value, Actions: []*policy.Action{{Name: "read"}}},
{Id: "additional", AttributeValue: &policy.Value{Fqn: definitionFQN + "/value/sales"}, Actions: []*policy.Action{{Name: "read"}}},
}
originalAttribute := proto.CloneOf(attr)
originalMapping := proto.CloneOf(mappings[1])
log := &logger.Logger{Logger: slog.New(slog.DiscardHandler)}
ctx := t.Context()
const constructions = 16
results := make(chan *PolicyDecisionPoint, constructions)
errors := make(chan error, constructions)
var workers sync.WaitGroup
for range constructions {
workers.Go(func() {
pdp, err := NewPolicyDecisionPoint(ctx, log, []*policy.Attribute{attr}, mappings, nil, true, false)
results <- pdp
errors <- err
})
}
workers.Wait()
close(results)
close(errors)
for err := range errors {
require.NoError(t, err)
}
for pdp := range results {
for _, mapping := range mappings {
got := pdp.allEntitleableAttributesByValueFQN[mapping.GetAttributeValue().GetFqn()]
require.Len(t, got.GetValue().GetSubjectMappings(), 1)
}
}
require.True(t, proto.Equal(originalAttribute, attr))
require.True(t, proto.Equal(originalMapping, mappings[1]))
}
Loading