Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
22 changes: 22 additions & 0 deletions service/internal/access/v2/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package access

import "errors"

var (
ErrMissingRequiredSDK = errors.New("access: missing required SDK")
ErrMissingRequiredLogger = errors.New("access: missing required logger")
ErrMissingEntityResolutionServiceSDKConnection = errors.New("access: missing required entity resolution SDK connection, cannot be nil")
ErrMissingRequiredPolicy = errors.New("access: both attribute definitions and subject mappings must be provided or neither")
ErrInvalidEntityType = errors.New("access: invalid entity type")
ErrInvalidAttributeDefinition = errors.New("access: invalid attribute definition")
ErrInvalidSubjectMapping = errors.New("access: invalid subject mapping")
ErrInvalidEntitledFQNsToActions = errors.New("access: invalid entitled FQNs to actions")
ErrInvalidResource = errors.New("access: invalid resource")
ErrInvalidEntityChain = errors.New("access: invalid entity chain")
ErrInvalidAction = errors.New("access: invalid action")
ErrFQNNotFound = errors.New("access: attribute value FQN not found in memory")
ErrDefinitionNotFound = errors.New("access: definition not found for FQN")
ErrFailedEvaluation = errors.New("access: failed to evaluate definition")
ErrMissingRequiredSpecifiedRule = errors.New("access: AttributeDefinition rule cannot be unspecified")
ErrUnrecognizedRule = errors.New("access: unrecognized AttributeDefinition rule")
)
313 changes: 313 additions & 0 deletions service/internal/access/v2/evaluate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
package access

import (
"context"
"fmt"
"log/slog"
"strings"

authz "github.com/opentdf/platform/protocol/go/authorization/v2"
"github.com/opentdf/platform/protocol/go/policy"
attrs "github.com/opentdf/platform/protocol/go/policy/attributes"
"github.com/opentdf/platform/service/internal/subjectmappingbuiltin"
"github.com/opentdf/platform/service/logger"
)

// getResourceDecision evaluates the access decision for a single resource, driving the flows
// between entitlement checks for the different types of resources
func getResourceDecision(
ctx context.Context,
logger *logger.Logger,
accessibleAttributeValues map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
action *policy.Action,
resource *authz.Resource,
) (*ResourceDecision, error) {
if err := validateGetResourceDecision(accessibleAttributeValues, entitlements, action, resource); err != nil {
return nil, err
}

logger.DebugContext(
ctx,
"getting decision on one resource",
slog.Any("resource", resource.GetResource()),
)

switch resource.GetResource().(type) {
case *authz.Resource_RegisteredResourceValueFqn:
// TODO: handle registered resources
// return evaluateRegisteredResourceValue(ctx, resource.GetRegisteredResourceValueFqn(), action, entitlements, accessibleAttributeValues)

case *authz.Resource_AttributeValues_:
return evaluateResourceAttributeValues(ctx, logger, resource.GetAttributeValues(), resource.GetEphemeralId(), action, entitlements, accessibleAttributeValues)

default:
return nil, fmt.Errorf("unsupported resource type: %w", ErrInvalidResource)
}

// should never reach here
return nil, fmt.Errorf("unable to get resource resource decision: %w", ErrInvalidResource)
}

// evaluateResourceAttributeValues evaluates a list of attribute values against the action and entitlements
func evaluateResourceAttributeValues(
ctx context.Context,
logger *logger.Logger,
resourceAttributeValues *authz.Resource_AttributeValues,
resourceID string,
action *policy.Action,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
accessibleAttributeValues map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue,
) (*ResourceDecision, error) {
// Group value FQNs by parent definition
definitionFqnToValueFqns := make(map[string][]string)
definitionsLookup := make(map[string]*policy.Attribute)
for idx, valueFQN := range resourceAttributeValues.GetFqns() {
// lowercase the value FQN to ensure case-insensitive matching
valueFQN = strings.ToLower(valueFQN)
resourceAttributeValues.Fqns[idx] = valueFQN

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

// Evaluate each definition by rule, resource attributes, action, and entitlements
passed := true
dataRuleResults := make([]DataRuleResult, 0)

for defFQN, resourceValueFQNs := range definitionFqnToValueFqns {
definition := definitionsLookup[defFQN]
if definition == nil {
return nil, fmt.Errorf("%w: %s", ErrDefinitionNotFound, defFQN)
}

dataRuleResult, err := evaluateDefinition(ctx, logger, entitlements, action, resourceValueFQNs, definition)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrFailedEvaluation, err.Error())
}
if !dataRuleResult.Passed {
passed = false
}

dataRuleResults = append(dataRuleResults, *dataRuleResult)
}

// Return results in the appropriate structure
return &ResourceDecision{
Passed: passed,
ResourceID: resourceID,
DataRuleResults: dataRuleResults,
}, nil
}

func evaluateDefinition(
ctx context.Context,
logger *logger.Logger,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
action *policy.Action,
resourceValueFQNs []string,
attrDefinition *policy.Attribute,
) (*DataRuleResult, error) {
var entitlementFailures []EntitlementFailure

logger.DebugContext(
ctx,
"evaluating definition",
slog.String("definition rule", attrDefinition.GetRule().String()),
slog.String("definition FQN", attrDefinition.GetFqn()),
slog.Any("entitlements", entitlements),
slog.String("action", action.GetName()),
slog.Any("resource value FQNs", resourceValueFQNs),
)

switch attrDefinition.GetRule() {
case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF:
entitlementFailures = allOfRule(ctx, logger, entitlements, action, resourceValueFQNs)

case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF:
entitlementFailures = anyOfRule(ctx, logger, entitlements, action, resourceValueFQNs)

case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY:
entitlementFailures = hierarchyRule(ctx, logger, entitlements, action, resourceValueFQNs, attrDefinition)

case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED:
return nil, fmt.Errorf("%w: %s, rule: %s", ErrMissingRequiredSpecifiedRule, attrDefinition.GetFqn(), attrDefinition.GetRule().String())
default:
return nil, fmt.Errorf("%w: %s", ErrUnrecognizedRule, attrDefinition.GetRule().String())
}

result := &DataRuleResult{
Passed: len(entitlementFailures) == 0,
RuleDefinition: attrDefinition,
}
if len(entitlementFailures) > 0 {
result.EntitlementFailures = entitlementFailures
}
return result, nil
}

