Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion service/internal/access/v2/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/opentdf/platform/service/internal/access/v2/obligations"
"github.com/opentdf/platform/service/internal/subjectmappingbuiltin"
"github.com/opentdf/platform/service/logger"
"google.golang.org/protobuf/types/known/wrapperspb"
)

var (
Expand All @@ -25,6 +26,19 @@ var (
ErrInvalidDynamicValueMapping = errors.New("access: invalid dynamic value mapping")
)

// isExplicitlyInactive reports whether an active state was loaded and is false. An unset state is
// not inactive: targeted lookups, synthetic values, and in-memory fixtures all leave it unset.
func isExplicitlyInactive(active *wrapperspb.BoolValue) bool {
return active != nil && !active.GetValue()
}

// isDeactivated reports whether an attribute value, or the definition owning it, is deactivated.
// Deactivated values must neither entitle an entity nor be entitleable on a resource.
func isDeactivated(attributeAndValue *attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue) bool {
return isExplicitlyInactive(attributeAndValue.GetValue().GetActive()) ||
isExplicitlyInactive(attributeAndValue.GetAttribute().GetActive())
}

// getDefinition parses the value FQN and uses it to retrieve the definition from the provided definitions map
func getDefinition(valueFQN string, allDefinitionsByDefFQN map[string]*policy.Attribute) (*policy.Attribute, error) {
parsed, err := identifier.Parse[*identifier.FullyQualifiedAttribute](valueFQN)
Expand Down Expand Up @@ -112,7 +126,7 @@ func populateLowerValuesIfHierarchy(
entitledActionsSet[action.GetName()] = action
}
for _, value := range definition.GetValues() {
if lower {
if lower && !isExplicitlyInactive(value.GetActive()) {
alreadyEntitledActions, exists := entitledActionsPerAttributeValueFqn[value.GetFqn()]
if !exists {
entitledActionsPerAttributeValueFqn[value.GetFqn()] = entitledActions
Expand Down Expand Up @@ -165,6 +179,9 @@ func populateHigherValuesIfHierarchy(
)
continue
}
if isDeactivated(fullValue) {
continue
}
decisionableAttributes[value.GetFqn()] = &attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue{
Value: fullValue.GetValue(),
Attribute: definition,
Expand Down Expand Up @@ -254,6 +271,15 @@ func getResourceDecisionableAttributes(

attributeAndValue, ok := entitleableAttributesByValueFQN[attrValueFQN]

// A deactivated value is left out of the decisionable set so the resource carrying it is
// denied downstream, and so it is never synthesized as an ad-hoc value below.
if ok && isDeactivated(attributeAndValue) {
logger.WarnContext(ctx, "deactivated attribute value on resource - denying access",
slog.String("attribute_value_fqn", attrValueFQN),
)
continue
}

if !ok {
// The value FQN is not a concrete policy value. A synthetic value is created
// when either direct entitlements are enabled (experimental) OR the parent
Expand All @@ -266,6 +292,15 @@ func getResourceDecisionableAttributes(
continue
}

// A deactivated definition cannot back a synthetic value, or an ad-hoc value under a
// deactivated definition would remain satisfiable.
if isExplicitlyInactive(parentDefinition.GetActive()) {
logger.WarnContext(ctx, "deactivated attribute definition on resource - denying access",
slog.String("attribute_value_fqn", attrValueFQN),
)
continue
}

_, hasDynamicMapping := dynamicMappingsByDefinitionFQN[parentDefinition.GetFqn()]
if !allowDirectEntitlements && !hasDynamicMapping {
// neither path enabled for this value: add to not found list and skip
Expand Down
46 changes: 46 additions & 0 deletions service/internal/access/v2/pdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ func (p *PolicyDecisionPoint) GetDecision(

for _, directEntitlement := range entityRepresentation.GetDirectEntitlements() {
fqn := directEntitlement.GetAttributeValueFqn()
if p.isDeactivatedValueFQN(fqn) {
l.DebugContext(ctx, "skipping direct entitlement of deactivated attribute value",
slog.String("attribute_value_fqn", fqn),
)
continue
}
actionNames := directEntitlement.GetActions()
// In strict namespaced-policy mode, direct-entitlement actions must carry
// the same namespace context as the entitled attribute value so they can
Expand Down Expand Up @@ -386,6 +392,12 @@ func (p *PolicyDecisionPoint) GetDecision(
return nil, nil, fmt.Errorf("%w: %w", ErrDynamicValueMappingEvaluation, err)
}
for fqn, actions := range dynamicEntitledFQNsToActions {
if p.isDeactivatedValueFQN(fqn) {
l.DebugContext(ctx, "skipping dynamic value mapping entitlement of deactivated attribute value",
slog.String("attribute_value_fqn", fqn),
)
continue
}
entitledFQNsToActions[fqn] = append(entitledFQNsToActions[fqn], actions...)
}
l.DebugContext(ctx, "evaluated dynamic value mappings", slog.Any("dynamic_entitled_value_fqns_to_actions", dynamicEntitledFQNsToActions))
Expand Down Expand Up @@ -460,6 +472,13 @@ func (p *PolicyDecisionPoint) GetDecisionRegisteredResource(
attrVal := aav.GetAttributeValue()
attrValFQN := attrVal.GetFqn()

if p.isDeactivatedValueFQN(attrValFQN) {
l.DebugContext(ctx, "skipping registered resource entitlement of deactivated attribute value",
slog.String("attribute_value_fqn", attrValFQN),
)
continue
}

requiredNamespaceFQN := ""
if attrAndValue, ok2 := decisionableAttributes[attrValFQN]; ok2 {
requiredNamespaceFQN = attrAndValue.GetAttribute().GetNamespace().GetFqn()
Expand Down Expand Up @@ -559,6 +578,12 @@ func (p *PolicyDecisionPoint) GetEntitlements(
actionsPerAttributeValueFqn := make(map[string]*authz.EntityEntitlements_ActionsList)

for valueFQN, actions := range fqnsToActions {
if p.isDeactivatedValueFQN(valueFQN) {
l.DebugContext(ctx, "skipping entitlement of deactivated attribute value",
slog.String("attribute_value_fqn", valueFQN),
)
continue
}
// If already entitled (such as via a higher entitled comprehensive hierarchy attr value), merge with existing
if alreadyEntitled, ok := actionsPerAttributeValueFqn[valueFQN]; ok {
actions = mergeDeduplicatedActions(make(map[string]*policy.Action), actions, alreadyEntitled.GetActions())
Expand Down Expand Up @@ -615,6 +640,13 @@ func (p *PolicyDecisionPoint) GetEntitlementsRegisteredResource(
attrVal := aav.GetAttributeValue()
attrValFQN := attrVal.GetFqn()

if p.isDeactivatedValueFQN(attrValFQN) {
l.DebugContext(ctx, "skipping entitlement of deactivated attribute value",
slog.String("attribute_value_fqn", attrValFQN),
)
continue
}

actionsList, actionsAreOK := actionsPerAttributeValueFqn[attrValFQN]
if !actionsAreOK {
actionsList = &authz.EntityEntitlements_ActionsList{
Expand Down Expand Up @@ -652,3 +684,17 @@ func (p *PolicyDecisionPoint) GetEntitlementsRegisteredResource(

return result, nil
}

// isDeactivatedValueFQN reports whether the value FQN, or the definition owning it, is deactivated.
// An FQN unknown to policy under an active definition is not deactivated: it is either denied or
// synthesized by the ad-hoc value paths.
func (p *PolicyDecisionPoint) isDeactivatedValueFQN(valueFQN string) bool {
if attributeAndValue, ok := p.allEntitleableAttributesByValueFQN[valueFQN]; ok {
return isDeactivated(attributeAndValue)
}
definition, err := getDefinition(valueFQN, p.allAttributesByDefinitionFQN)
if err != nil {
return false
}
return isExplicitlyInactive(definition.GetActive())
}
Loading
Loading