-
Notifications
You must be signed in to change notification settings - Fork 24
feat(authz): access pdp v2 with actions #2264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
677129d
feat(authz): access pdp v2 with actions
jakedoublev 9bf0e60
put back logging changes
jakedoublev 5cd84cb
cleanup
jakedoublev 00ead46
improvements
jakedoublev 2ac9eb1
cleanup
jakedoublev fc74ba2
copilot fixes
jakedoublev 62ec151
error logging suggestions
jakedoublev 31a4f3a
lint fixes
jakedoublev 0a94bdd
name errors where they are utilized
jakedoublev 94dc733
copilot suggestions
jakedoublev 1e8392e
gemini comments
jakedoublev dfae101
return errors more effectively if encountering registered resources a…
jakedoublev bcf4959
logging suggestions
jakedoublev dd7fd44
lower case resource attribute FQN at highest level in GetDecision flow
jakedoublev 4d33515
merge duplicate actions performance
jakedoublev e1f92d8
error assertions in tests
jakedoublev 01007b6
lint fix
jakedoublev 8566fc4
improve validator logic per suggestion
jakedoublev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,317 @@ | ||
package access | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"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" | ||
) | ||
|
||
var ( | ||
ErrInvalidResource = errors.New("access: invalid resource") | ||
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") | ||
) | ||
|
||
// 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) { | ||
// TODO: handle registered resources | ||
case *authz.Resource_RegisteredResourceValueFqn: | ||
return nil, fmt.Errorf("registered resources not supported yet: %w", ErrInvalidResource) | ||
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) | ||
} | ||
} | ||
|
||
// evaluateResourceAttributeValues evaluates a list of attribute values against the action and entitlements | ||
// and lowercases the FQNs to ensure case-insensitive matching | ||
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 _, valueFQN := range resourceAttributeValues.GetFqns() { | ||
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, errors.Join(ErrFailedEvaluation, err) | ||
} | ||
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 | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.