Skip to content
Draft
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
91 changes: 68 additions & 23 deletions service/authorization/v2/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
"github.com/go-viper/mapstructure/v2"
authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2"
authzV2Connect "github.com/opentdf/platform/protocol/go/authorization/v2/authorizationv2connect"
"github.com/opentdf/platform/protocol/go/policy"
otdf "github.com/opentdf/platform/sdk"
"github.com/opentdf/platform/service/internal/access/v2"
"github.com/opentdf/platform/service/logger"
ctxAuth "github.com/opentdf/platform/service/pkg/auth"
"github.com/opentdf/platform/service/pkg/cache"
"github.com/opentdf/platform/service/pkg/serviceregistry"
"go.opentelemetry.io/otel"
Expand All @@ -23,6 +25,13 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb"
)

var (
ErrFailedToBuildRequestContext = errors.New("failed to contextualize decision request")
ErrFailedToInitPDP = errors.New("failed to create JIT PDP")
ErrFailedToGetDecision = errors.New("failed to get decision")
ErrFailedToGetEntitlements = errors.New("failed to get entitlements")
)

type Service struct {
sdk *otdf.SDK
config *Config
Expand Down Expand Up @@ -138,15 +147,12 @@ func (as *Service) GetEntitlements(ctx context.Context, req *connect.Request[aut
// When authorization service can consume cached policy, switch to the other PDP (process based on policy passed in)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache)
if err != nil {
as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.Any("error", err))
return nil, connect.NewError(connect.CodeInternal, err)
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToGetEntitlements, ErrFailedToInitPDP, err))
}

entitlements, err := pdp.GetEntitlements(ctx, entityIdentifier, withComprehensiveHierarchy)
if err != nil {
// TODO: any bad request errors that aren't 500s?
as.logger.ErrorContext(ctx, "failed to get entitlements", slog.Any("error", err))
return nil, connect.NewError(connect.CodeInternal, err)
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToGetEntitlements, err))
}
rsp := &authzV2.GetEntitlementsResponse{
Entitlements: entitlements,
Expand All @@ -168,27 +174,34 @@ func (as *Service) GetDecision(ctx context.Context, req *connect.Request[authzV2

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache)
if err != nil {
as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.Any("error", err))
return nil, connect.NewError(connect.CodeInternal, err)
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}

request := req.Msg
entityIdentifier := request.GetEntityIdentifier()
action := request.GetAction()
resource := request.GetResource()
fulfillableObligations := request.GetFulfillableObligationFqns()

decisions, permitted, err := pdp.GetDecision(ctx, entityIdentifier, action, []*authzV2.Resource{resource})
reqContext, err := as.getDecisionRequestContext(ctx)
if err != nil {
as.logger.ErrorContext(ctx, "failed to get decision", slog.Any("error", err))
if errors.Is(err, access.ErrFQNNotFound) || errors.Is(err, access.ErrDefinitionNotFound) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
return nil, statusifyError(ctx, as.logger, err)
}

decisions, permitted, err := pdp.GetDecision(
ctx,
entityIdentifier,
action,
[]*authzV2.Resource{resource},
reqContext,
fulfillableObligations,
)
if err != nil {
return nil, statusifyError(ctx, as.logger, err)
}
resp, err := rollupSingleResourceDecision(permitted, decisions)
if err != nil {
as.logger.ErrorContext(ctx, "failed to rollup single-resource decision", slog.Any("error", err))
return nil, connect.NewError(connect.CodeInternal, err)
return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrFailedToRollupDecision, err))
}
return connect.NewResponse(resp), nil
}
Expand All @@ -206,21 +219,34 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to create JIT PDP"), err))
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}
request := req.Msg
entityIdentifier := request.GetEntityIdentifier()
action := request.GetAction()
resources := request.GetResources()
fulfillableObligations := request.GetFulfillableObligationFqns()

decisions, allPermitted, err := pdp.GetDecision(ctx, entityIdentifier, action, resources)
reqContext, err := as.getDecisionRequestContext(ctx)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to get decision"), err))
return nil, statusifyError(ctx, as.logger, err)
}

decisions, allPermitted, err := pdp.GetDecision(
ctx,
entityIdentifier,
action,
resources,
reqContext,
fulfillableObligations,
)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToGetDecision, err))
}

resourceDecisions, err := rollupMultiResourceDecisions(decisions)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to rollup multi-resource decision"), err))
return nil, connect.NewError(connect.CodeInternal, err)
}