// allOfRule validates that:
// 1. For each resource attribute value FQN, the action is entitled
// 2. If any FQN is not entitled, or the FQN is missing the requested action, the rule fails
func allOfRule(
_ context.Context,
_ *logger.Logger,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
action *policy.Action,
resourceValueFQNs []string,
) []EntitlementFailure {
actionName := action.GetName()
failures := make([]EntitlementFailure, 0, len(resourceValueFQNs)) // Pre-allocate for efficiency

// Single loop through all resource value FQNs
for _, valueFQN := range resourceValueFQNs {
hasEntitlement := false

// Check if this FQN has the entitled action
if entitledActions, ok := entitlements[valueFQN]; ok {
for _, entitledAction := range entitledActions {
if strings.EqualFold(entitledAction.GetName(), actionName) {
hasEntitlement = true
break
}
}
}

// If no entitlement found for this FQN, add to failures immediately
if !hasEntitlement {
failures = append(failures, EntitlementFailure{
AttributeValueFQN: valueFQN,
ActionName: actionName,
})
}
}

return failures
}

// anyOfRule validates that:
// 1. At least one resource attribute value FQN has the action entitled
// 2. If none of the FQNs are found the entitlements, the rule fails
// 3. If none of the matching FQNs in the entitlements contain the requested action, the rule fails
func anyOfRule(
_ context.Context,
_ *logger.Logger,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
action *policy.Action,
resourceValueFQNs []string,
) []EntitlementFailure {
// No resources to check
if len(resourceValueFQNs) == 0 {
return nil
}

actionName := action.GetName()
anyEntitlementFound := false
entitlementFailures := make([]EntitlementFailure, 0, len(resourceValueFQNs))

// Single loop through all resource value FQNs
for _, valueFQN := range resourceValueFQNs {
foundEntitlementForThisFQN := false

entitledActions, ok := entitlements[valueFQN]
if ok {
for _, entitledAction := range entitledActions {
if strings.EqualFold(entitledAction.GetName(), actionName) {
foundEntitlementForThisFQN = true
anyEntitlementFound = true
break
}
}
}

if !foundEntitlementForThisFQN {
entitlementFailures = append(entitlementFailures, EntitlementFailure{
AttributeValueFQN: valueFQN,
ActionName: actionName,
})
}
}

// Rule is satisfied if at least one FQN has the entitled action
if anyEntitlementFound {
return nil
}
return entitlementFailures
}

// hierarchyRule validates that:
// 1. The user has entitlement to the specified action on the highest value FQN in the hierarchy or any hierarchically higher value
// 2. The highest value FQN is determined by the lowest index in the hierarchy definition
// 3. If the highest value FQN or any higher value has the required entitlement, the rule passes with no failures
// 4. If no hierarchically relevant FQN has the required entitlement, the rule fails with all missing entitlements
func hierarchyRule(
_ context.Context,
_ *logger.Logger,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
action *policy.Action,
resourceValueFQNs []string,
attrDefinition *policy.Attribute,
) []EntitlementFailure {
// No resources to check
if len(resourceValueFQNs) == 0 {
return nil
}

actionName := action.GetName()

// Create a lookup map for the attribute value indices - O(n) where n is the number of values in the attribute
valueFQNToIndex := make(map[string]int, len(attrDefinition.GetValues()))
for idx, value := range attrDefinition.GetValues() {
valueFQNToIndex[value.GetFqn()] = idx
}

// Find the lowest indexed value FQN (highest in hierarchy) - O(m) where m is the number of resource values
lowestValueFQNIndex := len(attrDefinition.GetValues())
for _, valueFQN := range resourceValueFQNs {
if idx, exists := valueFQNToIndex[valueFQN]; exists && idx < lowestValueFQNIndex {
lowestValueFQNIndex = idx
}
}

// Check if the entitlements contain any values with index <= lowestValueFQNIndex
// This checks the requested value and any hierarchically higher values in a single pass - O(e) where e is entitlements count
for entitlementFQN, entitledActions := range entitlements {
// Check if this entitlement FQN has a valid index in the hierarchy
if idx, exists := valueFQNToIndex[entitlementFQN]; exists && idx <= lowestValueFQNIndex {
// Check if the required action is entitled
for _, entitledAction := range entitledActions {
if strings.EqualFold(entitledAction.GetName(), actionName) {
return nil // Found an entitled action at or above the hierarchy level, no failures
}
}
}
}

// The rule was not satisfied - collect failures - O(m) where m is the number of resource values
entitlementFailures := make([]EntitlementFailure, 0, len(resourceValueFQNs))
for _, valueFQN := range resourceValueFQNs {
foundValue := false
if entitledActions, ok := entitlements[valueFQN]; ok {
for _, entitledAction := range entitledActions {
if strings.EqualFold(entitledAction.GetName(), actionName) {
foundValue = true
break
}
}
}

if !foundValue {
entitlementFailures = append(entitlementFailures, EntitlementFailure{
AttributeValueFQN: valueFQN,
ActionName: actionName,
})
}
}

return entitlementFailures
}
Loading
Loading