diff --git a/service/internal/access/pdp.go b/service/internal/access/pdp.go index 6978d3685a..74723ae2f8 100644 --- a/service/internal/access/pdp.go +++ b/service/internal/access/pdp.go @@ -9,10 +9,33 @@ import ( "github.com/opentdf/platform/service/logger" ) +// === Structures === + +// Decision represents the overall access decision for an entity. +type Decision struct { + Access bool `json:"access" example:"false"` + Results []DataRuleResult `json:"entity_rule_result"` +} + +// 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"` + ValueFailures []ValueFailure `json:"value_failures"` +} + +// ValueFailure represents a specific failure when evaluating a data attribute. +type ValueFailure struct { + DataAttribute *policy.Value `json:"data_attribute"` + Message string `json:"message" example:"Criteria NOT satisfied for entity: {entity_id} - lacked attribute value: {attribute}"` +} + +// Pdp represents the Policy Decision Point component. type Pdp struct { logger *logger.Logger } +// NewPdp creates a new Policy Decision Point instance. func NewPdp(l *logger.Logger) *Pdp { return &Pdp{ logger: l, @@ -29,152 +52,200 @@ func (pdp *Pdp) DetermineAccess( attributeDefinitions []*policy.Attribute, ) (map[string]*Decision, error) { pdp.logger.DebugContext(ctx, "DetermineAccess") - // Group all the Data Attribute Values by their Definitions (that is, "/attr/"). - // Definitions contain the rule logic for how to evaluate the data Attribute Values as a group (i.e. ANY_OF/ALL_OF/HIERARCHY). - // - // For example, we may have one group for the Definition FQN "https://namespace.org/attr/MyAttr" - // with two Attribute Values on the data: - // - "https://namespace.org/attr/MyAttr/value/Value1") - // - "https://namespace.org/attr/MyAttr/value/Value2") - dataAttrValsByDefinition, err := GroupValuesByDefinition(dataAttributes) + + if len(dataAttributes) == 0 { + pdp.logger.DebugContext(ctx, "No data attributes provided") + return nil, fmt.Errorf("no data attributes provided") + } + + if len(attributeDefinitions) == 0 { + pdp.logger.DebugContext(ctx, "No attribute definitions provided") + return nil, fmt.Errorf("no attribute definitions provided") + } + + dataAttrValsByDefinition, err := pdp.groupDataAttributesByDefinition(ctx, dataAttributes) if err != nil { - pdp.logger.Error(fmt.Sprintf("error grouping data attributes by definition: %s", err.Error())) return nil, err } - // Unlike with Values, there should only be *one* Attribute Definition per FQN (e.g "https://namespace.org/attr/MyAttr") - fqnToDefinitionMap, err := GetFqnToDefinitionMap(ctx, attributeDefinitions, pdp.logger) + fqnToDefinitionMap, err := pdp.mapFqnToDefinitions(ctx, attributeDefinitions) if err != nil { - pdp.logger.Error(fmt.Sprintf("error grouping attribute definitions by FQN: %s", err.Error())) return nil, err } + return pdp.evaluateAttributes(ctx, dataAttrValsByDefinition, fqnToDefinitionMap, entityAttributeSets) +} + +// groups provided values +func (pdp *Pdp) groupDataAttributesByDefinition(ctx context.Context, dataAttributes []*policy.Value) (map[string][]*policy.Value, error) { + groupings := make(map[string][]*policy.Value) + + for _, v := range dataAttributes { + if v.GetAttribute() != nil { + defFqn := v.GetAttribute().GetFqn() + if defFqn != "" { + groupings[defFqn] = append(groupings[defFqn], v) + continue + } + } + + defFqn, err := GetDefinitionFqnFromValueFqn(v.GetFqn()) + if err != nil { + pdp.logger.ErrorContext(ctx, fmt.Sprintf("error getting definition FQN from value: %s", err.Error())) + return nil, err + } + + groupings[defFqn] = append(groupings[defFqn], v) + } + + return groupings, nil +} + +// maps defintion FQN to definition object +func (pdp *Pdp) mapFqnToDefinitions(ctx context.Context, attributeDefinitions []*policy.Attribute) (map[string]*policy.Attribute, error) { + grouped := make(map[string]*policy.Attribute) + + for _, def := range attributeDefinitions { + defFQN, err := GetDefinitionFqnFromDefinition(def) + if err != nil { + return nil, err + } + + if v, ok := grouped[defFQN]; ok { + pdp.logger.Warn(fmt.Sprintf("duplicate Attribute Definition FQN %s found when building FQN map which may indicate an issue", defFQN)) + pdp.logger.TraceContext(ctx, "duplicate attribute definitions found are: ", "attr1", v, "attr2", def) + } + + grouped[defFQN] = def + } + + return grouped, nil +} + +func (pdp *Pdp) evaluateAttributes( + ctx context.Context, + dataAttrValsByDefinition map[string][]*policy.Value, + fqnToDefinitionMap map[string]*policy.Attribute, + entityAttributeSets map[string][]string, +) (map[string]*Decision, error) { decisions := make(map[string]*Decision) - // Go through all the grouped data values under each definition FQN + for definitionFqn, distinctValues := range dataAttrValsByDefinition { pdp.logger.DebugContext(ctx, "Evaluating data attribute fqn", "fqn:", definitionFqn) + attrDefinition, ok := fqnToDefinitionMap[definitionFqn] if !ok { return nil, fmt.Errorf("expected an Attribute Definition under the FQN %s", definitionFqn) } - // If GroupBy is set, determine which entities (out of the set of entities and their respective Values) - // will be considered for evaluation under this Definition's Rule. - // - // If GroupBy is not set, then we always consider all entities for evaluation under a Rule - // - // If this rule simply does not apply to a given entity ID as defined by the Attribute Definition we have, - // and the entity Values that entity ID has, then that entity ID passed (or skipped) this rule. - var ( - entityRuleDecision map[string]DataRuleResult - err error - ) - switch attrDefinition.GetRule() { - case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF: - pdp.logger.DebugContext(ctx, "Evaluating under allOf", "name", definitionFqn) - entityRuleDecision, err = pdp.allOfRule(ctx, distinctValues, entityAttributeSets) - case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF: - pdp.logger.DebugContext(ctx, "Evaluating under anyOf", "name", definitionFqn) - entityRuleDecision, err = pdp.anyOfRule(ctx, distinctValues, entityAttributeSets) - case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY: - pdp.logger.DebugContext(ctx, "Evaluating under hierarchy", "name", definitionFqn) - entityRuleDecision, err = pdp.hierarchyRule(ctx, distinctValues, entityAttributeSets, attrDefinition.GetValues()) - case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED: - return nil, fmt.Errorf("unset AttributeDefinition rule: %s", attrDefinition.GetRule()) - default: - return nil, fmt.Errorf("unrecognized AttributeDefinition rule: %s", attrDefinition.GetRule()) - } + entityRuleDecision, err := pdp.evaluateRule(ctx, attrDefinition, distinctValues, entityAttributeSets) if err != nil { - return nil, fmt.Errorf("error evaluating rule: %s", err.Error()) + return nil, err } - // Roll up the per-data-rule decisions for each entity considered for this rule into the overall decision - for entityID, ruleResult := range entityRuleDecision { - entityDecision := decisions[entityID] - - ruleResult.RuleDefinition = attrDefinition - // If we do not yet have an overall decision for this entity, initialize the map - // with entityId as key and a Decision object as value - if entityDecision == nil { - decisions[entityID] = &Decision{ - Access: ruleResult.Passed, - Results: []DataRuleResult{ruleResult}, - } - } else { - // An overall Decision already exists for this entity, so update it with the new information - // from the last rule evaluation - - // boolean AND the new rule result for this entity and this rule with the existing access - // result for this entity and the previous rules - // to make sure we flip the overall access correctly, e.g if existing overall result is - // TRUE and this new rule result is FALSE, then overall result flips to FALSE. - // If it was previously FALSE it stays FALSE, etc - entityDecision.Access = entityDecision.Access && ruleResult.Passed - // Append the current rule result to the list of rule results. - entityDecision.Results = append(entityDecision.Results, ruleResult) - } - } + pdp.rollUpDecisions(entityRuleDecision, attrDefinition, decisions) } return decisions, nil } -// AllOf the Data Attribute Values should be present in AllOf the Entity's entityAttributeValue sets -// Accepts -// - a set of data Attribute Values with the same FQN -// - a map of entity Attribute Values keyed by entity ID -// Returns a map of DataRuleResults keyed by Subject -func (pdp *Pdp) allOfRule(ctx context.Context, dataAttrValuesOfOneDefinition []*policy.Value, entityAttributeValueFqns map[string][]string) (map[string]DataRuleResult, error) { +func (pdp *Pdp) evaluateRule( + ctx context.Context, + attrDefinition *policy.Attribute, + distinctValues []*policy.Value, + entityAttributeSets map[string][]string, +) (map[string]DataRuleResult, error) { + switch attrDefinition.GetRule() { + case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF: + pdp.logger.DebugContext(ctx, "Evaluating under allOf", "name", attrDefinition.GetFqn()) + return pdp.allOfRule(ctx, distinctValues, entityAttributeSets) + + case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF: + pdp.logger.DebugContext(ctx, "Evaluating under anyOf", "name", attrDefinition.GetFqn()) + return pdp.anyOfRule(ctx, distinctValues, entityAttributeSets) + + case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY: + pdp.logger.DebugContext(ctx, "Evaluating under hierarchy", "name", attrDefinition.GetFqn()) + return pdp.hierarchyRule(ctx, distinctValues, entityAttributeSets, attrDefinition.GetValues()) + + case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED: + return nil, fmt.Errorf("AttributeDefinition rule cannot be unspecified: %s, rule: %v", attrDefinition.GetFqn(), attrDefinition.GetRule()) + default: + return nil, fmt.Errorf("unrecognized AttributeDefinition rule: %s", attrDefinition.GetRule()) + } +} + +func (pdp *Pdp) rollUpDecisions( + entityRuleDecision map[string]DataRuleResult, + attrDefinition *policy.Attribute, + decisions map[string]*Decision, +) { + for entityID, ruleResult := range entityRuleDecision { + entityDecision := decisions[entityID] + ruleResult.RuleDefinition = attrDefinition + + if entityDecision == nil { + decisions[entityID] = &Decision{ + Access: ruleResult.Passed, + Results: []DataRuleResult{ruleResult}, + } + } else { + entityDecision.Access = entityDecision.Access && ruleResult.Passed + entityDecision.Results = append(entityDecision.Results, ruleResult) + } + } +} + +// allOfRule evaluates data attributes against entity attributes using the "all of" rule. +// All data attributes must be found in the entity's attributes to grant access. +func (pdp *Pdp) allOfRule( + ctx context.Context, + dataAttrValuesOfOneDefinition []*policy.Value, + entityAttributeValueFqns map[string][]string, +) (map[string]DataRuleResult, error) { ruleResultsByEntity := make(map[string]DataRuleResult) - // All of the data Attribute Values in the arg have the same Definition FQN + if len(dataAttrValuesOfOneDefinition) == 0 { + return ruleResultsByEntity, nil + } + def, err := GetDefinitionFqnFromValue(dataAttrValuesOfOneDefinition[0]) if err != nil { return nil, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) } + pdp.logger.DebugContext(ctx, "Evaluating allOf decision", "attribute definition FQN", def) - pdp.logger.TraceContext(ctx, "Attribute values for ", "attribute definition FQN", def, "values", dataAttrValuesOfOneDefinition) + pdp.logger.TraceContext(ctx, "Attribute values for", "attribute definition FQN", def, "values", dataAttrValuesOfOneDefinition) - // Go through every entity's attributeValues set... for entityID, entityAttrVals := range entityAttributeValueFqns { var valueFailures []ValueFailure - // Default to DENY - entityPassed := false + entityPassed := true groupedEntityAttrValsByDefinition, err := GroupValueFqnsByDefinition(entityAttrVals) if err != nil { return nil, fmt.Errorf("error grouping entity attribute values by definition: %s", err.Error()) } - // For every unique data Attribute Value in the set sharing the same FQN... - for dvIndex, dataAttrVal := range dataAttrValuesOfOneDefinition { + for _, dataAttrVal := range dataAttrValuesOfOneDefinition { attrDefFqn, err := GetDefinitionFqnFromValue(dataAttrVal) if err != nil { return nil, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) } + pdp.logger.DebugContext(ctx, "Evaluating allOf decision", "data attr fqn", attrDefFqn, "value", dataAttrVal.GetValue()) - // See if - // 1. there exists an entity Attribute Value in the set of Attribute Values - // with the same FQN as the data Attribute Value in question - // 2. It has the same VALUE as the data Attribute Value in question - found := getIsValueFoundInFqnValuesSet(dataAttrValuesOfOneDefinition[dvIndex], groupedEntityAttrValsByDefinition[attrDefFqn], pdp.logger) - - // If we did not find the data Attribute Value FQN + value in the entity Attribute Value set, - // then prepare a ValueFailure for that data Attribute Value for this entity + + found := getIsValueFoundInFqnValuesSet(dataAttrVal, groupedEntityAttrValsByDefinition[attrDefFqn], pdp.logger) if !found { denialMsg := fmt.Sprintf("AllOf not satisfied for data attr %s with value %s and entity %s", attrDefFqn, dataAttrVal.GetValue(), entityID) pdp.logger.WarnContext(ctx, denialMsg) - // Append the ValueFailure to the set of entity value failures valueFailures = append(valueFailures, ValueFailure{ - DataAttribute: dataAttrValuesOfOneDefinition[dvIndex], + DataAttribute: dataAttrVal, Message: denialMsg, }) + entityPassed = false } } - // If we have no value failures, we are good - entity passes this rule - if len(valueFailures) == 0 { - entityPassed = true - } ruleResultsByEntity[entityID] = DataRuleResult{ Passed: entityPassed, ValueFailures: valueFailures, @@ -184,26 +255,29 @@ func (pdp *Pdp) allOfRule(ctx context.Context, dataAttrValuesOfOneDefinition []* return ruleResultsByEntity, nil } -// AnyOf the Data Attribute Values can be present in AnyOf the Entity's Attribute Value FQN sets -// Accepts -// - a set of data Attribute Values with the same FQN -// - a map of entity Attribute Values keyed by entity ID -// Returns a map of DataRuleResults keyed by Subject entity ID -func (pdp *Pdp) anyOfRule(ctx context.Context, dataAttrValuesOfOneDefinition []*policy.Value, entityAttributeValueFqns map[string][]string) (map[string]DataRuleResult, error) { +// anyOfRule evaluates data attributes against entity attributes using the "any of" rule. +// At least one data attribute must be found in the entity's attributes to grant access. +func (pdp *Pdp) anyOfRule( + ctx context.Context, + dataAttrValuesOfOneDefinition []*policy.Value, + entityAttributeValueFqns map[string][]string, +) (map[string]DataRuleResult, error) { ruleResultsByEntity := make(map[string]DataRuleResult) - // All of the data Attribute Values in the arg have the same Definition FQN + if len(dataAttrValuesOfOneDefinition) == 0 { + return ruleResultsByEntity, nil + } + attrDefFqn, err := GetDefinitionFqnFromValue(dataAttrValuesOfOneDefinition[0]) if err != nil { return nil, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) } + pdp.logger.DebugContext(ctx, "Evaluating anyOf decision", "attribute definition FQN", attrDefFqn) - pdp.logger.TraceContext(ctx, "Attribute values for ", "attribute definition FQN", attrDefFqn, "values", dataAttrValuesOfOneDefinition) + pdp.logger.TraceContext(ctx, "Attribute values for", "attribute definition FQN", attrDefFqn, "values", dataAttrValuesOfOneDefinition) - // Go through every entity's Attribute Value set... for entityID, entityAttrValFqns := range entityAttributeValueFqns { var valueFailures []ValueFailure - // Default to DENY entityPassed := false entityAttrGroup, err := GroupValueFqnsByDefinition(entityAttrValFqns) @@ -211,33 +285,28 @@ func (pdp *Pdp) anyOfRule(ctx context.Context, dataAttrValuesOfOneDefinition []* return nil, fmt.Errorf("error grouping entity attribute values by definition: %s", err.Error()) } - // For every unique data Attribute Value in this set of data Attribute Value sharing the same FQN... - for dvIndex, dataAttrVal := range dataAttrValuesOfOneDefinition { + for _, dataAttrVal := range dataAttrValuesOfOneDefinition { pdp.logger.DebugContext(ctx, "Evaluating anyOf decision", "attribute definition FQN", attrDefFqn, "value", dataAttrVal.GetValue()) - // See if there exists an entity Attribute Value in the set of Attribute Values - // with the same FQN as the data Attribute Value in question - found := getIsValueFoundInFqnValuesSet(dataAttrVal, entityAttrGroup[attrDefFqn], pdp.logger) - // If we did not find the data Attribute Value FQN + value in the entity Attribute Value set, - // then prepare a ValueFailure for that data Attribute Value and value, for this entity - if !found { + found := getIsValueFoundInFqnValuesSet(dataAttrVal, entityAttrGroup[attrDefFqn], pdp.logger) + if found { + entityPassed = true + } else { denialMsg := fmt.Sprintf("anyOf not satisfied for data attr %s with value %s and entity %s - anyOf is permissive, so this doesn't mean overall failure", attrDefFqn, dataAttrVal.GetValue(), entityID) - pdp.logger.WarnContext(ctx, denialMsg) + pdp.logger.DebugContext(ctx, denialMsg) valueFailures = append(valueFailures, ValueFailure{ - DataAttribute: dataAttrValuesOfOneDefinition[dvIndex], + DataAttribute: dataAttrVal, Message: denialMsg, }) } } - // AnyOf - IF there were fewer value failures for this entity, for this Attribute Value FQN, - // then there are distinct data values, for this Attribute Value FQN, THEN this entity must - // possess AT LEAST ONE of the values in its entity Attribute Value group, - // and we have satisfied AnyOf - if len(valueFailures) < len(dataAttrValuesOfOneDefinition) { + if entityPassed { pdp.logger.DebugContext(ctx, "anyOf satisfied", "attribute definition FQN", attrDefFqn, "entityId", entityID) - entityPassed = true + } else { + pdp.logger.WarnContext(ctx, "anyOf not satisfied", "attribute definition FQN", attrDefFqn, "entityId", entityID) } + ruleResultsByEntity[entityID] = DataRuleResult{ Passed: entityPassed, ValueFailures: valueFailures, @@ -247,35 +316,31 @@ func (pdp *Pdp) anyOfRule(ctx context.Context, dataAttrValuesOfOneDefinition []* return ruleResultsByEntity, nil } -// Hierarchy rule compares the HIGHEST (that is, numerically lowest index) data Attribute Value for a given Attribute Value FQN -// with the LOWEST (that is, numerically highest index) entity value for a given Attribute Value FQN. -// -// If multiple data values (that is, Attribute Values) for a given hierarchy AttributeDefinition are present for the same FQN, the highest will be chosen and -// the others ignored. -// -// If multiple entity Attribute Values for a hierarchy AttributeDefinition are present for the same FQN, the lowest will be chosen, -// and the others ignored. -func (pdp *Pdp) hierarchyRule(ctx context.Context, dataAttrValuesOfOneDefinition []*policy.Value, entityAttributeValueFqns map[string][]string, order []*policy.Value) (map[string]DataRuleResult, error) { +// hierarchyRule evaluates data attributes against entity attributes using the hierarchy rule. +// Entity attributes must have equal or higher rank than the data attribute to grant access. +func (pdp *Pdp) hierarchyRule( + ctx context.Context, + dataAttrValuesOfOneDefinition []*policy.Value, + entityAttributeValueFqns map[string][]string, + order []*policy.Value, +) (map[string]DataRuleResult, error) { ruleResultsByEntity := make(map[string]DataRuleResult) highestDataAttrVal, err := pdp.getHighestRankedInstanceFromDataAttributes(ctx, order, dataAttrValuesOfOneDefinition, pdp.logger) if err != nil { return nil, fmt.Errorf("error getting highest ranked instance from data attributes: %s", err.Error()) } + if highestDataAttrVal == nil { pdp.logger.WarnContext(ctx, "No data attribute value found that matches attribute definition allowed values! All entity access will be rejected!") } else { pdp.logger.DebugContext(ctx, "Highest ranked hierarchy value on data attributes found", "value", highestDataAttrVal.GetValue()) } - // All the data Attribute Values in the arg have the same FQN. - // Go through every entity's Attribute Value set... for entityID, entityAttrs := range entityAttributeValueFqns { - // Default to DENY - entityPassed := false valueFailures := []ValueFailure{} + entityPassed := false - // Group entity Attribute Values by FQN... entityAttrGroup, err := GroupValueFqnsByDefinition(entityAttrs) if err != nil { return nil, fmt.Errorf("error grouping entity attribute values by definition: %s", err.Error()) @@ -286,34 +351,25 @@ func (pdp *Pdp) hierarchyRule(ctx context.Context, dataAttrValuesOfOneDefinition if err != nil { return nil, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) } - // For every unique data Attribute Value in this set of data Attribute Values sharing the same FQN... + pdp.logger.DebugContext(ctx, "Evaluating hierarchy decision", "attribute definition fqn", attrDefFqn, "value", highestDataAttrVal.GetValue()) pdp.logger.TraceContext(ctx, "Value obj", "value", highestDataAttrVal.GetValue(), "obj", highestDataAttrVal) - // Compare the (one or more) Attribute Values for this FQN to the (one) data Attribute Value, and see which is "higher". passed, err := entityRankGreaterThanOrEqualToDataRank(order, highestDataAttrVal, entityAttrGroup[attrDefFqn], pdp.logger) if err != nil { return nil, fmt.Errorf("error comparing entity rank to data rank: %s", err.Error()) } entityPassed = passed - // If the rank of the data Attribute Value is higher than the highest entity Attribute Value, then FAIL. if !entityPassed { denialMsg := fmt.Sprintf("Hierarchy - Entity: %s hierarchy values rank below data hierarchy value of %s", entityID, highestDataAttrVal.GetValue()) pdp.logger.WarnContext(ctx, denialMsg) - - // Since there is only one data value we (ultimately) consider in a HierarchyRule, we will only ever - // have one ValueFailure per entity at most valueFailures = append(valueFailures, ValueFailure{ DataAttribute: highestDataAttrVal, Message: denialMsg, }) } - // It's possible we couldn't FIND a highest data value - because none of the data values are in the set of valid attribute definition values! - // If this happens, we can't do a comparison, and access will be denied for every entity for this data attribute instance } else { - // If every data attribute value we're comparing against is invalid (that is, none of them exist in the attribute definition) - // then we must fail and return a nil instance. denialMsg := fmt.Sprintf("Hierarchy - No data values found exist in attribute definition, no hierarchy comparison possible, entity %s is denied", entityID) pdp.logger.WarnContext(ctx, denialMsg) valueFailures = append(valueFailures, ValueFailure{ @@ -321,6 +377,7 @@ func (pdp *Pdp) hierarchyRule(ctx context.Context, dataAttrValuesOfOneDefinition Message: denialMsg, }) } + ruleResultsByEntity[entityID] = DataRuleResult{ Passed: entityPassed, ValueFailures: valueFailures, @@ -330,110 +387,95 @@ func (pdp *Pdp) hierarchyRule(ctx context.Context, dataAttrValuesOfOneDefinition return ruleResultsByEntity, nil } -// It is possible that a data policy may have more than one Hierarchy value for the same data attribute definition -// name, e.g.: -// - "https://namespace.org/attr/MyHierarchyAttr/value/Value1" -// - "https://namespace.org/attr/MyHierarchyAttr/value/Value2" -// Since by definition hierarchy comparisons have to be one-data-value-to-many-entity-values, this won't work. -// So, in a scenario where there are multiple data values to choose from, grab the "highest" ranked value -// present in the set of data Attribute Values, and use that as the point of comparison, ignoring the "lower-ranked" data values. -// If we find a data value that does not exist in the attribute definition's list of valid values, we will skip it -// If NONE of the data values exist in the attribute definitions list of valid values, return a nil instance -func (pdp *Pdp) getHighestRankedInstanceFromDataAttributes(ctx context.Context, order []*policy.Value, dataAttributeGroup []*policy.Value, logger *logger.Logger) (*policy.Value, error) { - // For hierarchy, convention is 0 == most privileged, 1 == less privileged, etc - // So initialize with the LEAST privileged rank in the defined order +// getHighestRankedInstanceFromDataAttributes finds the data attribute with the highest rank in the hierarchy. +func (pdp *Pdp) getHighestRankedInstanceFromDataAttributes( + ctx context.Context, + order []*policy.Value, + dataAttributeGroup []*policy.Value, + logger *logger.Logger, +) (*policy.Value, error) { highestDVIndex := len(order) - 1 var highestRankedInstance *policy.Value + for _, dataAttr := range dataAttributeGroup { foundRank, err := getOrderOfValue(order, dataAttr, logger) if err != nil { return nil, fmt.Errorf("error getting order of value: %s", err.Error()) } + if foundRank == -1 { msg := fmt.Sprintf("Data value %s is not in %s and is not a valid value for this attribute - ignoring this invalid value and continuing to look for a valid one...", dataAttr.GetValue(), order) pdp.logger.WarnContext(ctx, msg) - // If this isn't a valid data value, skip this iteration and look at the next one - maybe it is? - // If none of them are valid, we should return a nil instance continue } + pdp.logger.DebugContext(ctx, "Found data", "rank", foundRank, "value", dataAttr.GetValue(), "maxRank", highestDVIndex) - // If this rank is a "higher rank" (that is, a lower index) than the last one, - // (or it is the same rank, to handle cases where the lowest is the only) - // it becomes the new high watermark rank. if foundRank <= highestDVIndex { pdp.logger.DebugContext(ctx, "Updating rank!") highestDVIndex = foundRank - gotAttr := dataAttr - highestRankedInstance = gotAttr + highestRankedInstance = dataAttr } } + return highestRankedInstance, nil } -// Check for a match of a singular Attribute Value in a set of Attribute Value FQNs -func getIsValueFoundInFqnValuesSet(v *policy.Value, fqns []string, l *logger.Logger) bool { +// getIsValueFoundInFqnValuesSet checks if a Value is present in a set of FQN strings. +func getIsValueFoundInFqnValuesSet( + v *policy.Value, + fqns []string, + l *logger.Logger, +) bool { valFqn := v.GetFqn() if valFqn == "" { l.Error(fmt.Sprintf("Unexpected empty FQN for value %+v", v)) return false } + for _, fqn := range fqns { if strings.EqualFold(valFqn, fqn) { return true } } + return false } -// Given set of ordered/ranked values, a data singular Attribute Value, and a set of entity Attribute Values, -// determine if the entity Attribute Values include a ranked value that equals or exceeds -// the rank of the data Attribute Value. -// For hierarchy, convention is 0 == most privileged, 1 == less privileged, etc +// entityRankGreaterThanOrEqualToDataRank compares entity attribute ranks against data attribute rank. func entityRankGreaterThanOrEqualToDataRank( - order []*policy.Value, dataAttribute *policy.Value, + order []*policy.Value, + dataAttribute *policy.Value, entityAttrValueFqnsGroup []string, log *logger.Logger, ) (bool, error) { - // default to least-perm result := false + dvIndex, err := getOrderOfValue(order, dataAttribute, log) if err != nil { return false, err } - // Compute the rank of the entity Attribute Value against the rank of the data Attribute Value - // While, for hierarchy, we only ever have a singular data value we're checking - // for a given data Attribute Value FQN, - // we may have *several* entity values for a given entity Attribute Value FQN - - // so if an entity has multiple values that can be compared for the hierarchy rule, - // we check all of them and go with the value that has the least-significant index when deciding access + for _, entityAttributeFqn := range entityAttrValueFqnsGroup { - // Ideally, the caller will have already ensured all the entity Attribute Values we've been provided - // have the same FQN as the data Attribute Value we're comparing against, - // but if they haven't for some reason only consider matching entity Attribute Values dataAttrDefFqn, err := GetDefinitionFqnFromValue(dataAttribute) if err != nil { return false, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) } + entityAttrDefFqn, err := GetDefinitionFqnFromValueFqn(entityAttributeFqn) if err != nil { return false, fmt.Errorf("error getting definition FQN from entity attribute value: %s", err.Error()) } + if dataAttrDefFqn == entityAttrDefFqn { evIndex, err := getOrderOfValueByFqn(order, entityAttributeFqn) if err != nil { return false, err } - // If the entity value isn't IN the order at all, - // then set it's rank to one below the lowest rank in the current - // order so it will always fail + if evIndex == -1 { evIndex = len(order) + 1 } - // If, at any point, we find an entity Attribute Value that is below the data Attribute Value in rank, - // (that is, numerically greater than the data rank) - // (or if the data value itself is < 0, indicating it's not actually part of the defined order) - // then we must immediately assume failure for this entity - // and return. + if evIndex > dvIndex || dvIndex == -1 { result = false return result, nil @@ -442,52 +484,57 @@ func entityRankGreaterThanOrEqualToDataRank( } } } + return result, nil } -// Given a set of ordered/ranked values and a singular Attribute Value, return the -// rank #/index of the singular Attribute Value. If the value is not found, return -1. -// For hierarchy, convention is 0 == most privileged, 1 == less privileged, etc. -func getOrderOfValue(order []*policy.Value, v *policy.Value, log *logger.Logger) (int, error) { +// getOrderOfValue finds the index of a value in the ordered list. +func getOrderOfValue( + order []*policy.Value, + v *policy.Value, + log *logger.Logger, +) (int, error) { val := v.GetValue() valFqn := v.GetFqn() + if val == "" { log.Debug(fmt.Sprintf("Unexpected empty 'value' in value: %+v, falling back to FQN", v)) return getOrderOfValueByFqn(order, valFqn) } - for idx := range order { - orderVal := order[idx].GetValue() - if orderVal == "" { - return -1, fmt.Errorf("unexpected empty value %+v in order at index %d", order[idx], idx) + for idx, orderVal := range order { + currentVal := orderVal.GetValue() + if currentVal == "" { + return -1, fmt.Errorf("unexpected empty value %+v in order at index %d", orderVal, idx) } - if orderVal == val { + + if currentVal == val { return idx, nil } } - // If we did not find the right index, return -1 return -1, nil } -// Given a set of ordered/ranked values and a singular Attribute Value, return the -// rank #/index of the singular Attribute Value. If the value is not found, return -1. -// For hierarchy, convention is 0 == most privileged, 1 == less privileged, etc +// getOrderOfValueByFqn finds the index of a value FQN in the ordered list. func getOrderOfValueByFqn(order []*policy.Value, valFqn string) (int, error) { for idx := range order { orderValFqn := order[idx].GetFqn() - // We should have this, but if not, rebuild it from the value + if orderValFqn == "" { defFqn, err := GetDefinitionFqnFromValue(order[idx]) if err != nil { return -1, fmt.Errorf("error getting definition FQN from value: %s", err.Error()) } + orderVal := order[idx].GetValue() if orderVal == "" { return -1, fmt.Errorf("unexpected empty value %+v in order at index %d", order[idx], idx) } + orderValFqn = fmt.Sprintf("%s/value/%s", defFqn, orderVal) } + if orderValFqn == valFqn { return idx, nil } @@ -496,121 +543,25 @@ func getOrderOfValueByFqn(order []*policy.Value, valFqn string) (int, error) { return -1, nil } -// A Decision represents the overall access decision for a specific entity, -// - that is, the aggregate result of comparing entity Attribute Values to every data Attribute Value. -type Decision struct { - // The important bit - does this entity Have Access or not, for this set of data attribute values - // This will be TRUE if, for *every* DataRuleResult in Results, EntityRuleResult.Passed == TRUE - // Otherwise, it will be false - Access bool `json:"access" example:"false"` - // Results will contain at most 1 DataRuleResult for each data Attribute Value. - // e.g. if we compare an entity's Attribute Values against 5 data Attribute Values, - // then there will be 5 rule results, each indicating whether this entity "passed" validation - // for that data Attribute Value or not. - // - // If an entity was skipped for a particular rule evaluation because of a GroupBy clause - // on the AttributeDefinition for a given data Attribute Value, however, then there may be - // FEWER DataRuleResults then there are DataRules - // - // e.g. there are 5 data Attribute Values, and two entities each with a set of Attribute Values, - // the definition for one of those data Attribute Values has a GroupBy clause that excludes the second entity - //-> the first entity will have 5 DataRuleResults with Passed = true - //-> the second entity will have 4 DataRuleResults Passed = true - //-> both will have Access == true. - Results []DataRuleResult `json:"entity_rule_result"` -} - -// DataRuleResult represents the rule-level (or AttributeDefinition-level) decision for a specific entity - -// the result of comparing entity Attribute Values to a single data AttributeDefinition/rule (with potentially many values) -// -// There may be multiple "instances" (that is, Attribute Values) of a single AttributeDefinition on both data and entities, -// each with a different value. -type DataRuleResult struct { - // Indicates whether, for this specific data AttributeDefinition, an entity satisfied - // the rule conditions (allof/anyof/hierarchy) - Passed bool `json:"passed" example:"false"` - // Contains the AttributeDefinition of the data attribute rule this result represents - RuleDefinition *policy.Attribute `json:"rule_definition"` - // May contain 0 or more ValueFailure types, depending on the RuleDefinition and which (if any) - // data Attribute Values/values the entity failed against - // - // For an AllOf rule, there should be no value failures if Passed=TRUE - // For an AnyOf rule, there should be fewer entity value failures than - // there are data attribute values in total if Passed=TRUE - // For a Hierarchy rule, there should be either no value failures if Passed=TRUE, - // or exactly one value failure if Passed=FALSE - ValueFailures []ValueFailure `json:"value_failures"` -} - -// ValueFailure indicates, for a given entity and data Attribute Value, which data values -// (aka specific data Attribute Value) the entity "failed" on. -// -// There may be multiple "instances" (that is, Attribute Values) of a single AttributeDefinition on both data and entities, -// each with a different value. -// -// A ValueFailure does not necessarily mean the requirements for an AttributeDefinition were not or will not be met, -// it is purely informational - there will be one value failure, per entity, per rule, per value the entity lacks - -// it is up to the rule itself (anyof/allof/hierarchy) to translate this into an overall failure or not. -type ValueFailure struct { - // The data attribute w/value that "caused" the denial - DataAttribute *policy.Value `json:"data_attribute"` - // Optional denial message - Message string `json:"message" example:"Criteria NOT satisfied for entity: {entity_id} - lacked attribute value: {attribute}"` -} - -// GroupDefinitionsByFqn takes a slice of Attribute Definitions and returns them as a map: -// FQN -> Attribute Definition -func GetFqnToDefinitionMap(ctx context.Context, attributeDefinitions []*policy.Attribute, log *logger.Logger) (map[string]*policy.Attribute, error) { - grouped := make(map[string]*policy.Attribute) - for _, def := range attributeDefinitions { - a, err := GetDefinitionFqnFromDefinition(def) - if err != nil { - return nil, err - } - if v, ok := grouped[a]; ok { - // TODO: is this really an error case, or is logging a warning okay? - log.Warn(fmt.Sprintf("duplicate Attribute Definition FQN %s found when building FQN map which may indicate an issue", a)) - log.TraceContext(ctx, "duplicate attribute definitions found are: ", "attr1", v, "attr2", def) - } - grouped[a] = def - } - return grouped, nil -} - -// Groups Attribute Values by their parent Attribute Definition FQN -func GroupValuesByDefinition(values []*policy.Value) (map[string][]*policy.Value, error) { - groupings := make(map[string][]*policy.Value) - for _, v := range values { - // If the parent Definition & its FQN are not nil, rely on them - if v.GetAttribute() != nil { - defFqn := v.GetAttribute().GetFqn() - if defFqn != "" { - groupings[defFqn] = append(groupings[defFqn], v) - continue - } - } - // Otherwise derive the grouping relation from the FQNs - defFqn, err := GetDefinitionFqnFromValueFqn(v.GetFqn()) - if err != nil { - return nil, err - } - groupings[defFqn] = append(groupings[defFqn], v) - } - return groupings, nil -} +// === Utilities for FQN/Definitions === +// GroupValueFqnsByDefinition groups value FQN strings by their attribute definition FQNs. func GroupValueFqnsByDefinition(valueFqns []string) (map[string][]string, error) { groupings := make(map[string][]string) + for _, v := range valueFqns { defFqn, err := GetDefinitionFqnFromValueFqn(v) if err != nil { return nil, err } + groupings[defFqn] = append(groupings[defFqn], v) } + return groupings, nil } +// GetDefinitionFqnFromValue extracts the definition FQN from a policy value. func GetDefinitionFqnFromValue(v *policy.Value) (string, error) { if v.GetAttribute() != nil { return GetDefinitionFqnFromDefinition(v.GetAttribute()) @@ -618,48 +569,51 @@ func GetDefinitionFqnFromValue(v *policy.Value) (string, error) { return GetDefinitionFqnFromValueFqn(v.GetFqn()) } -// Splits off the Value from the FQN to get the parent Definition FQN: -// -// Input: https:///attr//value/ -// Output: https:///attr/ +// GetDefinitionFqnFromValueFqn extracts the definition FQN from a value FQN string. func GetDefinitionFqnFromValueFqn(valueFqn string) (string, error) { if valueFqn == "" { return "", fmt.Errorf("unexpected empty value FQN in GetDefinitionFqnFromValueFqn") } + idx := strings.LastIndex(valueFqn, "/value/") if idx == -1 { return "", fmt.Errorf("value FQN (%s) is of unknown format with no '/value/' segment", valueFqn) } + defFqn := valueFqn[:idx] if defFqn == "" { return "", fmt.Errorf("value FQN (%s) is of unknown format with no known parent Definition", valueFqn) } + return defFqn, nil } +// GetDefinitionFqnFromDefinition constructs the FQN for an attribute definition. func GetDefinitionFqnFromDefinition(def *policy.Attribute) (string, error) { - // see if its FQN is already supplied fqn := def.GetFqn() if fqn != "" { return fqn, nil } - // otherwise build it from the namespace and name + ns := def.GetNamespace() if ns == nil { return "", fmt.Errorf("attribute definition has unexpectedly nil namespace") } + nsName := ns.GetName() if nsName == "" { return "", fmt.Errorf("attribute definition's Namespace has unexpectedly empty name") } + nsFqn := ns.GetFqn() attr := def.GetName() if attr == "" { return "", fmt.Errorf("attribute definition has unexpectedly empty name") } - // Namespace FQN contains 'https://' scheme prefix, but Namespace Name does not + if nsFqn != "" { return fmt.Sprintf("%s/attr/%s", nsFqn, attr), nil } + return fmt.Sprintf("https://%s/attr/%s", nsName, attr), nil } diff --git a/service/internal/access/pdp_test.go b/service/internal/access/pdp_test.go index 10c55c53e5..ffcfa51c6f 100644 --- a/service/internal/access/pdp_test.go +++ b/service/internal/access/pdp_test.go @@ -9,6 +9,21 @@ import ( "github.com/stretchr/testify/require" ) +const mockEntityID = "entity1" + +func createTestLogger() *logger.Logger { + // debug is too noisy + l, err := logger.NewLogger(logger.Config{ + Level: "info", + Type: "json", + Output: "stdout", + }) + if err != nil { + panic("Failed to create logger") + } + return l +} + func fqnBuilder(n string, a string, v string) string { fqn := "https://" switch { @@ -23,1269 +38,1176 @@ func fqnBuilder(n string, a string, v string) string { } } -var ( - mockNamespaces = []string{"example.org", "authority.gov", "somewhere.net"} - mockAttributeNames = []string{"MyAttr", "YourAttr", "TheirAttr"} - mockAttributeValues = []string{"Value1", "Value2", "Value3", "Value4", "Value5"} - - mockExtraneousValueFqn = fqnBuilder("meep.org", "meep", "beepbeep") - mockEntityID = "4f6636ca-c60c-40d1-9f3f-015086303f74" +func createMockEntity1Attributes(namespace, name string, values []string) map[string][]string { + attrs := make(map[string][]string) + for _, value := range values { + attrs[mockEntityID] = append(attrs[mockEntityID], fqnBuilder(namespace, name, value)) + } + return attrs +} - simpleAnyOfAttribute = policy.Attribute{ - Name: mockAttributeNames[0], +func createMockAttribute(namespace, name string, rule policy.AttributeRuleTypeEnum, values []string) *policy.Attribute { + attr := &policy.Attribute{ + Name: name, Namespace: &policy.Namespace{ - Name: mockNamespaces[0], - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: mockAttributeValues[0], - Fqn: fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[0]), - }, - { - Value: mockAttributeValues[1], - Fqn: fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[1]), - }, + Name: namespace, }, + Rule: rule, + Fqn: fqnBuilder(namespace, name, ""), + } + for _, value := range values { + attr.Values = append(attr.Values, &policy.Value{ + Value: value, + Fqn: fqnBuilder(namespace, name, value), + }) } + return attr +} - simpleAllOfAttribute = policy.Attribute{ - Name: mockAttributeNames[1], - Namespace: &policy.Namespace{ - Name: mockNamespaces[1], +// Refactored test structure to use table-driven tests and group related tests logically +func Test_AccessPDP_AnyOf(t *testing.T) { + values := []string{"value1", "value2", "value3"} + definition := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, values) + tests := []struct { + name string + entityAttrs map[string][]string + dataAttrs []*policy.Value + expectedAccess bool + expectedPass bool + }{ + { + name: "Pass - all definition values, all entitlements", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), values), + dataAttrs: definition.GetValues(), + expectedAccess: true, + expectedPass: true, }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, - Values: []*policy.Value{ - { - Value: mockAttributeValues[2], - Fqn: fqnBuilder(mockNamespaces[1], mockAttributeNames[1], mockAttributeValues[2]), - }, - { - Value: mockAttributeValues[3], - Fqn: fqnBuilder(mockNamespaces[1], mockAttributeNames[1], mockAttributeValues[3]), - }, + { + name: "Pass - subset of definition values, all entitlements", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), values), + dataAttrs: definition.GetValues()[1:], + expectedAccess: true, + expectedPass: true, }, - } - - simpleHierarchyAttribute = policy.Attribute{ - Name: mockAttributeNames[2], - Namespace: &policy.Namespace{ - Name: mockNamespaces[2], + { + name: "Pass - subset of definition values, matching entititlements", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), values[1:]), + dataAttrs: definition.GetValues()[1:], + expectedAccess: true, + expectedPass: true, }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - Fqn: fqnBuilder(mockNamespaces[2], mockAttributeNames[2], "Privileged"), - }, - { - Value: "LessPrivileged", - Fqn: fqnBuilder(mockNamespaces[2], mockAttributeNames[2], "LessPrivileged"), - }, - { - Value: "NotPrivilegedAtAll", - Fqn: fqnBuilder(mockNamespaces[2], mockAttributeNames[2], "NotPrivilegedAtAll"), - }, + { + name: "Pass - subset definition values, matching entitlements + extraneous entitlement", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"random_value", values[0]}), + dataAttrs: []*policy.Value{definition.GetValues()[0]}, + expectedAccess: true, + expectedPass: true, + }, + { + name: "Pass - all definition values, matching entitlements + extraneous entitlement", + entityAttrs: map[string][]string{mockEntityID: { + fqnBuilder("example.org", "myattr", "random_value"), + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), values[0]), + }}, + dataAttrs: definition.GetValues(), + expectedAccess: true, + expectedPass: true, + }, + { + name: "Fail - all definition values, no matching entitlements, extraneous definition entitlement value", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"random_value"}), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - all definition values, wrong definition name", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), "random_definition_name", values), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - all definition values, wrong namespace", + entityAttrs: createMockEntity1Attributes("wrong.namespace", definition.GetName(), values), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - all definition values, no entitlements at all", + entityAttrs: map[string][]string{mockEntityID: {}}, + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - subset definition values, no entitlements at all", + entityAttrs: map[string][]string{mockEntityID: {}}, + dataAttrs: definition.GetValues()[1:], + expectedAccess: false, + expectedPass: false, }, } -) -// AnyOf tests -func Test_AccessPDP_AnyOf_Pass(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - val0 := mockAttrDefinitions[0].GetValues()[0] - val1 := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - val0, - val1, - } + pdp := NewPdp(createTestLogger()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + decisions, err := pdp.DetermineAccess(t.Context(), tt.dataAttrs, tt.entityAttrs, []*policy.Attribute{definition}) - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, val1.GetValue()), - mockExtraneousValueFqn, + require.NoError(t, err) + if tt.expectedAccess { + assert.True(t, decisions[mockEntityID].Access) + } else { + assert.False(t, decisions[mockEntityID].Access) + } + if len(decisions[mockEntityID].Results) > 0 { + assert.Equal(t, tt.expectedPass, decisions[mockEntityID].Results[0].Passed) + } + }) } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions, - ) - - require.NoError(t, err) - assert.True(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.True(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, val0, decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) + // Test for empty data attributes + entityAttrs := createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"highest"}) + emptyDataAttrs := []*policy.Value{} + decisions, err := pdp.DetermineAccess(t.Context(), emptyDataAttrs, entityAttrs, []*policy.Attribute{definition}) + require.Error(t, err) + assert.Empty(t, decisions) } -func Test_AccessPDP_AnyOf_FailMissingValue(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], - } - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, "randomValue"), - mockExtraneousValueFqn, +func Test_AccessPDP_Hierarchy(t *testing.T) { + values := []string{"highest", "middle", "lowest"} + definition := createMockAttribute("somewhere.net", "theirattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, values) + tests := []struct { + name string + entityAttrs map[string][]string + dataAttrs []*policy.Value + expectedAccess bool + expectedPass bool + }{ + { + name: "Pass - highest privilege level data, highest entitlement", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"highest"}), + dataAttrs: definition.GetValues(), + expectedAccess: true, + expectedPass: true, + }, + { + name: "Pass - middle privilege level data, middle entitlement", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"middle"}), + dataAttrs: []*policy.Value{definition.GetValues()[1]}, + expectedAccess: true, + expectedPass: true, + }, + { + name: "Pass - middle privilege level data, highest entitlement", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"highest"}), + dataAttrs: []*policy.Value{definition.GetValues()[1]}, + expectedAccess: true, + expectedPass: true, + }, + { + name: "Pass - lowest privilege level data, lowest entitlement", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"lowest"}), + dataAttrs: []*policy.Value{definition.GetValues()[2]}, + expectedAccess: true, + expectedPass: true, + }, + { + name: "Pass - lowest privilege level data, middle entitlement", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"lowest"}), + dataAttrs: []*policy.Value{definition.GetValues()[2]}, + expectedAccess: true, + expectedPass: true, + }, + { + name: "Pass - lowest privilege level data, highest entitlement", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"lowest"}), + dataAttrs: []*policy.Value{definition.GetValues()[2]}, + expectedAccess: true, + expectedPass: true, + }, + { + name: "Fail - wrong namespace", + entityAttrs: createMockEntity1Attributes("wrongnamespace.net", definition.GetName(), []string{"highest"}), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - wrong definition name", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), "wrong_definition_name", []string{"highest"}), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - no matching entitlements", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"random_value"}), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - no entitlements at all", + entityAttrs: map[string][]string{mockEntityID: {}}, + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - - require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 2) - assert.Equal(t, mockDataAttrs[0], decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} - -func Test_AccessPDP_AnyOf_FailMissingAttr(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} - - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], - } + pdp := NewPdp(createTestLogger()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + decisions, err := pdp.DetermineAccess(t.Context(), tt.dataAttrs, tt.entityAttrs, []*policy.Attribute{definition}) - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder("dank.org", "noop", "randomVal"), - mockExtraneousValueFqn, + require.NoError(t, err) + if tt.expectedAccess { + assert.True(t, decisions[mockEntityID].Access) + } else { + assert.False(t, decisions[mockEntityID].Access) + } + if len(decisions[mockEntityID].Results) > 0 { + assert.Equal(t, tt.expectedPass, decisions[mockEntityID].Results[0].Passed) + } + }) } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - - require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 2) - assert.Equal(t, mockDataAttrs[0], decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) + // Test for empty data attributes + entityAttrs := createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"highest"}) + emptyDataAttrs := []*policy.Value{} + decisions, err := pdp.DetermineAccess(t.Context(), emptyDataAttrs, entityAttrs, []*policy.Attribute{definition}) + require.Error(t, err) + assert.Empty(t, decisions) } -func Test_AccessPDP_AnyOf_FailAttrWrongNamespace(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} - - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], +func Test_AccessPDP_AllOf(t *testing.T) { + values := []string{"value1", "value2", "value3"} + definition := createMockAttribute("example.com", "allofattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, values) + tests := []struct { + name string + entityAttrs map[string][]string + dataAttrs []*policy.Value + expectedAccess bool + expectedPass bool + }{ + { + name: "Pass - all definition values match entitlements", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), values), + dataAttrs: definition.GetValues(), + expectedAccess: true, + expectedPass: true, + }, + { + name: "Fail - missing one definition value in entitlements", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), values[:2]), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - no matching entitlements", + entityAttrs: createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"random_value"}), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - wrong namespace", + entityAttrs: createMockEntity1Attributes("wrongnamespace.com", definition.GetName(), values), + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, + { + name: "Fail - no entitlements at all", + entityAttrs: map[string][]string{mockEntityID: {}}, + dataAttrs: definition.GetValues(), + expectedAccess: false, + expectedPass: false, + }, } - mockEntityAttrs := map[string][]string{} - name := mockAttrDefinitions[0].GetName() - val1 := mockAttrDefinitions[0].GetValues()[0].GetValue() - mockEntityAttrs[mockEntityID] = []string{fqnBuilder("otherrandomnamespace.com", name, val1), mockExtraneousValueFqn} - - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + pdp := NewPdp(createTestLogger()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + decisions, err := pdp.DetermineAccess(t.Context(), tt.dataAttrs, tt.entityAttrs, []*policy.Attribute{definition}) - require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 2) - assert.Equal(t, mockDataAttrs[0], decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} - -func Test_AccessPDP_AnyOf_NoEntityAttributes_Fails(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} - - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], + require.NoError(t, err) + if tt.expectedAccess { + assert.True(t, decisions[mockEntityID].Access) + } else { + assert.False(t, decisions[mockEntityID].Access) + } + if len(decisions[mockEntityID].Results) > 0 { + assert.Equal(t, tt.expectedPass, decisions[mockEntityID].Results[0].Passed) + } + }) } - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{} - - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - - require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 2) - assert.Equal(t, mockDataAttrs[0], decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) + // Test for empty data attributes + entityAttrs := createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), []string{"highest"}) + emptyDataAttrs := []*policy.Value{} + decisions, err := pdp.DetermineAccess(t.Context(), emptyDataAttrs, entityAttrs, []*policy.Attribute{definition}) + require.Error(t, err) + assert.Empty(t, decisions) } -func Test_AccessPDP_AnyOf_NoDataAttributes_NoDecisions(t *testing.T) { - // There are no data attribute instances in this test so the data attribute definitions - // are useless, and should be ignored, but supply the definitions anyway to test that assumption - mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} - - mockDataAttrs := []*policy.Value{} - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[0]), - mockExtraneousValueFqn, - } +func Test_DetermineAccess_EmptyDataAttributes(t *testing.T) { + pdp := NewPdp(createTestLogger()) + decisions, err := pdp.DetermineAccess(t.Context(), []*policy.Value{}, map[string][]string{}, []*policy.Attribute{}) - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - - require.NoError(t, err) - assert.Nil(t, decisions[mockEntityID]) - // No data attributes -> no decisions to make -> no decisions per-entity - // (PDP Caller can do what it wants with this info - infer this means access for all, or infer this means failure) + require.Error(t, err) assert.Empty(t, decisions) } -func Test_AccessPDP_AnyOf_AllEntitiesFilteredOutOfDataAttributeComparison_NoDecisions(t *testing.T) { - entityID1 := "4f6636ca-c60c-40d1-9f3f-015086303f74" - entityID2 := "bubble@squeak.biz" - mockAttrDefinitions := []*policy.Attribute{ - &simpleAnyOfAttribute, - { - Name: mockAttributeNames[1], - Namespace: &policy.Namespace{ - Name: mockNamespaces[0], - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: mockAttributeValues[2], - Fqn: fqnBuilder(mockNamespaces[0], mockAttributeNames[1], mockAttributeValues[2]), - }, - { - Value: mockAttributeValues[3], - Fqn: fqnBuilder(mockNamespaces[0], mockAttributeNames[1], mockAttributeValues[3]), - }, - }, - }, - } - mockDataAttrs := []*policy.Value{} - mockEntityAttrs := map[string][]string{} - fqn1 := fqnBuilder("dank.org", mockAttrDefinitions[0].GetName(), mockAttrDefinitions[0].GetValues()[0].GetValue()) - fqn2 := mockExtraneousValueFqn - mockEntityAttrs[entityID1] = []string{ - fqn1, fqn2, - } - mockEntityAttrs[entityID2] = []string{ - fqn1, fqn2, - } - - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) +func Test_DetermineAccess_EmptyAttributeDefinitions(t *testing.T) { + pdp := NewPdp(createTestLogger()) + dataAttrs := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, []string{"value1"}).GetValues() + entityAttrs := createMockEntity1Attributes("example.org", "myattr", []string{"value1"}) - require.NoError(t, err) + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{}) - // Both the entities lack the necessary Attribute for the only data attribute we're comparing them against, - // so neither of them get a Decision -> no decisions to be made here. - assert.Nil(t, decisions[entityID1]) - assert.Nil(t, decisions[entityID2]) - // No data attributes -> no decisions to make -> no decisions per-entity - // (PDP Caller can do what it wants with this info - infer this means access for all, or infer this means failure) + require.Error(t, err) assert.Empty(t, decisions) } -// AllOf tests -func Test_AccessPDP_AllOf_Pass(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAllOfAttribute} - - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], - } - mockEntityAttrs := map[string][]string{} - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, mockAttrDefinitions[0].GetValues()[0].GetValue()), - fqnBuilder(ns, name, mockAttrDefinitions[0].GetValues()[1].GetValue()), - mockExtraneousValueFqn, - } - - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) +func Test_GroupDataAttributesByDefinition(t *testing.T) { + // Test case 1: Basic grouping + dataAttrs := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, []string{"value1", "value2"}).GetValues() + pdp := NewPdp(createTestLogger()) + grouped, err := pdp.groupDataAttributesByDefinition(t.Context(), dataAttrs) require.NoError(t, err) - assert.True(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.True(t, decisions[mockEntityID].Results[0].Passed) - assert.Empty(t, decisions[mockEntityID].Results[0].ValueFailures) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} + assert.Len(t, grouped, 1) + assert.Contains(t, grouped, "https://example.org/attr/myattr") + assert.Len(t, grouped["https://example.org/attr/myattr"], 2) + + // Test case 2: Multiple attributes with same definition + multiAttr := dataAttrs + multiAttr = append(multiAttr, + createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, []string{"value3"}).GetValues()...) + grouped, err = pdp.groupDataAttributesByDefinition(t.Context(), multiAttr) + require.NoError(t, err) + assert.Len(t, grouped, 1) + assert.Contains(t, grouped, "https://example.org/attr/myattr") + assert.Len(t, grouped["https://example.org/attr/myattr"], 3) + + // Test case 3: Multiple attributes with different definitions + multiDefAttrs := dataAttrs + multiDefAttrs = append(multiDefAttrs, + createMockAttribute("example.org", "otherattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, []string{"other1"}).GetValues()...) + grouped, err = pdp.groupDataAttributesByDefinition(t.Context(), multiDefAttrs) + require.NoError(t, err) + assert.Len(t, grouped, 2) + assert.Contains(t, grouped, "https://example.org/attr/myattr") + assert.Contains(t, grouped, "https://example.org/attr/otherattr") + assert.Len(t, grouped["https://example.org/attr/myattr"], 2) + assert.Len(t, grouped["https://example.org/attr/otherattr"], 1) -func Test_AccessPDP_AllOf_FailMissingValue(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAllOfAttribute} - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], - } - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(mockAttrDefinitions[0].GetNamespace().GetName(), mockAttrDefinitions[0].GetName(), mockAttrDefinitions[0].GetValues()[0].GetValue()), - mockExtraneousValueFqn, - fqnBuilder(mockAttrDefinitions[0].GetNamespace().GetName(), mockAttrDefinitions[0].GetName(), "otherValue"), + // Test case 4: Malformed FQN + malformedAttrs := []*policy.Value{ + {Value: "bad", Fqn: "malformed-url"}, } + grouped, err = pdp.groupDataAttributesByDefinition(t.Context(), malformedAttrs) + require.Error(t, err) + assert.Empty(t, grouped) +} - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) +func Test_MapFqnToDefinitions(t *testing.T) { + // Test case 1: Basic mapping + attr := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, []string{"value1"}) + pdp := NewPdp(createTestLogger()) + mapped, err := pdp.mapFqnToDefinitions(t.Context(), []*policy.Attribute{attr}) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockDataAttrs[0], decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} - -func Test_AccessPDP_AllOf_FailMissingAttr(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{ - &simpleAllOfAttribute, - } - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], - } - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder("dank.org", "noop", "randomVal"), - fqnBuilder("somewhere.com", "hello", "world"), - } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + assert.Len(t, mapped, 1) + assert.Contains(t, mapped, "https://example.org/attr/myattr") + assert.Equal(t, attr, mapped["https://example.org/attr/myattr"]) + // Test case 2: Multiple attributes + attr2 := createMockAttribute("example.com", "otherattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, []string{"otherval"}) + mapped, err = pdp.mapFqnToDefinitions(t.Context(), []*policy.Attribute{attr, attr2}) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 2) - assert.Equal(t, mockDataAttrs[0], decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) + assert.Len(t, mapped, 2) + assert.Contains(t, mapped, "https://example.org/attr/myattr") + assert.Contains(t, mapped, "https://example.com/attr/otherattr") + assert.Equal(t, attr, mapped["https://example.org/attr/myattr"]) + assert.Equal(t, attr2, mapped["https://example.com/attr/otherattr"]) + + // Test case 3: Nil attribute + mapped, err = pdp.mapFqnToDefinitions(t.Context(), []*policy.Attribute{nil}) + require.Error(t, err) + assert.Empty(t, mapped) } -func Test_AccessPDP_AllOf_FailAttrWrongNamespace(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[0].GetValues()[0], - } +func Test_GetHighestRankedInstanceFromDataAttributes(t *testing.T) { + // Test case 1: Basic hierarchy with medium rank + order := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, []string{"high", "medium", "low"}).GetValues() + dataAttrs := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, []string{"medium"}).GetValues() + pdp := NewPdp(createTestLogger()) + logger := createTestLogger() - wrongNs := "wrong" + mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(wrongNs, name, mockAttrDefinitions[0].GetValues()[0].GetValue()), - fqnBuilder(wrongNs, name, mockAttrDefinitions[0].GetValues()[1].GetValue()), - mockExtraneousValueFqn, - } - - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + highest, err := pdp.getHighestRankedInstanceFromDataAttributes(t.Context(), order, dataAttrs, logger) + require.NoError(t, err) + assert.NotNil(t, highest) + assert.Equal(t, "medium", highest.GetValue()) + // Test case 2: Multiple data attributes, should return highest + dataAttrs = createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + []string{"high", "medium", "low"}).GetValues() + highest, err = pdp.getHighestRankedInstanceFromDataAttributes(t.Context(), order, dataAttrs, logger) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 2) - assert.Equal(t, mockDataAttrs[0], decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) + assert.NotNil(t, highest) + assert.Equal(t, "high", highest.GetValue()) } -// Hierarchy tests -func Test_AccessPDP_Hierarchy_Pass(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - topValue := mockAttrDefinitions[0].GetValues()[0] - midValue := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - midValue, - topValue, +func Test_GetIsValueFoundInFqnValuesSet(t *testing.T) { + // Test case 1: Value exists in FQN set + value := &policy.Value{ + Value: "value1", + Fqn: "https://example.org/attr/myattr/value/value1", } + fqns := []string{"https://example.org/attr/myattr/value/value1", "https://example.org/attr/myattr/value/value2"} + + found := getIsValueFoundInFqnValuesSet(value, fqns, createTestLogger()) + assert.True(t, found) - mockEntityAttrs := map[string][]string{} - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, topValue.GetValue()), - mockExtraneousValueFqn, + // Test case 2: Value does not exist in FQN set + valueNotFound := &policy.Value{ + Value: "value3", + Fqn: "https://example.org/attr/myattr/value/value3", } + found = getIsValueFoundInFqnValuesSet(valueNotFound, fqns, createTestLogger()) + assert.False(t, found) - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + // Test case 3: Empty FQN set + found = getIsValueFoundInFqnValuesSet(value, []string{}, createTestLogger()) + assert.False(t, found) - require.NoError(t, err) - assert.True(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.True(t, decisions[mockEntityID].Results[0].Passed) - assert.Empty(t, decisions[mockEntityID].Results[0].ValueFailures) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} - -// TODO: Is this test accurate? Containing the top AND a lower value results in a fail? -func Test_AccessPDP_Hierarchy_FailEntityValueTooLow(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - topValue := mockAttrDefinitions[0].GetValues()[0] - midValue := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - midValue, - topValue, + // Test case 4: Different namespace but same value + valueDiffNamespace := &policy.Value{ + Value: "value1", + Fqn: "https://different.org/attr/myattr/value/value1", } - mockEntityAttrs := map[string][]string{} - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, topValue.GetValue()), - fqnBuilder(ns, name, midValue.GetValue()), - mockExtraneousValueFqn, - } - - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + found = getIsValueFoundInFqnValuesSet(valueDiffNamespace, fqns, createTestLogger()) + assert.False(t, found) - require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) + // Test case 5: Nil value + found = getIsValueFoundInFqnValuesSet(nil, fqns, createTestLogger()) + assert.False(t, found) } -func Test_AccessPDP_Hierarchy_FailEntityValueAndDataValuesBothLowest(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - lowValue := mockAttrDefinitions[0].GetValues()[2] - mockDataAttrs := []*policy.Value{ - lowValue, - } - mockEntityAttrs := map[string][]string{} - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, lowValue.GetValue()), - } +func Test_DetermineAccess_MultipleEntities(t *testing.T) { + pdp := NewPdp(createTestLogger()) - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + // Define two entity IDs + const entityID1 = "entity1" + const entityID2 = "entity2" - require.NoError(t, err) - assert.True(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.True(t, decisions[mockEntityID].Results[0].Passed) - assert.Empty(t, decisions[mockEntityID].Results[0].ValueFailures) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} + // Create attribute definition + values := []string{"value1", "value2", "value3"} + definition := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, values) -func Test_AccessPDP_Hierarchy_FailEntityValueOrder(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - topValue := mockAttrDefinitions[0].GetValues()[0] - midValue := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - midValue, - topValue, + // Create entity attributes + entityAttrs := map[string][]string{ + entityID1: { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), values[0]), + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), values[1]), + }, + entityID2: { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), values[2]), + }, } - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, midValue.GetValue()), - fqnBuilder(ns, name, topValue.GetValue()), - mockExtraneousValueFqn, + // Create data attributes + dataAttrs := []*policy.Value{ + definition.GetValues()[0], // value1 + definition.GetValues()[1], // value2 } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - + // Test 1: Basic case where one entity gets access and another doesn't + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{definition}) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} -func Test_AccessPDP_Hierarchy_FailMultipleHierarchyDataValues(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - topValue := mockAttrDefinitions[0].GetValues()[0] - midValue := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - topValue, - midValue, - } + // Entity 1 should have access (has entitlement for value1 and value2) + assert.True(t, decisions[entityID1].Access) + assert.True(t, decisions[entityID1].Results[0].Passed) - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, midValue.GetValue()), - fqnBuilder(ns, name, topValue.GetValue()), - mockExtraneousValueFqn, - } + // Entity 2 should not have access (has entitlement for value3, but data has value1 and value2) + assert.False(t, decisions[entityID2].Access) + assert.False(t, decisions[entityID2].Results[0].Passed) - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + // Test 2: Case where both entities get access + dataAttrs = []*policy.Value{ + definition.GetValues()[0], // value1 + definition.GetValues()[1], // value2 + definition.GetValues()[2], // value3 + } + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{definition}) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} - -func Test_AccessPDP_Hierarchy_FailEntityValueNotInOrder(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - topValue := mockAttrDefinitions[0].GetValues()[0] - midValue := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - midValue, - topValue, - } - mockEntityAttrs := map[string][]string{} - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, "unknownPrivilegeValue"), - mockExtraneousValueFqn, - } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + // Both entities should have access + assert.True(t, decisions[entityID1].Access) + assert.True(t, decisions[entityID1].Results[0].Passed) + assert.True(t, decisions[entityID2].Access) + assert.True(t, decisions[entityID2].Results[0].Passed) - require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} + // Test 3: Multi-attribute definitions + definition2 := createMockAttribute("example.com", "otherattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, []string{"othervalue"}) -func Test_AccessPDP_Hierarchy_FailDataValueNotInOrder(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockDataAttrs := []*policy.Value{ - { - Value: "UberPrivileged", - Fqn: fqnBuilder(ns, name, "UberPrivileged"), + // Update entity attributes + entityAttrs = map[string][]string{ + entityID1: { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), values[0]), + fqnBuilder(definition2.GetNamespace().GetName(), definition2.GetName(), "othervalue"), + }, + entityID2: { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), values[0]), + // entity2 lacks the second attribute }, } - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, mockAttrDefinitions[0].GetValues()[0].GetValue()), - mockExtraneousValueFqn, + // Data attributes with both definitions + dataAttrs = []*policy.Value{ + definition.GetValues()[0], // value1 + definition2.GetValues()[0], // othervalue } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{definition, definition2}) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) - assert.Nil(t, decisions[mockEntityID].Results[0].ValueFailures[0].DataAttribute) + + // Entity 1 should have access (has entitlements for both attributes) + assert.True(t, decisions[entityID1].Access) + assert.Len(t, decisions[entityID1].Results, 2) + assert.True(t, decisions[entityID1].Results[0].Passed) + assert.True(t, decisions[entityID1].Results[1].Passed) + + // Entity 2 should not have access (missing the second attribute) + assert.False(t, decisions[entityID2].Access) + assert.Len(t, decisions[entityID2].Results, 2) + assert.True(t, decisions[entityID2].Results[0].Passed) // First attribute passes + assert.False(t, decisions[entityID2].Results[1].Passed) // Second attribute fails } -func Test_AccessPDP_Hierarchy_PassWithMixedKnownAndUnknownDataOrder(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - topValue := mockAttrDefinitions[0].GetValues()[0] - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockDataAttrs := []*policy.Value{ - { - Value: "UberPrivileged", - Fqn: fqnBuilder(ns, name, "UberPrivileged"), - }, - topValue, - } +func Test_DetermineAccess_HierarchyWithMultipleEntities(t *testing.T) { + pdp := NewPdp(createTestLogger()) - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, topValue.GetValue()), - mockExtraneousValueFqn, - } + // Define entity IDs + const entityID1 = "entity1" + const entityID2 = "entity2" + const entityID3 = "entity3" - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + // Create hierarchy attribute + values := []string{"high", "medium", "low"} + definition := createMockAttribute("example.org", "hierarchy", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, values) - require.NoError(t, err) - assert.True(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.True(t, decisions[mockEntityID].Results[0].Passed) - assert.Empty(t, decisions[mockEntityID].Results[0].ValueFailures) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} - -func Test_AccessPDP_Hierarchy_FailWithWrongNamespace(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - midValue := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - midValue, - } - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder("wrong"+mockAttrDefinitions[0].GetNamespace().GetName(), mockAttrDefinitions[0].GetName(), midValue.GetValue()), - mockExtraneousValueFqn, + // Create entity attributes with different access levels + entityAttrs := map[string][]string{ + entityID1: { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), "high"), + }, + entityID2: { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), "medium"), + }, + entityID3: { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), "low"), + }, } - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - + // Test 1: High privilege data - only high privilege entity gets access + dataAttrs := []*policy.Value{definition.GetValues()[0]} // high + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{definition}) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} -func Test_AccessPDP_Hierarchy_FailWithMixedKnownAndUnknownEntityOrder(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} - topValue := mockAttrDefinitions[0].GetValues()[0] - midValue := mockAttrDefinitions[0].GetValues()[1] - mockDataAttrs := []*policy.Value{ - midValue, - topValue, - } - ns := mockAttrDefinitions[0].GetNamespace().GetName() - name := mockAttrDefinitions[0].GetName() - mockEntityAttrs := map[string][]string{} - mockEntityAttrs[mockEntityID] = []string{ - fqnBuilder(ns, name, topValue.GetValue()), - fqnBuilder(ns, name, "unknownPrivilegeValue"), - mockExtraneousValueFqn, - } - - accessPDP := NewPdp(logger.CreateTestLogger()) - decisions, err := accessPDP.DetermineAccess( - t.Context(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + assert.True(t, decisions[entityID1].Access) // high level entity + assert.False(t, decisions[entityID2].Access) // medium level entity + assert.False(t, decisions[entityID3].Access) // low level entity + // Test 2: Medium privilege data - high and medium get access + dataAttrs = []*policy.Value{definition.GetValues()[1]} // medium + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{definition}) require.NoError(t, err) - assert.False(t, decisions[mockEntityID].Access) - assert.Len(t, decisions[mockEntityID].Results, 1) - assert.False(t, decisions[mockEntityID].Results[0].Passed) - assert.Len(t, decisions[mockEntityID].Results[0].ValueFailures, 1) - assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityID].Results[0].RuleDefinition) -} -// Helper tests + assert.True(t, decisions[entityID1].Access) // high level entity + assert.True(t, decisions[entityID2].Access) // medium level entity + assert.False(t, decisions[entityID3].Access) // low level entity -// GetFqnToDefinitionMap tests -func Test_GetFqnToDefinitionMap(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{ - &simpleAnyOfAttribute, - &simpleAllOfAttribute, - &simpleHierarchyAttribute, - } - - fqnToDefinitionMap, err := GetFqnToDefinitionMap(t.Context(), mockAttrDefinitions, logger.CreateTestLogger()) + // Test 3: Low privilege data - all get access + dataAttrs = []*policy.Value{definition.GetValues()[2]} // low + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{definition}) require.NoError(t, err) - for _, attrDef := range mockAttrDefinitions { - fqn := fqnBuilder(attrDef.GetNamespace().GetName(), attrDef.GetName(), "") - assert.Equal(t, attrDef.GetName(), fqnToDefinitionMap[fqn].GetName()) - } -} + assert.True(t, decisions[entityID1].Access) // high level entity + assert.True(t, decisions[entityID2].Access) // medium level entity + assert.True(t, decisions[entityID3].Access) // low level entity -func Test_GetFqnToDefinitionMap_SucceedsWithDuplicateDefinitions(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{ - &simpleAnyOfAttribute, - &simpleAnyOfAttribute, + // Test 4: Entity with no matching attribute gets no access + entityAttrs["entityNoMatch"] = []string{ + fqnBuilder(definition.GetNamespace().GetName(), "wrongattr", "high"), } - fqnToDefinitionMap, err := GetFqnToDefinitionMap(t.Context(), mockAttrDefinitions, logger.CreateTestLogger()) + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{definition}) require.NoError(t, err) - expectedFqn := fqnBuilder(mockAttrDefinitions[0].GetNamespace().GetName(), mockAttrDefinitions[0].GetName(), "") - v, ok := fqnToDefinitionMap[expectedFqn] - assert.True(t, ok) - assert.Equal(t, mockAttrDefinitions[0].GetName(), v.GetName()) + + assert.False(t, decisions["entityNoMatch"].Access) + assert.False(t, decisions["entityNoMatch"].Results[0].Passed) } -// GroupValuesByDefinition tests -func Test_GroupValuesByDefinition_NoProvidedDefinitionFqn_Succeeds(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{ - &simpleAnyOfAttribute, - &simpleAllOfAttribute, - &simpleHierarchyAttribute, - } +func Test_DetermineAccess_ComplexScenarioWithMultipleEntities(t *testing.T) { + pdp := NewPdp(createTestLogger()) - // two values from each attribute definition, out of order - mockDataAttrs := []*policy.Value{ - mockAttrDefinitions[0].GetValues()[0], - mockAttrDefinitions[1].GetValues()[0], - mockAttrDefinitions[2].GetValues()[0], - mockAttrDefinitions[0].GetValues()[1], - mockAttrDefinitions[1].GetValues()[1], - mockAttrDefinitions[2].GetValues()[1], - } + // Define entity IDs + const entityID1 = "entity1" + const entityID2 = "entity2" - groupedValues, err := GroupValuesByDefinition(mockDataAttrs) - require.NoError(t, err) + // Create multiple attribute definitions of different types + hierarchyDef := createMockAttribute("example.org", "clearance", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + []string{"highest", "medium", "lowest"}) - for _, attrDef := range mockAttrDefinitions { - fqn := fqnBuilder(attrDef.GetNamespace().GetName(), attrDef.GetName(), "") - assert.Len(t, groupedValues[fqn], 2) - assert.Equal(t, attrDef.GetValues()[0], groupedValues[fqn][0]) - assert.Equal(t, attrDef.GetValues()[1], groupedValues[fqn][1]) - } -} + anyOfDef := createMockAttribute("domain.net", "department", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + []string{"engineering", "finance", "management"}) -func Test_GroupValuesByDefinition_WithProvidedDefinitionFqn_Succeeds(t *testing.T) { - attrFqn := fqnBuilder(mockNamespaces[0], mockAttributeNames[0], "") + allOfDef := createMockAttribute("namespace.com", "training", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + []string{"security", "compliance"}) - mockDataAttrs := []*policy.Value{ - { - Value: mockAttributeValues[0], - Attribute: &policy.Attribute{ - Fqn: attrFqn, - }, + // Create complex entity attributes + entityAttrs := map[string][]string{ + entityID1: { + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "highest"), + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "engineering"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "security"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "compliance"), }, - { - Value: mockAttributeValues[1], - Attribute: &policy.Attribute{ - Fqn: attrFqn, - }, + entityID2: { + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "medium"), + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "finance"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "security"), + // Missing "compliance" training }, } - groupedValues, err := GroupValuesByDefinition(mockDataAttrs) + // Test 1: Resource requiring all three attributes types + dataAttrs := []*policy.Value{ + hierarchyDef.GetValues()[1], // medium clearance + anyOfDef.GetValues()[0], // engineering department + allOfDef.GetValues()[0], // security training + allOfDef.GetValues()[1], // compliance training + } + + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{hierarchyDef, anyOfDef, allOfDef}) require.NoError(t, err) - assert.Len(t, groupedValues, 1) - for k, v := range groupedValues { - assert.Equal(t, attrFqn, k) - assert.Len(t, v, 2) - assert.Equal(t, mockDataAttrs[0], v[0]) - assert.Equal(t, mockDataAttrs[1], v[1]) + // Entity 1 should have access (meets all requirements) + assert.True(t, decisions[entityID1].Access) + assert.Len(t, decisions[entityID1].Results, 3) + passes := 0 + for _, result := range decisions[entityID1].Results { + if result.Passed { + passes++ + } } -} + assert.Equal(t, 3, passes) // should pass all 3 -// GroupValueFqnsByDefinition tests -func Test_GroupValueFqnsByDefinition(t *testing.T) { - mockFqns := []string{ - fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[0]), - fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[1]), - fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[2]), - fqnBuilder(mockNamespaces[0], mockAttributeNames[1], mockAttributeValues[0]), - fqnBuilder("authority.gov", "YourAttr", "Value2"), + // Entity 2 should not have access (meets clearance, wrong department, missing training) + assert.False(t, decisions[entityID2].Access) + assert.Len(t, decisions[entityID2].Results, 3) + passes = 0 + for _, result := range decisions[entityID2].Results { + if result.Passed { + passes++ + } + } + assert.Equal(t, 1, passes) // should pass 1 out of 3 + + // Test 2: Resource with different requirements + dataAttrs = []*policy.Value{ + hierarchyDef.GetValues()[2], // lowest clearance + anyOfDef.GetValues()[1], // finance department + allOfDef.GetValues()[0], // security training only } - groupedFqns, err := GroupValueFqnsByDefinition(mockFqns) + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{hierarchyDef, anyOfDef, allOfDef}) require.NoError(t, err) - assert.Len(t, groupedFqns, 3) - found := map[string]bool{} - for _, v := range mockFqns { - found[v] = false + // Entity 1 should have access for clearance and training, but fail department + assert.False(t, decisions[entityID1].Access) + assert.Len(t, decisions[entityID1].Results, 3) + passes = 0 + for _, result := range decisions[entityID1].Results { + if result.Passed { + passes++ + } } - - for _, v := range groupedFqns { - for _, fq := range v { - assert.Contains(t, mockFqns, fq) - assert.False(t, found[fq]) - found[fq] = true + assert.Equal(t, 2, passes) // should pass 2 out of 3 + + // Check individual results + // Note: The order of results may vary, so we check by value + foundClearance := false + foundDepartment := false + foundTraining := false + for _, result := range decisions[entityID1].Results { + switch result.RuleDefinition.GetName() { + case hierarchyDef.GetName(): + assert.True(t, result.Passed) // clearance (topsecret > confidential) + foundClearance = true + + case anyOfDef.GetName(): + assert.False(t, result.Passed) // department (engineering ≠ finance) + foundDepartment = true + case allOfDef.GetName(): + assert.True(t, result.Passed) // training (security only) + foundTraining = true } } + assert.True(t, foundClearance) + assert.True(t, foundDepartment) + assert.True(t, foundTraining) + + // Entity 2 should have access with matching clearance, department, and training + assert.True(t, decisions[entityID2].Access) + assert.Len(t, decisions[entityID2].Results, 3) + passes = 0 + for _, result := range decisions[entityID2].Results { + if result.Passed { + passes++ + } + } + assert.Equal(t, 3, passes) // should pass 3 out of 3 + + // Check individual results + // Note: The order of results may vary, so we check by value + foundClearance = false + foundDepartment = false + foundTraining = false + for _, result := range decisions[entityID2].Results { + switch result.RuleDefinition.GetName() { + case hierarchyDef.GetName(): + assert.True(t, result.Passed) // clearance (secret > confidential) + foundClearance = true + case anyOfDef.GetName(): + assert.True(t, result.Passed) // department (finance = finance) + foundDepartment = true + case allOfDef.GetName(): + assert.True(t, result.Passed) // training (only security required) + foundTraining = true + } + } + assert.True(t, foundClearance) + assert.True(t, foundDepartment) + assert.True(t, foundTraining) - for _, v := range found { - assert.True(t, v) + // Test 3: Resource entitling both entities + dataAttrs = []*policy.Value{ + hierarchyDef.GetValues()[2], // lowest clearance + anyOfDef.GetValues()[0], // engineering department + anyOfDef.GetValues()[1], // finance department + allOfDef.GetValues()[0], // security training only } -} -// GetDefinitionFqnFromValue tests -func Test_GetDefinitionFqnFromValue_Succeeds(t *testing.T) { - ns := mockNamespaces[1] - name := mockAttributeNames[2] - val := mockAttributeValues[2] - attrDefFqn := fqnBuilder(ns, name, "") + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{hierarchyDef, anyOfDef, allOfDef}) + require.NoError(t, err) - // With Attribute Def & its FQN, Attribute Def & Namespace, or Value FQN - mockValues := []*policy.Value{ - { - Value: val, - Attribute: &policy.Attribute{ - Fqn: attrDefFqn, - }, - }, - { - Attribute: &policy.Attribute{ - Namespace: &policy.Namespace{ - Name: ns, - }, - Name: name, - }, - }, - { - Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), - }, - } + // Entity 1 passes + assert.True(t, decisions[entityID1].Access) + assert.Len(t, decisions[entityID1].Results, 3) - for _, val := range mockValues { - got, err := GetDefinitionFqnFromValue(val) - require.NoError(t, err) - assert.Equal(t, attrDefFqn, got) + // Entity 2 passes + assert.True(t, decisions[entityID2].Access) + assert.Len(t, decisions[entityID2].Results, 3) + + // Test 4: Neither passes + dataAttrs = []*policy.Value{ + hierarchyDef.GetValues()[2], // lowest clearance + anyOfDef.GetValues()[2], // management department + allOfDef.GetValues()[0], // security training only } + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{hierarchyDef, anyOfDef, allOfDef}) + require.NoError(t, err) + // Entity 1 fails + assert.False(t, decisions[entityID1].Access) + assert.Len(t, decisions[entityID1].Results, 3) + // Entity 2 fails + assert.False(t, decisions[entityID2].Access) + assert.Len(t, decisions[entityID2].Results, 3) } -func Test_GetDefinitionFqnFromValue_FailsWithMissingPieces(t *testing.T) { - mockValues := []*policy.Value{ - // missing attr def & fqn - { - Value: mockAttributeValues[0], - }, - // contains attr def but no namespace - { - Attribute: &policy.Attribute{ - Name: mockAttributeNames[0], - }, - }, - // contains attr def's namespace but no name - { - Attribute: &policy.Attribute{ - Namespace: &policy.Namespace{ - Name: mockNamespaces[0], - }, - }, - }, - } +func Test_EdgeCases_EmptyEntityAttributes(t *testing.T) { + pdp := NewPdp(createTestLogger()) - for _, val := range mockValues { - def, err := GetDefinitionFqnFromValue(val) - require.Error(t, err) - assert.Zero(t, def) - } -} + // Create attribute definition + values := []string{"value1", "value2", "value3"} + definition := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, values) -// GetDefinitionFqnFromValueFqn tests -func Test_GetDefinitionFqnFromValueFqn_Succeeds(t *testing.T) { - ns := mockNamespaces[1] - name := mockAttributeNames[2] - val1 := mockAttributeValues[1] - val2 := mockAttributeValues[2] - attrDefFqn := fqnBuilder(ns, name, "") - mockValueFqns := []string{ - fqnBuilder(ns, name, val1), - fqnBuilder(ns, name, val2), - } + // Test with empty entity attributes map + dataAttrs := []*policy.Value{definition.GetValues()[0]} + emptyEntityAttrs := map[string][]string{} - for _, fqn := range mockValueFqns { - got, err := GetDefinitionFqnFromValueFqn(fqn) - require.NoError(t, err) - assert.Equal(t, attrDefFqn, got) - } + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, emptyEntityAttrs, []*policy.Attribute{definition}) + require.NoError(t, err) + assert.Empty(t, decisions) // No entities to make decisions for + + // Test with entity that has empty attributes array + entityWithEmptyAttrs := map[string][]string{"emptyEntity": {}} + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityWithEmptyAttrs, []*policy.Attribute{definition}) + require.NoError(t, err) + assert.False(t, decisions["emptyEntity"].Access) + assert.False(t, decisions["emptyEntity"].Results[0].Passed) } -func Test_GetDefinitionFqnFromValueFqn_FailsWithMissingPieces(t *testing.T) { - mockValueFqns := []string{ - "", - "/value/hello", - "https://namespace.org/attr/attrName/val/hello", - "namespace.org/attr/attrName/value", - } +func Test_EdgeCases_MalformedAttributes(t *testing.T) { + pdp := NewPdp(createTestLogger()) - for _, fqn := range mockValueFqns { - got, err := GetDefinitionFqnFromValueFqn(fqn) - require.Error(t, err) - assert.Zero(t, got) - } -} + // Create proper definition and data + values := []string{"value1"} + definition := createMockAttribute("example.org", "myattr", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, values) + dataAttrs := []*policy.Value{definition.GetValues()[0]} -// GetDefinitionFqnFromDefinition tests -func Test_GetDefinitionFqnFromDefinition_FromPartsSucceeds(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{ - &simpleAnyOfAttribute, - &simpleAllOfAttribute, - &simpleHierarchyAttribute, + // Test with entity having malformed FQN + malformedEntityAttrs := map[string][]string{ + "malformedEntity": {"not-a-valid-fqn"}, } - for _, attrDef := range mockAttrDefinitions { - fqn := fqnBuilder(attrDef.GetNamespace().GetName(), attrDef.GetName(), "") - got, err := GetDefinitionFqnFromDefinition(attrDef) - require.NoError(t, err) - assert.Equal(t, fqn, got) - } -} + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, malformedEntityAttrs, []*policy.Attribute{definition}) + require.Error(t, err) // Should error due to malformed FQN + assert.Empty(t, decisions) -func Test_GetDefinitionFqnFromDefinition_FromDefinedFqnSucceeds(t *testing.T) { - mockFqns := []string{ - fqnBuilder("example.org", "MyAttr", "Value1"), - fqnBuilder("authority.gov", "YourAttr", "Value2"), - } - mockAttrDefinitions := []*policy.Attribute{ - { - Fqn: mockFqns[0], - }, - { - Fqn: mockFqns[1], - }, + // Test with malformed data attribute + malformedDataAttr := []*policy.Value{ + {Value: "bad", Fqn: "not-a-valid-fqn"}, } - for i, attrDef := range mockAttrDefinitions { - got, err := GetDefinitionFqnFromDefinition(attrDef) - require.NoError(t, err) - assert.Equal(t, attrDef.GetFqn(), got) - assert.Equal(t, mockFqns[i], got) - } + decisions, err = pdp.DetermineAccess(t.Context(), malformedDataAttr, + createMockEntity1Attributes(definition.GetNamespace().GetName(), definition.GetName(), values), + []*policy.Attribute{definition}) + require.Error(t, err) // Should error due to malformed data attribute + assert.Empty(t, decisions) } -func Test_GetDefinitionFqnFromDefinition_FailsWithNoNamespace(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", +func Test_InvalidAttributeRuleType(t *testing.T) { + pdp := NewPdp(createTestLogger()) + + // Create an attribute with an unspecified rule type + invalidDef := &policy.Attribute{ + Name: "invalid", + Namespace: &policy.Namespace{ + Name: "example.org", + }, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED, + Fqn: "https://example.org/attr/invalid", + Values: []*policy.Value{ + {Value: "value1", Fqn: "https://example.org/attr/invalid/value/value1"}, }, } - for _, attrDef := range mockAttrDefinitions { - _, err := GetDefinitionFqnFromDefinition(attrDef) - require.Error(t, err) - } + dataAttrs := invalidDef.GetValues() + entityAttrs := createMockEntity1Attributes(invalidDef.GetNamespace().GetName(), invalidDef.GetName(), []string{"value1"}) + + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{invalidDef}) + require.Error(t, err) // Should error due to invalid rule type + assert.Empty(t, decisions) } -func Test_GetDefinitionFqnFromDefinition_FailsWithNoName(t *testing.T) { - mockAttrDefinitions := []*policy.Attribute{ - { - Namespace: &policy.Namespace{ - Name: "example.org", - }, +func Test_MixedRuleTypes(t *testing.T) { + pdp := NewPdp(createTestLogger()) + + // Create various attribute definitions with different rule types + anyOfDef := createMockAttribute("example.org", "anyof", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + []string{"a", "b"}) + allOfDef := createMockAttribute("example.org", "allof", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + []string{"c", "d"}) + hierarchyDef := createMockAttribute("example.org", "hierarchy", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + []string{"high", "medium", "low"}) + + // Create entity attributes + entityAttrs := map[string][]string{ + "entity1": { + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "a"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "c"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "d"), + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "high"), }, } - for _, attrDef := range mockAttrDefinitions { - _, err := GetDefinitionFqnFromDefinition(attrDef) - require.Error(t, err) + // Create data attributes covering all types + dataAttrs := []*policy.Value{ + anyOfDef.GetValues()[0], // a + allOfDef.GetValues()[0], // c + allOfDef.GetValues()[1], // d + hierarchyDef.GetValues()[0], // high } -} -// getIsValueFoundInFqnValuesSet -func Test_GetIsValueFoundInFqnValuesSet(t *testing.T) { - ns1 := mockNamespaces[1] - ns2 := mockNamespaces[2] - name := mockAttributeNames[2] - fqnsList := []string{ - fqnBuilder(ns1, name, mockAttributeValues[0]), - fqnBuilder(ns1, name, mockAttributeValues[1]), - fqnBuilder(ns1, name, mockAttributeValues[2]), - fqnBuilder(ns2, name, mockAttributeValues[0]), + // Test with all three rule types + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{anyOfDef, allOfDef, hierarchyDef}) + require.NoError(t, err) + + // Entity should have access, passing all rules + assert.True(t, decisions["entity1"].Access) + assert.Len(t, decisions["entity1"].Results, 3) + + // Check individual rule results + var anyOfResult, allOfResult, hierarchyResult *DataRuleResult + for _, result := range decisions["entity1"].Results { + switch result.RuleDefinition.GetName() { + case "anyof": + anyOfResult = &result + case "allof": + allOfResult = &result + case "hierarchy": + hierarchyResult = &result + } } - values := []struct { - val *policy.Value - expected bool - }{ - { - val: &policy.Value{ - Fqn: fqnsList[0], - }, - expected: true, - }, - { - val: &policy.Value{ - Fqn: fqnsList[1], - }, - expected: true, - }, - { - val: &policy.Value{ - Fqn: fqnsList[2], - }, - expected: true, - }, - { - val: &policy.Value{ - Fqn: fqnsList[3], - }, - expected: true, - }, - { - val: &policy.Value{ - Fqn: fqnBuilder(ns1, name, "unknownValue"), - }, - }, - { - val: nil, - }, - { - val: &policy.Value{ - Fqn: "", - }, + assert.NotNil(t, anyOfResult) + assert.NotNil(t, allOfResult) + assert.NotNil(t, hierarchyResult) + assert.True(t, anyOfResult.Passed) + assert.True(t, allOfResult.Passed) + assert.True(t, hierarchyResult.Passed) + + // Now make one rule fail (remove one of the allOf values from entity) + entityAttrs = map[string][]string{ + "entity1": { + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "a"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "c"), + // Missing d for allOf + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "high"), }, } - for i, v := range values { - assert.Equal(t, v.expected, getIsValueFoundInFqnValuesSet(v.val, fqnsList, logger.CreateTestLogger())) - if i == 3 { - assert.False(t, getIsValueFoundInFqnValuesSet(v.val, fqnsList[:3], logger.CreateTestLogger())) + decisions, err = pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{anyOfDef, allOfDef, hierarchyDef}) + require.NoError(t, err) + + // Entity should not have access (allOf failed) + assert.False(t, decisions["entity1"].Access) + assert.Len(t, decisions["entity1"].Results, 3) + + // Find the allOf result and verify it failed + for _, result := range decisions["entity1"].Results { + if result.RuleDefinition.GetName() == "allof" { + assert.False(t, result.Passed) + assert.NotEmpty(t, result.ValueFailures) + break } } } -// getOrderOfValue tests -func Test_GetOrderOfValue(t *testing.T) { - ns := mockNamespaces[1] - name := mockAttributeNames[2] +func Test_HierarchyEdgeCases(t *testing.T) { + pdp := NewPdp(createTestLogger()) + + // Create hierarchy attribute with unusual order + values := []string{"top", "middle", "bottom", "super-bottom"} + definition := createMockAttribute("example.org", "hierarchy", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, values) - values := []*policy.Value{ + // Test scenarios + tests := []struct { + name string + entityLevel string + dataLevel string + expectedAccess bool + }{ { - Value: mockAttributeValues[1], - Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), + name: "Entity at top, data at super-bottom", + entityLevel: "top", + dataLevel: "super-bottom", + expectedAccess: true, }, { - Value: mockAttributeValues[2], - Attribute: &policy.Attribute{ - Fqn: fqnBuilder(ns, name, ""), - }, + name: "Entity at top, data not in hierarchy", + entityLevel: "top", + dataLevel: "not-in-hierarchy", + expectedAccess: false, }, { - Value: mockAttributeValues[4], - Attribute: &policy.Attribute{ - Name: name, - Namespace: &policy.Namespace{ - Name: ns, - }, - }, + name: "Entity not in hierarchy, data in hierarchy", + entityLevel: "not-in-hierarchy", + dataLevel: "middle", + expectedAccess: false, }, { - Value: mockAttributeValues[0], + name: "Both entity and data not in hierarchy", + entityLevel: "unknown-level", + dataLevel: "not-in-hierarchy", + expectedAccess: false, }, } - for i := range values { - got, err := getOrderOfValue(values, values[i], logger.CreateTestLogger()) - require.NoError(t, err) - assert.Equal(t, i, got) - } - - // test with a value that doesn't exist in the list - idx, err := getOrderOfValue(values, &policy.Value{ - Value: "unknownValue", - }, logger.CreateTestLogger()) - require.NoError(t, err) - assert.Equal(t, -1, idx) -} - -func Test_GetOrderOfValue_FailsCorrectly(t *testing.T) { - ns := mockNamespaces[1] - name := mockAttributeNames[2] + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test data + dataValue := &policy.Value{ + Value: tt.dataLevel, + Fqn: fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), tt.dataLevel), + } - bad := []*policy.Value{ - { - Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), - }, - {}, - { - Attribute: &policy.Attribute{ - Name: name, - Namespace: &policy.Namespace{ - Name: ns, + entityAttrs := map[string][]string{ + "entity1": { + fqnBuilder(definition.GetNamespace().GetName(), definition.GetName(), tt.entityLevel), }, - }, - }, + } + + decisions, err := pdp.DetermineAccess(t.Context(), []*policy.Value{dataValue}, entityAttrs, []*policy.Attribute{definition}) + + if tt.dataLevel == "not-in-hierarchy" { + // When data is not in hierarchy, we expect an error + require.NoError(t, err) + assert.False(t, decisions["entity1"].Access) + } else { + if err == nil { + assert.Equal(t, tt.expectedAccess, decisions["entity1"].Access) + } else { + // Some invalid combinations might cause errors + assert.Contains(t, err.Error(), "error getting") + } + } + }) } +} - good := &policy.Value{ - Value: mockAttributeValues[1], - } +func Test_MultipleIdenticalDefinitions(t *testing.T) { + pdp := NewPdp(createTestLogger()) - for _, v := range bad { - order := []*policy.Value{v, good} - got, err := getOrderOfValue(order, good, logger.CreateTestLogger()) - require.Error(t, err) - assert.Equal(t, -1, got) - } + // Create two identical definitions with same FQN + def1 := createMockAttribute("example.org", "dup", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, []string{"value1"}) + def2 := createMockAttribute("example.org", "dup", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, []string{"value1"}) - // test with a value that doesn't exist in the list - idx, err := getOrderOfValue(append(bad, good), &policy.Value{ - Value: "unknownValue", - }, logger.CreateTestLogger()) - require.Error(t, err) - assert.Equal(t, -1, idx) + dataAttrs := def1.GetValues() + entityAttrs := createMockEntity1Attributes(def1.GetNamespace().GetName(), def1.GetName(), []string{"value1"}) + + // Should work, but log a warning about duplicate FQN (which we can't test directly) + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, []*policy.Attribute{def1, def2}) + require.NoError(t, err) + assert.True(t, decisions[mockEntityID].Access) } -// getOrderOfValueByFqn tests -func Test_GetOrderOfValueByFqn(t *testing.T) { - ns := mockNamespaces[0] - name := mockAttributeNames[0] - values := []*policy.Value{ - { - Fqn: fqnBuilder(ns, name, mockAttributeValues[0]), +func Test_DetermineAccess_MultipleEntities_AcrossRuleTypes(t *testing.T) { + pdp := NewPdp(createTestLogger()) + + // Define entity IDs + entityIDs := []string{"entity1", "entity2", "entity3", "entity4", "entity5"} + + // Create various attribute definitions + anyOfDef := createMockAttribute("example.org", "department", + policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + []string{"hr", "engineering", "sales", "marketing", "finance"}) + + allOfDef := createMockAttribute("example.org", "certifications", + policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, + []string{"security", "compliance", "governance"}) + + hierarchyDef := createMockAttribute("example.org", "access_level", + policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + []string{"admin", "manager", "user", "guest"}) + + // Create entity attributes with various combinations + entityAttrs := map[string][]string{ + entityIDs[0]: { // entity1: full access - has everything + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "engineering"), + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "hr"), + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "sales"), + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "marketing"), + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "finance"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "security"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "compliance"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "governance"), + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "admin"), }, - { - Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), + entityIDs[1]: { // entity2: engineering, missing some certs, manager level + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "engineering"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "security"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "compliance"), + // Missing governance + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "manager"), }, - { - Value: mockAttributeValues[2], - Attribute: &policy.Attribute{ - Fqn: fqnBuilder(ns, name, ""), - }, + entityIDs[2]: { // entity3: HR, all certs, user level + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "hr"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "security"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "compliance"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "governance"), + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "user"), }, - { - Value: mockAttributeValues[3], - Attribute: &policy.Attribute{ - Name: name, - Namespace: &policy.Namespace{ - Name: ns, - }, - }, + entityIDs[3]: { // entity4: Marketing, security only, guest level + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "marketing"), + fqnBuilder(allOfDef.GetNamespace().GetName(), allOfDef.GetName(), "security"), + // Missing compliance and governance + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "guest"), + }, + entityIDs[4]: { // entity5: Multiple departments (HR and Finance), no certs, manager level + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "hr"), + fqnBuilder(anyOfDef.GetNamespace().GetName(), anyOfDef.GetName(), "finance"), + // No certifications + fqnBuilder(hierarchyDef.GetNamespace().GetName(), hierarchyDef.GetName(), "manager"), }, } - for i := range values { - fqn := fqnBuilder(ns, name, mockAttributeValues[i]) - got, err := getOrderOfValueByFqn(values, fqn) + // Test Case 1: Engineering document requiring all certifications, manager level + t.Run("Engineering document with all certifications, manager level", func(t *testing.T) { + dataAttrs := []*policy.Value{ + anyOfDef.GetValues()[1], // engineering + allOfDef.GetValues()[0], // security + allOfDef.GetValues()[1], // compliance + allOfDef.GetValues()[2], // governance + hierarchyDef.GetValues()[1], // manager + } + + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, + []*policy.Attribute{anyOfDef, allOfDef, hierarchyDef}) require.NoError(t, err) - assert.Equal(t, i, got) - } -} -func Test_GetOrderOfValueByFqn_SadCases(t *testing.T) { - ns := mockNamespaces[0] - name := mockAttributeNames[0] - bad := []*policy.Value{ - // empty FQN and no parent parts - { - Fqn: "", - }, - // no definition FQN, no parts - { - Value: mockAttributeValues[1], - Attribute: &policy.Attribute{ - Fqn: "", - }, - }, - // full definition parts, no value - { - Attribute: &policy.Attribute{ - Name: name, - Namespace: &policy.Namespace{ - Name: ns, - }, - }, - }, - // missing namespace - { - Value: mockAttributeValues[1], - Attribute: &policy.Attribute{ - Name: name, - Namespace: &policy.Namespace{}, - }, - }, - // missing definition name - { - Value: mockAttributeValues[1], - Attribute: &policy.Attribute{ - Namespace: &policy.Namespace{ - Name: ns, - }, - }, - }, - // full definition FQN, no value - { - Attribute: &policy.Attribute{ - Fqn: fqnBuilder(ns, name, ""), - }, - }, - } - fqn := fqnBuilder(ns, name, mockAttributeValues[1]) - good := &policy.Value{ - Fqn: fqn, - } + // Only entity1 (admin with all certs) and entity2 (manager in engineering) should have access + assert.True(t, decisions[entityIDs[0]].Access) // entity1: full access + assert.False(t, decisions[entityIDs[1]].Access) // entity2: missing governance cert + assert.False(t, decisions[entityIDs[2]].Access) // entity3: HR department (wrong department) + assert.False(t, decisions[entityIDs[3]].Access) // entity4: Marketing, guest level, missing certs + assert.False(t, decisions[entityIDs[4]].Access) // entity5: HR+Finance but no certs + }) + + // Test Case 2: HR or Marketing document requiring only security cert, user level + t.Run("HR or Marketing document requiring security cert, user level", func(t *testing.T) { + dataAttrs := []*policy.Value{ + anyOfDef.GetValues()[0], // hr + anyOfDef.GetValues()[3], // marketing + allOfDef.GetValues()[0], // security cert only + hierarchyDef.GetValues()[2], // user level + } - for _, v := range bad { - order := []*policy.Value{v, good} - got, err := getOrderOfValueByFqn(order, fqn) - require.Error(t, err) - assert.Equal(t, -1, got) - } + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, + []*policy.Attribute{anyOfDef, allOfDef, hierarchyDef}) + require.NoError(t, err) + + assert.True(t, decisions[entityIDs[0]].Access) // entity1: admin has access to everything + assert.False(t, decisions[entityIDs[1]].Access) // entity2: engineering dept (wrong department) + assert.True(t, decisions[entityIDs[2]].Access) // entity3: HR with all certs + assert.False(t, decisions[entityIDs[3]].Access) // entity4: Marketing but guest level (too low) + assert.False(t, decisions[entityIDs[4]].Access) // entity5: HR dept, manager level (no security cert) + }) + + // Test Case 3: Generic document for all departments, no certs, guest level + t.Run("Document for any department, no certs, guest level", func(t *testing.T) { + dataAttrs := []*policy.Value{ + anyOfDef.GetValues()[0], // hr + anyOfDef.GetValues()[1], // engineering + anyOfDef.GetValues()[2], // sales + anyOfDef.GetValues()[3], // marketing + anyOfDef.GetValues()[4], // finance + // No certs required + hierarchyDef.GetValues()[3], // guest level + } + + decisions, err := pdp.DetermineAccess(t.Context(), dataAttrs, entityAttrs, + []*policy.Attribute{anyOfDef, hierarchyDef}) // Note: Excluding allOfDef + require.NoError(t, err) + + // All entities should have access (we're only checking anyOf department and hierarchy) + assert.True(t, decisions[entityIDs[0]].Access) // entity1: admin level + assert.True(t, decisions[entityIDs[1]].Access) // entity2: manager level + assert.True(t, decisions[entityIDs[2]].Access) // entity3: user level + assert.True(t, decisions[entityIDs[3]].Access) // entity4: guest level (minimum allowed) + assert.True(t, decisions[entityIDs[4]].Access) // entity5: manager level + + // Check rule results count + assert.Len(t, decisions[entityIDs[0]].Results, 2) // Only anyOf and hierarchy checks + }) }