resp := &authzV2.GetDecisionMultiResourceResponse{
Expand All @@ -246,27 +272,33 @@ func (as *Service) GetDecisionBulk(ctx context.Context, req *connect.Request[aut

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to create JIT PDP"), err))
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}

multiRequests := req.Msg.GetDecisionRequests()
decisionResponses := make([]*authzV2.GetDecisionMultiResourceResponse, len(multiRequests))

reqContext, err := as.getDecisionRequestContext(ctx)
if err != nil {
return nil, statusifyError(ctx, as.logger, err)
}

// TODO: revisit performance of this loop after introduction of caching and registered resource values within decisioning,
// as the same entity in multiple requests should only be resolved JIT once, not once per request if the same in each.
for idx, request := range multiRequests {
entityIdentifier := request.GetEntityIdentifier()
action := request.GetAction()
resources := request.GetResources()
fulfillableObligations := request.GetFulfillableObligationFqns()

decisions, allPermitted, err := pdp.GetDecision(ctx, entityIdentifier, action, resources)
decisions, allPermitted, err := pdp.GetDecision(ctx, entityIdentifier, action, resources, reqContext, fulfillableObligations)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to get bulk decision"), err))
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToGetDecision, err))
}

resourceDecisions, err := rollupMultiResourceDecisions(decisions)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to rollup bulk multi-resource decision"), err), slog.Int("index", idx))
return nil, statusifyError(ctx, as.logger, err, slog.Int("index", idx))
}

