perf(command): user grant pre-condition check using the search table (#8230)

# Which Problems Are Solved

Imporve the performance of user grant addition, especially for import.

# How the Problems Are Solved

Use the search table to query for the project grant state. 
This could easily be done by making the search used in
`checkProjectGrantPreCondition` reusable.

# Additional Changes

Chanded event declerations to `const` in the
`internal/repository/project` package.

# Additional Context

- Performance improvements for import are evaluated and acted upon
internally at the moment

---------

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Tim Möhlmann 2024-07-04 19:18:43 +03:00 committed by GitHub
parent d705cb11b7
commit ecfb9d0d6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 178 additions and 20 deletions

View File

@ -113,6 +113,8 @@ func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT
case feature.ImprovedPerformanceTypeProject: case feature.ImprovedPerformanceTypeProject:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT
case feature.ImprovedPerformanceTypeUserGrant:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT
default: default:
return feature_pb.ImprovedPerformance(typ) return feature_pb.ImprovedPerformance(typ)
} }
@ -141,6 +143,8 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp
return feature.ImprovedPerformanceTypeProjectGrant return feature.ImprovedPerformanceTypeProjectGrant
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT: case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT:
return feature.ImprovedPerformanceTypeProject return feature.ImprovedPerformanceTypeProject
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT:
return feature.ImprovedPerformanceTypeUserGrant
default: default:
return feature.ImprovedPerformanceTypeUnknown return feature.ImprovedPerformanceTypeUnknown
} }

View File

@ -259,38 +259,49 @@ func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectGra
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProjectGrant) { if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProjectGrant) {
return c.checkProjectGrantPreConditionOld(ctx, projectGrant) return c.checkProjectGrantPreConditionOld(ctx, projectGrant)
} }
existingRoleKeys, err := c.searchProjectGrantState(ctx, projectGrant.AggregateID, projectGrant.GrantedOrgID)
if err != nil {
return err
}
if projectGrant.HasInvalidRoles(existingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound")
}
return nil
}
func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID string) (existingRoleKeys []string, err error) {
results, err := c.eventstore.Search( results, err := c.eventstore.Search(
ctx, ctx,
// project state query // project state query
map[eventstore.FieldType]any{ map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType, eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: projectGrant.AggregateID, eventstore.FieldTypeAggregateID: projectID,
eventstore.FieldTypeFieldName: project.ProjectStateSearchField, eventstore.FieldTypeFieldName: project.ProjectStateSearchField,
eventstore.FieldTypeObjectType: project.ProjectSearchType, eventstore.FieldTypeObjectType: project.ProjectSearchType,
}, },
// granted org query // granted org query
map[eventstore.FieldType]any{ map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: org.AggregateType, eventstore.FieldTypeAggregateType: org.AggregateType,
eventstore.FieldTypeAggregateID: projectGrant.GrantedOrgID, eventstore.FieldTypeAggregateID: grantedOrgID,
eventstore.FieldTypeFieldName: org.OrgStateSearchField, eventstore.FieldTypeFieldName: org.OrgStateSearchField,
eventstore.FieldTypeObjectType: org.OrgSearchType, eventstore.FieldTypeObjectType: org.OrgSearchType,
}, },
// role query // role query
map[eventstore.FieldType]any{ map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType, eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: projectGrant.AggregateID, eventstore.FieldTypeAggregateID: projectID,
eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField, eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField,
eventstore.FieldTypeObjectType: project.ProjectRoleSearchType, eventstore.FieldTypeObjectType: project.ProjectRoleSearchType,
}, },
) )
if err != nil { if err != nil {
return err return nil, err
} }
var ( var (
existsProject bool existsProject bool
existsGrantedOrg bool existsGrantedOrg bool
existingRoleKeys []string
) )
for _, result := range results { for _, result := range results {
@ -299,34 +310,31 @@ func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectGra
var role string var role string
err := result.Value.Unmarshal(&role) err := result.Value.Unmarshal(&role)
if err != nil { if err != nil {
return err return nil, err
} }
existingRoleKeys = append(existingRoleKeys, role) existingRoleKeys = append(existingRoleKeys, role)
case org.OrgSearchType: case org.OrgSearchType:
var state domain.OrgState var state domain.OrgState
err := result.Value.Unmarshal(&state) err := result.Value.Unmarshal(&state)
if err != nil { if err != nil {
return err return nil, err
} }
existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved
case project.ProjectSearchType: case project.ProjectSearchType:
var state domain.ProjectState var state domain.ProjectState
err := result.Value.Unmarshal(&state) err := result.Value.Unmarshal(&state)
if err != nil { if err != nil {
return err return nil, err
} }
existsProject = state.Valid() && state != domain.ProjectStateRemoved existsProject = state.Valid() && state != domain.ProjectStateRemoved
} }
} }
if !existsProject { if !existsProject {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound")
} }
if !existsGrantedOrg { if !existsGrantedOrg {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
} }
if projectGrant.HasInvalidRoles(existingRoleKeys) { return existingRoleKeys, nil
return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound")
}
return nil
} }

