fix(api): implement missing filters on ListAdministrators endpoint (#11319)

# Which Problems Are Solved

A customer reported that multiple filters on the ListAdministrators
endpoint were returning a `List.Query.Invalid` error.
They were missing the implementation.

# How the Problems Are Solved

Implement `and`, `or`, `not` and `role` filters.

# Additional Changes

None

# Additional Context

- reported by customer
- requires backport to v4.x
This commit is contained in:
Livio Spring
2026-01-12 06:22:34 +00:00
committed by GitHub
parent 049622720c
commit 4b6add514f
4 changed files with 261 additions and 6 deletions
@@ -584,6 +584,117 @@ func TestServer_ListAdministrators(t *testing.T) {
Administrators: []*internal_permission.Administrator{projectGrantAdministrator1, projectGrantAdministrator2},
},
},
{
name: "list, role filter",
args: args{
ctx: iamOwnerCtx,
req: &internal_permission.ListAdministratorsRequest{
Filters: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_Role{
Role: &internal_permission.RoleFilter{
RoleKey: "PROJECT_OWNER",
},
},
},
},
Pagination: &filter.PaginationRequest{Asc: true, Limit: 2},
},
},
want: &internal_permission.ListAdministratorsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 4, // 3 project admins with PROJECT_OWNER role and the project creator
AppliedLimit: 2,
},
Administrators: []*internal_permission.Administrator{projectAdministrator1, projectAdministrator2},
},
},
{
name: "list, and with not filter",
args: args{
ctx: iamOwnerCtx,
req: &internal_permission.ListAdministratorsRequest{
Filters: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_And{
And: &internal_permission.AndFilter{
Queries: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_Role{
Role: &internal_permission.RoleFilter{
RoleKey: "PROJECT_OWNER",
},
},
},
{
Filter: &internal_permission.AdministratorSearchFilter_Not{
Not: &internal_permission.NotFilter{
Query: &internal_permission.AdministratorSearchFilter{
Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{
InUserIdsFilter: &filter.InIDsFilter{
Ids: []string{projectAdministrator1.GetUser().GetId(), projectAdministrator2.GetUser().GetId()},
},
},
},
},
},
},
},
},
},
},
},
Pagination: &filter.PaginationRequest{Asc: true, Limit: 1},
},
},
want: &internal_permission.ListAdministratorsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 2, // 1 project admin and the project creator
AppliedLimit: 1,
},
Administrators: []*internal_permission.Administrator{projectAdministrator3},
},
},
{
name: "list, or filter",
args: args{
ctx: iamOwnerCtx,
req: &internal_permission.ListAdministratorsRequest{
Filters: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_Or{
Or: &internal_permission.OrFilter{
Queries: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{
InUserIdsFilter: &filter.InIDsFilter{
Ids: []string{projectGrantAdministrator1.GetUser().GetId()},
},
},
},
{
Filter: &internal_permission.AdministratorSearchFilter_Role{
Role: &internal_permission.RoleFilter{
RoleKey: "PROJECT_OWNER",
},
},
},
},
},
},
},
},
Pagination: &filter.PaginationRequest{Asc: true, Limit: 2},
},
},
want: &internal_permission.ListAdministratorsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 5, // 3 project grant admins, the project creator and the project grant admin
AppliedLimit: 2,
},
Administrators: []*internal_permission.Administrator{projectAdministrator1, projectAdministrator2},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -766,7 +877,6 @@ func TestServer_ListAdministrators_PermissionV2(t *testing.T) {
instancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedOrganizationResp.GetOrganizationId(), userProjectGrantResp.GetUserId())
patProjectGrantResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId())
projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token)
type args struct {
ctx context.Context
dep func(*internal_permission.ListAdministratorsRequest, *internal_permission.ListAdministratorsResponse)
@@ -33,7 +33,7 @@ func (s *Server) listAdministratorsRequestToModel(req *internal_permission.ListA
if err != nil {
return nil, err
}
queries, err := administratorSearchFiltersToQuery(req.Filters)
queries, err := administratorSearchFiltersToQuery(req.Filters, 0) // start at level 0
if err != nil {
return nil, err
}
@@ -63,10 +63,10 @@ func administratorFieldNameToSortingColumn(field internal_permission.Administrat
}
}
func administratorSearchFiltersToQuery(queries []*internal_permission.AdministratorSearchFilter) (_ []query.SearchQuery, err error) {
func administratorSearchFiltersToQuery(queries []*internal_permission.AdministratorSearchFilter, level uint8) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, qry := range queries {
q[i], err = administratorFilterToModel(qry)
q[i], err = administratorFilterToQuery(qry, level)
if err != nil {
return nil, err
}
@@ -74,7 +74,11 @@ func administratorSearchFiltersToQuery(queries []*internal_permission.Administra
return q, nil
}
func administratorFilterToModel(filter *internal_permission.AdministratorSearchFilter) (query.SearchQuery, error) {
func administratorFilterToQuery(filter *internal_permission.AdministratorSearchFilter, level uint8) (query.SearchQuery, error) {
if level > 20 {
// can't go deeper than 20 levels of nesting.
return nil, zerrors.ThrowInvalidArgument(nil, "PERM-zsQ97", "Errors.Query.TooManyNestingLevels")
}
switch q := filter.Filter.(type) {
case *internal_permission.AdministratorSearchFilter_InUserIdsFilter:
return inUserIDsFilterToQuery(q.InUserIdsFilter)
@@ -90,8 +94,16 @@ func administratorFilterToModel(filter *internal_permission.AdministratorSearchF
return userDisplayNameFilterToQuery(q.UserDisplayName)
case *internal_permission.AdministratorSearchFilter_Resource:
return resourceFilterToQuery(q.Resource)
case *internal_permission.AdministratorSearchFilter_Role:
return roleFilterToQuery(q.Role)
case *internal_permission.AdministratorSearchFilter_And:
return andQueryToQuery(q.And, level)
case *internal_permission.AdministratorSearchFilter_Or:
return orQueryToQuery(q.Or, level)
case *internal_permission.AdministratorSearchFilter_Not:
return notQueryToQuery(q.Not, level)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid")
return nil, zerrors.ThrowInvalidArgument(nil, "PERM-vR9nC", "List.Query.Invalid")
}
}
@@ -151,6 +163,32 @@ func resourceFilterToQuery(q *internal_permission.ResourceFilter) (query.SearchQ
return nil, nil
}
func roleFilterToQuery(q *internal_permission.RoleFilter) (query.SearchQuery, error) {
return query.NewMembershipRoleQuery(q.GetRoleKey())
}
func orQueryToQuery(q *internal_permission.OrFilter, level uint8) (query.SearchQuery, error) {
mappedQueries, err := administratorSearchFiltersToQuery(q.Queries, level+1)
if err != nil {
return nil, err
}
return query.NewOrQuery(mappedQueries...)
}
func andQueryToQuery(q *internal_permission.AndFilter, level uint8) (query.SearchQuery, error) {
mappedQueries, err := administratorSearchFiltersToQuery(q.Queries, level+1)
if err != nil {
return nil, err
}
return query.NewAndQuery(mappedQueries...)
}
func notQueryToQuery(q *internal_permission.NotFilter, level uint8) (query.SearchQuery, error) {
mappedQuery, err := administratorFilterToQuery(q.Query, level+1)
if err != nil {
return nil, err
}
return query.NewNotQuery(mappedQuery)
}
func administratorsToPb(administrators []*query.Administrator) []*internal_permission.Administrator {
a := make([]*internal_permission.Administrator, len(administrators))
for i, admin := range administrators {
@@ -0,0 +1,103 @@
package internal_permission
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2"
)
func Test_administratorFilterToQuery(t *testing.T) {
now := time.Now().UTC()
type args struct {
filter *internal_permission.AdministratorSearchFilter
level uint8
}
tests := []struct {
name string
args args
want query.SearchQuery
wantErr error
}{
{
name: "max nested queries",
args: args{
filter: &internal_permission.AdministratorSearchFilter{
Filter: &internal_permission.AdministratorSearchFilter_And{
And: &internal_permission.AndFilter{
Queries: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_CreationDate{
CreationDate: &filter.TimestampFilter{
Timestamp: timestamppb.New(now),
},
},
},
},
},
},
},
level: 19,
},
want: func() query.SearchQuery {
dateQuery, _ := query.NewMembershipCreationDateQuery(now, query.TimestampEquals)
q, _ := query.NewAndQuery(dateQuery)
return q
}(),
},
{
name: "too many nested queries",
args: args{
filter: &internal_permission.AdministratorSearchFilter{
Filter: &internal_permission.AdministratorSearchFilter_And{
And: &internal_permission.AndFilter{
Queries: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_And{
And: &internal_permission.AndFilter{
Queries: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_CreationDate{
CreationDate: &filter.TimestampFilter{
Timestamp: timestamppb.New(time.Now().UTC()),
},
},
},
},
},
},
},
},
},
},
},
level: 19,
},
wantErr: zerrors.ThrowInvalidArgument(nil, "PERM-zsQ97", "Errors.Query.TooManyNestingLevels"),
},
{
name: "invalid filter",
args: args{
filter: &internal_permission.AdministratorSearchFilter{
Filter: nil,
},
level: 0,
},
wantErr: zerrors.ThrowInvalidArgument(nil, "PERM-vR9nC", "List.Query.Invalid"),
},
// rest is tested in integration tests
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := administratorFilterToQuery(tt.args.filter, tt.args.level)
assert.Equal(t, tt.want, got)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
+4
View File
@@ -106,6 +106,10 @@ func NewMembershipIsIAMQuery() (SearchQuery, error) {
return NewNotNullQuery(InstanceMemberIAMID)
}
func NewMembershipRoleQuery(role string) (SearchQuery, error) {
return NewTextQuery(membershipRoles, role, TextListContains)
}
func (q *MembershipSearchQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = q.SearchRequest.toQuery(query)
for _, q := range q.Queries {