diff --git a/service/internal/access/v2/helpers.go b/service/internal/access/v2/helpers.go index 6c14462f02..4fde2d01b8 100644 --- a/service/internal/access/v2/helpers.go +++ b/service/internal/access/v2/helpers.go @@ -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 diff --git a/service/internal/access/v2/just_in_time_pdp.go b/service/internal/access/v2/just_in_time_pdp.go index b50dde0e84..1bc4a0f335 100644 --- a/service/internal/access/v2/just_in_time_pdp.go +++ b/service/internal/access/v2/just_in_time_pdp.go @@ -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" @@ -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" @@ -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) } @@ -150,8 +156,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) } @@ -269,6 +277,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, ®isteredresources.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( diff --git a/service/internal/access/v2/pdp.go b/service/internal/access/v2/pdp.go index 7649ad9ed9..581fe34475 100644 --- a/service/internal/access/v2/pdp.go +++ b/service/internal/access/v2/pdp.go @@ -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" @@ -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 ( @@ -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 @@ -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 } @@ -299,3 +317,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 +} diff --git a/service/internal/access/v2/pdp_test.go b/service/internal/access/v2/pdp_test.go index 6d0a6065e0..e256cda832 100644 --- a/service/internal/access/v2/pdp_test.go +++ b/service/internal/access/v2/pdp_test.go @@ -41,6 +41,15 @@ func createAttrValueFQN(namespace, name, value string) string { return attr.FQN() } +// Helper function to create registered resource value FQNs +func createRegisteredResourceValueFQN(name, value string) string { + resourceValue := &identifier.FullyQualifiedRegisteredResourceValue{ + Name: name, + Value: value, + } + return resourceValue.FQN() +} + // Attribute FQNs using identifier package var ( // Base attribute FQNs @@ -79,6 +88,16 @@ var ( testPlatformHybridFQN = createAttrValueFQN(testSecondaryNamespace, "platform", "hybrid") ) +// Registered resource value FQNs using identifier package +var ( + regResValNoActionAttrValFQN string + regResValSingleActionAttrValFQN string + regResValDuplicateActionAttrValFQN string + regResValMultiActionSingleAttrValFQN string + regResValMultiActionMultiAttrValFQN string + regResValComprehensiveHierarchyActionAttrValFQN string +) + // Standard action definitions used across tests var ( testActionRead = &policy.Action{Name: actions.ActionNameRead} @@ -116,6 +135,15 @@ type PDPTestSuite struct { adminEntity *entityresolutionV2.EntityRepresentation developerEntity *entityresolutionV2.EntityRepresentation analystEntity *entityresolutionV2.EntityRepresentation + + // Test registered resources + regRes *policy.RegisteredResource + regResValNoActionAttrVal *policy.RegisteredResourceValue + regResValSingleActionAttrVal *policy.RegisteredResourceValue + regResValDuplicateActionAttrVal *policy.RegisteredResourceValue + regResValMultiActionSingleAttrVal *policy.RegisteredResourceValue + regResValMultiActionMultiAttrVal *policy.RegisteredResourceValue + regResValComprehensiveHierarchyActionAttrVal *policy.RegisteredResourceValue } } @@ -324,6 +352,127 @@ func (s *PDPTestSuite) SetupTest() { "department": "finance", "country": []any{"uk"}, }) + + // Initialize test registered resources + regResValNoActionAttrVal := &policy.RegisteredResourceValue{ + Value: "no-action-attr-val", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{}, + } + regResValSingleActionAttrVal := &policy.RegisteredResourceValue{ + Value: "single-action-attr-val", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Action: testActionCreate, + AttributeValue: &policy.Value{ + Fqn: testClassSecretFQN, + Value: "secret", + }, + }, + }, + } + regResValDuplicateActionAttrVal := &policy.RegisteredResourceValue{ + Value: "duplicate-action-attr-val", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Action: testActionCreate, + AttributeValue: &policy.Value{ + Fqn: testClassSecretFQN, + Value: "secret", + }, + }, + { + Action: testActionCreate, + AttributeValue: &policy.Value{ + Fqn: testClassSecretFQN, + Value: "secret", + }, + }, + }, + } + regResValMultiActionSingleAttrVal := &policy.RegisteredResourceValue{ + Value: "multi-action-single-attr-val", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Action: testActionCreate, + AttributeValue: &policy.Value{ + Fqn: testPlatformCloudFQN, + Value: "cloud", + }, + }, + { + Action: testActionRead, + AttributeValue: &policy.Value{ + Fqn: testPlatformCloudFQN, + Value: "cloud", + }, + }, + }, + } + regResValMultiActionMultiAttrVal := &policy.RegisteredResourceValue{ + Value: "multi-action-multi-attr-val", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Action: testActionCreate, + AttributeValue: &policy.Value{ + Fqn: testClassSecretFQN, + Value: "secret", + }, + }, + { + Action: testActionUpdate, + AttributeValue: &policy.Value{ + Fqn: testPlatformCloudFQN, + Value: "cloud", + }, + }, + { + Action: testActionDelete, + AttributeValue: &policy.Value{ + Fqn: testPlatformCloudFQN, + Value: "cloud", + }, + }, + }, + } + regResValComprehensiveHierarchyActionAttrVal := &policy.RegisteredResourceValue{ + Value: "comprehensive-hierarchy-action-attr-val", + ActionAttributeValues: []*policy.RegisteredResourceValue_ActionAttributeValue{ + { + Action: testActionRead, + AttributeValue: &policy.Value{ + Fqn: testClassSecretFQN, + Value: "secret", + }, + }, + }, + } + + regRes := &policy.RegisteredResource{ + Name: "test-res", + Values: []*policy.RegisteredResourceValue{ + regResValNoActionAttrVal, + regResValSingleActionAttrVal, + regResValDuplicateActionAttrVal, + regResValMultiActionSingleAttrVal, + regResValMultiActionMultiAttrVal, + regResValComprehensiveHierarchyActionAttrVal, + }, + } + + s.fixtures.regRes = regRes + s.fixtures.regResValNoActionAttrVal = regResValNoActionAttrVal + s.fixtures.regResValSingleActionAttrVal = regResValSingleActionAttrVal + s.fixtures.regResValDuplicateActionAttrVal = regResValDuplicateActionAttrVal + s.fixtures.regResValMultiActionSingleAttrVal = regResValMultiActionSingleAttrVal + s.fixtures.regResValMultiActionMultiAttrVal = regResValMultiActionMultiAttrVal + s.fixtures.regResValComprehensiveHierarchyActionAttrVal = regResValComprehensiveHierarchyActionAttrVal + + regResValNoActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValNoActionAttrVal.GetValue()) + regResValSingleActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValSingleActionAttrVal.GetValue()) + regResValDuplicateActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValDuplicateActionAttrVal.GetValue()) + regResValMultiActionSingleAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValMultiActionSingleAttrVal.GetValue()) + regResValMultiActionMultiAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValMultiActionMultiAttrVal.GetValue()) + regResValComprehensiveHierarchyActionAttrValFQN = createRegisteredResourceValueFQN(regRes.GetName(), regResValComprehensiveHierarchyActionAttrVal.GetValue()) } // TestPDPSuite runs the test suite @@ -336,16 +485,18 @@ func (s *PDPTestSuite) TestNewPolicyDecisionPoint() { f := s.fixtures tests := []struct { - name string - attributes []*policy.Attribute - subjectMappings []*policy.SubjectMapping - expectError bool + name string + attributes []*policy.Attribute + subjectMappings []*policy.SubjectMapping + registeredResources []*policy.RegisteredResource + expectError bool }{ { - name: "valid initialization", - attributes: []*policy.Attribute{f.classificationAttr, f.departmentAttr, f.countryAttr}, - subjectMappings: []*policy.SubjectMapping{f.secretMapping, f.confidentialMapping, f.rndMapping}, - expectError: false, + name: "valid initialization", + attributes: []*policy.Attribute{f.classificationAttr, f.departmentAttr, f.countryAttr}, + subjectMappings: []*policy.SubjectMapping{f.secretMapping, f.confidentialMapping, f.rndMapping}, + registeredResources: []*policy.RegisteredResource{f.regRes}, + expectError: false, }, { name: "nil attributes and nil subject mappings", @@ -365,11 +516,18 @@ func (s *PDPTestSuite) TestNewPolicyDecisionPoint() { subjectMappings: nil, expectError: true, }, + { + name: "non-nil attributes and subject mappings but nil registered resources", + attributes: []*policy.Attribute{f.classificationAttr}, + subjectMappings: []*policy.SubjectMapping{f.secretMapping}, + registeredResources: nil, + expectError: false, + }, } for _, tc := range tests { s.Run(tc.name, func() { - pdp, err := NewPolicyDecisionPoint(s.T().Context(), s.logger, tc.attributes, tc.subjectMappings) + pdp, err := NewPolicyDecisionPoint(s.T().Context(), s.logger, tc.attributes, tc.subjectMappings, tc.registeredResources) if tc.expectError { s.Require().Error(err) @@ -392,6 +550,7 @@ func (s *PDPTestSuite) Test_GetDecision_MultipleResources() { s.logger, []*policy.Attribute{f.classificationAttr, f.departmentAttr}, []*policy.SubjectMapping{f.secretMapping, f.topSecretMapping, f.confidentialMapping, f.engineeringMapping, f.financeMapping}, + []*policy.RegisteredResource{}, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -587,6 +746,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { f.secretMapping, f.topSecretMapping, printConfidentialMapping, allActionsPublicMapping, f.engineeringMapping, f.financeMapping, viewProjectAlphaMapping, }, + []*policy.RegisteredResource{}, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -698,6 +858,7 @@ func (s *PDPTestSuite) Test_GetDecision_PartialActionEntitlement() { s.logger, []*policy.Attribute{f.classificationAttr}, []*policy.SubjectMapping{allActionsPublicMapping, restrictedMapping}, + []*policy.RegisteredResource{}, ) s.Require().NoError(err) s.Require().NotNil(classificationPDP) @@ -744,6 +905,7 @@ func (s *PDPTestSuite) Test_GetDecision_CombinedAttributeRules_SingleResource() f.engineeringMapping, f.financeMapping, f.rndMapping, f.usaMapping, f.ukMapping, f.projectAlphaMapping, f.platformCloudMapping, }, + []*policy.RegisteredResource{}, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -1103,6 +1265,7 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() { f.platformCloudMapping, onPremMapping, hybridMapping, f.usaMapping, }, + []*policy.RegisteredResource{}, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -1397,6 +1560,7 @@ func (s *PDPTestSuite) Test_GetEntitlements() { f.engineeringMapping, f.financeMapping, f.rndMapping, f.usaMapping, }, + []*policy.RegisteredResource{}, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -1695,6 +1859,7 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { lowerMiddleMapping, bottomMapping, }, + []*policy.RegisteredResource{}, ) s.Require().NoError(err) s.Require().NotNil(pdp) @@ -1764,6 +1929,167 @@ func (s *PDPTestSuite) Test_GetEntitlements_AdvancedHierarchy() { s.Contains(bottomActionNames, customActionGather, "Bottom level should have gather action") } +func (s *PDPTestSuite) Test_GetEntitlementsRegisteredResource() { + f := s.fixtures + + pdp, err := NewPolicyDecisionPoint( + s.T().Context(), + s.logger, + []*policy.Attribute{f.classificationAttr}, + []*policy.SubjectMapping{}, + []*policy.RegisteredResource{f.regRes}, + ) + s.Require().NoError(err) + s.Require().NotNil(pdp) + + s.Run("Invalid registered resource value FQN format", func() { + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + "invalid_fqn_format", + false, + ) + + s.Require().Error(err) + s.Require().ErrorIs(err, identifier.ErrInvalidFQNFormat) + s.Require().Nil(entitlements) + }) + + s.Run("Valid but non-existent registered resource value FQN", func() { + validButNonexistentFQN := createRegisteredResourceValueFQN("test-res-not-exist", "test-value-not-exist") + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + validButNonexistentFQN, + false, + ) + + s.Require().Error(err) + s.Require().ErrorIs(err, ErrInvalidRegisteredResourceValue) + s.Require().Nil(entitlements) + }) + + s.Run("no action attribute values", func() { + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + regResValNoActionAttrValFQN, + false, + ) + + s.Require().NoError(err) + s.Require().NotNil(entitlements) + s.Require().Len(entitlements, 1) + entityEntitlement := entitlements[0] + s.Equal(regResValNoActionAttrValFQN, entityEntitlement.GetEphemeralId()) + s.Require().Empty(entityEntitlement.GetActionsPerAttributeValueFqn()) + }) + + s.Run("single action attribute value", func() { + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + regResValSingleActionAttrValFQN, + false, + ) + + s.Require().NoError(err) + s.Require().NotNil(entitlements) + s.Require().Len(entitlements, 1) + entityEntitlement := entitlements[0] + s.Equal(regResValSingleActionAttrValFQN, entityEntitlement.GetEphemeralId()) + actionsPerAttrValueFQN := entityEntitlement.GetActionsPerAttributeValueFqn() + s.Require().Len(actionsPerAttrValueFQN, 1) + actionsList := actionsPerAttrValueFQN[testClassSecretFQN] + s.ElementsMatch(actionNames(actionsList.GetActions()), []string{actions.ActionNameCreate}) + }) + + s.Run("duplicate action attribute values", func() { + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + regResValDuplicateActionAttrValFQN, + false, + ) + s.Require().NoError(err) + s.Require().NotNil(entitlements) + s.Require().Len(entitlements, 1) + entityEntitlement := entitlements[0] + s.Equal(regResValDuplicateActionAttrValFQN, entityEntitlement.GetEphemeralId()) + actionsPerAttrValueFQN := entityEntitlement.GetActionsPerAttributeValueFqn() + s.Require().Len(actionsPerAttrValueFQN, 1) + actionsList := actionsPerAttrValueFQN[testClassSecretFQN] + s.ElementsMatch(actionNames(actionsList.GetActions()), []string{actions.ActionNameCreate}) + }) + + s.Run("multiple actions single attribute value", func() { + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + regResValMultiActionSingleAttrValFQN, + false, + ) + + s.Require().NoError(err) + s.Require().NotNil(entitlements) + s.Require().Len(entitlements, 1) + entityEntitlement := entitlements[0] + s.Equal(regResValMultiActionSingleAttrValFQN, entityEntitlement.GetEphemeralId()) + actionsPerAttrValueFQN := entityEntitlement.GetActionsPerAttributeValueFqn() + s.Require().Len(actionsPerAttrValueFQN, 1) + actionsList := actionsPerAttrValueFQN[testPlatformCloudFQN] + s.ElementsMatch(actionNames(actionsList.GetActions()), []string{actions.ActionNameCreate, actions.ActionNameRead}) + }) + + s.Run("multiple actions multiple attribute values", func() { + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + regResValMultiActionMultiAttrValFQN, + false, + ) + + s.Require().NoError(err) + s.Require().NotNil(entitlements) + s.Require().Len(entitlements, 1) + entityEntitlement := entitlements[0] + s.Equal(regResValMultiActionMultiAttrValFQN, entityEntitlement.GetEphemeralId()) + actionsPerAttrValueFQN := entityEntitlement.GetActionsPerAttributeValueFqn() + s.Require().Len(actionsPerAttrValueFQN, 2) + secretActionsList := actionsPerAttrValueFQN[testClassSecretFQN] + s.ElementsMatch(actionNames(secretActionsList.GetActions()), []string{actions.ActionNameCreate}) + cloudActionsList := actionsPerAttrValueFQN[testPlatformCloudFQN] + s.ElementsMatch(actionNames(cloudActionsList.GetActions()), []string{actions.ActionNameUpdate, actions.ActionNameDelete}) + }) + + s.Run("comprehensive hierarchy action attribute value", func() { + entitlements, err := pdp.GetEntitlementsRegisteredResource( + s.T().Context(), + regResValComprehensiveHierarchyActionAttrValFQN, + true, // With comprehensive hierarchy + ) + + s.Require().NoError(err) + s.Require().NotNil(entitlements) + s.Require().Len(entitlements, 1) + entityEntitlement := entitlements[0] + s.Equal(regResValComprehensiveHierarchyActionAttrValFQN, entityEntitlement.GetEphemeralId()) + + actionsPerAttributeValueFQN := entityEntitlement.GetActionsPerAttributeValueFqn() + + // secret should give access to all lower classifications (secret > confidential > public) + s.Require().Len(actionsPerAttributeValueFQN, 3) + s.Contains(actionsPerAttributeValueFQN, testClassSecretFQN) + s.Contains(actionsPerAttributeValueFQN, testClassConfidentialFQN) + s.Contains(actionsPerAttributeValueFQN, testClassPublicFQN) + + // but not higher classifications + s.NotContains(actionsPerAttributeValueFQN, testClassTopSecretFQN) + + // all actions for secret, confidential, and public should be the same + secretActions := actionsPerAttributeValueFQN[testClassSecretFQN] + confidentialActions := actionsPerAttributeValueFQN[testClassConfidentialFQN] + publicActions := actionsPerAttributeValueFQN[testClassPublicFQN] + expectedActionNames := []string{actions.ActionNameRead} + s.ElementsMatch(actionNames(secretActions.GetActions()), expectedActionNames) + s.ElementsMatch(actionNames(confidentialActions.GetActions()), expectedActionNames) + s.ElementsMatch(actionNames(publicActions.GetActions()), expectedActionNames) + }) +} + // Helper functions for all tests // assertDecisionResult is a helper function to assert that a decision result for a given FQN matches the expected pass/fail state diff --git a/service/internal/access/v2/validators.go b/service/internal/access/v2/validators.go index 7e21b1895e..444acf1338 100644 --- a/service/internal/access/v2/validators.go +++ b/service/internal/access/v2/validators.go @@ -97,6 +97,38 @@ func validateAttribute(attribute *policy.Attribute) error { return nil } +// validateRegisteredResource validates the registered resource is valid for an entitlement decision +// +// registered resource: +// +// - must not be nil +// - must have a non-empty name +func validateRegisteredResource(registeredResource *policy.RegisteredResource) error { + if registeredResource == nil { + return fmt.Errorf("registered resource is nil: %w", ErrInvalidRegisteredResource) + } + if registeredResource.GetName() == "" { + return fmt.Errorf("registered resource name is empty: %w", ErrInvalidRegisteredResource) + } + return nil +} + +// validateRegisteredResourceValue validates the registered resource value is valid for an entitlement decision +// +// registered resource value: +// +// - must not be nil +// - must have a non-empty name +func validateRegisteredResourceValue(registeredResourceValue *policy.RegisteredResourceValue) error { + if registeredResourceValue == nil { + return fmt.Errorf("registered resource value is nil: %w", ErrInvalidRegisteredResourceValue) + } + if registeredResourceValue.GetValue() == "" { + return fmt.Errorf("registered resource value is empty: %w", ErrInvalidRegisteredResourceValue) + } + return nil +} + // validateEntityRepresentations validates the entity representations are valid for an entitlement decision // // - entityRepresentations: must have at least one non-nil entity representation diff --git a/service/internal/access/v2/validators_test.go b/service/internal/access/v2/validators_test.go index 16408bcb95..0d9f57d226 100644 --- a/service/internal/access/v2/validators_test.go +++ b/service/internal/access/v2/validators_test.go @@ -283,6 +283,84 @@ func TestValidateAttribute(t *testing.T) { } } +func TestValidateRegisteredResource(t *testing.T) { + tests := []struct { + name string + registeredResource *policy.RegisteredResource + wantErr error + }{ + { + name: "Valid registered resource", + registeredResource: &policy.RegisteredResource{ + Name: "valid-resource", + }, + wantErr: nil, + }, + { + name: "Nil registered resource", + registeredResource: nil, + wantErr: ErrInvalidRegisteredResource, + }, + { + name: "Empty registered resource name", + registeredResource: &policy.RegisteredResource{ + Name: "", + }, + wantErr: ErrInvalidRegisteredResource, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateRegisteredResource(tt.registeredResource) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateRegisteredResourceValue(t *testing.T) { + tests := []struct { + name string + registeredResourceValue *policy.RegisteredResourceValue + wantErr error + }{ + { + name: "Valid registered resource value", + registeredResourceValue: &policy.RegisteredResourceValue{ + Value: "valid-value", + }, + wantErr: nil, + }, + { + name: "Nil registered resource value", + registeredResourceValue: nil, + wantErr: ErrInvalidRegisteredResourceValue, + }, + { + name: "Empty registered resource value", + registeredResourceValue: &policy.RegisteredResourceValue{ + Value: "", + }, + wantErr: ErrInvalidRegisteredResourceValue, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateRegisteredResourceValue(tt.registeredResourceValue) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + func TestValidateEntityRepresentations(t *testing.T) { tests := []struct { name string