diff --git a/service/authorization/v2/authorization.go b/service/authorization/v2/authorization.go index afbfd296ff..9091528e8c 100644 --- a/service/authorization/v2/authorization.go +++ b/service/authorization/v2/authorization.go @@ -3,7 +3,6 @@ package authorization import ( "context" "errors" - "fmt" "log/slog" "connectrpc.com/connect" @@ -82,14 +81,14 @@ 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) if err != nil { - as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.String("error", err.Error())) + as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.Any("error", err)) return nil, connect.NewError(connect.CodeInternal, 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.String("error", err.Error())) + as.logger.ErrorContext(ctx, "failed to get entitlements", slog.Any("error", err)) return nil, connect.NewError(connect.CodeInternal, err) } @@ -113,9 +112,10 @@ func (as *Service) GetDecision(ctx context.Context, req *connect.Request[authzV2 pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk) if err != nil { - as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.String("error", err.Error())) + as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.Any("error", err)) return nil, connect.NewError(connect.CodeInternal, err) } + request := req.Msg entityIdentifier := request.GetEntityIdentifier() action := request.GetAction() @@ -123,13 +123,15 @@ func (as *Service) GetDecision(ctx context.Context, req *connect.Request[authzV2 decisions, permitted, err := pdp.GetDecision(ctx, entityIdentifier, action, []*authzV2.Resource{resource}) if err != nil { - // TODO: any bad request errors that aren't 500s? - as.logger.ErrorContext(ctx, "failed to get decision", slog.String("error", err.Error())) + as.logger.ErrorContext(ctx, "failed to get decision", slog.Any("error", err), slog.Any("request", request)) + 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) } resp, err := rollupSingleResourceDecision(permitted, decisions) if err != nil { - as.logger.ErrorContext(ctx, "failed to rollup single resource decision", slog.String("error", err.Error())) + as.logger.ErrorContext(ctx, "failed to rollup single-resource decision", slog.Any("error", err), slog.Any("request", request)) return nil, connect.NewError(connect.CodeInternal, err) } return connect.NewResponse(resp), nil @@ -148,8 +150,7 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk) if err != nil { - as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.String("error", err.Error())) - return nil, connect.NewError(connect.CodeInternal, err) + return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to create JIT PDP"), err)) } request := req.Msg entityIdentifier := request.GetEntityIdentifier() @@ -158,15 +159,12 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re decisions, allPermitted, err := pdp.GetDecision(ctx, entityIdentifier, action, resources) if err != nil { - // TODO: any bad request errors that aren't 500s? - as.logger.ErrorContext(ctx, "failed to get decision", slog.String("error", err.Error())) - return nil, connect.NewError(connect.CodeInternal, err) + return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to get decision"), err), slog.Any("request", request)) } - resourceDecisions, err := rollupMultiResourceDecision(decisions) + resourceDecisions, err := rollupMultiResourceDecisions(decisions) if err != nil { - as.logger.ErrorContext(ctx, "failed to rollup multi resource decision", slog.String("error", err.Error())) - return nil, connect.NewError(connect.CodeInternal, err) + return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to rollup multi-resource decision"), err), slog.Any("request", request)) } resp := &authzV2.GetDecisionMultiResourceResponse{ @@ -180,73 +178,52 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re } // GetDecisionBulk for multiple requests, each comprising a combination of entity chain, action, and one or more resources -func (as *Service) GetDecisionBulk(_ context.Context, _ *connect.Request[authzV2.GetDecisionBulkRequest]) (*connect.Response[authzV2.GetDecisionBulkResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("GetDecisionBulk not implemented")) -} +func (as *Service) GetDecisionBulk(ctx context.Context, req *connect.Request[authzV2.GetDecisionBulkRequest]) (*connect.Response[authzV2.GetDecisionBulkResponse], error) { + as.logger.DebugContext(ctx, "getting decision bulk") -// rollupMultiResourceDecision creates a standardized response for multi-resource decisions -// by processing the decisions returned from the PDP. -func rollupMultiResourceDecision( - decisions []*access.Decision, -) ([]*authzV2.ResourceDecision, error) { - if len(decisions) == 0 { - return nil, errors.New("no decisions returned") - } + ctx, span := as.Tracer.Start(ctx, "GetDecisionBulk") + defer span.End() - var resourceDecisions []*authzV2.ResourceDecision + // Extract trace context from the incoming request + propagator := otel.GetTextMapPropagator() + ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - for idx, decision := range decisions { - if decision == nil { - return nil, fmt.Errorf("nil decision at index %d", idx) - } - if len(decision.Results) == 0 { - return nil, errors.New("no decision results returned") - } - for _, result := range decision.Results { - access := authzV2.Decision_DECISION_DENY - if result.Passed { - access = authzV2.Decision_DECISION_PERMIT - } - resourceDecision := &authzV2.ResourceDecision{ - Decision: access, - EphemeralResourceId: result.ResourceID, - } - resourceDecisions = append(resourceDecisions, resourceDecision) - } + pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk) + if err != nil { + return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to create JIT PDP"), err)) } - return resourceDecisions, nil -} + multiRequests := req.Msg.GetDecisionRequests() + decisionResponses := make([]*authzV2.GetDecisionMultiResourceResponse, len(multiRequests)) -// rollupSingleResourceDecision creates a standardized response for a single resource decision -// by processing the decision returned from the PDP. -func rollupSingleResourceDecision( - permitted bool, - decisions []*access.Decision, -) (*authzV2.GetDecisionResponse, error) { - if len(decisions) == 0 { - return nil, errors.New("no decisions returned") - } + // 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() - decision := decisions[0] - if decision == nil { - return nil, errors.New("nil decision at index 0") - } + decisions, allPermitted, err := pdp.GetDecision(ctx, entityIdentifier, action, resources) + if err != nil { + return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to get bulk decision"), err), slog.Any("request", request)) + } - if len(decision.Results) == 0 { - return nil, errors.New("no decision results returned") - } + 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.Any("request", request), slog.Int("index", idx)) + } - result := decision.Results[0] - access := authzV2.Decision_DECISION_DENY - if permitted { - access = authzV2.Decision_DECISION_PERMIT + decisionResponse := &authzV2.GetDecisionMultiResourceResponse{ + AllPermitted: &wrapperspb.BoolValue{ + Value: allPermitted, + }, + ResourceDecisions: resourceDecisions, + } + decisionResponses[idx] = decisionResponse } - resourceDecision := &authzV2.ResourceDecision{ - Decision: access, - EphemeralResourceId: result.ResourceID, + + rsp := &authzV2.GetDecisionBulkResponse{ + DecisionResponses: decisionResponses, } - return &authzV2.GetDecisionResponse{ - Decision: resourceDecision, - }, nil + return connect.NewResponse(rsp), nil } diff --git a/service/authorization/v2/authorization_test.go b/service/authorization/v2/authorization_test.go index 110638c5a6..665976e5be 100644 --- a/service/authorization/v2/authorization_test.go +++ b/service/authorization/v2/authorization_test.go @@ -2,6 +2,7 @@ package authorization import ( "errors" + "math/rand" "testing" "buf.build/go/protovalidate" @@ -11,6 +12,7 @@ import ( access "github.com/opentdf/platform/service/internal/access/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) var ( @@ -20,26 +22,15 @@ var ( sampleResourceFQN = "https://example.com/attr/hier/value/highest" sampleResourceFQN2 = "https://example.com/attr/hier/value/lowest" sampleRegisteredResourceFQN = "https://example.com/reg_res/system/value/internal" -) -func getValidator() protovalidate.Validator { - v, err := protovalidate.New() - if err != nil { - panic(err) - } - return v -} - -func Test_GetDecisionRequest_Succeeds(t *testing.T) { - v := getValidator() - - cases := []struct { + // Good multi-resource requests that should pass validation + goodMultiResourceRequests = []struct { name string - request *authzV2.GetDecisionRequest + request *authzV2.GetDecisionMultiResourceRequest }{ { - name: "entity: token, action: create, resource: attribute values", - request: &authzV2.GetDecisionRequest{ + name: "entity: token, action: create, multiple resources: attribute values", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -49,37 +40,27 @@ func Test_GetDecisionRequest_Succeeds(t *testing.T) { }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, }, }, - }, - }, - }, - { - name: "entity: token, action: create, resource: registered", - request: &authzV2.GetDecisionRequest{ - EntityIdentifier: &authzV2.EntityIdentifier{ - Identifier: &authzV2.EntityIdentifier_Token{ - Token: &entity.Token{ - EphemeralId: "123", - Jwt: "sample-jwt-token", + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN2}, + }, }, }, }, - Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: sampleRegisteredResourceFQN, - }, - }, }, }, { - name: "entity: chain, action: create, resource: attribute values", - request: &authzV2.GetDecisionRequest{ + name: "entity: chain, action: create, multiple resources: mixed types", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_EntityChain{ EntityChain: &entity.EntityChain{ @@ -90,113 +71,68 @@ func Test_GetDecisionRequest_Succeeds(t *testing.T) { EntityType: &entity.Entity_EmailAddress{EmailAddress: "test@test.com"}, Category: entity.Entity_CATEGORY_SUBJECT, }, - { - EphemeralId: "chained-2", - EntityType: &entity.Entity_ClientId{ - ClientId: "client-123", - }, - Category: entity.Entity_CATEGORY_ENVIRONMENT, - }, }, }, }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN, sampleResourceFQN2}, - }, - }, - }, - }, - }, - { - name: "entity: chain, action: create, resource: registered", - request: &authzV2.GetDecisionRequest{ - EntityIdentifier: &authzV2.EntityIdentifier{ - Identifier: &authzV2.EntityIdentifier_EntityChain{ - EntityChain: &entity.EntityChain{ - EphemeralId: "1234", - Entities: []*entity.Entity{ - { - EphemeralId: "chained-1", - EntityType: &entity.Entity_ClientId{ - ClientId: "client-123", - }, - Category: entity.Entity_CATEGORY_ENVIRONMENT, - }, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, }, }, }, - }, - Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: sampleRegisteredResourceFQN, + { + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: sampleRegisteredResourceFQN, + }, }, }, }, }, { - name: "entity: registered resource, action: create, resource: attribute values", - request: &authzV2.GetDecisionRequest{ + name: "entity: registered resource, action: create, multiple resources: registered", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_RegisteredResourceValueFqn{ RegisteredResourceValueFqn: sampleRegisteredResourceFQN, }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN2}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: sampleRegisteredResourceFQN, }, }, - }, - }, - }, - { - name: "entity: registered resource, action: create, resource: registered", - request: &authzV2.GetDecisionRequest{ - EntityIdentifier: &authzV2.EntityIdentifier{ - Identifier: &authzV2.EntityIdentifier_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: sampleRegisteredResourceFQN, - }, - }, - Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: sampleRegisteredResourceFQN, + { + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: "https://example.com/another/registered/resource", + }, }, }, }, }, } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - err := v.Validate(tc.request) - require.NoError(t, err, "validation should succeed for request: %s", tc.name) - }) - } -} - -func Test_GetDecisionRequest_Fails(t *testing.T) { - v := getValidator() - cases := []struct { + // Bad multi-resource requests that should fail validation + badMultiResourceRequests = []struct { name string - request *authzV2.GetDecisionRequest + request *authzV2.GetDecisionMultiResourceRequest expectedValidationError string }{ { name: "missing entity identifier", - request: &authzV2.GetDecisionRequest{ + request: &authzV2.GetDecisionMultiResourceRequest{ Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, }, }, }, @@ -205,7 +141,7 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, { name: "missing action", - request: &authzV2.GetDecisionRequest{ + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -214,10 +150,12 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, }, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, }, }, }, @@ -225,8 +163,8 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { expectedValidationError: "action", }, { - name: "missing resource", - request: &authzV2.GetDecisionRequest{ + name: "action missing name", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -235,13 +173,22 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, }, - Action: sampleActionCreate, + Action: &policy.Action{}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, + }, + }, + }, }, - expectedValidationError: "resource", + expectedValidationError: "name", }, { - name: "action missing name", - request: &authzV2.GetDecisionRequest{ + name: "missing resources", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -250,20 +197,13 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, }, - Action: &policy.Action{}, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, - }, - }, + Action: sampleActionCreate, }, - expectedValidationError: "name", + expectedValidationError: "resources", }, { - name: "registered resource FQN is empty", - request: &authzV2.GetDecisionRequest{ + name: "empty resources array", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -272,16 +212,14 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, }, - Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_RegisteredResourceValueFqn{}, - }, + Action: sampleActionCreate, + Resources: []*authzV2.Resource{}, }, - expectedValidationError: "resource", + expectedValidationError: "resources", }, { - name: "registered resource FQN is invalid", - request: &authzV2.GetDecisionRequest{ + name: "invalid resource - registered resource FQN is empty", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -291,17 +229,17 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: "invalid format", + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_RegisteredResourceValueFqn{}, }, }, }, expectedValidationError: "resource", }, { - name: "resource attribute value FQNs are empty", - request: &authzV2.GetDecisionRequest{ + name: "invalid resource - empty attribute values", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -311,10 +249,12 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{}, + }, }, }, }, @@ -322,8 +262,8 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { expectedValidationError: "resource", }, { - name: "resource attribute value FQNs are invalid", - request: &authzV2.GetDecisionRequest{ + name: "invalid resource - invalid attribute values", + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -333,10 +273,12 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{"invalid-format"}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{"invalid-format"}, + }, }, }, }, @@ -345,7 +287,7 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, { name: "token entity with empty JWT", - request: &authzV2.GetDecisionRequest{ + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -354,10 +296,12 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, }, }, }, @@ -366,7 +310,7 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, { name: "entity chain with no entities", - request: &authzV2.GetDecisionRequest{ + request: &authzV2.GetDecisionMultiResourceRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_EntityChain{ EntityChain: &entity.EntityChain{ @@ -375,40 +319,60 @@ func Test_GetDecisionRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resource: &authzV2.Resource{ - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, }, }, }, }, expectedValidationError: "entities", }, + { + name: "registered resource as entity with invalid URI", + request: &authzV2.GetDecisionMultiResourceRequest{ + EntityIdentifier: &authzV2.EntityIdentifier{ + Identifier: &authzV2.EntityIdentifier_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: "invalid uri", + }, + }, + Action: sampleActionCreate, + Resources: []*authzV2.Resource{ + { + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, + }, + }, + }, + }, + expectedValidationError: "registered_resource_value_fqn", + }, } +) - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - err := v.Validate(tc.request) - if err == nil { - t.Errorf("expected validation error for request: %s, but got none", tc.name) - } else { - assert.Contains(t, err.Error(), tc.expectedValidationError, "validation error should contain expected message") - } - }) +func getValidator() protovalidate.Validator { + v, err := protovalidate.New() + if err != nil { + panic(err) } + return v } -func Test_GetDecisionMultiResourceRequest_Succeeds(t *testing.T) { +func Test_GetDecisionRequest_Succeeds(t *testing.T) { v := getValidator() cases := []struct { name string - request *authzV2.GetDecisionMultiResourceRequest + request *authzV2.GetDecisionRequest }{ { - name: "entity: token, action: create, multiple resources: attribute values", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "entity: token, action: create, resource: attribute values", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -418,27 +382,37 @@ func Test_GetDecisionMultiResourceRequest_Succeeds(t *testing.T) { }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, }, }, - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN2}, - }, + }, + }, + }, + { + name: "entity: token, action: create, resource: registered", + request: &authzV2.GetDecisionRequest{ + EntityIdentifier: &authzV2.EntityIdentifier{ + Identifier: &authzV2.EntityIdentifier_Token{ + Token: &entity.Token{ + EphemeralId: "123", + Jwt: "sample-jwt-token", }, }, }, + Action: sampleActionCreate, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: sampleRegisteredResourceFQN, + }, + }, }, }, { - name: "entity: chain, action: create, multiple resources: mixed types", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "entity: chain, action: create, resource: attribute values", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_EntityChain{ EntityChain: &entity.EntityChain{ @@ -449,46 +423,84 @@ func Test_GetDecisionMultiResourceRequest_Succeeds(t *testing.T) { EntityType: &entity.Entity_EmailAddress{EmailAddress: "test@test.com"}, Category: entity.Entity_CATEGORY_SUBJECT, }, + { + EphemeralId: "chained-2", + EntityType: &entity.Entity_ClientId{ + ClientId: "client-123", + }, + Category: entity.Entity_CATEGORY_ENVIRONMENT, + }, }, }, }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN, sampleResourceFQN2}, + }, + }, + }, + }, + }, + { + name: "entity: chain, action: create, resource: registered", + request: &authzV2.GetDecisionRequest{ + EntityIdentifier: &authzV2.EntityIdentifier{ + Identifier: &authzV2.EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + EphemeralId: "1234", + Entities: []*entity.Entity{ + { + EphemeralId: "chained-1", + EntityType: &entity.Entity_ClientId{ + ClientId: "client-123", + }, + Category: entity.Entity_CATEGORY_ENVIRONMENT, + }, }, }, }, - { - Resource: &authzV2.Resource_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: sampleRegisteredResourceFQN, - }, + }, + Action: sampleActionCreate, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: sampleRegisteredResourceFQN, }, }, }, }, { - name: "entity: registered resource, action: create, multiple resources: registered", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "entity: registered resource, action: create, resource: attribute values", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_RegisteredResourceValueFqn{ RegisteredResourceValueFqn: sampleRegisteredResourceFQN, }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: sampleRegisteredResourceFQN, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN2}, }, }, - { - Resource: &authzV2.Resource_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: "https://example.com/another/registered/resource", - }, + }, + }, + }, + { + name: "entity: registered resource, action: create, resource: registered", + request: &authzV2.GetDecisionRequest{ + EntityIdentifier: &authzV2.EntityIdentifier{ + Identifier: &authzV2.EntityIdentifier_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: sampleRegisteredResourceFQN, + }, + }, + Action: sampleActionCreate, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: sampleRegisteredResourceFQN, }, }, }, @@ -503,24 +515,21 @@ func Test_GetDecisionMultiResourceRequest_Succeeds(t *testing.T) { } } -func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { +func Test_GetDecisionRequest_Fails(t *testing.T) { v := getValidator() - cases := []struct { name string - request *authzV2.GetDecisionMultiResourceRequest + request *authzV2.GetDecisionRequest expectedValidationError string }{ { name: "missing entity identifier", - request: &authzV2.GetDecisionMultiResourceRequest{ + request: &authzV2.GetDecisionRequest{ Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, }, }, }, @@ -529,7 +538,7 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, { name: "missing action", - request: &authzV2.GetDecisionMultiResourceRequest{ + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -538,12 +547,10 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, }, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, }, }, }, @@ -551,8 +558,8 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { expectedValidationError: "action", }, { - name: "action missing name", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "missing resource", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -561,22 +568,13 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, }, - Action: &policy.Action{}, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, - }, - }, - }, + Action: sampleActionCreate, }, - expectedValidationError: "name", + expectedValidationError: "resource", }, { - name: "missing resources", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "action missing name", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -585,13 +583,20 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, }, - Action: sampleActionCreate, + Action: &policy.Action{}, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, + }, + }, + }, }, - expectedValidationError: "resources", + expectedValidationError: "name", }, { - name: "empty resources array", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "registered resource FQN is empty", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -600,14 +605,16 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, }, - Action: sampleActionCreate, - Resources: []*authzV2.Resource{}, + Action: sampleActionCreate, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_RegisteredResourceValueFqn{}, + }, }, - expectedValidationError: "resources", + expectedValidationError: "resource", }, { - name: "invalid resource - registered resource FQN is empty", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "registered resource FQN is invalid", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -617,17 +624,17 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_RegisteredResourceValueFqn{}, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_RegisteredResourceValueFqn{ + RegisteredResourceValueFqn: "invalid format", }, }, }, expectedValidationError: "resource", }, { - name: "invalid resource - empty attribute values", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "resource attribute value FQNs are empty", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -637,12 +644,10 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{}, - }, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{}, }, }, }, @@ -650,8 +655,8 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { expectedValidationError: "resource", }, { - name: "invalid resource - invalid attribute values", - request: &authzV2.GetDecisionMultiResourceRequest{ + name: "resource attribute value FQNs are invalid", + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -661,12 +666,10 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{"invalid-format"}, - }, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{"invalid-format"}, }, }, }, @@ -675,7 +678,7 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, { name: "token entity with empty JWT", - request: &authzV2.GetDecisionMultiResourceRequest{ + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_Token{ Token: &entity.Token{ @@ -684,12 +687,10 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, }, }, }, @@ -698,7 +699,7 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, { name: "entity chain with no entities", - request: &authzV2.GetDecisionMultiResourceRequest{ + request: &authzV2.GetDecisionRequest{ EntityIdentifier: &authzV2.EntityIdentifier{ Identifier: &authzV2.EntityIdentifier_EntityChain{ EntityChain: &entity.EntityChain{ @@ -707,39 +708,16 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { }, }, Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, + Resource: &authzV2.Resource{ + Resource: &authzV2.Resource_AttributeValues_{ + AttributeValues: &authzV2.Resource_AttributeValues{ + Fqns: []string{sampleResourceFQN}, }, }, }, }, expectedValidationError: "entities", }, - { - name: "registered resource as entity with invalid URI", - request: &authzV2.GetDecisionMultiResourceRequest{ - EntityIdentifier: &authzV2.EntityIdentifier{ - Identifier: &authzV2.EntityIdentifier_RegisteredResourceValueFqn{ - RegisteredResourceValueFqn: "invalid uri", - }, - }, - Action: sampleActionCreate, - Resources: []*authzV2.Resource{ - { - Resource: &authzV2.Resource_AttributeValues_{ - AttributeValues: &authzV2.Resource_AttributeValues{ - Fqns: []string{sampleResourceFQN}, - }, - }, - }, - }, - }, - expectedValidationError: "registered_resource_value_fqn", - }, } for _, tc := range cases { @@ -754,6 +732,111 @@ func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { } } +func Test_GetDecisionMultiResourceRequest_Succeeds(t *testing.T) { + v := getValidator() + + for _, tc := range goodMultiResourceRequests { + t.Run(tc.name, func(t *testing.T) { + err := v.Validate(tc.request) + require.NoError(t, err, "validation should succeed for request: %s", tc.name) + }) + } +} + +func Test_GetDecisionMultiResourceRequest_Fails(t *testing.T) { + v := getValidator() + + for _, tc := range badMultiResourceRequests { + t.Run(tc.name, func(t *testing.T) { + err := v.Validate(tc.request) + if err == nil { + t.Errorf("expected validation error for request: %s, but got none", tc.name) + } else { + assert.Contains(t, err.Error(), tc.expectedValidationError, "validation error should contain expected message") + } + }) + } +} + +func Test_GetDecisionBulkRequest_Succeeds(t *testing.T) { + v := getValidator() + + cases := make([]*authzV2.GetDecisionBulkRequest, 5) + + for i := range 5 { + // Randomly pick two good multi-resource requests, repeated at least one time each, and combine into a bulk request + firstReq := rand.Intn(len(goodMultiResourceRequests)) + firstCount := rand.Intn(10) + 1 + + secondReq := rand.Intn(len(goodMultiResourceRequests)) + secondCount := rand.Intn(10) + 1 + + actions := []string{"create", "read", "update", "delete", "custom_1", "CUSTOM-2"} + + reqs := make([]*authzV2.GetDecisionMultiResourceRequest, firstCount+secondCount) + for j := range firstCount { + originalReq := goodMultiResourceRequests[firstReq].request + clonedReq, _ := proto.Clone(originalReq).(*authzV2.GetDecisionMultiResourceRequest) + clonedReq.Action = &policy.Action{ + Name: actions[rand.Intn(len(actions))], + } + reqs[j] = clonedReq + } + for j := firstCount; j < firstCount+secondCount; j++ { + originalReq := goodMultiResourceRequests[secondReq].request + clonedReq, _ := proto.Clone(originalReq).(*authzV2.GetDecisionMultiResourceRequest) + clonedReq.Action = &policy.Action{ + Name: actions[rand.Intn(len(actions))], + } + reqs[j] = clonedReq + } + + cases[i] = &authzV2.GetDecisionBulkRequest{ + DecisionRequests: reqs, + } + } + + for _, testReq := range cases { + err := v.Validate(testReq) + require.NoError(t, err) + } +} + +func Test_GetDecisionBulkRequest_Fails(t *testing.T) { + v := getValidator() + + cases := make([]struct { + name string + request *authzV2.GetDecisionBulkRequest + }, len(badMultiResourceRequests)) + + goodRequests := make([]*authzV2.GetDecisionMultiResourceRequest, len(goodMultiResourceRequests)) + for i, goodReq := range goodMultiResourceRequests { + goodRequests[i] = goodReq.request + } + + for i, badReq := range badMultiResourceRequests { + requests := make([]*authzV2.GetDecisionMultiResourceRequest, 0, len(goodRequests)+1) + requests = append(requests, goodRequests...) + requests = append(requests, badReq.request) + + cases[i] = struct { + name string + request *authzV2.GetDecisionBulkRequest + }{ + badReq.name, + &authzV2.GetDecisionBulkRequest{ + DecisionRequests: requests, + }, + } + } + + for _, testReq := range cases { + err := v.Validate(testReq.request) + require.Error(t, err, "validation should fail for request: %s", testReq.name) + } +} + func Test_GetEntitlementsRequest_Succeeds(t *testing.T) { v := getValidator() @@ -1021,7 +1104,7 @@ func Test_RollupSingleResourceDecision(t *testing.T) { } } -func Test_RollupMultiResourceDecision(t *testing.T) { +func Test_RollupMultiResourceDecisions(t *testing.T) { tests := []struct { name string decisions []*access.Decision @@ -1197,7 +1280,7 @@ func Test_RollupMultiResourceDecision(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - result, err := rollupMultiResourceDecision(tc.decisions) + result, err := rollupMultiResourceDecisions(tc.decisions) if tc.expectedError != nil { require.Error(t, err) @@ -1211,7 +1294,7 @@ func Test_RollupMultiResourceDecision(t *testing.T) { } } -func Test_RollupMultiResourceDecision_Simple(t *testing.T) { +func Test_RollupMultiResourceDecisions_Simple(t *testing.T) { // This test checks the minimal viable structure to pass through rollupMultiResourceDecision decision := &access.Decision{ Results: []access.ResourceDecision{ @@ -1224,7 +1307,7 @@ func Test_RollupMultiResourceDecision_Simple(t *testing.T) { decisions := []*access.Decision{decision} - result, err := rollupMultiResourceDecision(decisions) + result, err := rollupMultiResourceDecisions(decisions) require.NoError(t, err) assert.Len(t, result, 1) @@ -1232,17 +1315,17 @@ func Test_RollupMultiResourceDecision_Simple(t *testing.T) { assert.Equal(t, authzV2.Decision_DECISION_PERMIT, result[0].GetDecision()) } -func Test_RollupMultiResourceDecision_WithNilChecks(t *testing.T) { +func Test_RollupMultiResourceDecisions_WithNilChecks(t *testing.T) { t.Run("nil decisions array", func(t *testing.T) { var decisions []*access.Decision - _, err := rollupMultiResourceDecision(decisions) + _, err := rollupMultiResourceDecisions(decisions) require.Error(t, err) assert.Contains(t, err.Error(), "no decisions returned") }) t.Run("nil decision in array", func(t *testing.T) { decisions := []*access.Decision{nil} - _, err := rollupMultiResourceDecision(decisions) + _, err := rollupMultiResourceDecisions(decisions) require.Error(t, err) assert.Contains(t, err.Error(), "nil decision at index 0") }) @@ -1254,7 +1337,7 @@ func Test_RollupMultiResourceDecision_WithNilChecks(t *testing.T) { Results: nil, }, } - _, err := rollupMultiResourceDecision(decisions) + _, err := rollupMultiResourceDecisions(decisions) require.Error(t, err) assert.Contains(t, err.Error(), "no decision results returned") }) diff --git a/service/authorization/v2/helpers.go b/service/authorization/v2/helpers.go new file mode 100644 index 0000000000..0565a63e34 --- /dev/null +++ b/service/authorization/v2/helpers.go @@ -0,0 +1,94 @@ +package authorization + +import ( + "context" + "errors" + "fmt" + + "connectrpc.com/connect" + authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/service/internal/access/v2" + "github.com/opentdf/platform/service/logger" +) + +// rollupMultiResourceDecisions creates a standardized response for multi-resource decisions +// by processing the decisions returned from the PDP. +func rollupMultiResourceDecisions( + decisions []*access.Decision, +) ([]*authzV2.ResourceDecision, error) { + if len(decisions) == 0 { + return nil, errors.New("no decisions returned") + } + + var resourceDecisions []*authzV2.ResourceDecision + + for idx, decision := range decisions { + if decision == nil { + return nil, fmt.Errorf("nil decision at index %d", idx) + } + if len(decision.Results) == 0 { + return nil, errors.New("no decision results returned") + } + for _, result := range decision.Results { + access := authzV2.Decision_DECISION_DENY + if result.Passed { + access = authzV2.Decision_DECISION_PERMIT + } + resourceDecision := &authzV2.ResourceDecision{ + Decision: access, + EphemeralResourceId: result.ResourceID, + } + resourceDecisions = append(resourceDecisions, resourceDecision) + } + } + + return resourceDecisions, nil +} + +// rollupSingleResourceDecision creates a standardized response for a single resource decision +// by processing the decision returned from the PDP. +func rollupSingleResourceDecision( + permitted bool, + decisions []*access.Decision, +) (*authzV2.GetDecisionResponse, error) { + if len(decisions) == 0 { + return nil, errors.New("no decisions returned") + } + + decision := decisions[0] + if decision == nil { + return nil, errors.New("nil decision at index 0") + } + + if len(decision.Results) == 0 { + return nil, errors.New("no decision results returned") + } + + result := decision.Results[0] + access := authzV2.Decision_DECISION_DENY + if permitted { + access = authzV2.Decision_DECISION_PERMIT + } + resourceDecision := &authzV2.ResourceDecision{ + Decision: access, + EphemeralResourceId: result.ResourceID, + } + return &authzV2.GetDecisionResponse{ + Decision: resourceDecision, + }, nil +} + +// Checks for known error types and returns standardized error codes and messages +func statusifyError(ctx context.Context, l *logger.Logger, err error, logs ...any) error { + l = l.With("error", err.Error()) + if errors.Is(err, access.ErrFQNNotFound) { + l.ErrorContext(ctx, "FQN not found", logs...) + return connect.NewError(connect.CodeNotFound, err) + } + if errors.Is(err, access.ErrDefinitionNotFound) { + l.ErrorContext(ctx, "definition not found", logs...) + return connect.NewError(connect.CodeNotFound, err) + } + l.ErrorContext(ctx, "unexpected error", logs...) + return connect.NewError(connect.CodeInternal, err) +} diff --git a/service/internal/access/v2/evaluate.go b/service/internal/access/v2/evaluate.go index eb329e1b5a..d7f8d625e9 100644 --- a/service/internal/access/v2/evaluate.go +++ b/service/internal/access/v2/evaluate.go @@ -16,7 +16,7 @@ import ( var ( ErrInvalidResource = errors.New("access: invalid resource") - ErrFQNNotFound = errors.New("access: attribute value FQN not found in memory") + ErrFQNNotFound = errors.New("access: attribute value FQN not found") ErrDefinitionNotFound = errors.New("access: definition not found for FQN") ErrFailedEvaluation = errors.New("access: failed to evaluate definition") ErrMissingRequiredSpecifiedRule = errors.New("access: AttributeDefinition rule cannot be unspecified") diff --git a/service/internal/access/v2/pdp.go b/service/internal/access/v2/pdp.go index 1078c12d76..4289c2fc3e 100644 --- a/service/internal/access/v2/pdp.go +++ b/service/internal/access/v2/pdp.go @@ -195,7 +195,7 @@ func (p *PolicyDecisionPoint) GetDecision( attributeAndValue, ok := p.allEntitleableAttributesByValueFQN[valueFQN] if !ok { - return nil, fmt.Errorf("resource value FQN not found in memory [%s]: %w", valueFQN, ErrInvalidResource) + return nil, fmt.Errorf("%w [%s]: %w", ErrFQNNotFound, valueFQN, ErrInvalidResource) } decisionableAttributes[valueFQN] = attributeAndValue