Skip to content

Commit a199aa7

Browse files
authored
feat(authz): DSPX-894 auth svc registered resource GetEntitlement support (#2358)
### Proposed Changes * add `GetEntitlementsRegisteredResources` method to pdp access v2 logic * add test coverage ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions
1 parent bb58b78 commit a199aa7

File tree

6 files changed

+571
-17
lines changed

6 files changed

+571
-17
lines changed

service/internal/access/v2/helpers.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import (
1414
)
1515

1616
var (
17-
ErrInvalidSubjectMapping = errors.New("access: invalid subject mapping")
18-
ErrInvalidAttributeDefinition = errors.New("access: invalid attribute definition")
17+
ErrInvalidSubjectMapping = errors.New("access: invalid subject mapping")
18+
ErrInvalidAttributeDefinition = errors.New("access: invalid attribute definition")
19+
ErrInvalidRegisteredResource = errors.New("access: invalid registered resource")
20+
ErrInvalidRegisteredResourceValue = errors.New("access: invalid registered resource value")
1921
)
2022

2123
// getDefinition parses the value FQN and uses it to retrieve the definition from the provided definitions canmap

service/internal/access/v2/just_in_time_pdp.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"log/slog"
8+
"strings"
89

910
"github.com/opentdf/platform/lib/flattening"
1011
authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2"
@@ -13,6 +14,7 @@ import (
1314
entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2"
1415
"github.com/opentdf/platform/protocol/go/policy"
1516
attrs "github.com/opentdf/platform/protocol/go/policy/attributes"
17+
"github.com/opentdf/platform/protocol/go/policy/registeredresources"
1618
"github.com/opentdf/platform/protocol/go/policy/subjectmapping"
1719
otdfSDK "github.com/opentdf/platform/sdk"
1820

@@ -63,7 +65,11 @@ func NewJustInTimePDP(
6365
if err != nil {
6466
return nil, fmt.Errorf("failed to fetch all subject mappings: %w", err)
6567
}
66-
pdp, err := NewPolicyDecisionPoint(ctx, l, allAttributes, allSubjectMappings)
68+
allRegisteredResources, err := p.fetchAllRegisteredResources(ctx)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to fetch all registered resources: %w", err)
71+
}
72+
pdp, err := NewPolicyDecisionPoint(ctx, l, allAttributes, allSubjectMappings, allRegisteredResources)
6773
if err != nil {
6874
return nil, fmt.Errorf("failed to create new policy decision point: %w", err)
6975
}
@@ -150,8 +156,10 @@ func (p *JustInTimePDP) GetEntitlements(
150156

151157
case *authzV2.EntityIdentifier_RegisteredResourceValueFqn:
152158
p.logger.DebugContext(ctx, "getting decision - resolving registered resource value FQN")
153-
return nil, errors.New("registered resources not yet implemented")
154-
// TODO: implement this case
159+
valueFQN := strings.ToLower(entityIdentifier.GetRegisteredResourceValueFqn())
160+
// registered resources do not have entity representations, so we can skip the remaining logic
161+
return p.pdp.GetEntitlementsRegisteredResource(ctx, valueFQN, withComprehensiveHierarchy)
162+
155163
default:
156164
return nil, fmt.Errorf("entity type %T: %w", entityIdentifier.GetIdentifier(), ErrInvalidEntityType)
157165
}
@@ -269,6 +277,34 @@ func (p *JustInTimePDP) fetchAllSubjectMappings(ctx context.Context) ([]*policy.
269277
return smList, nil
270278
}
271279

280+
// fetchAllRegisteredResources retrieves all registered resources within policy
281+
func (p *JustInTimePDP) fetchAllRegisteredResources(ctx context.Context) ([]*policy.RegisteredResource, error) {
282+
// If quantity of registered resources exceeds maximum list pagination, all are needed to determine entitlements
283+
var nextOffset int32
284+
rrList := make([]*policy.RegisteredResource, 0)
285+
286+
for {
287+
listed, err := p.sdk.RegisteredResources.ListRegisteredResources(ctx, &registeredresources.ListRegisteredResourcesRequest{
288+
// defer to service default for limit pagination
289+
Pagination: &policy.PageRequest{
290+
Offset: nextOffset,
291+
},
292+
})
293+
if err != nil {
294+
return nil, fmt.Errorf("failed to list registered resources: %w", err)
295+
}
296+
297+
nextOffset = listed.GetPagination().GetNextOffset()
298+
rrList = append(rrList, listed.GetResources()...)
299+
300+
if nextOffset <= 0 {
301+
break
302+
}
303+
}
304+
305+
return rrList, nil
306+
}
307+
272308
// resolveEntitiesFromEntityChain roundtrips to ERS to resolve the provided entity chain
273309
// and optionally skips environment entities (which is expected behavior in decision flow)
274310
func (p *JustInTimePDP) resolveEntitiesFromEntityChain(

service/internal/access/v2/pdp.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"errors"
66
"fmt"
77
"log/slog"
8+
"slices"
89
"strconv"
910
"strings"
1011

12+
"github.com/opentdf/platform/lib/identifier"
1113
authz "github.com/opentdf/platform/protocol/go/authorization/v2"
1214
entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2"
1315
"github.com/opentdf/platform/protocol/go/policy"
@@ -47,7 +49,7 @@ type EntitlementFailure struct {
4749
type PolicyDecisionPoint struct {
4850
logger *logger.Logger
4951
allEntitleableAttributesByValueFQN map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue
50-
// allRegisteredResourcesByValueFQN map[string]*policy.RegisteredResourceValue
52+
allRegisteredResourceValuesByFQN map[string]*policy.RegisteredResourceValue
5153
}
5254

5355
var (
@@ -67,8 +69,7 @@ func NewPolicyDecisionPoint(
6769
l *logger.Logger,
6870
allAttributeDefinitions []*policy.Attribute,
6971
allSubjectMappings []*policy.SubjectMapping,
70-
// TODO: take in all registered resources and store them in memory by value FQN
71-
// allRegisteredResources []*policy.RegisteredResource,
72+
allRegisteredResources []*policy.RegisteredResource,
7273
) (*PolicyDecisionPoint, error) {
7374
var err error
7475

@@ -126,9 +127,26 @@ func NewPolicyDecisionPoint(
126127
allEntitleableAttributesByValueFQN[mappedValueFQN] = mapped
127128
}
128129

130+
allRegisteredResourceValuesByFQN := make(map[string]*policy.RegisteredResourceValue)
131+
for _, rr := range allRegisteredResources {
132+
if err := validateRegisteredResource(rr); err != nil {
133+
return nil, fmt.Errorf("invalid registered resource: %w", err)
134+
}
135+
rrName := rr.GetName()
136+
137+
for _, v := range rr.GetValues() {
138+
fullyQualifiedValue := identifier.FullyQualifiedRegisteredResourceValue{
139+
Name: rrName,
140+
Value: v.GetValue(),
141+
}
142+
allRegisteredResourceValuesByFQN[fullyQualifiedValue.FQN()] = v
143+
}
144+
}
145+
129146
pdp := &PolicyDecisionPoint{
130147
l,
131148
allEntitleableAttributesByValueFQN,
149+
allRegisteredResourceValuesByFQN,
132150
}
133151
return pdp, nil
134152
}
@@ -299,3 +317,65 @@ func (p *PolicyDecisionPoint) GetEntitlements(
299317
)
300318
return result, nil
301319
}
320+
321+
func (p *PolicyDecisionPoint) GetEntitlementsRegisteredResource(
322+
ctx context.Context,
323+
registeredResourceValueFQN string,
324+
withComprehensiveHierarchy bool,
325+
) ([]*authz.EntityEntitlements, error) {
326+
l := p.logger.With("withComprehensiveHierarchy", strconv.FormatBool(withComprehensiveHierarchy))
327+
l.DebugContext(ctx, "getting entitlements for registered resource value", slog.String("fqn", registeredResourceValueFQN))
328+
329+
if _, err := identifier.Parse[*identifier.FullyQualifiedRegisteredResourceValue](registeredResourceValueFQN); err != nil {
330+
return nil, err
331+
}
332+
333+
registeredResourceValue := p.allRegisteredResourceValuesByFQN[registeredResourceValueFQN]
334+
if err := validateRegisteredResourceValue(registeredResourceValue); err != nil {
335+
return nil, err
336+
}
337+
338+
actionsPerAttributeValueFqn := make(map[string]*authz.EntityEntitlements_ActionsList)
339+
340+
for _, aav := range registeredResourceValue.GetActionAttributeValues() {
341+
action := aav.GetAction()
342+
attrVal := aav.GetAttributeValue()
343+
attrValFQN := attrVal.GetFqn()
344+
345+
actionsList, ok := actionsPerAttributeValueFqn[attrValFQN]
346+
if !ok {
347+
actionsList = &authz.EntityEntitlements_ActionsList{
348+
Actions: make([]*policy.Action, 0),
349+
}
350+
}
351+
352+
if !slices.ContainsFunc(actionsList.GetActions(), func(a *policy.Action) bool {
353+
return a.GetName() == action.GetName()
354+
}) {
355+
actionsList.Actions = append(actionsList.Actions, action)
356+
}
357+
358+
actionsPerAttributeValueFqn[attrValFQN] = actionsList
359+
360+
if withComprehensiveHierarchy {
361+
err := populateLowerValuesIfHierarchy(attrValFQN, p.allEntitleableAttributesByValueFQN, actionsList, actionsPerAttributeValueFqn)
362+
if err != nil {
363+
return nil, fmt.Errorf("error populating comprehensive lower hierarchy values for registered resource value FQN [%s]: %w", attrValFQN, err)
364+
}
365+
}
366+
}
367+
368+
result := []*authz.EntityEntitlements{
369+
{
370+
EphemeralId: registeredResourceValueFQN,
371+
ActionsPerAttributeValueFqn: actionsPerAttributeValueFqn,
372+
},
373+
}
374+
l.DebugContext(
375+
ctx,
376+
"entitlement results for registered resource value",
377+
slog.Any("entitlements", result),
378+
)
379+
380+
return result, nil
381+
}

0 commit comments

Comments
 (0)