Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
b52ff2d
initial reg res support in pdp structs
ryanulit May 29, 2025
de123d6
Merge branch 'main' into feat/reg-res-auth-svc
ryanulit May 29, 2025
b8cea45
Merge branch 'main' into feat/reg-res-auth-svc
ryanulit Jun 2, 2025
cf9f891
rename to reflect actual items in list
ryanulit Jun 2, 2025
d802060
initial setup for reg res GetEntitlements
ryanulit Jun 2, 2025
299ecba
completed GetEntitlements impl
ryanulit Jun 3, 2025
5c5cdcc
clean up validators and add tests
ryanulit Jun 3, 2025
4e55eda
more validator cleanup
ryanulit Jun 3, 2025
53fe2dc
add reg res value fqn validation in GetEntitlements func
ryanulit Jun 3, 2025
c444216
simplify error returned
ryanulit Jun 3, 2025
98801ea
update tests to include registered resources
ryanulit Jun 3, 2025
17b4538
initial test cases for reg res GetEntitlements func
ryanulit Jun 3, 2025
787e114
fix lint finding
ryanulit Jun 3, 2025
488f3d7
fix lint
ryanulit Jun 4, 2025
53f29e1
test refactor
ryanulit Jun 4, 2025
42a8422
pr suggestions
ryanulit Jun 5, 2025
f18a23d
split up resource and resource value validation to support resources …
ryanulit Jun 5, 2025
4fb99bd
initial logic stub
ryanulit Jun 5, 2025
cd2aa18
initial full implementation
ryanulit Jun 5, 2025
daa9a68
Merge branch 'main' into DSPX-895-auth-svc-rr-get-decision-support
ryanulit Jun 6, 2025
1f38e7e
rename param to infer reg res value as an entity
ryanulit Jun 6, 2025
d3f60ef
rename valueFQN to regResValueFQN for clarity
ryanulit Jun 6, 2025
90b6471
refactor resource decisionable attributes logic
ryanulit Jun 6, 2025
04df6de
Revert "refactor resource decisionable attributes logic"
ryanulit Jun 10, 2025
52ec7ed
simplify reg res value validation
ryanulit Jun 10, 2025
a2b3685
add evaluation implementation
ryanulit Jun 10, 2025
a420a46
add todo validation
ryanulit Jun 10, 2025
6d41da3
fix comment typo
ryanulit Jun 11, 2025
e4d83df
fix failing test and add decisionable attr refactor back in
ryanulit Jun 11, 2025
9845259
refactor getResourceDecision to include reg res values and remove dea…
ryanulit Jun 12, 2025
c33ba54
move getResourceDecisionableAttributes func to helpers file for easie…
ryanulit Jun 12, 2025
f609ab2
add reg res to existing tests for multi resource GetDecision
ryanulit Jun 16, 2025
85bfb86
update multi-resource tests to use reg res that match subject mapppin…
ryanulit Jun 17, 2025
69fc1ed
Merge remote-tracking branch 'origin' into DSPX-895-auth-svc-rr-get-d…
jakedoublev Jun 18, 2025
97eb4a7
update getDecision partial tests to use reg res to mapping matches
ryanulit Jun 18, 2025
010e73d
update comments
ryanulit Jun 18, 2025
2319da6
lint
alkalescent Jun 19, 2025
8a8720e
extra args
alkalescent Jun 19, 2025
d5ca7a0
mark todos with issue number
alkalescent Jun 20, 2025
9496645
use correct logger
alkalescent Jun 23, 2025
9e85a02
move log to DSPX-1295 todo
alkalescent Jun 23, 2025
d0bd639
add todo for unit tests
alkalescent Jun 23, 2025
3d5a51d
feat(kas): expose provider config from key details. (#2459)
c-r33d Jun 18, 2025
d89567c
feat(core): ERS cache setup, fix cache initialization (#2458)
elizabethhealy Jun 18, 2025
76a46e2
chore(ci): generate public key instead of certificate (#2455)
strantalis Jun 18, 2025
15aca16
chore(docs): describe `LegacyPublicKey` and run `make proto-generate`…
b-long Jun 20, 2025
7f245e6
chore: improve external contributor check (#2447)
jrschumacher Jun 20, 2025
079c109
chore(ci): need github app token to call org members api (#2463)
strantalis Jun 20, 2025
fc7b9ee
feat(policy)!: disable kas grants in favor of key mappings (#2220)
strantalis Jun 23, 2025
e77008b
chore(ci): bump github/codeql-action from 3.28.18 to 3.28.19 (#2350)
dependabot[bot] Jun 23, 2025
d49a566
feat(sdk): Allow key splits with same algo (#2454)
c-r33d Jun 23, 2025
59e8612
feat: inject logger and cache manager to key managers (#2461)
strantalis Jun 23, 2025
bfb0061
chore(main): release protocol/go 0.5.0 (#2464)
opentdf-automation[bot] Jun 23, 2025
61d46b2
feat(main): Add Close() method to cache manager (#2465)
elizabethhealy Jun 23, 2025
002f934
chore(ci): enable sloglint (#2462)
jakedoublev Jun 23, 2025
03a108f
lint fixes
jakedoublev Jun 23, 2025
2c16db1
rm 'request' logs
jakedoublev Jun 23, 2025
cce6704
fix log message
jakedoublev Jun 23, 2025
635c9a9
Merge remote-tracking branch 'origin' into DSPX-895-auth-svc-rr-get-d…
jakedoublev Jun 23, 2025
7cac3b0
feat(policy): allow ListRegisteredResources to return ActionAttribute…
jakedoublev Jun 23, 2025
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
6 changes: 4 additions & 2 deletions service/internal/access/v2/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import (
)

var (
ErrInvalidSubjectMapping = errors.New("access: invalid subject mapping")
ErrInvalidAttributeDefinition = errors.New("access: invalid attribute definition")
ErrInvalidSubjectMapping = errors.New("access: invalid subject mapping")
ErrInvalidAttributeDefinition = errors.New("access: invalid attribute definition")
ErrInvalidRegisteredResource = errors.New("access: invalid registered resource")
ErrInvalidRegisteredResourceValue = errors.New("access: invalid registered resource value")
)

// getDefinition parses the value FQN and uses it to retrieve the definition from the provided definitions canmap
Expand Down
55 changes: 49 additions & 6 deletions service/internal/access/v2/just_in_time_pdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"strings"

"github.com/opentdf/platform/lib/flattening"
authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2"
Expand All @@ -13,6 +14,7 @@ import (
entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2"
"github.com/opentdf/platform/protocol/go/policy"
attrs "github.com/opentdf/platform/protocol/go/policy/attributes"
"github.com/opentdf/platform/protocol/go/policy/registeredresources"
"github.com/opentdf/platform/protocol/go/policy/subjectmapping"
otdfSDK "github.com/opentdf/platform/sdk"

Expand Down Expand Up @@ -63,7 +65,11 @@ func NewJustInTimePDP(
if err != nil {
return nil, fmt.Errorf("failed to fetch all subject mappings: %w", err)
}
pdp, err := NewPolicyDecisionPoint(ctx, l, allAttributes, allSubjectMappings)
allRegisteredResources, err := p.fetchAllRegisteredResources(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch all registered resources: %w", err)
}
pdp, err := NewPolicyDecisionPoint(ctx, l, allAttributes, allSubjectMappings, allRegisteredResources)
if err != nil {
return nil, fmt.Errorf("failed to create new policy decision point: %w", err)
}
Expand Down Expand Up @@ -94,10 +100,17 @@ func (p *JustInTimePDP) GetDecision(
case *authzV2.EntityIdentifier_Token:
entityRepresentations, err = p.resolveEntitiesFromToken(ctx, entityIdentifier.GetToken(), skipEnvironmentEntities)

// TODO: implement this case
case *authzV2.EntityIdentifier_RegisteredResourceValueFqn:
p.logger.DebugContext(ctx, "getting decision - resolving registered resource value FQN")
return nil, false, errors.New("registered resources not yet implemented")
valueFQN := strings.ToLower(entityIdentifier.GetRegisteredResourceValueFqn())
// registered resources do not have entity representations, so only one decision to make and we can skip the remaining logic
decision, err := p.pdp.GetDecisionRegisteredResource(ctx, valueFQN, action, resources)
if err != nil {
return nil, false, fmt.Errorf("failed to get decision for registered resource value FQN [%s]: %w", valueFQN, err)
}
if decision == nil {
return nil, false, fmt.Errorf("decision is nil for registered resource value FQN [%s]", valueFQN)
}
return []*Decision{decision}, decision.Access, nil

default:
return nil, false, ErrInvalidEntityType
Expand Down Expand Up @@ -150,8 +163,10 @@ func (p *JustInTimePDP) GetEntitlements(

case *authzV2.EntityIdentifier_RegisteredResourceValueFqn:
p.logger.DebugContext(ctx, "getting decision - resolving registered resource value FQN")
return nil, errors.New("registered resources not yet implemented")
// TODO: implement this case
valueFQN := strings.ToLower(entityIdentifier.GetRegisteredResourceValueFqn())
// registered resources do not have entity representations, so we can skip the remaining logic
return p.pdp.GetEntitlementsRegisteredResource(ctx, valueFQN, withComprehensiveHierarchy)

default:
return nil, fmt.Errorf("entity type %T: %w", entityIdentifier.GetIdentifier(), ErrInvalidEntityType)
}
Expand Down Expand Up @@ -269,6 +284,34 @@ func (p *JustInTimePDP) fetchAllSubjectMappings(ctx context.Context) ([]*policy.
return smList, nil
}

// fetchAllRegisteredResources retrieves all registered resources within policy
func (p *JustInTimePDP) fetchAllRegisteredResources(ctx context.Context) ([]*policy.RegisteredResource, error) {
// If quantity of registered resources exceeds maximum list pagination, all are needed to determine entitlements
var nextOffset int32
rrList := make([]*policy.RegisteredResource, 0)

for {
listed, err := p.sdk.RegisteredResources.ListRegisteredResources(ctx, &registeredresources.ListRegisteredResourcesRequest{
// defer to service default for limit pagination
Pagination: &policy.PageRequest{
Offset: nextOffset,
},
})
if err != nil {
return nil, fmt.Errorf("failed to list registered resources: %w", err)
}

nextOffset = listed.GetPagination().GetNextOffset()
rrList = append(rrList, listed.GetResources()...)

if nextOffset <= 0 {
break
}
}

return rrList, nil
}

// resolveEntitiesFromEntityChain roundtrips to ERS to resolve the provided entity chain
// and optionally skips environment entities (which is expected behavior in decision flow)
func (p *JustInTimePDP) resolveEntitiesFromEntityChain(
Expand Down
191 changes: 188 additions & 3 deletions service/internal/access/v2/pdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"errors"
"fmt"
"log/slog"
"slices"
"strconv"
"strings"

"github.com/opentdf/platform/lib/identifier"
authz "github.com/opentdf/platform/protocol/go/authorization/v2"
entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2"
"github.com/opentdf/platform/protocol/go/policy"
Expand Down Expand Up @@ -47,7 +49,7 @@ type EntitlementFailure struct {
type PolicyDecisionPoint struct {
logger *logger.Logger
allEntitleableAttributesByValueFQN map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue
// allRegisteredResourcesByValueFQN map[string]*policy.RegisteredResourceValue
allRegisteredResourceValuesByFQN map[string]*policy.RegisteredResourceValue
}

var (
Expand All @@ -67,8 +69,7 @@ func NewPolicyDecisionPoint(
l *logger.Logger,
allAttributeDefinitions []*policy.Attribute,
allSubjectMappings []*policy.SubjectMapping,
// TODO: take in all registered resources and store them in memory by value FQN
// allRegisteredResources []*policy.RegisteredResource,
allRegisteredResources []*policy.RegisteredResource,
) (*PolicyDecisionPoint, error) {
var err error

Expand Down Expand Up @@ -126,9 +127,26 @@ func NewPolicyDecisionPoint(
allEntitleableAttributesByValueFQN[mappedValueFQN] = mapped
}

allRegisteredResourceValuesByFQN := make(map[string]*policy.RegisteredResourceValue)
for _, rr := range allRegisteredResources {
if err := validateRegisteredResource(rr); err != nil {
return nil, fmt.Errorf("invalid registered resource: %w", err)
}
rrName := rr.GetName()

for _, v := range rr.GetValues() {
fullyQualifiedValue := identifier.FullyQualifiedRegisteredResourceValue{
Name: rrName,
Value: v.GetValue(),
}
allRegisteredResourceValuesByFQN[fullyQualifiedValue.FQN()] = v
}
}

pdp := &PolicyDecisionPoint{
l,
allEntitleableAttributesByValueFQN,
allRegisteredResourceValuesByFQN,
}
return pdp, nil
}
Expand Down Expand Up @@ -227,6 +245,111 @@ func (p *PolicyDecisionPoint) GetDecision(
return decision, nil
}

func (p *PolicyDecisionPoint) GetDecisionRegisteredResource(
ctx context.Context,
registeredResourceValueFQN string,
action *policy.Action,
resources []*authz.Resource,
) (*Decision, error) {
l := p.logger.With("registeredResourceValueFQN", registeredResourceValueFQN)
l = l.With("action", action.GetName())
l.DebugContext(ctx, "getting decision", slog.Int("resourcesCount", len(resources)))

if err := validateGetDecisionRegisteredResource(registeredResourceValueFQN, action, resources); err != nil {
return nil, err
}

registeredResourceValue := p.allRegisteredResourceValuesByFQN[registeredResourceValueFQN]
if err := validateRegisteredResourceValue(registeredResourceValue); err != nil {
return nil, err
}

// Filter all attributes down to only those that relevant to the entitlement decisioning of these specific resources
decisionableAttributes := make(map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue)

for idx, resource := range resources {
// Assign indexed ephemeral ID for resource if not already set
if resource.GetEphemeralId() == "" {
resource.EphemeralId = "resource-" + strconv.Itoa(idx)
}

for idx, valueFQN := range resource.GetAttributeValues().GetFqns() {
// lowercase each resource attribute value FQN for case consistent map key lookups
valueFQN = strings.ToLower(valueFQN)
resource.GetAttributeValues().Fqns[idx] = valueFQN

// If same value FQN more than once, skip
if _, ok := decisionableAttributes[valueFQN]; ok {
continue
}

attributeAndValue, ok := p.allEntitleableAttributesByValueFQN[valueFQN]
if !ok {
return nil, fmt.Errorf("resource value FQN not found in memory [%s]: %w", valueFQN, ErrInvalidResource)
}

decisionableAttributes[valueFQN] = attributeAndValue
err := populateHigherValuesIfHierarchy(ctx, p.logger, valueFQN, attributeAndValue.GetAttribute(), p.allEntitleableAttributesByValueFQN, decisionableAttributes)
if err != nil {
return nil, fmt.Errorf("error populating higher hierarchy attribute values: %w", err)
}
}
}
l.DebugContext(ctx, "filtered to only entitlements relevant to decisioning", slog.Int("decisionableAttributeValuesCount", len(decisionableAttributes)))

entitledFQNsToActions := make(map[string][]*policy.Action)
for _, aav := range registeredResourceValue.GetActionAttributeValues() {
aavAction := aav.GetAction()
if action.GetName() != aavAction.GetName() {
l.DebugContext(ctx, "skipping action not matching Decision Request action", slog.String("actionName", aavAction.GetName()))
continue
}

attrVal := aav.GetAttributeValue()
attrValFQN := attrVal.GetFqn()
actionsList, ok := entitledFQNsToActions[attrValFQN]
if !ok {
actionsList = make([]*policy.Action, 0)
}

if !slices.ContainsFunc(actionsList, func(a *policy.Action) bool {
return a.GetName() == aavAction.GetName()
}) {
actionsList = append(actionsList, aavAction)
}

entitledFQNsToActions[attrValFQN] = actionsList

// todo: does hierarchy (low or high) need to be populated here?
}

decision := &Decision{
Access: true,
Results: make([]ResourceDecision, len(resources)),
}

for idx, resource := range resources {
resourceDecision, err := getResourceDecision(ctx, p.logger, decisionableAttributes, entitledFQNsToActions, action, resource)
if err != nil || resourceDecision == nil {
return nil, fmt.Errorf("error evaluating a decision on resource [%v]: %w", resource, err)
}
if !resourceDecision.Passed {
decision.Access = false
}

l.DebugContext(
ctx,
"resourceDecision result",
slog.Bool("passed", resourceDecision.Passed),
slog.String("resourceID", resourceDecision.ResourceID),
slog.Int("dataRuleResultsCount", len(resourceDecision.DataRuleResults)),
)
decision.Results[idx] = *resourceDecision
}

return decision, nil
}

func (p *PolicyDecisionPoint) GetEntitlements(
ctx context.Context,
entityRepresentations []*entityresolutionV2.EntityRepresentation,
Expand Down Expand Up @@ -299,3 +422,65 @@ func (p *PolicyDecisionPoint) GetEntitlements(
)
return result, nil
}

func (p *PolicyDecisionPoint) GetEntitlementsRegisteredResource(
ctx context.Context,
registeredResourceValueFQN string,
withComprehensiveHierarchy bool,
) ([]*authz.EntityEntitlements, error) {
l := p.logger.With("withComprehensiveHierarchy", strconv.FormatBool(withComprehensiveHierarchy))
l.DebugContext(ctx, "getting entitlements for registered resource value", slog.String("fqn", registeredResourceValueFQN))

if _, err := identifier.Parse[*identifier.FullyQualifiedRegisteredResourceValue](registeredResourceValueFQN); err != nil {
return nil, err
}

registeredResourceValue := p.allRegisteredResourceValuesByFQN[registeredResourceValueFQN]
if err := validateRegisteredResourceValue(registeredResourceValue); err != nil {
return nil, err
}

actionsPerAttributeValueFqn := make(map[string]*authz.EntityEntitlements_ActionsList)

for _, aav := range registeredResourceValue.GetActionAttributeValues() {
action := aav.GetAction()
attrVal := aav.GetAttributeValue()
attrValFQN := attrVal.GetFqn()

actionsList, ok := actionsPerAttributeValueFqn[attrValFQN]
if !ok {
actionsList = &authz.EntityEntitlements_ActionsList{
Actions: make([]*policy.Action, 0),
}
}

if !slices.ContainsFunc(actionsList.GetActions(), func(a *policy.Action) bool {
return a.GetName() == action.GetName()
}) {
actionsList.Actions = append(actionsList.Actions, action)
}

actionsPerAttributeValueFqn[attrValFQN] = actionsList

if withComprehensiveHierarchy {
err := populateLowerValuesIfHierarchy(attrValFQN, p.allEntitleableAttributesByValueFQN, actionsList, actionsPerAttributeValueFqn)
if err != nil {
return nil, fmt.Errorf("error populating comprehensive lower hierarchy values for registered resource value FQN [%s]: %w", attrValFQN, err)
}
}
}

result := []*authz.EntityEntitlements{
{
EphemeralId: registeredResourceValueFQN,
ActionsPerAttributeValueFqn: actionsPerAttributeValueFqn,
},
}
l.DebugContext(
ctx,
"entitlement results for registered resource value",
slog.Any("entitlements", result),
)

return result, nil
}
Loading
Loading