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
38 changes: 27 additions & 11 deletions service/internal/access/v2/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var (
// between entitlement checks for the different types of resources
func getResourceDecision(
ctx context.Context,
logger *logger.Logger,
l *logger.Logger,
accessibleAttributeValues map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
action *policy.Action,
Expand All @@ -37,7 +37,7 @@ func getResourceDecision(
return nil, err
}

logger.DebugContext(
l.DebugContext(
ctx,
"getting decision on one resource",
slog.Any("resource", resource.GetResource()),
Expand All @@ -48,7 +48,7 @@ func getResourceDecision(
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)
return evaluateResourceAttributeValues(ctx, l, resource.GetAttributeValues(), resource.GetEphemeralId(), action, entitlements, accessibleAttributeValues)

default:
return nil, fmt.Errorf("unsupported resource type: %w", ErrInvalidResource)
Expand Down Expand Up @@ -121,7 +121,6 @@ func evaluateDefinition(

l = l.With("definitionRule", attrDefinition.GetRule().String())
l = l.With("definitionFQN", attrDefinition.GetFqn())
l = l.With("action", action.GetName())

l.DebugContext(
ctx,
Expand All @@ -146,9 +145,15 @@ func evaluateDefinition(
}

passed := len(entitlementFailures) == 0
simpleAttribute := &policy.Attribute{
Id: attrDefinition.GetId(),
Fqn: attrDefinition.GetFqn(),
Rule: attrDefinition.GetRule(),
}
result := &DataRuleResult{
Passed: passed,
RuleDefinition: attrDefinition,
Passed: passed,
Attribute: simpleAttribute,
ResourceValueFQNs: resourceValueFQNs,
}
l.DebugContext(ctx, "definition evaluation result", slog.Bool("passed", passed))
if !passed {
Expand Down Expand Up @@ -252,8 +257,8 @@ func anyOfRule(
// 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,
ctx context.Context,
l *logger.Logger,
entitlements subjectmappingbuiltin.AttributeValueFQNsToActions,
action *policy.Action,
resourceValueFQNs []string,
Expand All @@ -265,15 +270,16 @@ func hierarchyRule(
}

actionName := action.GetName()
attrValues := attrDefinition.GetValues()

// 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 := make(map[string]int, len(attrValues))
for idx, value := range attrValues {
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())
lowestValueFQNIndex := len(attrValues)
for _, valueFQN := range resourceValueFQNs {
if idx, exists := valueFQNToIndex[valueFQN]; exists && idx < lowestValueFQNIndex {
lowestValueFQNIndex = idx
Expand All @@ -288,6 +294,16 @@ func hierarchyRule(
// Check if the required action is entitled
for _, entitledAction := range entitledActions {
if strings.EqualFold(entitledAction.GetName(), actionName) {
l.DebugContext(ctx, "hierarchy rule satisfied",
slog.Group("entitled_by_value",
slog.String("FQN", entitlementFQN),
slog.Int("index", idx),
),
slog.Group("resource_highest_hierarchy_value",
slog.String("FQN", attrValues[lowestValueFQNIndex].GetFqn()),
slog.Int("index", lowestValueFQNIndex),
),
)
return nil // Found an entitled action at or above the hierarchy level, no failures
}
}
Expand Down
21 changes: 18 additions & 3 deletions service/internal/access/v2/pdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
attrs "github.com/opentdf/platform/protocol/go/policy/attributes"
"github.com/opentdf/platform/service/internal/subjectmappingbuiltin"
"github.com/opentdf/platform/service/logger"
"github.com/opentdf/platform/service/logger/audit"
)

// Decision represents the overall access decision for an entity.
Expand All @@ -34,7 +35,8 @@ type ResourceDecision struct {
// DataRuleResult represents the result of evaluating one rule for an entity.
type DataRuleResult struct {
Passed bool `json:"passed" example:"false"`
RuleDefinition *policy.Attribute `json:"rule_definition"`
ResourceValueFQNs []string `json:"resource_value_fqns"`
Attribute *policy.Attribute `json:"attribute"`
EntitlementFailures []EntitlementFailure `json:"entitlement_failures"`
}

Expand Down Expand Up @@ -197,7 +199,7 @@ func (p *PolicyDecisionPoint) GetDecision(
}

decisionableAttributes[valueFQN] = attributeAndValue
err := populateHigherValuesIfHierarchy(ctx, p.logger, valueFQN, attributeAndValue.GetAttribute(), p.allEntitleableAttributesByValueFQN, decisionableAttributes)
err := populateHigherValuesIfHierarchy(ctx, l, valueFQN, attributeAndValue.GetAttribute(), p.allEntitleableAttributesByValueFQN, decisionableAttributes)
if err != nil {
return nil, fmt.Errorf("error populating higher hierarchy attribute values: %w", err)
}
Expand All @@ -223,7 +225,7 @@ func (p *PolicyDecisionPoint) GetDecision(
}

for idx, resource := range resources {
resourceDecision, err := getResourceDecision(ctx, p.logger, decisionableAttributes, entitledFQNsToActions, action, resource)
resourceDecision, err := getResourceDecision(ctx, l, decisionableAttributes, entitledFQNsToActions, action, resource)
if err != nil || resourceDecision == nil {
return nil, fmt.Errorf("error evaluating a decision on resource [%v]: %w", resource, err)
}
Expand All @@ -242,6 +244,19 @@ func (p *PolicyDecisionPoint) GetDecision(
decision.Results[idx] = *resourceDecision
}

auditDecision := audit.GetDecisionResultDeny
if decision.Access {
auditDecision = audit.GetDecisionResultPermit
}

l.Audit.GetDecisionV2(ctx, audit.GetDecisionV2EventParams{
EntityID: entityRepresentation.GetOriginalId(),
ActionName: action.GetName(),
Decision: auditDecision,
Entitlements: entitledFQNsToActions,
ResourceDecisions: decision.Results,
})

return decision, nil
}

Expand Down
10 changes: 5 additions & 5 deletions service/internal/access/v2/pdp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,7 @@ func (s *PDPTestSuite) Test_GetDecision_CombinedAttributeRules_SingleResource()
s.Equal("secret-engineering-usa-uk-resource", resourceDecision.ResourceID)
s.Len(resourceDecision.DataRuleResults, 3)
for _, ruleResult := range resourceDecision.DataRuleResults {
switch ruleResult.RuleDefinition.GetFqn() {
switch ruleResult.Attribute.GetFqn() {
case testClassificationFQN:
s.True(ruleResult.Passed)
case testDepartmentFQN:
Expand Down Expand Up @@ -1077,7 +1077,7 @@ func (s *PDPTestSuite) Test_GetDecision_CombinedAttributeRules_SingleResource()
} else {
failCount++
// Check that failure is for country attribute
s.Contains(dataRule.RuleDefinition.GetFqn(), "department")
s.Contains(dataRule.Attribute.GetFqn(), "department")
}
}
s.Equal(2, passCount, "Two attributes should pass")
Expand Down Expand Up @@ -1122,7 +1122,7 @@ func (s *PDPTestSuite) Test_GetDecision_CombinedAttributeRules_SingleResource()
if dataRule.Passed {
passCount++
// Only the platform attribute should pass for delete
s.Contains(dataRule.RuleDefinition.GetFqn(), "platform")
s.Contains(dataRule.Attribute.GetFqn(), "platform")
} else {
failCount++
}
Expand Down Expand Up @@ -1315,10 +1315,10 @@ func (s *PDPTestSuite) Test_GetDecision_AcrossNamespaces() {
s.Len(onlyDecision.DataRuleResults, 3)
for _, dataRule := range onlyDecision.DataRuleResults {
if dataRule.Passed {
isExpected := dataRule.RuleDefinition.GetFqn() == testPlatformFQN || dataRule.RuleDefinition.GetFqn() == testClassificationFQN
isExpected := dataRule.Attribute.GetFqn() == testPlatformFQN || dataRule.Attribute.GetFqn() == testClassificationFQN
s.True(isExpected, "Platform and classification should pass")
} else {
s.Equal(testProjectFQN, dataRule.RuleDefinition.GetFqn(), "Project should fail")
s.Equal(testProjectFQN, dataRule.Attribute.GetFqn(), "Project should fail")
s.Len(dataRule.EntitlementFailures, 1)
s.Equal(testProjectAlphaFQN, dataRule.EntitlementFailures[0].AttributeValueFQN)
}
Expand Down
53 changes: 53 additions & 0 deletions service/logger/audit/getDecision.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"time"

"github.com/opentdf/platform/service/internal/subjectmappingbuiltin"
)

type DecisionResult int
Expand Down Expand Up @@ -41,6 +43,15 @@ type GetDecisionEventParams struct {
FQNs []string
}

type GetDecisionV2EventParams struct {
EntityID string
ActionName string
Decision DecisionResult
Entitlements subjectmappingbuiltin.AttributeValueFQNsToActions
// Allow ResourceDecisions to be typed by the caller as structure is in-flight
ResourceDecisions interface{}
}

func CreateGetDecisionEvent(ctx context.Context, params GetDecisionEventParams) (*EventObject, error) {
auditDataFromContext := GetAuditDataFromContext(ctx)

Expand Down Expand Up @@ -77,6 +88,48 @@ func CreateGetDecisionEvent(ctx context.Context, params GetDecisionEventParams)
}, nil
}

func CreateV2GetDecisionEvent(ctx context.Context, params GetDecisionV2EventParams) (*EventObject, error) {
auditDataFromContext := GetAuditDataFromContext(ctx)

// Get result from decision
result := ActionResultSuccess
if params.Decision == GetDecisionResultDeny {
result = ActionResultFailure
}

actorAttributes := []interface{}{
struct {
Entitlements subjectmappingbuiltin.AttributeValueFQNsToActions `json:"entitlements"`
}{
Entitlements: params.Entitlements,
},
}

return &EventObject{
Object: auditEventObject{
ID: params.EntityID + "-" + params.ActionName,
Type: ObjectTypeEntityObject,
Name: "decisionRequest-" + params.ActionName,
},
Action: eventAction{
Type: ActionTypeRead,
Result: result,
},
Actor: auditEventActor{
ID: params.EntityID,
Attributes: actorAttributes,
},
EventMetaData: params.ResourceDecisions,
ClientInfo: eventClientInfo{
Platform: "authorization.v2",
UserAgent: auditDataFromContext.UserAgent,
RequestIP: auditDataFromContext.RequestIP,
},
RequestID: auditDataFromContext.RequestID,
Timestamp: time.Now().Format(time.RFC3339),
}, nil
}

func buildActorAttributes(entityChainEntitlements []EntityChainEntitlement) []interface{} {
actorAttributes := make([]interface{}, len(entityChainEntitlements))
for i, v := range entityChainEntitlements {
Expand Down
9 changes: 9 additions & 0 deletions service/logger/audit/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ func (a *Logger) GetDecision(ctx context.Context, eventParams GetDecisionEventPa
a.logger.Log(ctx, LevelAudit, "decision", "audit", *auditEvent)
}

func (a *Logger) GetDecisionV2(ctx context.Context, eventParams GetDecisionV2EventParams) {
event, err := CreateV2GetDecisionEvent(ctx, eventParams)
if err != nil {
a.logger.ErrorContext(ctx, "error creating v2 get decision audit event", "err", err)
return
}
a.logger.Log(ctx, LevelAudit, "decision", "audit", *event)
}

func (a *Logger) rewrapBase(ctx context.Context, eventParams RewrapAuditEventParams) {
auditEvent, err := CreateRewrapAuditEvent(ctx, eventParams)
if err != nil {
Expand Down
Loading