diff --git a/docs/grpc/index.html b/docs/grpc/index.html index 5470a50081..0e0a50642e 100644 --- a/docs/grpc/index.html +++ b/docs/grpc/index.html @@ -2487,7 +2487,7 @@

EntityEntitlements

- attribute_id + attribute_value_fqns string repeated

@@ -2580,7 +2580,7 @@

GetEntitlementsRequest

GetEntitlementsResponse

-

Example Response for a request of : Get entitlements for bob and alice (both represented using an email address

{

"entitlements": [

{

"entityId": "e1",

"attributeValueReferences": [

{

"attributeFqn": "http://www.example.org/attr/foo/value/bar"

}

]

},

{

"entityId": "e2",

"attributeValueReferences": [

{

"attributeFqn": "http://www.example.org/attr/color/value/red"

}

]

}

]

}

+

Example Response for a request of : Get entitlements for bob and alice (both represented using an email address

{

"entitlements": [

{

"entityId": "e1",

"attributeValueFqns": ["https://www.example.org/attr/foo/value/bar"]

},

{

"entityId": "e2",

"attributeValueFqns": ["https://www.example.org/attr/color/value/red"]

}

]

}

diff --git a/docs/openapi/authorization/authorization.swagger.json b/docs/openapi/authorization/authorization.swagger.json index e3698072a1..4549c172ba 100644 --- a/docs/openapi/authorization/authorization.swagger.json +++ b/docs/openapi/authorization/authorization.swagger.json @@ -214,7 +214,7 @@ "entityId": { "type": "string" }, - "attributeId": { + "attributeValueFqns": { "type": "array", "items": { "type": "string" @@ -245,7 +245,7 @@ } } }, - "description": "{\n\"entitlements\": [\n{\n\"entityId\": \"e1\",\n\"attributeValueReferences\": [\n{\n\"attributeFqn\": \"http://www.example.org/attr/foo/value/bar\"\n}\n]\n},\n{\n\"entityId\": \"e2\",\n\"attributeValueReferences\": [\n{\n\"attributeFqn\": \"http://www.example.org/attr/color/value/red\"\n}\n]\n}\n]\n}", + "description": "{\n\"entitlements\": [\n{\n\"entityId\": \"e1\",\n\"attributeValueFqns\": [\"https://www.example.org/attr/foo/value/bar\"]\n},\n{\n\"entityId\": \"e2\",\n\"attributeValueFqns\": [\"https://www.example.org/attr/color/value/red\"]\n}\n]\n}", "title": "Example Response for a request of : Get entitlements for bob and alice (both represented using an email address" }, "authorizationResourceAttribute": { diff --git a/internal/access/attributeinstance.go b/internal/access/attributeinstance.go deleted file mode 100644 index d46e242d4e..0000000000 --- a/internal/access/attributeinstance.go +++ /dev/null @@ -1,97 +0,0 @@ -package access - -import ( - "fmt" - "net/url" - "strings" -) - -// attributeInstance is created by selecting the Authority, Name and a specific Value from -// an AttributeDefinition. -// -// An attributeInstance is a single, unique attribute, with a single value. -// -// Applied to an entity, the attributeInstance becomes an entity attribute. -// Applied to data, the attributeInstance becomes a data attribute. -// -// When making an access decisions, these two kinds of AttributeInstances are compared with each other. -// -// Example attributeInstance: -// https://derp.com/attr/Blob/value/Green -> -// -// Authority = https://derp.com -// Name = Blob -// CanonicalName = Authority + Name https://derp.com/attr/Blob -// Value = Green -type AttributeInstance struct { - Authority string `json:"authority"` - Name string `json:"name"` - Value string `json:"value"` -} - -// Implement the standard "stringify" interface -// and return a string in the canonical attributeInstance format of -// -// /attr//value/ -func (attr AttributeInstance) String() string { - return fmt.Sprintf("%s/attr/%s/value/%s", - attr.Authority, - attr.Name, - attr.Value, - ) -} - -// GetCanonicalName For cases where just the canonical name of this attributeInstance is required -// (e.g. /attr/ - the authority and name, but not the value): -// -// /attr/ -func (attr AttributeInstance) GetCanonicalName() string { - return fmt.Sprintf("%s/attr/%s", - attr.Authority, - attr.Name, - ) -} - -func (attr AttributeInstance) GetAuthority() string { - return attr.Authority -} - -// ParseInstanceFromURI Accepts a valid attribute instance URI (authority + name + value in the canonical -// format 'https://example.org/attr/MyAttrName/value/MyAttrValue') and returns an -// attributeInstance. -// -// Strings that are not valid URLs will result in a parsing failure, and return an error. -func ParseInstanceFromURI(attributeURI string) (AttributeInstance, error) { - parsedAttr, err := url.Parse(attributeURI) - if err != nil { - return AttributeInstance{}, err - } - - // Needs to be absolute - that is, rooted with a scheme, and not relative. - if !parsedAttr.IsAbs() { - return AttributeInstance{}, fmt.Errorf("Could not parse attributeURI %s - is not an absolute URI", attributeURI) - } - - pathParts := strings.Split(strings.Trim(parsedAttr.Path, "/"), "/") - // If we don't end up with exactly 4 segments, e.g. `attr/MyAttrName/value/MyAttrValue` -> - // then something is wrong, this is not a canonical attr representation and we need to return an error - if len(pathParts) != 4 { - return AttributeInstance{}, fmt.Errorf("Could not parse attributeURI %s - path %s is not in canonical format, parts were %s", attributeURI, parsedAttr.Path, pathParts) - } - - authority := fmt.Sprintf("%s://%s", parsedAttr.Scheme, parsedAttr.Hostname()) // == https://example.org - name := pathParts[1] // == MyAttrName - value := pathParts[3] // == MyAttrValue - - return AttributeInstance{ - Authority: authority, // Just scheme://host of the attribute - that is, the authority - Name: name, - Value: value, - }, nil -} - -// ParseInstanceFromParts Accepts attribute namespace, name and value strings, and returns an attributeInstance -func ParseInstanceFromParts(namespace, name, value string) (AttributeInstance, error) { - fmtAttr := fmt.Sprintf("%s/attr/%s/value/%s", namespace, name, value) - return ParseInstanceFromURI(fmtAttr) -} diff --git a/internal/access/pdp.go b/internal/access/pdp.go index 58f9f24117..9fc40450e3 100644 --- a/internal/access/pdp.go +++ b/internal/access/pdp.go @@ -4,79 +4,85 @@ import ( "context" "fmt" "log/slog" + "strings" "github.com/opentdf/platform/protocol/go/policy" ) -type Pdp struct { -} +type Pdp struct{} func NewPdp() *Pdp { return &Pdp{} } -// DetermineAccess will take data AttributeInstances, data AttributeDefinitions, and entity attributeInstance sets, and -// compare every data attributeInstance against every entity's attributeInstance set, generating a rolled-up decision -// result for each entity, as well as a detailed breakdown of every data attributeInstance comparison. +// DetermineAccess will take data Attribute Values, entities mapped entityId to Attribute Value FQNs, and data AttributeDefinitions, +// compare every data Attribute against every entity's set of Attribute Values, generating a rolled-up decision +// result for each entity, as well as a detailed breakdown of every data comparison. func (pdp *Pdp) DetermineAccess( ctx context.Context, - dataAttributes []AttributeInstance, - entityAttributeSets map[string][]AttributeInstance, + dataAttributes []*policy.Value, + entityAttributeSets map[string][]string, attributeDefinitions []*policy.Attribute, ) (map[string]*Decision, error) { slog.DebugContext(ctx, "DetermineAccess") - // Cluster (e.g. group) all the Data AttributeInstances by CanonicalName (that is, "/attr/") - // AttributeInstances in the same cluster/group (keyed by CanonicalName) will be different "instances" of the same attribute, - // potentially with different values. - // - // (e.g. we may have one cluster keyed by "https://authority.org/attr/MyAttr" - // with two attributes having different values inside that cluster: - // - "https://authority.org/attr/MyAttr/value/Value1") - // - "https://authority.org/attr/MyAttr/value/Value2") - clusteredDataAttrs := ClusterByCanonicalNameAI(dataAttributes) - // Similarly, cluster (e.g. group) all the previously-fetched AttributeDefinitions (one definition per Data attributeInstance) - // by CanonicalName (that is, "/attr/") + // 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). // - // Unlike with AttributeInstances, there should only be *one* AttributeDefinition per CanonicalName (e.g "https://authority.org/attr/MyAttr") - clusteredDefinitions := ClusterByCanonicalNameAD(attributeDefinitions) + // 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 err != nil { + slog.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(attributeDefinitions) + if err != nil { + slog.Error(fmt.Sprintf("error grouping attribute definitions by FQN: %s", err.Error())) + return nil, err + } decisions := make(map[string]*Decision) - // Go through all the clustered data attrs by canonical name - for canonicalName, distinctValues := range clusteredDataAttrs { - slog.DebugContext(ctx, "Evaluating data attribute", "name", canonicalName) - // Correctness check - we should only have been given 1 AttributeDefinition for per attribute CanonicalName - // If not, then calling code is broken, so complain. - if len(clusteredDefinitions[canonicalName]) != 1 { - return nil, fmt.Errorf("expected 1 AttributeDefinition per attribute CanonicalName %s", canonicalName) + // Go through all the grouped data values under each definition FQN + for definitionFqn, distinctValues := range dataAttrValsByDefinition { + slog.DebugContext(ctx, "Evaluating data attribute fqn", definitionFqn, slog.Any("values", distinctValues)) + attrDefinition, ok := fqnToDefinitionMap[definitionFqn] + if !ok { + return nil, fmt.Errorf("expected an Attribute Definition under the FQN %s", definitionFqn) } - // For every canonical name we have a cluster for in the data attr set, - // look up its AttributeDefinition (again, should be exactly 1) - attrDefinition := clusteredDefinitions[canonicalName][0] - // If GroupBy is set, determine which entities (out of the set of entities and their respective AttributeInstances) - // will be considered for evaluation under this rule definition. + // 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 definition + // 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 AttributeDefinition we have, - // and the entity AttributeInstances that entity ID has, then that entity ID passed (or skipped) this rule. - filteredEntities := entityAttributeSets - var entityRuleDecision map[string]DataRuleResult - switch attrDefinition.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: - slog.DebugContext(ctx, "Evaluating under allOf", "name", canonicalName, "values", distinctValues) - entityRuleDecision = pdp.allOfRule(ctx, distinctValues, filteredEntities) + slog.DebugContext(ctx, "Evaluating under allOf", "name", definitionFqn, "values", distinctValues) + entityRuleDecision, err = pdp.allOfRule(ctx, distinctValues, entityAttributeSets) case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF: - slog.DebugContext(ctx, "Evaluating under anyOf", "name", canonicalName, "values", distinctValues) - entityRuleDecision = pdp.anyOfRule(ctx, distinctValues, filteredEntities) + slog.DebugContext(ctx, "Evaluating under anyOf", "name", definitionFqn, "values", distinctValues) + entityRuleDecision, err = pdp.anyOfRule(ctx, distinctValues, entityAttributeSets) case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY: - slog.DebugContext(ctx, "Evaluating under hierarchy", "name", canonicalName, "values", distinctValues) - entityRuleDecision = pdp.hierarchyRule(ctx, distinctValues, filteredEntities, attrDefinition.Values) + slog.DebugContext(ctx, "Evaluating under hierarchy", "name", definitionFqn, "values", distinctValues) + entityRuleDecision, err = pdp.hierarchyRule(ctx, distinctValues, entityAttributeSets, attrDefinition.Values) case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED: return nil, fmt.Errorf("unset AttributeDefinition rule: %s", attrDefinition.Rule) default: return nil, fmt.Errorf("unrecognized AttributeDefinition rule: %s", attrDefinition.Rule) } + if err != nil { + return nil, fmt.Errorf("error evaluating rule: %s", err.Error()) + } // Roll up the per-data-rule decisions for each entity considered for this rule into the overall decision for entityId, ruleResult := range entityRuleDecision { @@ -108,44 +114,54 @@ func (pdp *Pdp) DetermineAccess( return decisions, nil } -// AllOf the Data attributeInstance CanonicalName+Value pairs should be present in AllOf the Entity's attributeInstance sets +// AllOf the Data Attribute Values should be present in AllOf the Entity's entityAttributeValue sets // Accepts -// - a set of data AttributeInstances with the same canonical name -// - a map of entity AttributeInstances keyed by entity ID +// - 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, dataAttrsBySingleCanonicalName []AttributeInstance, entityAttributes map[string][]AttributeInstance) map[string]DataRuleResult { +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 AttributeInstances in the arg have the same canonical name. - slog.DebugContext(ctx, "Evaluating all-of decision", "name", dataAttrsBySingleCanonicalName[0].GetCanonicalName()) + // All of the data Attribute Values in the arg have the same Definition FQN + def, err := GetDefinitionFqnFromValue(dataAttrValuesOfOneDefinition[0]) + if err != nil { + return nil, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) + } + slog.DebugContext(ctx, "Evaluating allOf decision", "attribute definition FQN", def) - // Go through every entity's attributeInstance set... - for entityId, entityAttrs := range entityAttributes { + // Go through every entity's attributeValues set... + for entityId, entityAttrVals := range entityAttributeValueFqns { var valueFailures []ValueFailure // Default to DENY entityPassed := false - // Cluster entity AttributeInstances by canonical name... - entityAttrCluster := ClusterByCanonicalNameAI(entityAttrs) - // For every unique data attributeInstance (that is, unique data attribute value) in this set of data AttributeInstances sharing the same canonical name... - for dvIndex, dataAttrVal := range dataAttrsBySingleCanonicalName { - dvCanonicalName := dataAttrVal.GetCanonicalName() - slog.DebugContext(ctx, "Evaluating all-of decision for data attr %s with value %s", dvCanonicalName, dataAttrVal.Value) + 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 { + attrDefFqn, err := GetDefinitionFqnFromValue(dataAttrVal) + if err != nil { + return nil, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) + } + slog.DebugContext(ctx, "Evaluating allOf decision for data attr %s with value %s", attrDefFqn, dataAttrVal.GetValue()) // See if - // 1. there exists an entity attributeInstance in the set of AttributeInstances - // with the same canonical name as the data attributeInstance in question - // 2. It has the same VALUE as the data attributeInstance in question - found := findInstanceValueInClusterAI(&dataAttrsBySingleCanonicalName[dvIndex], entityAttrCluster[dvCanonicalName]) + // 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]) denialMsg := "" - // If we did not find the data attributeInstance canonical name + value in the entity attributeInstance set, - //then prepare a ValueFailure for that data attributeInstance (that is, attribute value), for this entity + // 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 if !found { - denialMsg = fmt.Sprintf("AllOf not satisfied for canonical data attr+value %s and entity %s", dataAttrVal, entityId) + denialMsg = fmt.Sprintf("AllOf not satisfied for data attr %s with value %s and entity %s", attrDefFqn, dataAttrVal.GetValue(), entityId) slog.WarnContext(ctx, denialMsg) // Append the ValueFailure to the set of entity value failures valueFailures = append(valueFailures, ValueFailure{ - DataAttribute: &dataAttrsBySingleCanonicalName[dvIndex], + DataAttribute: dataAttrValuesOfOneDefinition[dvIndex], Message: denialMsg, }) } @@ -161,57 +177,61 @@ func (pdp *Pdp) allOfRule(ctx context.Context, dataAttrsBySingleCanonicalName [] } } - return ruleResultsByEntity + return ruleResultsByEntity, nil } -// AnyOf the Data attributeInstance CanonicalName+Value pairs can be present in AnyOf the Entity's attributeInstance sets +// AnyOf the Data Attribute Values can be present in AnyOf the Entity's Attribute Value FQN sets // Accepts -// - a set of data AttributeInstances with the same canonical name -// - a map of entity AttributeInstances keyed by entity ID -// Returns a map of DataRuleResults keyed by Subject -func (pdp *Pdp) anyOfRule(ctx context.Context, dataAttrsBySingleCanonicalName []AttributeInstance, entityAttributes map[string][]AttributeInstance) map[string]DataRuleResult { +// - 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) { ruleResultsByEntity := make(map[string]DataRuleResult) - dvCanonicalName := dataAttrsBySingleCanonicalName[0].GetCanonicalName() - // All the data AttributeInstances in the arg have the same canonical name. - slog.DebugContext(ctx, "Evaluating anyOf decision", "attr", dvCanonicalName) + // All of the data Attribute Values in the arg have the same Definition FQN + attrDefFqn, err := GetDefinitionFqnFromValue(dataAttrValuesOfOneDefinition[0]) + if err != nil { + return nil, fmt.Errorf("error getting definition FQN from data attribute value: %s", err.Error()) + } + slog.DebugContext(ctx, "Evaluating anyOf decision", "attribute definition FQN", attrDefFqn) - // Go through every entity's attributeInstance set... - for entityId, entityAttrs := range entityAttributes { + // Go through every entity's Attribute Value set... + for entityId, entityAttrValFqns := range entityAttributeValueFqns { var valueFailures []ValueFailure // Default to DENY entityPassed := false - // Cluster entity AttributeInstances by canonical name... - entityAttrCluster := ClusterByCanonicalNameAI(entityAttrs) - // For every unique data attributeInstance (that is, value) in this set of data attributeInstance sharing the same canonical name... - for dvIndex, dataAttrVal := range dataAttrsBySingleCanonicalName { - slog.DebugContext(ctx, "Evaluating anyOf decision", "attr", dvCanonicalName, "value", dataAttrVal.Value) - // See if - // 1. there exists an entity attributeInstance in the set of AttributeInstances - // with the same canonical name as the data attributeInstance in question - // 2. It has the same VALUE as the data attributeInstance in question - found := findInstanceValueInClusterAI(&dataAttrsBySingleCanonicalName[dvIndex], entityAttrCluster[dvCanonicalName]) + entityAttrGroup, err := GroupValueFqnsByDefinition(entityAttrValFqns) + if err != nil { + 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 { + slog.DebugContext(ctx, "Evaluating anyOf decision", "attribute definition FQN", attrDefFqn, "value", dataAttrVal.Value) + // 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]) denialMsg := "" - // If we did not find the data attributeInstance canonical name + value in the entity attributeInstance set, - //then prepare a ValueFailure for that data attributeInstance and value, for this entity + // 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 { - denialMsg = fmt.Sprintf("anyOf not satisfied for canonical data attr+value %s and entity %s - anyOf is permissive, so this doesn't mean overall failure", dataAttrVal, entityId) + 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) slog.WarnContext(ctx, denialMsg) valueFailures = append(valueFailures, ValueFailure{ - DataAttribute: &dataAttrsBySingleCanonicalName[dvIndex], + DataAttribute: dataAttrValuesOfOneDefinition[dvIndex], Message: denialMsg, }) } } - // AnyOf - IF there were fewer value failures for this entity, for this attributeInstance canonical name, - //then there are distict data values, for this attributeInstance canonical name, THEN this entity must - //possess AT LEAST ONE of the values in its entity attributeInstance cluster, - //and we have satisfied AnyOf - if len(valueFailures) < len(dataAttrsBySingleCanonicalName) { - slog.DebugContext(ctx, "anyOf satisfied for canonical data", "attr", dvCanonicalName, "entityId", entityId) + // AnyOf - IF there were fewer value failures for this entity, for this Attribute Value FQN, + // then there are distict 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) { + slog.DebugContext(ctx, "anyOf satisfied", "attribute definition FQN", attrDefFqn, "entityId", entityId) entityPassed = true } ruleResultsByEntity[entityId] = DataRuleResult{ @@ -220,53 +240,67 @@ func (pdp *Pdp) anyOfRule(ctx context.Context, dataAttrsBySingleCanonicalName [] } } - return ruleResultsByEntity + return ruleResultsByEntity, nil } -// Hierarchy rule compares the HIGHEST (that is, numerically lowest index) data attributeInstance (that is, value) for a given attributeInstance canonical name -// with the LOWEST (that is, numerically highest index) entity value for a given attributeInstance canonical name. +// 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, AttributeInstances) for a given hierarchy AttributeDefinition are present for the same canonical name, the highest will be chosen and +// 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 AttributeInstances (that is, values) for a hierarchy AttributeDefinition are present for the same canonical name, the lowest will be chosen, +// 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, dataAttrsBySingleCanonicalName []AttributeInstance, entityAttributes map[string][]AttributeInstance, order []*policy.Value) map[string]DataRuleResult { +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) - highestDataInstance := pdp.getHighestRankedInstanceFromDataAttributes(ctx, order, dataAttrsBySingleCanonicalName) - if highestDataInstance == nil { + highestDataAttrVal, err := pdp.getHighestRankedInstanceFromDataAttributes(ctx, order, dataAttrValuesOfOneDefinition) + if err != nil { + return nil, fmt.Errorf("error getting highest ranked instance from data attributes: %s", err.Error()) + } + if highestDataAttrVal == nil { slog.WarnContext(ctx, "No data attribute value found that matches attribute definition allowed values! All entity access will be rejected!") } else { - slog.DebugContext(ctx, "Highest ranked hierarchy value on data attributes found", "value", highestDataInstance) + slog.DebugContext(ctx, "Highest ranked hierarchy value on data attributes found", "value", highestDataAttrVal) } - // All the data AttributeInstances in the arg have the same canonical name. + // All the data Attribute Values in the arg have the same FQN. - // Go through every entity's attributeInstance set... - for entityId, entityAttrs := range entityAttributes { + // Go through every entity's Attribute Value set... + for entityId, entityAttrs := range entityAttributeValueFqns { // Default to DENY entityPassed := false valueFailures := []ValueFailure{} - // Cluster entity AttributeInstances by canonical name... - entityAttrCluster := ClusterByCanonicalNameAI(entityAttrs) - if highestDataInstance != nil { - dvCanonicalName := highestDataInstance.GetCanonicalName() - // For every unique data attributeInstance (that is, value) in this set of data AttributeInstances sharing the same canonical name... - slog.DebugContext(ctx, "Evaluating hierarchy decision", "name", dvCanonicalName, "value", highestDataInstance.Value) + // 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()) + } + + if highestDataAttrVal != nil { + attrDefFqn, err := GetDefinitionFqnFromValue(highestDataAttrVal) + 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... + slog.DebugContext(ctx, "Evaluating hierarchy decision", "attribute definition fqn", attrDefFqn, "value", highestDataAttrVal.Value) - // Compare the (one or more) AttributeInstances (that is, values) for this canonical name to the (one) data attributeInstance, and see which is "higher". - entityPassed = entityRankGreaterThanOrEqualToDataRank(order, highestDataInstance, entityAttrCluster[dvCanonicalName]) + // 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]) + 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 attributeInstance (that is, value) is higher than the highest entity attributeInstance, then FAIL. + // 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, highestDataInstance.Value) + denialMsg := fmt.Sprintf("Hierarchy - Entity: %s hierarchy values rank below data hierarchy value of %s", entityId, highestDataAttrVal.Value) slog.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: highestDataInstance, + DataAttribute: highestDataAttrVal, Message: denialMsg, }) } @@ -288,30 +322,33 @@ func (pdp *Pdp) hierarchyRule(ctx context.Context, dataAttrsBySingleCanonicalNam } } - return ruleResultsByEntity + return ruleResultsByEntity, nil } -// It is possible that a data policy may have more than one Hierarchy value for the same data attribute canonical +// It is possible that a data policy may have more than one Hierarchy value for the same data attribute definition // name, e.g.: -// - "https://authority.org/attr/MyHierarchyAttr/value/Value1" -// - "https://authority.org/attr/MyHierarchyAttr/value/Value2" +// - "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 AttributeInstances, and use that as the point of comparison, ignoring the "lower-ranked" data values. +// 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, dataAttributeCluster []AttributeInstance) *AttributeInstance { +func (pdp *Pdp) getHighestRankedInstanceFromDataAttributes(ctx context.Context, order []*policy.Value, dataAttributeGroup []*policy.Value) (*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 highestDVIndex := len(order) - 1 - var highestRankedInstance *AttributeInstance - for _, dataAttr := range dataAttributeCluster { - foundRank := getOrderOfValue(order, dataAttr.Value) + var highestRankedInstance *policy.Value + for _, dataAttr := range dataAttributeGroup { + foundRank, err := getOrderOfValue(order, dataAttr) + 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.Value, order) slog.WarnContext(ctx, msg) // If this isnt 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 + // If none of them are valid, we should return a nil instance continue } slog.DebugContext(ctx, "Found data", "rank", foundRank, "value", dataAttr.Value, "maxRank", highestDVIndex) @@ -322,96 +359,152 @@ func (pdp *Pdp) getHighestRankedInstanceFromDataAttributes(ctx context.Context, slog.DebugContext(ctx, "Updating rank!") highestDVIndex = foundRank gotAttr := dataAttr - highestRankedInstance = &gotAttr + highestRankedInstance = gotAttr } } - return highestRankedInstance + return highestRankedInstance, nil } -func findInstanceValueInClusterAI(a *AttributeInstance, instances []AttributeInstance) bool { - for _, ai := range instances { - if ai.Value == a.Value && ai.GetCanonicalName() == GetCanonicalName(*a) { +// Check for a match of a singular Attribute Value in a set of Attribute Value FQNs +func getIsValueFoundInFqnValuesSet(v *policy.Value, fqns []string) bool { + valFqn := v.GetFqn() + if valFqn == "" { + slog.Error(fmt.Sprintf("Unexpected empty FQN for value %+v", v)) + return false + } + for _, fqn := range fqns { + if valFqn == fqn { return true } } return false } -// Given set of ordered/ranked values, a data singular attributeInstance, and a set of entity AttributeInstances, -// determine if the entity AttributeInstances include a ranked value that equals or exceeds -// the rank of the data attributeInstance value. +// 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 -func entityRankGreaterThanOrEqualToDataRank(order []*policy.Value, dataAttribute *AttributeInstance, entityAttributeCluster []AttributeInstance) bool { +func entityRankGreaterThanOrEqualToDataRank(order []*policy.Value, dataAttribute *policy.Value, entityAttrValueFqnsGroup []string) (bool, error) { // default to least-perm result := false - dvIndex := getOrderOfValue(order, dataAttribute.Value) - // Compute the rank of the entity AttributeInstance value against the rank of the data attributeInstance value + dvIndex, err := getOrderOfValue(order, dataAttribute) + 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 attributeInstance canonical name, - // we may have *several* entity values for a given entity attributeInstance canonical name - + // 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 _, entityAttribute := range entityAttributeCluster { - // Ideally, the caller will have already ensured all the entity attributeInstance we've been provided - //have the same canonical name as the data attributeInstance we're comparing against, - // but if they haven't for some reason only consider matching entity attributeInstance - if dataAttribute.GetCanonicalName() == entityAttribute.GetCanonicalName() { - evIndex := getOrderOfValue(order, entityAttribute.Value) + 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 + // 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 attributeInstance value that is below the data attributeInstance value in rank, + // 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. + // then we must immediately assume failure for this entity + // and return. if evIndex > dvIndex || dvIndex == -1 { result = false - return result + return result, nil } else if evIndex <= dvIndex { result = true } } } - return result + return result, nil } -// Given a set of ordered/ranked values and a singular attributeInstance, -// return the rank #/index of the singular attributeInstance -func getOrderOfValue(order []*policy.Value, value string) int { - // For hierarchy, convention is 0 == most privileged, 1 == less privileged, etc - dvIndex := -1 // -1 == Not Found in the set - this should always be a failure. - for index := range order { - if order[index].Value == value { - dvIndex = index +// 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) (int, error) { + val := v.GetValue() + valFqn := v.GetFqn() + if val == "" { + slog.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) + } + if orderVal == val { + return idx, nil } } - // We either found the right index, or we return -1 - return dvIndex + // 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 +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 + } + } + + return -1, nil } // A Decision represents the overall access decision for a specific entity, -// - that is, the aggregate result of comparing entity AttributeInstances to every data attributeInstance. +// - 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 + // 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 attributeInstance. - //e.g. if we compare an entity's AttributeInstances against 5 data AttributeInstances, + // 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 attributeInstance or not. + //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 attributeInstance, however, then there may be + //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 AttributeInstances, and two entities each with a set of AttributeInstances, - //the definition for one of those data AttributeInstances has a GroupBy clause that excludes the second entity + //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. @@ -419,18 +512,18 @@ type Decision struct { } // DataRuleResult represents the rule-level (or AttributeDefinition-level) decision for a specific entity - -// the result of comparing entity AttributeInstances to a single data AttributeDefinition/rule (with potentially many values) +// the result of comparing entity Attribute Values to a single data AttributeDefinition/rule (with potentially many values) // -// There may be multiple "instances" (that is, AttributeInstances) of a single AttributeDefinition on both data and entities, +// 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) + // 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 AttributeInstances/values the entity failed against + //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 @@ -440,10 +533,10 @@ type DataRuleResult struct { ValueFailures []ValueFailure `json:"value_failures"` } -// ValueFailure indicates, for a given entity and data attributeInstance, which data values -// (aka specific data attributeInstance) the entity "failed" on. +// 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, AttributeInstances) of a single AttributeDefinition on both data and entities, +// 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, @@ -451,92 +544,112 @@ type DataRuleResult struct { // 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 *AttributeInstance `json:"data_attribute"` + 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}"` } -// Clusterable is an interface that either AttributeInstances or AttributeDefinitions can implement, -// to support easily "clustering" or grouping a slice of either by their shared CanonicalName or Authority. -type Clusterable interface { - // GetCanonicalName Returns the canonical URI representation of this clusterable thing, in the format - // :///attr/ - GetCanonicalName() string - // GetAuthority Returns the authority of this clusterable thing, in the format - // :// - GetAuthority() string - GroupBy() *AttributeInstance - Rule() policy.AttributeRuleTypeEnum - Order() []string -} - -// ClusterByAuthority takes a slice of Clusterables, and returns them as a map, -// where the map is keyed by each unique Authorities (e.g. 'https://myauthority.org') found in the slice of Clusterables -func ClusterByAuthority[attrCluster Clusterable](attrs []attrCluster) map[string][]attrCluster { - clusters := make(map[string][]attrCluster) - - for _, instance := range attrs { - clusters[instance.GetAuthority()] = append(clusters[instance.GetAuthority()], instance) +// GroupDefinitionsByFqn takes a slice of Attribute Definitions and returns them as a map: +// FQN -> Attribute Definition +func GetFqnToDefinitionMap(attributeDefinitions []*policy.Attribute) (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? + slog.Warn(fmt.Sprintf("duplicate Attribute Definition FQN %s found when building FQN map: %v and %v, which may indicate an issue", a, v, def)) + } + grouped[a] = def } - - return clusters + return grouped, nil } -// ClusterByCanonicalName takes a slice of Clusterable (attributeInstance OR AttributeDefinition), -// and returns them as a map, where the map is keyed by each unique CanonicalName -// (e.g. Authority+Name, 'https://myauthority.org/attr/') found in the slice of Clusterables -func ClusterByCanonicalName(attrs []Clusterable) map[string][]Clusterable { - clusters := make(map[string][]Clusterable) - - for _, instance := range attrs { - clusters[instance.GetCanonicalName()] = append(clusters[instance.GetCanonicalName()], instance) +// 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 clusters + return groupings, nil } -// ClusterByCanonicalNameAD takes a slice of Clusterable (attributeInstance OR AttributeDefinition), -// and returns them as a map, where the map is keyed by each unique CanonicalName -// (e.g. Authority+Name, 'https://myauthority.org/attr/') found in the slice of Clusterables -func ClusterByCanonicalNameAD(ads []*policy.Attribute) map[string][]*policy.Attribute { - clusters := make(map[string][]*policy.Attribute) - // FIXME - for _, instance := range ads { - a := GetCanonicalNameADV(instance) - clusters[a] = append(clusters[a], instance) +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 clusters + return groupings, nil } -func ClusterByCanonicalNameAI(attributes []AttributeInstance) map[string][]AttributeInstance { - clusters := make(map[string][]AttributeInstance) - for _, a := range attributes { - clusters[a.GetCanonicalName()] = append(clusters[a.GetCanonicalName()], a) +func GetDefinitionFqnFromValue(v *policy.Value) (string, error) { + if v.GetAttribute() != nil { + return GetDefinitionFqnFromDefinition(v.GetAttribute()) } - return clusters + return GetDefinitionFqnFromValueFqn(v.GetFqn()) } -// GetCanonicalName Returns the canonical URI representation of this AttributeDefinition: +// Splits off the Value from the FQN to get the parent Definition FQN: // -// :///attr/ -func GetCanonicalName(ai AttributeInstance) string { - return fmt.Sprintf("%s/attr/%s", - ai.Authority, - ai.Name, - ) -} - -func GetCanonicalNameADV(instance *policy.Attribute) string { - return fmt.Sprintf("%s/attr/%s", - instance.Namespace.Name, - instance.Name, - ) +// Input: https:///attr//value/ +// Output: https:///attr/ +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 } -// GetAuthority Returns the authority of this AttributeDefinition: -// -// :// -func GetAuthority(attrdef AttributeInstance) string { - return attrdef.Authority +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/internal/access/pdp_test.go b/internal/access/pdp_test.go index aaf8ef4078..7f79cc28e6 100644 --- a/internal/access/pdp_test.go +++ b/internal/access/pdp_test.go @@ -8,53 +8,104 @@ import ( "github.com/stretchr/testify/assert" ) -// AnyOf tests -func Test_AccessPDP_AnyOf_Pass(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", +func fqnBuilder(n string, a string, v string) string { + fqn := "https://" + if n != "" && a != "" && v != "" { + return fqn + n + "/attr/" + a + "/value/" + v + } else if n != "" && a != "" && v == "" { + return fqn + n + "/attr/" + a + } else if n != "" && a == "" { + return fqn + n + } else { + panic("Invalid FQN") + } +} + +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" + + simpleAnyOfAttribute = policy.Attribute{ + Name: mockAttributeNames[0], + 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]), }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, + { + Value: mockAttributeValues[1], + Fqn: fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[1]), }, }, } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, + + simpleAllOfAttribute = policy.Attribute{ + Name: mockAttributeNames[1], + Namespace: &policy.Namespace{ + Name: mockNamespaces[1], }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, + 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]), + }, }, } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { + + simpleHierarchyAttribute = policy.Attribute{ + Name: mockAttributeNames[2], + Namespace: &policy.Namespace{ + Name: mockNamespaces[2], + }, + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, + Values: []*policy.Value{ + { + Value: "Privileged", + Fqn: fqnBuilder(mockNamespaces[2], mockAttributeNames[2], "Privileged"), + }, { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Value2", + Value: "LessPrivileged", + Fqn: fqnBuilder(mockNamespaces[2], mockAttributeNames[2], "LessPrivileged"), }, { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", + Value: "NotPrivilegedAtAll", + Fqn: fqnBuilder(mockNamespaces[2], mockAttributeNames[2], "NotPrivilegedAtAll"), }, }, } +) + +// AnyOf tests +func Test_AccessPDP_AnyOf_Pass(t *testing.T) { + mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + val0 := mockAttrDefinitions[0].Values[0] + val1 := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + val0, + val1, + } + + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, val1.Value), + mockExtraneousValueFqn, + } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -64,60 +115,28 @@ func Test_AccessPDP_AnyOf_Pass(t *testing.T) { ) assert.Nil(t, err) - assert.True(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.True(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[1], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.True(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.True(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, val0, decisions[mockEntityId].Results[0].ValueFailures[0].DataAttribute) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } func Test_AccessPDP_AnyOf_FailMissingValue(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Value4", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, "randomValue"), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -126,60 +145,28 @@ func Test_AccessPDP_AnyOf_FailMissingValue(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 2, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[0], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 2, len(decisions[mockEntityId].Results[0].ValueFailures)) + 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) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} + + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://dank.org", - Name: "noop", - Value: "Value4", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder("dank.org", "noop", "randomVal"), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -188,60 +175,27 @@ func Test_AccessPDP_AnyOf_FailMissingAttr(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 2, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[0], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 2, len(decisions[mockEntityId].Results[0].ValueFailures)) + 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_FailAttrWrongNamespace(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://dank.org", - Name: "MyAttr", - Value: "Value1", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} + + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } + + mockEntityAttrs := map[string][]string{} + name := mockAttrDefinitions[0].Name + val1 := mockAttrDefinitions[0].Values[0].Value + mockEntityAttrs[mockEntityId] = []string{fqnBuilder("otherrandomnamespace.com", name, val1), mockExtraneousValueFqn} + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -250,49 +204,25 @@ func Test_AccessPDP_AnyOf_FailAttrWrongNamespace(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 2, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[0], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 2, len(decisions[mockEntityId].Results[0].ValueFailures)) + 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) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: {}, + mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} + + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } + + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{} + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -301,50 +231,26 @@ func Test_AccessPDP_AnyOf_NoEntityAttributes_Fails(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 2, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[0], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 2, len(decisions[mockEntityId].Results[0].ValueFailures)) + 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_NoDataAttributes_NoDecisions(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" // 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{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{} - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://dank.org", - Name: "MyAttr", - Value: "Value1", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} + + mockDataAttrs := []*policy.Value{} + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(mockNamespaces[0], mockAttributeNames[0], mockAttributeValues[0]), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -353,7 +259,7 @@ func Test_AccessPDP_AnyOf_NoDataAttributes_NoDecisions(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.Nil(t, decisions[entityID]) + 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) assert.Equal(t, 0, len(decisions)) @@ -363,63 +269,34 @@ func Test_AccessPDP_AnyOf_AllEntitiesFilteredOutOfDataAttributeComparison_NoDeci entityID1 := "4f6636ca-c60c-40d1-9f3f-015086303f74" entityID2 := "bubble@squeak.biz" mockAttrDefinitions := []*policy.Attribute{ + &simpleAnyOfAttribute, { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - { - Name: "YourAttr", + Name: mockAttributeNames[1], Namespace: &policy.Namespace{ - Name: "https://example.org", + Name: mockNamespaces[0], }, Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, Values: []*policy.Value{ { - Value: "Value3", + Value: mockAttributeValues[2], + Fqn: fqnBuilder(mockNamespaces[0], mockAttributeNames[1], mockAttributeValues[2]), }, { - Value: "Value4", + Value: mockAttributeValues[3], + Fqn: fqnBuilder(mockNamespaces[0], mockAttributeNames[1], mockAttributeValues[3]), }, }, }, } - mockDataAttrs := []AttributeInstance{} - mockEntityAttrs := map[string][]AttributeInstance{ - entityID1: { - { - Authority: "https://dank.org", - Name: "MyAttr", - Value: "Value1", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, - entityID2: { - { - Authority: "https://dank.org", - Name: "MyAttr", - Value: "Value1", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockDataAttrs := []*policy.Value{} + mockEntityAttrs := map[string][]string{} + fqn1 := fqnBuilder("dank.org", mockAttrDefinitions[0].Name, mockAttrDefinitions[0].Values[0].Value) + fqn2 := mockExtraneousValueFqn + mockEntityAttrs[entityID1] = []string{ + fqn1, fqn2, + } + mockEntityAttrs[entityID2] = []string{ + fqn1, fqn2, } accessPDP := NewPdp() @@ -442,56 +319,21 @@ func Test_AccessPDP_AnyOf_AllEntitiesFilteredOutOfDataAttributeComparison_NoDeci // AllOf tests func Test_AccessPDP_AllOf_Pass(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleAllOfAttribute} + + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Value1", - }, - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Value2", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockEntityAttrs := map[string][]string{} + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, mockAttrDefinitions[0].Values[0].Value), + fqnBuilder(ns, name, mockAttrDefinitions[0].Values[1].Value), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -500,64 +342,26 @@ func Test_AccessPDP_AllOf_Pass(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.True(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.True(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 0, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.True(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.True(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 0, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } func Test_AccessPDP_AllOf_FailMissingValue(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleAllOfAttribute} + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Value1", - }, - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Value4", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(mockAttrDefinitions[0].Namespace.Name, mockAttrDefinitions[0].Name, mockAttrDefinitions[0].Values[0].Value), + mockExtraneousValueFqn, + fqnBuilder(mockAttrDefinitions[0].Namespace.Name, mockAttrDefinitions[0].Name, "otherValue"), } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -566,59 +370,26 @@ func Test_AccessPDP_AllOf_FailMissingValue(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[0], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + 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) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, + &simpleAllOfAttribute, } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://dank.org", - Name: "noop", - Value: "Value4", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder("dank.org", "noop", "randomVal"), + fqnBuilder("somewhere.com", "hello", "world"), } accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( @@ -628,65 +399,30 @@ func Test_AccessPDP_AllOf_FailMissingAttr(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 2, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[0], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 2, len(decisions[mockEntityId].Results[0].ValueFailures)) + 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_FailAttrWrongNamespace(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, - Values: []*policy.Value{ - { - Value: "Value1", - }, - { - Value: "Value2", - }, - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleAnyOfAttribute} + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[0].Values[0], } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://dank.org", - Name: "MyAttr", - Value: "Value1", - }, - { - Authority: "https://dank.org", - Name: "MyAttr", - Value: "Value2", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + + wrongNs := "wrong" + mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(wrongNs, name, mockAttrDefinitions[0].Values[0].Value), + fqnBuilder(wrongNs, name, mockAttrDefinitions[0].Values[1].Value), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -695,64 +431,32 @@ func Test_AccessPDP_AllOf_FailAttrWrongNamespace(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 2, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, &mockDataAttrs[0], decisions[entityID].Results[0].ValueFailures[0].DataAttribute) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 2, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockDataAttrs[0], decisions[mockEntityId].Results[0].ValueFailures[0].DataAttribute) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } // Hierarchy tests func Test_AccessPDP_Hierarchy_Pass(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + topValue := mockAttrDefinitions[0].Values[0] + midValue := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + midValue, + topValue, } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Privileged", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + + mockEntityAttrs := map[string][]string{} + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, topValue.Value), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -761,67 +465,32 @@ func Test_AccessPDP_Hierarchy_Pass(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.True(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.True(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 0, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.True(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.True(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 0, len(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) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + topValue := mockAttrDefinitions[0].Values[0] + midValue := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + midValue, + topValue, } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Privileged", - }, - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "LessPrivileged", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockEntityAttrs := map[string][]string{} + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, topValue.Value), + fqnBuilder(ns, name, midValue.Value), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -830,52 +499,26 @@ func Test_AccessPDP_Hierarchy_FailEntityValueTooLow(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } func Test_AccessPDP_Hierarchy_FailEntityValueAndDataValuesBothLowest(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, - }, - }, - } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[2].Value, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + lowValue := mockAttrDefinitions[0].Values[2] + mockDataAttrs := []*policy.Value{ + lowValue, } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "NotPrivilegedAtAll", - }, - }, + mockEntityAttrs := map[string][]string{} + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, lowValue.Value), } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -884,67 +527,31 @@ func Test_AccessPDP_Hierarchy_FailEntityValueAndDataValuesBothLowest(t *testing. mockAttrDefinitions) assert.Nil(t, err) - assert.True(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.True(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 0, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.True(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.True(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 0, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } func Test_AccessPDP_Hierarchy_FailEntityValueOrder(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + topValue := mockAttrDefinitions[0].Values[0] + midValue := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + midValue, + topValue, } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "LessPrivileged", - }, - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Privileged", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, midValue.Value), + fqnBuilder(ns, name, topValue.Value), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -953,67 +560,31 @@ func Test_AccessPDP_Hierarchy_FailEntityValueOrder(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } func Test_AccessPDP_Hierarchy_FailMultipleHierarchyDataValues(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + topValue := mockAttrDefinitions[0].Values[0] + midValue := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + topValue, + midValue, } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "LessPrivileged", - }, - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Privileged", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, midValue.Value), + fqnBuilder(ns, name, topValue.Value), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -1022,62 +593,29 @@ func Test_AccessPDP_Hierarchy_FailMultipleHierarchyDataValues(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } func Test_AccessPDP_Hierarchy_FailEntityValueNotInOrder(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ - { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, - }, - }, + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + topValue := mockAttrDefinitions[0].Values[0] + midValue := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + midValue, + topValue, } - mockDataAttrs := []AttributeInstance{ - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, - }, - { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, - }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "UberPrivileged", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + mockEntityAttrs := map[string][]string{} + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, "unknownPrivilegeValue"), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -1086,57 +624,65 @@ func Test_AccessPDP_Hierarchy_FailEntityValueNotInOrder(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } func Test_AccessPDP_Hierarchy_FailDataValueNotInOrder(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockDataAttrs := []*policy.Value{ { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, - }, + Value: "UberPrivileged", + Fqn: fqnBuilder(ns, name, "UberPrivileged"), }, } - mockDataAttrs := []AttributeInstance{ + + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, mockAttrDefinitions[0].Values[0].Value), + mockExtraneousValueFqn, + } + + accessPDP := NewPdp() + decisions, err := accessPDP.DetermineAccess( + ctx.Background(), + mockDataAttrs, + mockEntityAttrs, + mockAttrDefinitions) + + assert.Nil(t, err) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) + assert.Nil(t, decisions[mockEntityId].Results[0].ValueFailures[0].DataAttribute) +} + +func Test_AccessPDP_Hierarchy_PassWithMixedKnownAndUnknownDataOrder(t *testing.T) { + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + topValue := mockAttrDefinitions[0].Values[0] + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockDataAttrs := []*policy.Value{ { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: "UberPrivileged", + Value: "UberPrivileged", + Fqn: fqnBuilder(ns, name, "UberPrivileged"), }, + topValue, } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Privileged", - }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", - }, - }, + + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, topValue.Value), + mockExtraneousValueFqn, } + accessPDP := NewPdp() decisions, err := accessPDP.DetermineAccess( ctx.Background(), @@ -1145,203 +691,601 @@ func Test_AccessPDP_Hierarchy_FailDataValueNotInOrder(t *testing.T) { mockAttrDefinitions) assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) - assert.Nil(t, decisions[entityID].Results[0].ValueFailures[0].DataAttribute) + assert.True(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.True(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 0, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) } -func Test_AccessPDP_Hierarchy_PassWithMixedKnownAndUnknownDataOrder(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} +func Test_AccessPDP_Hierarchy_FailWithWrongNamespace(t *testing.T) { + mockAttrDefinitions := []*policy.Attribute{&simpleHierarchyAttribute} + midValue := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + midValue, + } + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder("wrong"+mockAttrDefinitions[0].Namespace.Name, mockAttrDefinitions[0].Name, midValue.Value), + mockExtraneousValueFqn, + } + + accessPDP := NewPdp() + decisions, err := accessPDP.DetermineAccess( + ctx.Background(), + mockDataAttrs, + mockEntityAttrs, + mockAttrDefinitions) + + assert.Nil(t, err) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + 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].Values[0] + midValue := mockAttrDefinitions[0].Values[1] + mockDataAttrs := []*policy.Value{ + midValue, + topValue, + } + ns := mockAttrDefinitions[0].Namespace.Name + name := mockAttrDefinitions[0].Name + mockEntityAttrs := map[string][]string{} + mockEntityAttrs[mockEntityId] = []string{ + fqnBuilder(ns, name, topValue.Value), + fqnBuilder(ns, name, "unknownPrivilegeValue"), + mockExtraneousValueFqn, + } + + accessPDP := NewPdp() + decisions, err := accessPDP.DetermineAccess( + ctx.Background(), + mockDataAttrs, + mockEntityAttrs, + mockAttrDefinitions) + + assert.Nil(t, err) + assert.False(t, decisions[mockEntityId].Access) + assert.Equal(t, 1, len(decisions[mockEntityId].Results)) + assert.False(t, decisions[mockEntityId].Results[0].Passed) + assert.Equal(t, 1, len(decisions[mockEntityId].Results[0].ValueFailures)) + assert.Equal(t, mockAttrDefinitions[0], decisions[mockEntityId].Results[0].RuleDefinition) +} + +// Helper tests + +// GetFqnToDefinitionMap tests +func Test_GetFqnToDefinitionMap(t *testing.T) { + mockAttrDefinitions := []*policy.Attribute{ + &simpleAnyOfAttribute, + &simpleAllOfAttribute, + &simpleHierarchyAttribute, + } + + fqnToDefinitionMap, err := GetFqnToDefinitionMap(mockAttrDefinitions) + assert.Nil(t, err) + + for _, attrDef := range mockAttrDefinitions { + fqn := fqnBuilder(attrDef.Namespace.Name, attrDef.Name, "") + assert.Equal(t, attrDef.GetName(), fqnToDefinitionMap[fqn].GetName()) + } +} + +func Test_GetFqnToDefinitionMap_SucceedsWithDuplicateDefinitions(t *testing.T) { mockAttrDefinitions := []*policy.Attribute{ + &simpleAnyOfAttribute, + &simpleAnyOfAttribute, + } + + fqnToDefinitionMap, err := GetFqnToDefinitionMap(mockAttrDefinitions) + assert.Nil(t, err) + expectedFqn := fqnBuilder(mockAttrDefinitions[0].Namespace.Name, mockAttrDefinitions[0].Name, "") + v, ok := fqnToDefinitionMap[expectedFqn] + assert.True(t, ok) + assert.Equal(t, mockAttrDefinitions[0].GetName(), v.GetName()) +} + +// GroupValuesByDefinition tests +func Test_GroupValuesByDefinition_NoProvidedDefinitionFqn_Succeeds(t *testing.T) { + mockAttrDefinitions := []*policy.Attribute{ + &simpleAnyOfAttribute, + &simpleAllOfAttribute, + &simpleHierarchyAttribute, + } + + // two values from each attribute definition, out of order + mockDataAttrs := []*policy.Value{ + mockAttrDefinitions[0].Values[0], + mockAttrDefinitions[1].Values[0], + mockAttrDefinitions[2].Values[0], + mockAttrDefinitions[0].Values[1], + mockAttrDefinitions[1].Values[1], + mockAttrDefinitions[2].Values[1], + } + + groupedValues, err := GroupValuesByDefinition(mockDataAttrs) + assert.Nil(t, err) + + for _, attrDef := range mockAttrDefinitions { + fqn := fqnBuilder(attrDef.Namespace.Name, attrDef.Name, "") + assert.Equal(t, 2, len(groupedValues[fqn])) + assert.Equal(t, attrDef.Values[0], groupedValues[fqn][0]) + assert.Equal(t, attrDef.Values[1], groupedValues[fqn][1]) + } +} + +func Test_GroupValuesByDefinition_WithProvidedDefinitionFqn_Succeeds(t *testing.T) { + attrFqn := fqnBuilder(mockNamespaces[0], mockAttributeNames[0], "") + + mockDataAttrs := []*policy.Value{ { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", + Value: mockAttributeValues[0], + Attribute: &policy.Attribute{ + Fqn: attrFqn, }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, + }, + { + Value: mockAttributeValues[1], + Attribute: &policy.Attribute{ + Fqn: attrFqn, }, }, } - mockDataAttrs := []AttributeInstance{ + + groupedValues, err := GroupValuesByDefinition(mockDataAttrs) + assert.Nil(t, err) + + assert.Equal(t, 1, len(groupedValues)) + for k, v := range groupedValues { + assert.Equal(t, attrFqn, k) + assert.Equal(t, 2, len(v)) + assert.Equal(t, mockDataAttrs[0], v[0]) + assert.Equal(t, mockDataAttrs[1], v[1]) + } +} + +// 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"), + } + + groupedFqns, err := GroupValueFqnsByDefinition(mockFqns) + assert.Nil(t, err) + + assert.Equal(t, 3, len(groupedFqns)) + found := map[string]bool{} + for _, v := range mockFqns { + found[v] = false + } + + for _, v := range groupedFqns { + for _, fq := range v { + assert.Contains(t, mockFqns, fq) + assert.False(t, found[fq]) + found[fq] = true + } + } + + for _, v := range found { + assert.True(t, v) + } +} + +// GetDefinitionFqnFromValue tests +func Test_GetDefinitionFqnFromValue_Succeeds(t *testing.T) { + ns := mockNamespaces[1] + name := mockAttributeNames[2] + val := mockAttributeValues[2] + attrDefFqn := fqnBuilder(ns, name, "") + + // With Attribute Def & its FQN, Attribute Def & Namespace, or Value FQN + mockValues := []*policy.Value{ + { + Value: val, + Attribute: &policy.Attribute{ + Fqn: attrDefFqn, + }, + }, { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: "UberPrivileged", + Attribute: &policy.Attribute{ + Namespace: &policy.Namespace{ + Name: ns, + }, + Name: name, + }, }, { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, + Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), }, } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Privileged", + + for _, val := range mockValues { + got, err := GetDefinitionFqnFromValue(val) + assert.Nil(t, err) + assert.Equal(t, attrDefFqn, got) + } +} + +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], }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", + }, + // contains attr def's namespace but no name + { + Attribute: &policy.Attribute{ + Namespace: &policy.Namespace{ + Name: mockNamespaces[0], + }, }, }, } - accessPDP := NewPdp() - decisions, err := accessPDP.DetermineAccess( - ctx.Background(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - assert.Nil(t, err) - assert.True(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.True(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 0, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + for _, val := range mockValues { + def, err := GetDefinitionFqnFromValue(val) + assert.NotNil(t, err) + assert.Zero(t, def) + } } -func Test_AccessPDP_Hierarchy_FailWithWrongNamespace(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} +// 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), + } + + for _, fqn := range mockValueFqns { + got, err := GetDefinitionFqnFromValueFqn(fqn) + assert.Nil(t, err) + assert.Equal(t, attrDefFqn, got) + } +} + +func Test_GetDefinitionFqnFromValueFqn_FailsWithMissingPieces(t *testing.T) { + mockValueFqns := []string{ + "", + "/value/hello", + "https://namespace.org/attr/attrName/val/hello", + "namespace.org/attr/attrName/value", + } + + for _, fqn := range mockValueFqns { + got, err := GetDefinitionFqnFromValueFqn(fqn) + assert.NotNil(t, err) + assert.Zero(t, got) + } +} + +// GetDefinitionFqnFromDefinition tests +func Test_GetDefinitionFqnFromDefinition_FromPartsSucceeds(t *testing.T) { + mockAttrDefinitions := []*policy.Attribute{ + &simpleAnyOfAttribute, + &simpleAllOfAttribute, + &simpleHierarchyAttribute, + } + + for _, attrDef := range mockAttrDefinitions { + fqn := fqnBuilder(attrDef.Namespace.Name, attrDef.Name, "") + got, err := GetDefinitionFqnFromDefinition(attrDef) + assert.Nil(t, err) + assert.Equal(t, fqn, got) + } +} + +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], + }, + } + + for i, attrDef := range mockAttrDefinitions { + got, err := GetDefinitionFqnFromDefinition(attrDef) + assert.Nil(t, err) + assert.Equal(t, attrDef.Fqn, got) + assert.Equal(t, mockFqns[i], got) + } +} + +func Test_GetDefinitionFqnFromDefinition_FailsWithNoNamespace(t *testing.T) { mockAttrDefinitions := []*policy.Attribute{ { Name: "MyAttr", + }, + } + + for _, attrDef := range mockAttrDefinitions { + _, err := GetDefinitionFqnFromDefinition(attrDef) + assert.NotNil(t, err) + } +} + +func Test_GetDefinitionFqnFromDefinition_FailsWithNoName(t *testing.T) { + mockAttrDefinitions := []*policy.Attribute{ + { Namespace: &policy.Namespace{ - Name: "https://example.org", - }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", - }, - { - Value: "NotPrivilegedAtAll", - }, + Name: "example.org", }, }, } - mockDataAttrs := []AttributeInstance{ + + for _, attrDef := range mockAttrDefinitions { + _, err := GetDefinitionFqnFromDefinition(attrDef) + assert.NotNil(t, err) + } +} + +// 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]), + } + + values := []struct { + val *policy.Value + expected bool + }{ { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, + val: &policy.Value{ + Fqn: fqnsList[0], + }, + expected: true, }, - } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.net", - Name: "MyAttr", - Value: "Privileged", + { + val: &policy.Value{ + Fqn: fqnsList[1], }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", + 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: "", }, }, } - accessPDP := NewPdp() - decisions, err := accessPDP.DetermineAccess( - ctx.Background(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) - assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + for i, v := range values { + assert.Equal(t, v.expected, getIsValueFoundInFqnValuesSet(v.val, fqnsList)) + if i == 3 { + assert.False(t, getIsValueFoundInFqnValuesSet(v.val, fqnsList[:3])) + } + } } -func Test_AccessPDP_Hierarchy_FailWithMixedKnownAndUnknownEntityOrder(t *testing.T) { - entityID := "4f6636ca-c60c-40d1-9f3f-015086303f74" - attrAuthorities := []string{"https://example.org"} - mockAttrDefinitions := []*policy.Attribute{ +// getOrderOfValue tests +func Test_GetOrderOfValue(t *testing.T) { + ns := mockNamespaces[1] + name := mockAttributeNames[2] + + values := []*policy.Value{ { - Name: "MyAttr", - Namespace: &policy.Namespace{ - Name: "https://example.org", + Value: mockAttributeValues[1], + Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), + }, + { + Value: mockAttributeValues[2], + Attribute: &policy.Attribute{ + Fqn: fqnBuilder(ns, name, ""), }, - Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY, - Values: []*policy.Value{ - { - Value: "Privileged", - }, - { - Value: "LessPrivileged", + }, + { + Value: mockAttributeValues[4], + Attribute: &policy.Attribute{ + Name: name, + Namespace: &policy.Namespace{ + Name: ns, }, - { - Value: "NotPrivilegedAtAll", + }, + }, + { + Value: mockAttributeValues[0], + }, + } + + for i := range values { + got, err := getOrderOfValue(values, values[i]) + assert.Nil(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", + }) + assert.NoError(t, err) + assert.Equal(t, -1, idx) +} + +func Test_GetOrderOfValue_FailsCorrectly(t *testing.T) { + ns := mockNamespaces[1] + name := mockAttributeNames[2] + + bad := []*policy.Value{ + { + Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), + }, + {}, + { + Attribute: &policy.Attribute{ + Name: name, + Namespace: &policy.Namespace{ + Name: ns, }, }, }, } - mockDataAttrs := []AttributeInstance{ + + good := &policy.Value{ + Value: mockAttributeValues[1], + } + + for _, v := range bad { + order := []*policy.Value{v, good} + got, err := getOrderOfValue(order, good) + assert.NotNil(t, err) + assert.Equal(t, -1, got) + } + + // test with a value that doesn't exist in the list + idx, err := getOrderOfValue(append(bad, good), &policy.Value{ + Value: "unknownValue", + }) + assert.NotNil(t, err) + assert.Equal(t, -1, idx) +} + +// getOrderOfValueByFqn tests +func Test_GetOrderOfValueByFqn(t *testing.T) { + ns := mockNamespaces[0] + name := mockAttributeNames[0] + values := []*policy.Value{ + { + Fqn: fqnBuilder(ns, name, mockAttributeValues[0]), + }, + { + Fqn: fqnBuilder(ns, name, mockAttributeValues[1]), + }, { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[1].Value, + Value: mockAttributeValues[2], + Attribute: &policy.Attribute{ + Fqn: fqnBuilder(ns, name, ""), + }, }, { - Authority: attrAuthorities[0], - Name: mockAttrDefinitions[0].Name, - Value: mockAttrDefinitions[0].Values[0].Value, + Value: mockAttributeValues[3], + Attribute: &policy.Attribute{ + Name: name, + Namespace: &policy.Namespace{ + Name: ns, + }, + }, }, } - mockEntityAttrs := map[string][]AttributeInstance{ - entityID: { - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "Privileged", + + for i := range values { + fqn := fqnBuilder(ns, name, mockAttributeValues[i]) + got, err := getOrderOfValueByFqn(values, fqn) + assert.Nil(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: "", }, - { - Authority: "https://example.org", - Name: "MyAttr", - Value: "UberPrivileged", + }, + // full definition parts, no value + { + Attribute: &policy.Attribute{ + Name: name, + Namespace: &policy.Namespace{ + Name: ns, + }, }, - { - Authority: "https://meep.org", - Name: "meep", - Value: "beepbeep", + }, + // 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, ""), }, }, } - accessPDP := NewPdp() - decisions, err := accessPDP.DetermineAccess( - ctx.Background(), - mockDataAttrs, - mockEntityAttrs, - mockAttrDefinitions) + fqn := fqnBuilder(ns, name, mockAttributeValues[1]) + good := &policy.Value{ + Fqn: fqn, + } - assert.Nil(t, err) - assert.False(t, decisions[entityID].Access) - assert.Equal(t, 1, len(decisions[entityID].Results)) - assert.False(t, decisions[entityID].Results[0].Passed) - assert.Equal(t, 1, len(decisions[entityID].Results[0].ValueFailures)) - assert.Equal(t, mockAttrDefinitions[0], decisions[entityID].Results[0].RuleDefinition) + for _, v := range bad { + order := []*policy.Value{v, good} + got, err := getOrderOfValueByFqn(order, fqn) + assert.NotNil(t, err) + assert.Equal(t, -1, got) + } } + +// TODO: entityRankGreaterThanOrEqualToDataRank diff --git a/protocol/go/authorization/authorization.pb.go b/protocol/go/authorization/authorization.pb.go index b31f840e26..d4ab63a689 100644 --- a/protocol/go/authorization/authorization.pb.go +++ b/protocol/go/authorization/authorization.pb.go @@ -725,8 +725,8 @@ type EntityEntitlements struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` - AttributeId []string `protobuf:"bytes,2,rep,name=attribute_id,json=attributeId,proto3" json:"attribute_id,omitempty"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + AttributeValueFqns []string `protobuf:"bytes,2,rep,name=attribute_value_fqns,json=attributeValueFqns,proto3" json:"attribute_value_fqns,omitempty"` } func (x *EntityEntitlements) Reset() { @@ -768,9 +768,9 @@ func (x *EntityEntitlements) GetEntityId() string { return "" } -func (x *EntityEntitlements) GetAttributeId() []string { +func (x *EntityEntitlements) GetAttributeValueFqns() []string { if x != nil { - return x.AttributeId + return x.AttributeValueFqns } return nil } @@ -829,19 +829,11 @@ func (x *ResourceAttribute) GetAttributeFqns() []string { // "entitlements": [ // { // "entityId": "e1", -// "attributeValueReferences": [ -// { -// "attributeFqn": "http://www.example.org/attr/foo/value/bar" -// } -// ] +// "attributeValueFqns": ["https://www.example.org/attr/foo/value/bar"] // }, // { // "entityId": "e2", -// "attributeValueReferences": [ -// { -// "attributeFqn": "http://www.example.org/attr/color/value/red" -// } -// ] +// "attributeValueFqns": ["https://www.example.org/attr/color/value/red"] // } // ] // } @@ -988,50 +980,51 @@ var file_authorization_authorization_proto_rawDesc = []byte{ 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x63, - 0x6f, 0x70, 0x65, 0x22, 0x54, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, + 0x6f, 0x70, 0x65, 0x22, 0x63, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x22, 0x3a, 0x0a, 0x11, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x25, - 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x46, 0x71, 0x6e, 0x73, 0x22, 0x60, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x45, 0x0a, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x32, 0x86, 0x02, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x72, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x13, 0x22, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x7a, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, - 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x22, 0x10, - 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x42, 0xb2, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x12, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, - 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x19, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x46, 0x71, 0x6e, 0x73, 0x22, 0x3a, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x25, 0x0a, + 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x66, 0x71, 0x6e, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x46, 0x71, 0x6e, 0x73, 0x22, 0x60, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x45, 0x0a, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x32, 0x86, 0x02, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x72, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, + 0x22, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x7a, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x22, 0x10, 0x2f, + 0x76, 0x31, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, + 0xb2, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x12, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, + 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x19, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/services/authorization/authorization.go b/services/authorization/authorization.go index b7a696b78a..e4826c2a74 100644 --- a/services/authorization/authorization.go +++ b/services/authorization/authorization.go @@ -14,7 +14,7 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/open-policy-agent/opa/metrics" "github.com/open-policy-agent/opa/profiler" - "github.com/open-policy-agent/opa/sdk" + opaSdk "github.com/open-policy-agent/opa/sdk" "github.com/opentdf/platform/internal/access" "github.com/opentdf/platform/internal/entitlements" "github.com/opentdf/platform/internal/opa" @@ -44,16 +44,22 @@ func NewRegistration() serviceregistry.Registration { } } -var RetrieveAttributeDefinitions = func(ctx context.Context, ra *authorization.ResourceAttribute, as AuthorizationService) (*attr.GetAttributeValuesByFqnsResponse, error) { - return as.sdk.Attributes.GetAttributeValuesByFqns(ctx, &attr.GetAttributeValuesByFqnsRequest{ +// abstracted into variable for mocking in tests +var retrieveAttributeDefinitions = func(ctx context.Context, ra *authorization.ResourceAttribute, sdk *otdf.SDK) (map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue, error) { + resp, err := sdk.Attributes.GetAttributeValuesByFqns(ctx, &attr.GetAttributeValuesByFqnsRequest{ WithValue: &policy.AttributeValueSelector{ WithSubjectMaps: true, }, Fqns: ra.AttributeFqns, }) + if err != nil { + return nil, err + } + return resp.GetFqnAttributeValues(), nil } -var RetrieveEntitlements = func(ctx context.Context, req *authorization.GetEntitlementsRequest, as AuthorizationService) (*authorization.GetEntitlementsResponse, error) { +// abstracted into variable for mocking in tests +var retrieveEntitlements = func(ctx context.Context, req *authorization.GetEntitlementsRequest, as AuthorizationService) (*authorization.GetEntitlementsResponse, error) { return as.GetEntitlements(ctx, req) } @@ -66,72 +72,53 @@ func (as AuthorizationService) GetDecisions(ctx context.Context, req *authorizat } for _, dr := range req.DecisionRequests { for _, ra := range dr.ResourceAttributes { - slog.Debug("getting resource attributes", slog.String("FQNs", strings.Join(ra.AttributeFqns, ", "))) + slog.Debug("getting resource attributes", slog.String("FQNs", strings.Join(ra.GetAttributeFqns(), ", "))) - // get attribute definitions - getAttrsRes, err := RetrieveAttributeDefinitions(ctx, ra, as) + // get attribute definition/value combinations + dataAttrDefsAndVals, err := retrieveAttributeDefinitions(ctx, ra, as.sdk) if err != nil { // TODO: should all decisions in a request fail if one FQN lookup fails? - return nil, services.HandleError(err, services.ErrGetRetrievalFailed, slog.String("fqns", strings.Join(ra.AttributeFqns, ", "))) + return nil, services.HandleError(err, services.ErrGetRetrievalFailed, slog.String("fqns", strings.Join(ra.GetAttributeFqns(), ", "))) } - // get list of attributes from response var attrDefs []*policy.Attribute - for _, v := range getAttrsRes.GetFqnAttributeValues() { + var attrVals []*policy.Value + for _, v := range dataAttrDefsAndVals { attrDefs = append(attrDefs, v.GetAttribute()) - } - - // format resource fqns as attribute instances for accesspdp - var dataAttrs []access.AttributeInstance - for _, x := range ra.AttributeFqns { - inst, err := access.ParseInstanceFromURI(x) - if err != nil { - // TODO: should all decisions in a request fail if one FQDN to attributeinstance conversion fails? - return nil, services.HandleError(err, services.ErrGetRetrievalFailed, slog.String("attribute instance conversion failed for resource fqn ", x)) - } - dataAttrs = append(dataAttrs, inst) + attrVals = append(attrVals, v.GetValue()) } for _, ec := range dr.EntityChains { - // fmt.Printf("\nTODO: make access decision here with these fully qualified attributes: %+v\n", attrs) - // get the entities entitlements + // + // TODO: we should already have the subject mappings here and be able to just use OPA to trim down the known data attr values to the ones matched up with the entities + // entities := ec.GetEntities() req := authorization.GetEntitlementsRequest{ Entities: entities, Scope: ra, } - ecEntitlements, err := RetrieveEntitlements(ctx, &req, as) + ecEntitlements, err := retrieveEntitlements(ctx, &req, as) if err != nil { // TODO: should all decisions in a request fail if one entity entitlement lookup fails? return nil, services.HandleError(err, services.ErrGetRetrievalFailed, slog.String("getEntitlements request failed ", req.String())) } - // format subject fqns as attribute instances for accesspdp - entityAttrs := make(map[string][]access.AttributeInstance) - for _, e := range ecEntitlements.Entitlements { - // currently just adding each entity retuned to same list - var thisEntityAttrs []access.AttributeInstance - for _, x := range e.GetAttributeId() { - inst, err := access.ParseInstanceFromURI(x) - if err != nil { - // TODO: should all decisions in a request fail if one FQDN to attributeinstance conversion fails? - return nil, services.HandleError(err, services.ErrGetRetrievalFailed, slog.String("attribute instance conversion failed for subject fqn ", x)) - } - thisEntityAttrs = append(thisEntityAttrs, inst) - } - entityAttrs[e.EntityId] = thisEntityAttrs + // currently just adding each entity retuned to same list + entityAttrValues := make(map[string][]string) + for _, e := range ecEntitlements.GetEntitlements() { + entityAttrValues[e.EntityId] = e.GetAttributeValueFqns() } // call access-pdp accessPDP := access.NewPdp() decisions, err := accessPDP.DetermineAccess( ctx, - dataAttrs, - entityAttrs, + attrVals, + entityAttrValues, attrDefs, ) if err != nil { // TODO: should all decisions in a request fail if one entity entitlement lookup fails? - return nil, services.HandleError(err, services.ErrGetRetrievalFailed, slog.String("determinsAccess request to accesspdp failed", "")) + return nil, services.HandleError(err, services.ErrGetRetrievalFailed, slog.String("DetermineAccess request to Access PDP failed", "")) } // check the decisions decision := authorization.DecisionResponse_DECISION_PERMIT @@ -149,7 +136,7 @@ func (as AuthorizationService) GetDecisions(ctx context.Context, req *authorizat Standard: policy.Action_STANDARD_ACTION_TRANSMIT, }, }, - ResourceAttributesId: "resourceAttributesId_stub" + ra.String(), + ResourceAttributesId: ra.GetAttributeFqns()[0], } rsp.DecisionResponses = append(rsp.DecisionResponses, decisionResp) } @@ -192,7 +179,7 @@ func (as AuthorizationService) GetEntitlements(ctx context.Context, req *authori if slog.Default().Enabled(ctx, slog.LevelDebug) { _ = json.NewEncoder(os.Stdout).Encode(in) } - options := sdk.DecisionOptions{ + options := opaSdk.DecisionOptions{ Now: time.Now(), Path: "opentdf/entitlements/attributes", // change to /resolve_entities to get output of idp_plugin Input: in, @@ -231,8 +218,8 @@ func (as AuthorizationService) GetEntitlements(ctx context.Context, req *authori } // FIXME use index rsp.Entitlements[0] = &authorization.EntityEntitlements{ - EntityId: req.Entities[0].Id, - AttributeId: saa, + EntityId: req.Entities[0].Id, + AttributeValueFqns: saa, } slog.DebugContext(ctx, "opa", "rsp", fmt.Sprintf("%+v", rsp)) return rsp, nil diff --git a/services/authorization/authorization.proto b/services/authorization/authorization.proto index 36201e9745..d7a7b6995f 100644 --- a/services/authorization/authorization.proto +++ b/services/authorization/authorization.proto @@ -169,7 +169,7 @@ message GetEntitlementsRequest { message EntityEntitlements { string entity_id = 1; - repeated string attribute_id = 2; + repeated string attribute_value_fqns = 2; } //A logical bucket of attributes belonging to a "Resource" @@ -185,19 +185,11 @@ message ResourceAttribute { "entitlements": [ { "entityId": "e1", - "attributeValueReferences": [ - { - "attributeFqn": "http://www.example.org/attr/foo/value/bar" - } - ] + "attributeValueFqns": ["https://www.example.org/attr/foo/value/bar"] }, { "entityId": "e2", - "attributeValueReferences": [ - { - "attributeFqn": "http://www.example.org/attr/color/value/red" - } - ] + "attributeValueFqns": ["https://www.example.org/attr/color/value/red"] } ] } diff --git a/services/authorization/authorization_test.go b/services/authorization/authorization_test.go index 67de64269e..2d7bd23080 100644 --- a/services/authorization/authorization_test.go +++ b/services/authorization/authorization_test.go @@ -1,4 +1,4 @@ -package authorization_test +package authorization import ( "context" @@ -9,26 +9,33 @@ import ( "github.com/opentdf/platform/protocol/go/authorization" "github.com/opentdf/platform/protocol/go/policy" - "github.com/opentdf/platform/protocol/go/policy/attributes" attr "github.com/opentdf/platform/protocol/go/policy/attributes" - authorizationSvc "github.com/opentdf/platform/services/authorization" + otdf "github.com/opentdf/platform/sdk" "github.com/stretchr/testify/assert" ) -var entitlementsResponse authorization.GetEntitlementsResponse -var getAttributesByValueFqnsResponse attributes.GetAttributeValuesByFqnsResponse +var ( + entitlementsResponse authorization.GetEntitlementsResponse + getAttributesByValueFqnsResponse attr.GetAttributeValuesByFqnsResponse + mockNamespace = "www.example.org" + mockAttrName = "foo" + mockAttrValue1 = "value1" + mockAttrValue2 = "value2" + mockFqn1 = fmt.Sprintf("https://%s/attr/%s/value/%s", mockNamespace, mockAttrName, mockAttrValue1) + mockFqn2 = fmt.Sprintf("https://%s/attr/%s/value/%s", mockNamespace, mockAttrName, mockAttrValue2) +) -var MockRetrieveAttributeDefinitions = func(ctx context.Context, ra *authorization.ResourceAttribute, as authorizationSvc.AuthorizationService) (*attr.GetAttributeValuesByFqnsResponse, error) { - fmt.Print("Using mocked GetAttributeValuesByFqns") - return &getAttributesByValueFqnsResponse, nil +func mockRetrieveAttributeDefinitions(ctx context.Context, ra *authorization.ResourceAttribute, sdk *otdf.SDK) (map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue, error) { + fmt.Println("Using mocked GetAttributeValuesByFqns: " + getAttributesByValueFqnsResponse.String()) + return getAttributesByValueFqnsResponse.GetFqnAttributeValues(), nil } -var MockRetrieveEntitlements = func(ctx context.Context, req *authorization.GetEntitlementsRequest, as authorizationSvc.AuthorizationService) (*authorization.GetEntitlementsResponse, error) { - fmt.Print("Using mocked GetEntitlements") +func mockRetrieveEntitlements(ctx context.Context, req *authorization.GetEntitlementsRequest, as AuthorizationService) (*authorization.GetEntitlementsResponse, error) { + fmt.Println("Using mocked GetEntitlements: " + entitlementsResponse.String()) return &entitlementsResponse, nil } -func TestGetDecisionsAllOfPass(t *testing.T) { +func showLogsInTest() { logLevel := &slog.LevelVar{} // INFO logLevel.Set(slog.LevelDebug) @@ -38,130 +45,166 @@ func TestGetDecisionsAllOfPass(t *testing.T) { logger := slog.New(slog.NewJSONHandler(os.Stdout, opts)) slog.SetDefault(logger) +} + +func Test_GetDecisionsAllOf_Pass(t *testing.T) { + showLogsInTest() + + retrieveAttributeDefinitions = mockRetrieveAttributeDefinitions + retrieveEntitlements = mockRetrieveEntitlements - authorizationSvc.RetrieveAttributeDefinitions = MockRetrieveAttributeDefinitions - authorizationSvc.RetrieveEntitlements = MockRetrieveEntitlements // set entitlementsResponse and getAttributesByValueFqnsResponse entitlementsResponse = authorization.GetEntitlementsResponse{Entitlements: []*authorization.EntityEntitlements{ { - EntityId: "e1", - AttributeId: []string{"http://www.example.org/attr/foo/value/value1"}, - }}} + EntityId: "e1", + AttributeValueFqns: []string{mockFqn1}, + }, + }} attrDef := policy.Attribute{ - Name: "foo", + Name: mockAttrName, Namespace: &policy.Namespace{ - Name: "http://www.example.org", + Name: mockNamespace, }, Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, Values: []*policy.Value{ { - Value: "value1", - }, - { - Value: "value2", + Value: mockAttrValue1, }, }, } - getAttributesByValueFqnsResponse = attributes.GetAttributeValuesByFqnsResponse{FqnAttributeValues: map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue{ - "http://www.example.org/attr/foo/value/value1": { + getAttributesByValueFqnsResponse = attr.GetAttributeValuesByFqnsResponse{FqnAttributeValues: map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue{ + "https://www.example.org/attr/foo/value/value1": { Attribute: &attrDef, - Value: &policy.Value{}, - }}} + Value: &policy.Value{ + Fqn: mockFqn1, + }, + }, + }} // set the request req := authorization.GetDecisionsRequest{DecisionRequests: []*authorization.DecisionRequest{ - {Actions: []*policy.Action{}, + { + Actions: []*policy.Action{}, EntityChains: []*authorization.EntityChain{ - {Id: "ec1", + { + Id: "ec1", Entities: []*authorization.Entity{ {Id: "e1", EntityType: &authorization.Entity_UserName{UserName: "bob.smith"}}, - }}, + }, + }, }, ResourceAttributes: []*authorization.ResourceAttribute{ - {AttributeFqns: []string{"http://www.example.org/attr/foo/value/value1"}}, + {AttributeFqns: []string{mockFqn1}}, }, }, }} - as := authorizationSvc.AuthorizationService{} - var ctxb = context.Background() + as := AuthorizationService{} + retrieveEntitlements = mockRetrieveEntitlements + ctxb := context.Background() resp, err := as.GetDecisions(ctxb, &req) assert.Nil(t, err) assert.NotNil(t, resp) - // some asserts about resp + // one entitlement, one attribute value throughout fmt.Print(resp.String()) - assert.Equal(t, len(resp.DecisionResponses), 1) + assert.Equal(t, 1, len(resp.DecisionResponses)) assert.Equal(t, resp.DecisionResponses[0].Decision, authorization.DecisionResponse_DECISION_PERMIT) -} - -func TestGetDecisionsAllOfFail(t *testing.T) { - logLevel := &slog.LevelVar{} // INFO - logLevel.Set(slog.LevelDebug) - opts := &slog.HandlerOptions{ - Level: logLevel, - } - logger := slog.New(slog.NewJSONHandler(os.Stdout, opts)) + // TODO: uncomment the below when authorization service responds with multiple decisions instead of just a sole permit/deny + // run again with two attribute values throughout + // attrDef.Values = append(attrDef.Values, &policy.Value{ + // Value: mockAttrValue2, + // }) + // getAttributesByValueFqnsResponse.FqnAttributeValues["https://www.example.org/attr/foo/value/value2"] = &attr.GetAttributeValuesByFqnsResponse_AttributeAndValue{ + // Attribute: &attrDef, + // Value: &policy.Value{ + // Fqn: mockFqn2, + // }, + // } + // entitlementsResponse.Entitlements[0].AttributeValueFqns = []string{mockFqn1, mockFqn2} + + // resp, err = as.GetDecisions(ctxb, &req) + // assert.Nil(t, err) + // assert.Equal(t, 1, len(resp.DecisionResponses)) + // assert.Equal(t, resp.DecisionResponses[0].Decision, authorization.DecisionResponse_DECISION_PERMIT) + // assert.Equal(t, resp.DecisionResponses[1].Decision, authorization.DecisionResponse_DECISION_PERMIT) +} - slog.SetDefault(logger) +func Test_GetDecisions_AllOf_Fail(t *testing.T) { + showLogsInTest() - authorizationSvc.RetrieveAttributeDefinitions = MockRetrieveAttributeDefinitions - authorizationSvc.RetrieveEntitlements = MockRetrieveEntitlements + retrieveAttributeDefinitions = mockRetrieveAttributeDefinitions + retrieveEntitlements = mockRetrieveEntitlements // set entitlementsResponse and getAttributesByValueFqnsResponse entitlementsResponse = authorization.GetEntitlementsResponse{Entitlements: []*authorization.EntityEntitlements{ { - EntityId: "e1", - AttributeId: []string{"http://www.example.org/attr/foo/value/value1"}, - }}} + EntityId: "e1", + AttributeValueFqns: []string{mockFqn1}, + }, + }} attrDef := policy.Attribute{ - Name: "foo", + Name: mockAttrName, Namespace: &policy.Namespace{ - Name: "http://www.example.org", + Name: mockNamespace, }, Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF, Values: []*policy.Value{ { - Value: "value1", + Value: mockAttrValue1, }, { - Value: "value2", + Value: mockAttrValue2, }, }, } - getAttributesByValueFqnsResponse = attributes.GetAttributeValuesByFqnsResponse{FqnAttributeValues: map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue{ - "http://www.example.org/attr/foo/value/value1": { + getAttributesByValueFqnsResponse = attr.GetAttributeValuesByFqnsResponse{FqnAttributeValues: map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue{ + "https://www.example.org/attr/foo/value/value1": { + Attribute: &attrDef, + Value: &policy.Value{ + Fqn: mockFqn1, + }, + }, + "https://www.example.org/attr/foo/value/value2": { Attribute: &attrDef, - Value: &policy.Value{}, - }}} + Value: &policy.Value{ + Fqn: mockFqn2, + }, + }, + }} // set the request req := authorization.GetDecisionsRequest{DecisionRequests: []*authorization.DecisionRequest{ - {Actions: []*policy.Action{}, + { + Actions: []*policy.Action{}, EntityChains: []*authorization.EntityChain{ - {Id: "ec1", + { + Id: "ec1", Entities: []*authorization.Entity{ {Id: "e1", EntityType: &authorization.Entity_UserName{UserName: "bob.smith"}}, - }}, + }, + }, }, ResourceAttributes: []*authorization.ResourceAttribute{ - {AttributeFqns: []string{"http://www.example.org/attr/foo/value/value1", "http://www.example.org/attr/foo/value/value2"}}, + {AttributeFqns: []string{mockFqn1, mockFqn2}}, }, }, }} - as := authorizationSvc.AuthorizationService{} - var ctxb = context.Background() + as := AuthorizationService{} + ctxb := context.Background() resp, err := as.GetDecisions(ctxb, &req) assert.Nil(t, err) assert.NotNil(t, resp) - // some asserts about resp + // NOTE: there should be two decision responses, one for each data attribute value, but authorization service + // only responds with one permit/deny at the moment + // entitlements only contain the first FQN, so we have a deny decision fmt.Print(resp.String()) assert.Equal(t, len(resp.DecisionResponses), 1) assert.Equal(t, resp.DecisionResponses[0].Decision, authorization.DecisionResponse_DECISION_DENY)