Skip to content
Merged
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
30 changes: 24 additions & 6 deletions service/internal/access/v2/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@ func getResourceDecision(
slog.Any("resource", resource.GetResource()),
)

if len(accessibleAttributeValues) == 0 {
l.WarnContext(ctx, "resource is not able to be entitled", slog.Any("resource", resource.GetResource()))
return failure, nil
}

switch resource.GetResource().(type) {
case *authz.Resource_RegisteredResourceValueFqn:
registeredResourceValueFQN = strings.ToLower(resource.GetRegisteredResourceValueFqn())
Expand Down Expand Up @@ -110,6 +105,12 @@ func getResourceDecision(
return nil, fmt.Errorf("unsupported resource type: %w", ErrInvalidResource)
}

// Cannot entitle any resource
if len(accessibleAttributeValues) == 0 {
l.WarnContext(ctx, "resource is not able to be entitled", slog.Any("resource", resource.GetResource()))
return failure, nil
}

return evaluateResourceAttributeValues(ctx, l, resourceAttributeValues, resourceID, registeredResourceValueFQN, action, entitlements, accessibleAttributeValues)
}

Expand All @@ -128,17 +129,34 @@ func evaluateResourceAttributeValues(
// Group value FQNs by parent definition
definitionFqnToValueFqns := make(map[string][]string)
definitionsLookup := make(map[string]*policy.Attribute)
notFoundFQNs := make([]string, 0)

for _, valueFQN := range resourceAttributeValues.GetFqns() {
attributeAndValue, ok := accessibleAttributeValues[valueFQN]
if !ok {
return nil, fmt.Errorf("%w: %s", ErrFQNNotFound, valueFQN)
notFoundFQNs = append(notFoundFQNs, valueFQN)
continue
}
definition := attributeAndValue.GetAttribute()
definitionFqnToValueFqns[definition.GetFqn()] = append(definitionFqnToValueFqns[definition.GetFqn()], valueFQN)
definitionsLookup[definition.GetFqn()] = definition
}

// If ANY FQNs are missing, DENY the resource
if len(notFoundFQNs) > 0 {
l.WarnContext(ctx, "attribute value FQN(s) not found - denying access to resource",
slog.Any("not_found_fqns", notFoundFQNs),
slog.String("resource_id", resourceID))
result := &ResourceDecision{
Entitled: false,
ResourceID: resourceID,
}
if resourceName != "" {
result.ResourceName = resourceName
}
return result, nil
}

// Evaluate each definition by rule, resource attributes, action, and entitlements
passed := true
dataRuleResults := make([]DataRuleResult, 0)
Expand Down
189 changes: 174 additions & 15 deletions service/internal/access/v2/evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,18 +751,32 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() {
expectError: false,
},
{
name: "unknown attribute value FQN",
name: "partial FQNs not found - should DENY",
resourceAttrs: &authz.Resource_AttributeValues{
Fqns: []string{
levelMidFQN,
"https://namespace.com/attr/department/value/unknown", // This FQN doesn't exist in accessibleAttributeValues
"https://namespace.com/attr/department/value/unknown",
},
},
entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{
levelMidFQN: []*policy.Action{actionRead},
},
// Should NOT error - but should DENY resource (ANY missing FQN = DENY)
expectAccessible: false,
expectError: true,
expectError: false,
},
{
name: "all FQNs not found - should DENY",
resourceAttrs: &authz.Resource_AttributeValues{
Fqns: []string{
createAttrValueFQN(baseNamespace, "significance", "critical"),
createAttrValueFQN(baseNamespace, "significance", "major"),
},
},
entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{},
// Should NOT error - but should DENY resource (no FQNs exist)
expectAccessible: false,
expectError: false,
},
}

