From 5720e3cca977801572cb9f9c061ad771dfaba2a7 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Wed, 5 Aug 2026 11:52:39 -0400 Subject: [PATCH 1/4] add hint on v1 --- service/authorization/authorization.go | 8 +++ service/authorization/authorization_test.go | 68 +++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/service/authorization/authorization.go b/service/authorization/authorization.go index 9d159c93fc..f666d0846e 100644 --- a/service/authorization/authorization.go +++ b/service/authorization/authorization.go @@ -728,6 +728,14 @@ func retrieveAttributeDefinitions(ctx context.Context, attrFqns []string, sdk *o Fqns: attrFqns, }) if err != nil { + // A resource_exhausted here means the attribute definitions for the requested FQNs + // exceed the max message size (e.g. an attribute with a very large number of values). + // v1 cannot page this internal load; point the caller at v2, which can. See #3821. + if connect.CodeOf(err) == connect.CodeResourceExhausted { + return nil, connect.NewError(connect.CodeResourceExhausted, fmt.Errorf( + "attribute definitions for the requested FQNs are too large for the v1 authorization API; "+ + "upgrade to the v2 authorization API (authorization.v2.AuthorizationService): %w", err)) + } return nil, err } // If `allow_traversal` is true for an attribute definition diff --git a/service/authorization/authorization_test.go b/service/authorization/authorization_test.go index 775165da7a..22b07f161b 100644 --- a/service/authorization/authorization_test.go +++ b/service/authorization/authorization_test.go @@ -1283,6 +1283,74 @@ func Test_GetDecisions_RA_FQN_Edge_Cases(t *testing.T) { assert.Equal(t, authorization.DecisionResponse_DECISION_PERMIT, resp.Msg.GetDecisionResponses()[0].GetDecision()) } +// When the attribute-definition load fails with resource_exhausted (an attribute with a very +// large number of values, #3821), the v1 GetDecisions error should tell the caller to upgrade +// to v2, while preserving the resource_exhausted code and the underlying message. +func Test_GetDecisions_ResourceExhausted_HintsV2(t *testing.T) { + logger := logger.CreateTestLogger() + + listAttributeResp = attr.ListAttributesResponse{} + resolveEntitiesResp = entityresolution.ResolveEntitiesResponse{ + EntityRepresentations: []*entityresolution.EntityRepresentation{{OriginalId: "e1"}}, + } + + testrego := rego.New( + rego.SetRegoVersion(ast.RegoV0), + rego.Query("data.example.p"), + rego.Module("example.rego", + `package example + p = {"e1":[]} { true }`, + )) + prepared, err := testrego.PrepareForEval(t.Context()) + require.NoError(t, err) + + as := AuthorizationService{ + logger: logger, + sdk: &otdf.SDK{ + SubjectMapping: &mySubjectMappingClient{}, + Attributes: &myAttributesClient{}, + EntityResoution: &myERSClient{}, + }, + eval: prepared, + Tracer: noop.NewTracerProvider().Tracer(""), + } + + getAttributesByValueFqnsResponse = attr.GetAttributeValuesByFqnsResponse{} + errGetAttributesByValueFqns = connect.NewError(connect.CodeResourceExhausted, errors.New("message size 8205724 is larger than configured max 4194304")) + + req := connect.Request[authorization.GetDecisionsRequest]{ + Msg: &authorization.GetDecisionsRequest{ + DecisionRequests: []*authorization.DecisionRequest{ + { + Actions: []*policy.Action{}, + EntityChains: []*authorization.EntityChain{ + { + Id: "ec1", + Entities: []*authorization.Entity{ + {Id: "e1", EntityType: &authorization.Entity_UserName{UserName: "bob.smith"}, Category: authorization.Entity_CATEGORY_SUBJECT}, + }, + }, + }, + ResourceAttributes: []*authorization.ResourceAttribute{ + {AttributeValueFqns: []string{"https://example.com/attr/foo/value/value1"}}, + }, + }, + }, + }, + } + + ctx := audit.ContextWithActorID(t.Context(), "test-actor-id") + resp, err := as.GetDecisions(ctx, &req) + + require.Error(t, err) + assert.Nil(t, resp) + assert.Equal(t, connect.CodeResourceExhausted, connect.CodeOf(err), "resource_exhausted code should be preserved") + assert.Contains(t, err.Error(), "v2", "error should point the caller to the v2 authorization API") + assert.Contains(t, err.Error(), "4194304", "underlying limit message should be preserved") + + errGetAttributesByValueFqns = nil +} + func Test_GetDecisionsAllOf_Pass_EC_RA_Length_Mismatch(t *testing.T) { logger := logger.CreateTestLogger() From 6163ea3f4567251ef6dc05a415b6d699ad7f5668 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Wed, 5 Aug 2026 12:33:26 -0400 Subject: [PATCH 2/4] preserve code --- service/authorization/authorization.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/service/authorization/authorization.go b/service/authorization/authorization.go index f666d0846e..28bbae6b59 100644 --- a/service/authorization/authorization.go +++ b/service/authorization/authorization.go @@ -516,6 +516,11 @@ func (as *AuthorizationService) getDecisions(ctx context.Context, dr *authorizat if err == nil { dataAttrDefsAndVals, err = retrieveAttributeDefinitions(ctx, allPertinentFQNS.GetAttributeValueFqns(), as.sdk) } + // Preserve the resource_exhausted code and v2 upgrade hint from retrieveAttributeDefinitions + // rather than flattening it to a generic internal error (#3821). + if err != nil && connect.CodeOf(err) == connect.CodeResourceExhausted { + return nil, err + } if err != nil { // if attribute an FQN does not exist // return deny for all entity chains aginst this RAs From 075335704f441f7936dda88da57ad27c5a532f06 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Wed, 5 Aug 2026 17:08:27 -0400 Subject: [PATCH 3/4] add warning --- service/authorization/authorization.go | 16 +++++----------- service/authorization/authorization_test.go | 3 +-- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/service/authorization/authorization.go b/service/authorization/authorization.go index 28bbae6b59..0ff99d24c4 100644 --- a/service/authorization/authorization.go +++ b/service/authorization/authorization.go @@ -516,10 +516,12 @@ func (as *AuthorizationService) getDecisions(ctx context.Context, dr *authorizat if err == nil { dataAttrDefsAndVals, err = retrieveAttributeDefinitions(ctx, allPertinentFQNS.GetAttributeValueFqns(), as.sdk) } - // Preserve the resource_exhausted code and v2 upgrade hint from retrieveAttributeDefinitions - // rather than flattening it to a generic internal error (#3821). + // A resource_exhausted here means the attribute definitions for the requested FQNs are too large + // for v1 to page; surface the v2 upgrade hint. All other errors keep the generic fallback. (#3821) if err != nil && connect.CodeOf(err) == connect.CodeResourceExhausted { - return nil, err + return nil, db.StatusifyError(ctx, as.logger, err, + db.ErrTextGetRetrievalFailed+": attribute definitions for the requested FQNs are too large for the v1 authorization API; upgrade to the v2 authorization API (authorization.v2.AuthorizationService)", + slog.String("fqns", strings.Join(allPertinentFQNS.GetAttributeValueFqns(), ", "))) } if err != nil { // if attribute an FQN does not exist @@ -733,14 +735,6 @@ func retrieveAttributeDefinitions(ctx context.Context, attrFqns []string, sdk *o Fqns: attrFqns, }) if err != nil { - // A resource_exhausted here means the attribute definitions for the requested FQNs - // exceed the max message size (e.g. an attribute with a very large number of values). - // v1 cannot page this internal load; point the caller at v2, which can. See #3821. - if connect.CodeOf(err) == connect.CodeResourceExhausted { - return nil, connect.NewError(connect.CodeResourceExhausted, fmt.Errorf( - "attribute definitions for the requested FQNs are too large for the v1 authorization API; "+ - "upgrade to the v2 authorization API (authorization.v2.AuthorizationService): %w", err)) - } return nil, err } // If `allow_traversal` is true for an attribute definition diff --git a/service/authorization/authorization_test.go b/service/authorization/authorization_test.go index 22b07f161b..117b840bcc 100644 --- a/service/authorization/authorization_test.go +++ b/service/authorization/authorization_test.go @@ -1344,9 +1344,8 @@ func Test_GetDecisions_ResourceExhausted_HintsV2(t *testing.T) { require.Error(t, err) assert.Nil(t, resp) - assert.Equal(t, connect.CodeResourceExhausted, connect.CodeOf(err), "resource_exhausted code should be preserved") + assert.Equal(t, connect.CodeInternal, connect.CodeOf(err), "resource_exhausted is surfaced as internal") assert.Contains(t, err.Error(), "v2", "error should point the caller to the v2 authorization API") - assert.Contains(t, err.Error(), "4194304", "underlying limit message should be preserved") errGetAttributesByValueFqns = nil } From 2fbed53ed4d2de7aa1c11a286498b75e3c50a7d5 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Wed, 5 Aug 2026 17:16:08 -0400 Subject: [PATCH 4/4] address opencode comment --- service/authorization/authorization_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/authorization/authorization_test.go b/service/authorization/authorization_test.go index 117b840bcc..ae50f085f0 100644 --- a/service/authorization/authorization_test.go +++ b/service/authorization/authorization_test.go @@ -1317,6 +1317,8 @@ func Test_GetDecisions_ResourceExhausted_HintsV2(t *testing.T) { getAttributesByValueFqnsResponse = attr.GetAttributeValuesByFqnsResponse{} errGetAttributesByValueFqns = connect.NewError(connect.CodeResourceExhausted, errors.New("message size 8205724 is larger than configured max 4194304")) + // Reset the shared mock error even if an assertion below aborts the test. + t.Cleanup(func() { errGetAttributesByValueFqns = nil }) req := connect.Request[authorization.GetDecisionsRequest]{ Msg: &authorization.GetDecisionsRequest{ @@ -1346,8 +1348,6 @@ func Test_GetDecisions_ResourceExhausted_HintsV2(t *testing.T) { assert.Nil(t, resp) assert.Equal(t, connect.CodeInternal, connect.CodeOf(err), "resource_exhausted is surfaced as internal") assert.Contains(t, err.Error(), "v2", "error should point the caller to the v2 authorization API") - - errGetAttributesByValueFqns = nil } func Test_GetDecisionsAllOf_Pass_EC_RA_Length_Mismatch(t *testing.T) {