View File

@ -4,8 +4,12 @@ import (
"context" "context"
"reflect" "reflect"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/usergrant" "github.com/zitadel/zitadel/internal/repository/usergrant"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
@ -288,6 +292,140 @@ func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, res
} }
func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) { func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) {
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeUserGrant) {
return c.checkUserGrantPreConditionOld(ctx, usergrant, resourceOwner)
}
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil {
return err
}
existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant, resourceOwner)
if err != nil {
return err
}
if usergrant.HasInvalidRoles(existingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound")
}
return nil
}
// this code needs to be rewritten anyways as soon as we improved the fields handling
//
//nolint:gocognit
func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (existingRoleKeys []string, err error) {
criteria := []map[eventstore.FieldType]any{
// project state query
{
eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: userGrant.ProjectID,
eventstore.FieldTypeFieldName: project.ProjectStateSearchField,
eventstore.FieldTypeObjectType: project.ProjectSearchType,
},
// granted org query
{
eventstore.FieldTypeAggregateType: org.AggregateType,
eventstore.FieldTypeAggregateID: resourceOwner,
eventstore.FieldTypeFieldName: org.OrgStateSearchField,
eventstore.FieldTypeObjectType: org.OrgSearchType,
},
}
if userGrant.ProjectGrantID != "" {
criteria = append(criteria, map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: userGrant.ProjectID,
eventstore.FieldTypeObjectType: project.ProjectGrantSearchType,
eventstore.FieldTypeObjectID: userGrant.ProjectGrantID,
})
} else {
criteria = append(criteria, map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: userGrant.ProjectID,
eventstore.FieldTypeObjectType: project.ProjectRoleSearchType,
eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField,
})
}
results, err := c.eventstore.Search(ctx, criteria...)
if err != nil {
return nil, err
}
var (
existsProject bool
existsGrantedOrg bool
existsGrant bool
)
for _, result := range results {
switch result.Object.Type {
case project.ProjectRoleSearchType:
var role string
err := result.Value.Unmarshal(&role)
if err != nil {
return nil, err
}
existingRoleKeys = append(existingRoleKeys, role)
case org.OrgSearchType:
var state domain.OrgState
err := result.Value.Unmarshal(&state)
if err != nil {
return nil, err
}
existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved
case project.ProjectSearchType:
var state domain.ProjectState
err := result.Value.Unmarshal(&state)
if err != nil {
return nil, err
}
existsProject = state.Valid() && state != domain.ProjectStateRemoved
case project.ProjectGrantSearchType:
switch result.FieldName {
case project.ProjectGrantGrantedOrgIDSearchField:
var orgID string
err := result.Value.Unmarshal(&orgID)
if err != nil || orgID != resourceOwner {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
}
case project.ProjectGrantStateSearchField:
var state domain.ProjectGrantState
err := result.Value.Unmarshal(&state)
if err != nil {
return nil, err
}
existsGrant = state.Valid() && state != domain.ProjectGrantStateRemoved
case project.ProjectGrantRoleKeySearchField:
var role string
err := result.Value.Unmarshal(&role)
if err != nil {
return nil, err
}
existingRoleKeys = append(existingRoleKeys, role)
case project.ProjectGrantGrantIDSearchField:
var grantID string
err := result.Value.Unmarshal(&grantID)
if err != nil || grantID != userGrant.ProjectGrantID {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-huvKF", "Errors.Project.Grant.NotFound")
}
}
}
}
if !existsProject {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound")
}
if !existsGrantedOrg {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
}
if userGrant.ProjectGrantID != "" && !existsGrant {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-huvKF", "Errors.Project.Grant.NotFound")
}
return existingRoleKeys, nil
}
func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()