Expand All @@ -782,19 +796,30 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() {

if tc.expectError {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.NotNil(resourceDecision)
s.Equal(tc.expectAccessible, resourceDecision.Entitled)

// Check results array has the correct length based on grouping by definition
definitions := make(map[string]bool)
for _, fqn := range tc.resourceAttrs.GetFqns() {
if attrAndValue, ok := s.accessibleAttrValues[fqn]; ok {
definitions[attrAndValue.GetAttribute().GetFqn()] = true
}
return
}

s.Require().NoError(err)
s.NotNil(resourceDecision)
s.Equal(tc.expectAccessible, resourceDecision.Entitled)

// Check results array has the correct length based on grouping by definition
// If ANY FQN is missing, DataRuleResults should be empty (resource is denied without evaluation)
definitions := make(map[string]bool)
allFQNsExist := true
for _, fqn := range tc.resourceAttrs.GetFqns() {
if attrAndValue, ok := s.accessibleAttrValues[fqn]; ok {
definitions[attrAndValue.GetAttribute().GetFqn()] = true
} else {
allFQNsExist = false
}
}

if allFQNsExist {
s.Len(resourceDecision.DataRuleResults, len(definitions))
} else {
// Any missing FQN means DENY without evaluation
s.Empty(resourceDecision.DataRuleResults)
}
})
}
Expand Down Expand Up @@ -887,7 +912,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() {
expectPass: false,
},
{
name: "nonexistent registered resource value",
name: "nonexistent registered resource value - should DENY",
resource: &authz.Resource{
Resource: &authz.Resource_RegisteredResourceValueFqn{
RegisteredResourceValueFqn: nonExistentRegResValueFQN,
Expand All @@ -898,6 +923,73 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() {
expectError: false,
expectPass: false,
},
{
name: "attribute value FQNs not found, namespace & definition exist - should DENY",
resource: &authz.Resource{
Resource: &authz.Resource_AttributeValues_{
AttributeValues: &authz.Resource_AttributeValues{
Fqns: []string{
createAttrValueFQN(baseNamespace, "department", "doesnotexist"),
},
},
},
EphemeralId: "test-attr-missing-fqns",
},
entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{},
expectError: false,
expectPass: false,
},
{
name: "attribute value FQNs not found, namespace exists - should DENY",
resource: &authz.Resource{
Resource: &authz.Resource_AttributeValues_{
AttributeValues: &authz.Resource_AttributeValues{
Fqns: []string{
createAttrValueFQN(baseNamespace, "unknown", "doesnotexist"),
},
},
},
EphemeralId: "test-attr-missing-fqns",
},
entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{},
expectError: false,
expectPass: false,
},
{
name: "attribute value FQNs not found, namespace does not exist - should DENY",
resource: &authz.Resource{
Resource: &authz.Resource_AttributeValues_{
AttributeValues: &authz.Resource_AttributeValues{
Fqns: []string{
"https://doesnot.exist/attr/severity/value/high",
},
},
},
EphemeralId: "test-attr-missing-fqns",
},
entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{},
expectError: false,
expectPass: false,
},
{
name: "attribute value FQNs partially exist - should DENY",
resource: &authz.Resource{
Resource: &authz.Resource_AttributeValues_{
AttributeValues: &authz.Resource_AttributeValues{
Fqns: []string{
levelMidFQN,
"https://doesnot.exist/attr/severity/value/high",
},
},
},
EphemeralId: "test-attr-values-partially-exist",
},
entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{
levelMidFQN: []*policy.Action{actionRead},
},
expectError: false,
expectPass: false,
},
{
name: "invalid nil resource",
resource: nil,
Expand Down Expand Up @@ -943,3 +1035,70 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() {
})
}
}

func (s *EvaluateTestSuite) Test_getResourceDecision_MultiResources_GranularDenials() {
nonExistentFQN := createAttrValueFQN(baseNamespace, "space", "cosmic")

// Resource 1: Valid FQN, entity is entitled
resource1 := &authz.Resource{
Resource: &authz.Resource_AttributeValues_{
AttributeValues: &authz.Resource_AttributeValues{
Fqns: []string{levelHighestFQN},
},
},
EphemeralId: "valid-resource-1",
}

// Resource 2: Non-existent FQN
resource2 := &authz.Resource{
Resource: &authz.Resource_AttributeValues_{
AttributeValues: &authz.Resource_AttributeValues{
Fqns: []string{nonExistentFQN},
},
},
EphemeralId: "invalid-resource-2",
}

// Resource 3: Valid FQN, entity is entitled
resource3 := &authz.Resource{
Resource: &authz.Resource_AttributeValues_{
AttributeValues: &authz.Resource_AttributeValues{
Fqns: []string{levelMidFQN},
},
},
EphemeralId: "valid-resource-3",
}

entitlements := subjectmappingbuiltin.AttributeValueFQNsToActions{
levelHighestFQN: []*policy.Action{actionRead},
levelMidFQN: []*policy.Action{actionRead},
}

testCases := []struct {
name string
resource *authz.Resource
expectedEntitled bool
}{
{"valid resource 1", resource1, true},
{"invalid resource 2 (missing FQN)", resource2, false},
{"valid resource 3", resource3, true},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
decision, err := getResourceDecision(
s.T().Context(),
s.logger,
s.accessibleAttrValues,
s.accessibleRegisteredResourceValues,
entitlements,
s.action,
tc.resource,
)

s.Require().NoError(err, "Should not error for resource: %s", tc.name)
s.Require().NotNil(decision)
s.Equal(tc.expectedEntitled, decision.Entitled, "Entitlement mismatch for: %s", tc.name)
})
}
}
Loading
Loading