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
case feature.ImprovedPerformanceTypeProject:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT
case feature.ImprovedPerformanceTypeUserGrant:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT
default:
return feature_pb.ImprovedPerformance(typ)
}
@ -141,6 +143,8 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp
return feature.ImprovedPerformanceTypeProjectGrant
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT:
return feature.ImprovedPerformanceTypeProject
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT:
return feature.ImprovedPerformanceTypeUserGrant
default:
return feature.ImprovedPerformanceTypeUnknown
}

View File

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

View File

@ -4,8 +4,12 @@ import (
"context"
"reflect"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"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/telemetry/tracing"
"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) {
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)
defer func() { span.EndWithError(err) }()

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc-gen-go v1.34.2
// protoc (unknown)
// 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_goTypes = []interface{}{
var file_zitadel_protoc_gen_zitadel_v2_options_proto_goTypes = []any{
(*Options)(nil), // 0: zitadel.protoc_gen_zitadel.v2.Options
(*AuthOption)(nil), // 1: zitadel.protoc_gen_zitadel.v2.AuthOption
(*CustomHTTPResponse)(nil), // 2: zitadel.protoc_gen_zitadel.v2.CustomHTTPResponse
@ -274,7 +274,7 @@ func file_zitadel_protoc_gen_zitadel_v2_options_proto_init() {
return
}
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 {
case 0:
return &v.state
@ -286,7 +286,7 @@ func file_zitadel_protoc_gen_zitadel_v2_options_proto_init() {
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 {
case 0:
return &v.state
@ -298,7 +298,7 @@ func file_zitadel_protoc_gen_zitadel_v2_options_proto_init() {
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 {
case 0:
return &v.state

View File

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