@@ -19,6 +19,7 @@ import (
1919
2020 "github.com/opentdf/platform/service/internal/access/v2/obligations"
2121 "github.com/opentdf/platform/service/logger"
22+ "github.com/opentdf/platform/service/logger/audit"
2223)
2324
2425var (
@@ -141,7 +142,7 @@ func (p *JustInTimePDP) GetDecision(
141142 )
142143
143144 // Because there are three possible types of entities, check obligations first to more easily handle decisioning logic
144- allTriggeredObligationsCanBeFulfilled , requiredObligationsPerResource , err := p .obligationsPDP .GetAllTriggeredObligationsAreFulfilled (
145+ obligationDecision , err := p .obligationsPDP .GetAllTriggeredObligationsAreFulfilled (
145146 ctx ,
146147 resources ,
147148 action ,
@@ -165,26 +166,22 @@ func (p *JustInTimePDP) GetDecision(
165166 case * authzV2.EntityIdentifier_RegisteredResourceValueFqn :
166167 regResValueFQN := strings .ToLower (entityIdentifier .GetRegisteredResourceValueFqn ())
167168 // Registered resources do not have entity representations, so only one decision is made
168- decision , err := p .pdp .GetDecisionRegisteredResource (ctx , regResValueFQN , action , resources )
169+ decision , entitlements , err := p .pdp .GetDecisionRegisteredResource (ctx , regResValueFQN , action , resources )
169170 if err != nil {
170171 return nil , false , fmt .Errorf ("failed to get decision for registered resource value FQN [%s]: %w" , regResValueFQN , err )
171172 }
172173 if decision == nil {
173174 return nil , false , fmt .Errorf ("decision is nil for registered resource value FQN [%s]" , regResValueFQN )
174175 }
175176
176- // If not entitled, obligations are not considered
177- if ! decision .Access {
178- return []* Decision {decision }, decision .Access , nil
179- }
180-
181- // Access should only be granted if entitled AND obligations fulfilled
182- decision .Access = allTriggeredObligationsCanBeFulfilled
183- for idx , required := range requiredObligationsPerResource {
184- decision .Results [idx ].RequiredObligationValueFQNs = required
185- }
177+ // Update resource decisions with obligations and set final access decision
178+ hasRequiredObligations := len (obligationDecision .RequiredObligationValueFQNs ) > 0
179+ entitledWithAnyObligationsSatisfied := decision .AllPermitted && (! hasRequiredObligations || obligationDecision .AllObligationsSatisfied )
180+ decision .AllPermitted = entitledWithAnyObligationsSatisfied
181+ decision = setResourceDecisionsWithObligations (decision , obligationDecision )
186182
187- return []* Decision {decision }, decision .Access , nil
183+ p .auditDecision (ctx , regResValueFQN , action , decision , entitlements , fulfillableObligationValueFQNs , obligationDecision )
184+ return []* Decision {decision }, decision .AllPermitted , nil
188185
189186 default :
190187 return nil , false , ErrInvalidEntityType
@@ -195,38 +192,69 @@ func (p *JustInTimePDP) GetDecision(
195192
196193 // Make initial entitlement decisions
197194 entityDecisions := make ([]* Decision , len (entityRepresentations ))
195+ entityEntitlements := make ([]map [string ][]* policy.Action , len (entityRepresentations ))
198196 allPermitted := true
199197 for idx , entityRep := range entityRepresentations {
200- d , err := p .pdp .GetDecision (ctx , entityRep , action , resources )
198+ d , entitlements , err := p .pdp .GetDecision (ctx , entityRep , action , resources )
201199 if err != nil {
202200 return nil , false , fmt .Errorf ("failed to get decision for entityRepresentation with original id [%s]: %w" , entityRep .GetOriginalId (), err )
203201 }
204202 if d == nil {
205203 return nil , false , fmt .Errorf ("decision is nil: %w" , err )
206204 }
207- if ! d .Access {
205+ // If any entity lacks access to any resource, update overall decision denial
206+ if ! d .AllPermitted {
208207 allPermitted = false
209208 }
210209 entityDecisions [idx ] = d
210+ entityEntitlements [idx ] = entitlements
211211 }
212212
213- // If even one entity was denied access, obligations are not considered or returned
214- if ! allPermitted {
215- return entityDecisions , allPermitted , nil
216- }
213+ // Update resource decisions with obligations and set final access decision
214+ hasRequiredObligations := len ( obligationDecision . RequiredObligationValueFQNs ) > 0
215+ allEntitledWithAnyObligationsSatisfied := allPermitted && ( ! hasRequiredObligations || obligationDecision . AllObligationsSatisfied )
216+ allPermitted = allEntitledWithAnyObligationsSatisfied
217217
218- // Access should only be granted if entitled AND obligations fulfilled
219- allPermitted = allTriggeredObligationsCanBeFulfilled
220- // Obligations are not entity-specific at this time so will be the same across every entity
221- for _ , decision := range entityDecisions {
222- for idx , required := range requiredObligationsPerResource {
223- decision .Results [idx ].RequiredObligationValueFQNs = required
224- }
218+ // Propagate obligations within policy on each resource decision object
219+ for entityIdx , decision := range entityDecisions {
220+ // TODO: figure out this multi-entity response?
221+ // entitledWithAnyObligationsSatisfied := decision.AllPermitted && (!hasRequiredObligations || obligationDecision.AllObligationsSatisfied)
222+ // decision.AllPermitted = entitledWithAnyObligationsSatisfied
223+ decision = setResourceDecisionsWithObligations (decision , obligationDecision )
224+ decision .AllPermitted = allPermitted
225+ entityRepID := entityRepresentations [entityIdx ].GetOriginalId ()
226+ p .auditDecision (ctx , entityRepID , action , decision , entityEntitlements [entityIdx ], fulfillableObligationValueFQNs , obligationDecision )
225227 }
226228
227229 return entityDecisions , allPermitted , nil
228230}
229231
232+ // setResourceDecisionsWithObligations updates all resource decisions with obligation
233+ // information and sets each resource passed state
234+ func setResourceDecisionsWithObligations (
235+ decision * Decision ,
236+ obligationDecision obligations.ObligationPolicyDecision ,
237+ ) * Decision {
238+ hasRequiredObligations := len (obligationDecision .RequiredObligationValueFQNs ) > 0
239+
240+ for idx := range decision .Results {
241+ resourceDecision := & decision .Results [idx ]
242+
243+ if hasRequiredObligations {
244+ // Update with specific obligation data from the obligations PDP
245+ perResource := obligationDecision .RequiredObligationValueFQNsPerResource [idx ]
246+ resourceDecision .ObligationsSatisfied = perResource .ObligationsSatisfied
247+ resourceDecision .RequiredObligationValueFQNs = perResource .RequiredObligationValueFQNs
248+ } else {
249+ // No required obligations means all obligations are satisfied
250+ resourceDecision .ObligationsSatisfied = true
251+ }
252+
253+ resourceDecision .Passed = resourceDecision .Entitled && resourceDecision .ObligationsSatisfied
254+ }
255+ return decision
256+ }
257+
230258// GetEntitlements retrieves the entitlements for the provided entity chain.
231259// It resolves the entity chain to get the entity representations and then calls the embedded PDP to get the entitlements.
232260func (p * JustInTimePDP ) GetEntitlements (
@@ -287,8 +315,6 @@ func (p *JustInTimePDP) GetEntitlements(
287315func (p * JustInTimePDP ) getMatchedSubjectMappings (
288316 ctx context.Context ,
289317 entityRepresentations []* entityresolutionV2.EntityRepresentation ,
290- // updated with the results, attrValue FQN to attribute and value with subject mappings
291- // entitleableAttributes map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue,
292318) ([]* policy.SubjectMapping , error ) {
293319 // Break the entity down the entities into their properties/selectors and retrieve only those subject mappings
294320 subjectProperties := make ([]* policy.SubjectProperty , 0 )
@@ -400,3 +426,30 @@ func (p *JustInTimePDP) resolveEntitiesFromRequestToken(
400426
401427 return p .resolveEntitiesFromToken (ctx , token , skipEnvironmentEntities )
402428}
429+
430+ // auditDecision logs a GetDecisionV2 audit event with obligation information
431+ func (p * JustInTimePDP ) auditDecision (
432+ ctx context.Context ,
433+ entityID string ,
434+ action * policy.Action ,
435+ decision * Decision ,
436+ entitlements map [string ][]* policy.Action ,
437+ fulfillableObligationValueFQNs []string ,
438+ obligationDecision obligations.ObligationPolicyDecision ,
439+ ) {
440+ // Determine audit decision result
441+ auditDecision := audit .GetDecisionResultDeny
442+ if decision .AllPermitted {
443+ auditDecision = audit .GetDecisionResultPermit
444+ }
445+
446+ p .logger .Audit .GetDecisionV2 (ctx , audit.GetDecisionV2EventParams {
447+ EntityID : entityID ,
448+ ActionName : action .GetName (),
449+ Decision : auditDecision ,
450+ Entitlements : entitlements ,
451+ FulfillableObligationValueFQNs : fulfillableObligationValueFQNs ,
452+ ObligationsSatisfied : obligationDecision .AllObligationsSatisfied ,
453+ ResourceDecisions : decision .Results ,
454+ })
455+ }
0 commit comments