perf(query): org permission function for resources (#9677)

# Which Problems Are Solved

Classic permission checks execute for every returned row on resource
based search APIs. Complete background and problem definition can be
found here: https://github.com/zitadel/zitadel/issues/9188

# How the Problems Are Solved

- PermissionClause function now support dynamic query building, so it
supports multiple cases.
- PermissionClause is applied to all list resources which support org
level permissions.
- Wrap permission logic into wrapper functions so we keep the business
logic clean.

# Additional Changes

- Handle org ID optimization in the query package, so it is reusable for
all resources, instead of extracting the filter in the API.
- Cleanup and test system user conversion in the authz package. (context
middleware)
- Fix: `core_integration_db_up` make recipe was missing the postgres
service.

# Additional Context

- Related to https://github.com/zitadel/zitadel/issues/9190
This commit is contained in:
Tim Möhlmann
2025-04-15 19:38:25 +03:00
committed by GitHub
parent 3b8a2ab811
commit a2f60f2e7a
23 changed files with 741 additions and 172 deletions

View File

@@ -7,8 +7,6 @@ import (
"slices"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -26,14 +24,13 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID,
ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier)
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, systemRolePermissionMapping)
if err != nil {
return nil, err
}
if requiredAuthOption.Permission == authenticated {
return func(parent context.Context) context.Context {
parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData)
return context.WithValue(parent, dataKey, ctxData)
}, nil
}
@@ -54,7 +51,6 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID,
parent = context.WithValue(parent, dataKey, ctxData)
parent = context.WithValue(parent, allPermissionsKey, allPermissions)
parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions)
parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData)
return parent
}, nil
}
@@ -131,42 +127,32 @@ func GetAllPermissionCtxIDs(perms []string) []string {
return ctxIDs
}
type SystemUserPermissionsDBQuery struct {
MemberType string `json:"member_type"`
AggregateID string `json:"aggregate_id"`
ObjectID string `json:"object_id"`
Permissions []string `json:"permissions"`
type SystemUserPermissions struct {
MemberType MemberType `json:"member_type"`
AggregateID string `json:"aggregate_id"`
ObjectID string `json:"object_id"`
Permissions []string `json:"permissions"`
}
func addGetSystemUserRolesToCtx(ctx context.Context, systemUserRoleMap []RoleMapping, ctxData CtxData) context.Context {
if len(ctxData.SystemMemberships) == 0 {
return ctx
// systemMembershipsToUserPermissions converts system memberships based on roles,
// to SystemUserPermissions, using the passed role mapping.
func systemMembershipsToUserPermissions(memberships Memberships, roleMap []RoleMapping) []SystemUserPermissions {
if memberships == nil {
return nil
}
systemUserPermissions := make([]SystemUserPermissionsDBQuery, len(ctxData.SystemMemberships))
for i, systemPerm := range ctxData.SystemMemberships {
systemUserPermissions := make([]SystemUserPermissions, len(memberships))
for i, systemPerm := range memberships {
permissions := make([]string, 0, len(systemPerm.Roles))
for _, role := range systemPerm.Roles {
permissions = append(permissions, getPermissionsFromRole(systemUserRoleMap, role)...)
permissions = append(permissions, getPermissionsFromRole(roleMap, role)...)
}
slices.Sort(permissions)
permissions = slices.Compact(permissions)
permissions = slices.Compact(permissions) // remove duplicates
systemUserPermissions[i].MemberType = systemPerm.MemberType.String()
systemUserPermissions[i].MemberType = systemPerm.MemberType
systemUserPermissions[i].AggregateID = systemPerm.AggregateID
systemUserPermissions[i].ObjectID = systemPerm.ObjectID
systemUserPermissions[i].Permissions = permissions
}
return context.WithValue(ctx, systemUserRolesKey, systemUserPermissions)
}
func GetSystemUserPermissions(ctx context.Context) []SystemUserPermissionsDBQuery {
getSystemUserRolesFuncValue := ctx.Value(systemUserRolesKey)
if getSystemUserRolesFuncValue == nil {
return nil
}
systemUserRoles, ok := getSystemUserRolesFuncValue.([]SystemUserPermissionsDBQuery)
if !ok {
logging.WithFields("Authz").Error("unable to cast []SystemUserPermissionsDBQuery")
return nil
}
return systemUserRoles
return systemUserPermissions
}

View File

@@ -3,6 +3,8 @@ package authz
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -276,3 +278,127 @@ func Test_GetPermissionCtxIDs(t *testing.T) {
})
}
}
func Test_systemMembershipsToUserPermissions(t *testing.T) {
roleMap := []RoleMapping{
{
Role: "FOO_BAR",
Permissions: []string{"foo.bar.read", "foo.bar.write"},
},
{
Role: "BAR_FOO",
Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"},
},
}
type args struct {
memberships Memberships
roleMap []RoleMapping
}
tests := []struct {
name string
args args
want []SystemUserPermissions
}{
{
name: "nil memberships",
args: args{
memberships: nil,
roleMap: roleMap,
},
want: nil,
},
{
name: "empty memberships",
args: args{
memberships: Memberships{},
roleMap: roleMap,
},
want: []SystemUserPermissions{},
},
{
name: "single membership",
args: args{
memberships: Memberships{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"FOO_BAR"},
},
},
roleMap: roleMap,
},
want: []SystemUserPermissions{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"foo.bar.read", "foo.bar.write"},
},
},
},
{
name: "multiple memberships",
args: args{
memberships: Memberships{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"FOO_BAR"},
},
{
MemberType: MemberTypeIAM,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"BAR_FOO"},
},
},
roleMap: roleMap,
},
want: []SystemUserPermissions{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"foo.bar.read", "foo.bar.write"},
},
{
MemberType: MemberTypeIAM,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"},
},
},
},
{
name: "multiple roles",
args: args{
memberships: Memberships{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"FOO_BAR", "BAR_FOO"},
},
},
roleMap: roleMap,
},
want: []SystemUserPermissions{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read", "foo.bar.write"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := systemMembershipsToUserPermissions(tt.args.memberships, tt.args.roleMap)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -1,4 +1,4 @@
//go:generate enumer -type MemberType -trimprefix MemberType
//go:generate enumer -type MemberType -trimprefix MemberType -json
package authz
@@ -22,17 +22,17 @@ const (
dataKey key = 2
allPermissionsKey key = 3
instanceKey key = 4
systemUserRolesKey key = 5
)
type CtxData struct {
UserID string
OrgID string
ProjectID string
AgentID string
PreferredLanguage string
ResourceOwner string
SystemMemberships Memberships
UserID string
OrgID string
ProjectID string
AgentID string
PreferredLanguage string
ResourceOwner string
SystemMemberships Memberships
SystemUserPermissions []SystemUserPermissions
}
func (ctxData CtxData) IsZero() bool {
@@ -98,7 +98,7 @@ func (s SystemTokenVerifierFunc) VerifySystemToken(ctx context.Context, token st
return s(ctx, token, orgID)
}
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier) (_ CtxData, err error) {
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier, systemRoleMap []RoleMapping) (_ CtxData, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
tokenWOBearer, err := extractBearerToken(token)
@@ -133,13 +133,14 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain st
}
}
return CtxData{
UserID: userID,
OrgID: orgID,
ProjectID: projectID,
AgentID: agentID,
PreferredLanguage: prefLang,
ResourceOwner: resourceOwner,
SystemMemberships: sysMemberships,
UserID: userID,
OrgID: orgID,
ProjectID: projectID,
AgentID: agentID,
PreferredLanguage: prefLang,
ResourceOwner: resourceOwner,
SystemMemberships: sysMemberships,
SystemUserPermissions: systemMembershipsToUserPermissions(sysMemberships, systemRoleMap),
}, nil
}

View File

@@ -1,8 +1,9 @@
// Code generated by "enumer -type MemberType -trimprefix MemberType"; DO NOT EDIT.
// Code generated by "enumer -type MemberType -trimprefix MemberType -json"; DO NOT EDIT.
package authz
import (
"encoding/json"
"fmt"
"strings"
)
@@ -92,3 +93,20 @@ func (i MemberType) IsAMemberType() bool {
}
return false
}
// MarshalJSON implements the json.Marshaler interface for MemberType
func (i MemberType) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for MemberType
func (i *MemberType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("MemberType should be a string, got %s", data)
}
var err error
*i, err = MemberTypeString(s)
return err
}