Skip to content

Commit 58cb3f6

Browse files
authored
fix(authz): handle pagination in authz service (opentdf#1797)
### Proposed Changes * Handle pagination limit/offset in `authorization.GetEntitlements` * If quantity of attributes or subject mappings exceeds default "list" API limit as configured, we need to retrieve all to make accurate entitlement decision ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions
1 parent 7c4c74f commit 58cb3f6

File tree

2 files changed

+184
-10
lines changed

2 files changed

+184
-10
lines changed

service/authorization/authorization.go

+49-10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/open-policy-agent/opa/rego"
1717
"github.com/opentdf/platform/protocol/go/authorization"
1818
"github.com/opentdf/platform/protocol/go/authorization/authorizationconnect"
19+
"github.com/opentdf/platform/protocol/go/common"
1920
"github.com/opentdf/platform/protocol/go/entityresolution"
2021
"github.com/opentdf/platform/protocol/go/policy"
2122
attr "github.com/opentdf/platform/protocol/go/policy/attributes"
@@ -444,22 +445,60 @@ func makeScopeMap(scope *authorization.ResourceAttribute) map[string]bool {
444445

445446
func (as *AuthorizationService) GetEntitlements(ctx context.Context, req *connect.Request[authorization.GetEntitlementsRequest]) (*connect.Response[authorization.GetEntitlementsResponse], error) {
446447
as.logger.DebugContext(ctx, "getting entitlements")
447-
attrsRes, err := as.sdk.Attributes.ListAttributes(ctx, &attr.ListAttributesRequest{})
448-
if err != nil {
449-
as.logger.ErrorContext(ctx, "failed to list attributes", slog.String("error", err.Error()))
450-
return nil, connect.NewError(connect.CodeInternal, errors.New("failed to list attributes"))
448+
449+
var nextOffset int32
450+
attrsList := make([]*policy.Attribute, 0)
451+
subjectMappingsList := make([]*policy.SubjectMapping, 0)
452+
453+
// If quantity of attributes exceeds maximum list pagination, all are needed to determine entitlements
454+
for {
455+
listed, err := as.sdk.Attributes.ListAttributes(ctx, &attr.ListAttributesRequest{
456+
State: common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE,
457+
Pagination: &policy.PageRequest{
458+
Offset: nextOffset,
459+
},
460+
})
461+
if err != nil {
462+
as.logger.ErrorContext(ctx, "failed to list attributes", slog.String("error", err.Error()))
463+
return nil, connect.NewError(connect.CodeInternal, errors.New("failed to list attributes"))
464+
}
465+
466+
nextOffset = listed.GetPagination().GetNextOffset()
467+
attrsList = append(attrsList, listed.GetAttributes()...)
468+
469+
// offset becomes zero when list is exhausted
470+
if nextOffset <= 0 {
471+
break
472+
}
451473
}
452-
subMapsRes, err := as.sdk.SubjectMapping.ListSubjectMappings(ctx, &subjectmapping.ListSubjectMappingsRequest{})
453-
if err != nil {
454-
as.logger.ErrorContext(ctx, "failed to list subject mappings", slog.String("error", err.Error()))
455-
return nil, connect.NewError(connect.CodeInternal, errors.New("failed to list subject mappings"))
474+
475+
// If quantity of subject mappings exceeds maximum list pagination, all are needed to determine entitlements
476+
nextOffset = 0
477+
for {
478+
listed, err := as.sdk.SubjectMapping.ListSubjectMappings(ctx, &subjectmapping.ListSubjectMappingsRequest{
479+
Pagination: &policy.PageRequest{
480+
Offset: nextOffset,
481+
},
482+
})
483+
if err != nil {
484+
as.logger.ErrorContext(ctx, "failed to list subject mappings", slog.String("error", err.Error()))
485+
return nil, connect.NewError(connect.CodeInternal, errors.New("failed to list subject mappings"))
486+
}
487+
488+
nextOffset = listed.GetPagination().GetNextOffset()
489+
subjectMappingsList = append(subjectMappingsList, listed.GetSubjectMappings()...)
490+
491+
// offset becomes zero when list is exhausted
492+
if nextOffset <= 0 {
493+
break
494+
}
456495
}
457496
// create a lookup map of attribute value FQNs (based on request scope)
458497
scopeMap := makeScopeMap(req.Msg.GetScope())
459498
// create a lookup map of subject mappings by attribute value ID
460-
subMapsByVal := makeSubMapsByValLookup(subMapsRes.GetSubjectMappings())
499+
subMapsByVal := makeSubMapsByValLookup(subjectMappingsList)
461500
// create a lookup map of attribute values by FQN (for rego query)
462-
fqnAttrVals := makeValsByFqnsLookup(attrsRes.GetAttributes(), subMapsByVal, scopeMap)
501+
fqnAttrVals := makeValsByFqnsLookup(attrsList, subMapsByVal, scopeMap)
463502
avf := &attr.GetAttributeValuesByFqnsResponse{
464503
FqnAttributeValues: fqnAttrVals,
465504
}

service/authorization/authorization_test.go

+135
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ type mySubjectMappingClient struct {
5757
sm.SubjectMappingServiceClient
5858
}
5959

60+
type paginatedMockSubjectMappingClient struct {
61+
sm.SubjectMappingServiceClient
62+
}
63+
6064
func (*mySubjectMappingClient) ListSubjectMappings(_ context.Context, _ *sm.ListSubjectMappingsRequest, _ ...grpc.CallOption) (*sm.ListSubjectMappingsResponse, error) {
6165
return &listSubjectMappings, nil
6266
}
@@ -69,6 +73,52 @@ func (*myERSClient) ResolveEntities(_ context.Context, _ *entityresolution.Resol
6973
return &resolveEntitiesResp, nil
7074
}
7175

76+
var (
77+
smPaginationOffset = 3
78+
smListCallCount = 0
79+
)
80+
81+
func (*paginatedMockSubjectMappingClient) ListSubjectMappings(_ context.Context, _ *sm.ListSubjectMappingsRequest, _ ...grpc.CallOption) (*sm.ListSubjectMappingsResponse, error) {
82+
smListCallCount++
83+
// simulate paginated list and policy LIST behavior
84+
if smPaginationOffset > 0 {
85+
rsp := &sm.ListSubjectMappingsResponse{
86+
SubjectMappings: nil,
87+
Pagination: &policy.PageResponse{
88+
NextOffset: int32(smPaginationOffset),
89+
},
90+
}
91+
smPaginationOffset = 0
92+
return rsp, nil
93+
}
94+
return &listSubjectMappings, nil
95+
}
96+
97+
type paginatedMockAttributesClient struct {
98+
attr.AttributesServiceClient
99+
}
100+
101+
var (
102+
attrPaginationOffset = 3
103+
attrListCallCount = 0
104+
)
105+
106+
func (*paginatedMockAttributesClient) ListAttributes(_ context.Context, _ *attr.ListAttributesRequest, _ ...grpc.CallOption) (*attr.ListAttributesResponse, error) {
107+
attrListCallCount++
108+
// simulate paginated list and policy LIST behavior
109+
if attrPaginationOffset > 0 {
110+
rsp := &attr.ListAttributesResponse{
111+
Attributes: nil,
112+
Pagination: &policy.PageResponse{
113+
NextOffset: int32(attrPaginationOffset),
114+
},
115+
}
116+
attrPaginationOffset = 0
117+
return rsp, nil
118+
}
119+
return &listAttributeResp, nil
120+
}
121+
72122
func TestGetComprehensiveHierarchy(t *testing.T) {
73123
as := &AuthorizationService{
74124
logger: logger.CreateTestLogger(),
@@ -763,6 +813,91 @@ func Test_GetEntitlementsFqnCasing(t *testing.T) {
763813
assert.Equal(t, []string{"https://www.example.org/attr/foo/value/value1"}, resp.Msg.GetEntitlements()[0].GetAttributeValueFqns())
764814
}
765815

816+
func Test_GetEntitlements_HandlesPagination(t *testing.T) {
817+
logger := logger.CreateTestLogger()
818+
819+
listAttributeResp = attr.ListAttributesResponse{}
820+
attrDef := policy.Attribute{
821+
Name: mockAttrName,
822+
Namespace: &policy.Namespace{
823+
Name: mockNamespace,
824+
},
825+
Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF,
826+
Values: []*policy.Value{
827+
{
828+
Value: mockAttrValue1,
829+
},
830+
{
831+
Value: mockAttrValue2,
832+
},
833+
},
834+
}
835+
listAttributeResp.Attributes = []*policy.Attribute{&attrDef}
836+
userRepresentation := map[string]interface{}{
837+
"A": "B",
838+
"C": "D",
839+
}
840+
userStruct, _ := structpb.NewStruct(userRepresentation)
841+
resolveEntitiesResp = entityresolution.ResolveEntitiesResponse{
842+
EntityRepresentations: []*entityresolution.EntityRepresentation{
843+
{
844+
OriginalId: "e1",
845+
AdditionalProps: []*structpb.Struct{
846+
userStruct,
847+
},
848+
},
849+
},
850+
}
851+
852+
ctxb := context.Background()
853+
854+
rego := rego.New(
855+
rego.Query("data.example.p"),
856+
rego.Module("example.rego",
857+
`package example
858+
p = {"e1":["https://www.example.org/attr/foo/value/value1"]} { true }`,
859+
))
860+
861+
// Run evaluation.
862+
prepared, err := rego.PrepareForEval(ctxb)
863+
require.NoError(t, err)
864+
865+
as := AuthorizationService{
866+
logger: logger, sdk: &otdf.SDK{
867+
SubjectMapping: &paginatedMockSubjectMappingClient{},
868+
Attributes: &paginatedMockAttributesClient{},
869+
EntityResoution: &myERSClient{},
870+
},
871+
eval: prepared,
872+
}
873+
874+
req := connect.Request[authorization.GetEntitlementsRequest]{
875+
Msg: &authorization.GetEntitlementsRequest{
876+
Entities: []*authorization.Entity{{Id: "e1", EntityType: &authorization.Entity_ClientId{ClientId: "testclient"}, Category: authorization.Entity_CATEGORY_ENVIRONMENT}},
877+
// Using mixed case here
878+
Scope: &authorization.ResourceAttribute{AttributeValueFqns: []string{"https://www.example.org/attr/foo/value/VaLuE1"}},
879+
},
880+
}
881+
882+
for fqn := range makeScopeMap(req.Msg.GetScope()) {
883+
assert.Equal(t, fqn, strings.ToLower(fqn))
884+
}
885+
886+
resp, err := as.GetEntitlements(ctxb, &req)
887+
888+
require.NoError(t, err)
889+
assert.NotNil(t, resp)
890+
assert.Len(t, resp.Msg.GetEntitlements(), 1)
891+
assert.Equal(t, "e1", resp.Msg.GetEntitlements()[0].GetEntityId())
892+
assert.Equal(t, []string{"https://www.example.org/attr/foo/value/value1"}, resp.Msg.GetEntitlements()[0].GetAttributeValueFqns())
893+
894+
// paginated successfully
895+
assert.Equal(t, 2, smListCallCount)
896+
assert.Zero(t, smPaginationOffset)
897+
assert.Equal(t, 2, attrListCallCount)
898+
assert.Zero(t, attrPaginationOffset)
899+
}
900+
766901
func Test_GetEntitlementsWithComprehensiveHierarchy(t *testing.T) {
767902
logger := logger.CreateTestLogger()
768903
attrDef := policy.Attribute{

0 commit comments

Comments
 (0)