View File

@ -18,8 +18,14 @@ const (
ProjectGrantStateActive ProjectGrantStateActive
ProjectGrantStateInactive ProjectGrantStateInactive
ProjectGrantStateRemoved ProjectGrantStateRemoved
projectGrantStateMax
) )
func (s ProjectGrantState) Valid() bool {
return s > ProjectGrantStateUnspecified && s < projectGrantStateMax
}
func (p *ProjectGrant) IsValid() bool { func (p *ProjectGrant) IsValid() bool {
return p.GrantedOrgID != "" return p.GrantedOrgID != ""
} }

View File

@ -44,6 +44,7 @@ const (
ImprovedPerformanceTypeOrgByID ImprovedPerformanceTypeOrgByID
ImprovedPerformanceTypeProjectGrant ImprovedPerformanceTypeProjectGrant
ImprovedPerformanceTypeProject ImprovedPerformanceTypeProject
ImprovedPerformanceTypeUserGrant
) )
func (f Features) ShouldUseImprovedPerformance(typ ImprovedPerformanceType) bool { func (f Features) ShouldUseImprovedPerformance(typ ImprovedPerformanceType) bool {

View File

@ -8,7 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
) )
var ( const (
UniqueRoleType = "project_role" UniqueRoleType = "project_role"
roleEventTypePrefix = projectEventTypePrefix + "role." roleEventTypePrefix = projectEventTypePrefix + "role."
RoleAddedType = roleEventTypePrefix + "added" RoleAddedType = roleEventTypePrefix + "added"

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.33.0 // protoc-gen-go v1.34.2
// protoc (unknown) // protoc (unknown)
// source: zitadel/protoc_gen_zitadel/v2/options.proto // source: zitadel/protoc_gen_zitadel/v2/options.proto
@ -250,7 +250,7 @@ func file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescGZIP() []byte {
} }
var file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_zitadel_protoc_gen_zitadel_v2_options_proto_goTypes = []interface{}{ var file_zitadel_protoc_gen_zitadel_v2_options_proto_goTypes = []any{
(*Options)(nil), // 0: zitadel.protoc_gen_zitadel.v2.Options (*Options)(nil), // 0: zitadel.protoc_gen_zitadel.v2.Options
(*AuthOption)(nil), // 1: zitadel.protoc_gen_zitadel.v2.AuthOption (*AuthOption)(nil), // 1: zitadel.protoc_gen_zitadel.v2.AuthOption
(*CustomHTTPResponse)(nil), // 2: zitadel.protoc_gen_zitadel.v2.CustomHTTPResponse (*CustomHTTPResponse)(nil), // 2: zitadel.protoc_gen_zitadel.v2.CustomHTTPResponse
@ -274,7 +274,7 @@ func file_zitadel_protoc_gen_zitadel_v2_options_proto_init() {
return return
} }
if !protoimpl.UnsafeEnabled { if !protoimpl.UnsafeEnabled {
file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*Options); i { switch v := v.(*Options); i {
case 0: case 0:
return &v.state return &v.state
@ -286,7 +286,7 @@ func file_zitadel_protoc_gen_zitadel_v2_options_proto_init() {
return nil return nil
} }
} }
file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*AuthOption); i { switch v := v.(*AuthOption); i {
case 0: case 0:
return &v.state return &v.state
@ -298,7 +298,7 @@ func file_zitadel_protoc_gen_zitadel_v2_options_proto_init() {
return nil return nil
} }
} }
file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*CustomHTTPResponse); i { switch v := v.(*CustomHTTPResponse); i {
case 0: case 0:
return &v.state return &v.state

View File

@ -59,4 +59,5 @@ enum ImprovedPerformance {
// correctnes of data. // correctnes of data.
IMPROVED_PERFORMANCE_PROJECT_GRANT = 2; IMPROVED_PERFORMANCE_PROJECT_GRANT = 2;
IMPROVED_PERFORMANCE_PROJECT = 3; IMPROVED_PERFORMANCE_PROJECT = 3;
IMPROVED_PERFORMANCE_USER_GRANT = 4;
} }