decisionResponse := &authzV2.GetDecisionMultiResourceResponse{
Expand All @@ -283,3 +315,16 @@ func (as *Service) GetDecisionBulk(ctx context.Context, req *connect.Request[aut
}
return connect.NewResponse(rsp), nil
}

// Builds a decision request context out of contextual metadata for the downstream obligation trigger/fulfillment decisioning
func (as *Service) getDecisionRequestContext(ctx context.Context) (*policy.RequestContext, error) {
clientID, err := ctxAuth.GetClientIDFromContext(ctx)
if err == nil {
return nil, errors.Join(ErrFailedToBuildRequestContext, err)
}
return &policy.RequestContext{
Pep: &policy.PolicyEnforcementPoint{
ClientId: clientID,
},
}, nil
}
61 changes: 25 additions & 36 deletions service/authorization/v2/authorization_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package authorization

import (
"errors"
"math/rand"
"strconv"
"testing"
Expand Down Expand Up @@ -1286,12 +1285,11 @@ func Test_GetEntitlementsRequest_Fails(t *testing.T) {

func Test_RollupSingleResourceDecision(t *testing.T) {
tests := []struct {
name string
permitted bool
decisions []*access.Decision
expectedResult *authzV2.GetDecisionResponse
expectedError error
errorMsgContain string
name string
permitted bool
decisions []*access.Decision
expectedResult *authzV2.GetDecisionResponse
expectedError error
}{
{
name: "should return permit decision when permitted is true",
Expand Down Expand Up @@ -1336,12 +1334,11 @@ func Test_RollupSingleResourceDecision(t *testing.T) {
expectedError: nil,
},
{
name: "should return error when no decisions are provided",
permitted: true,
decisions: []*access.Decision{},
expectedResult: nil,
expectedError: errors.New("no decisions returned"),
errorMsgContain: "no decisions returned",
name: "should return error when no decisions are provided",
permitted: true,
decisions: []*access.Decision{},
expectedResult: nil,
expectedError: ErrNoDecisions,
},
{
name: "should return error when decision has no results",
Expand All @@ -1352,9 +1349,8 @@ func Test_RollupSingleResourceDecision(t *testing.T) {
Results: []access.ResourceDecision{},
},
},
expectedResult: nil,
expectedError: errors.New("no decision results returned"),
errorMsgContain: "no decision results returned",
expectedResult: nil,
expectedError: ErrDecisionMustHaveResults,
},
}

Expand All @@ -1364,7 +1360,7 @@ func Test_RollupSingleResourceDecision(t *testing.T) {

if tc.expectedError != nil {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorMsgContain)
require.ErrorIs(t, err, tc.expectedError)
assert.Nil(t, result)
} else {
require.NoError(t, err)
Expand All @@ -1376,11 +1372,10 @@ func Test_RollupSingleResourceDecision(t *testing.T) {

func Test_RollupMultiResourceDecisions(t *testing.T) {
tests := []struct {
name string
decisions []*access.Decision
expectedResult []*authzV2.ResourceDecision
expectedError error
errorMsgContain string
name string
decisions []*access.Decision
expectedResult []*authzV2.ResourceDecision
expectedError error
}{
{
name: "should return multiple permit decisions",
Expand Down Expand Up @@ -1414,7 +1409,6 @@ func Test_RollupMultiResourceDecisions(t *testing.T) {
EphemeralResourceId: "resource-456",
},
},
expectedError: nil,
},
{
name: "should return mix of permit and deny decisions",
Expand Down Expand Up @@ -1448,7 +1442,6 @@ func Test_RollupMultiResourceDecisions(t *testing.T) {
EphemeralResourceId: "resource-456",
},
},
expectedError: nil,
},
{
name: "should rely on results and default to false decisions",
Expand Down Expand Up @@ -1490,7 +1483,6 @@ func Test_RollupMultiResourceDecisions(t *testing.T) {
EphemeralResourceId: "resource-456",
},
},
expectedError: nil,
},
{
name: "should ignore global access and care about resource decisions predominantly",
Expand Down Expand Up @@ -1532,7 +1524,6 @@ func Test_RollupMultiResourceDecisions(t *testing.T) {
EphemeralResourceId: "resource-456",
},
},
expectedError: nil,
},
{
name: "should return error when decision has no results",
Expand All @@ -1542,9 +1533,7 @@ func Test_RollupMultiResourceDecisions(t *testing.T) {
Results: []access.ResourceDecision{},
},
},
expectedResult: nil,
expectedError: errors.New("no decision results returned"),
errorMsgContain: "no decision results returned",
expectedError: ErrDecisionMustHaveResults,
},
}

Expand All @@ -1554,7 +1543,7 @@ func Test_RollupMultiResourceDecisions(t *testing.T) {

if tc.expectedError != nil {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorMsgContain)
require.ErrorIs(t, err, tc.expectedError)
assert.Nil(t, result)
} else {
require.NoError(t, err)
Expand Down Expand Up @@ -1590,14 +1579,14 @@ func Test_RollupMultiResourceDecisions_WithNilChecks(t *testing.T) {
var decisions []*access.Decision
_, err := rollupMultiResourceDecisions(decisions)
require.Error(t, err)
assert.Contains(t, err.Error(), "no decisions returned")
require.ErrorIs(t, err, ErrNoDecisions)
})

t.Run("nil decision in array", func(t *testing.T) {
decisions := []*access.Decision{nil}
_, err := rollupMultiResourceDecisions(decisions)
require.Error(t, err)
assert.Contains(t, err.Error(), "nil decision at index 0")
require.ErrorIs(t, err, ErrDecisionCannotBeNil)
})

t.Run("nil Results field", func(t *testing.T) {
Expand All @@ -1609,7 +1598,7 @@ func Test_RollupMultiResourceDecisions_WithNilChecks(t *testing.T) {
}
_, err := rollupMultiResourceDecisions(decisions)
require.Error(t, err)
assert.Contains(t, err.Error(), "no decision results returned")
require.ErrorIs(t, err, ErrDecisionMustHaveResults)
})
}

Expand All @@ -1618,14 +1607,14 @@ func Test_RollupSingleResourceDecision_WithNilChecks(t *testing.T) {
var decisions []*access.Decision
_, err := rollupSingleResourceDecision(true, decisions)
require.Error(t, err)
assert.Contains(t, err.Error(), "no decisions returned")
require.ErrorIs(t, err, ErrNoDecisions)
})

t.Run("nil decision in array", func(t *testing.T) {
decisions := []*access.Decision{nil}
_, err := rollupSingleResourceDecision(true, decisions)
require.Error(t, err)
assert.Contains(t, err.Error(), "nil decision at index 0")
require.ErrorIs(t, err, ErrDecisionCannotBeNil)
})

t.Run("nil Results field", func(t *testing.T) {
Expand All @@ -1637,7 +1626,7 @@ func Test_RollupSingleResourceDecision_WithNilChecks(t *testing.T) {
}
_, err := rollupSingleResourceDecision(true, decisions)
require.Error(t, err)
assert.Contains(t, err.Error(), "no decision results returned")
require.ErrorIs(t, err, ErrDecisionMustHaveResults)
})
}

Expand Down
Loading
Loading