mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 11:27:33 +00:00
feat(api): feature flags (#7356)
* feat(api): feature API proto definitions * update proto based on discussion with @livio-a * cleanup old feature flag stuff * authz instance queries * align defaults * projection definitions * define commands and event reducers * implement system and instance setter APIs * api getter implementation * unit test repository package * command unit tests * unit test Get queries * grpc converter unit tests * migrate the V1 features * migrate oidc to dynamic features * projection unit test * fix instance by host * fix instance by id data type in sql * fix linting errors * add system projection test * fix behavior inversion * resolve proto file comments * rename SystemDefaultLoginInstanceEventType to SystemLoginDefaultOrgEventType so it's consistent with the instance level event * use write models and conditional set events * system features integration tests * instance features integration tests * error on empty request * documentation entry * typo in feature.proto * fix start unit tests * solve linting error on key case switch * remove system defaults after discussion with @eliobischof * fix system feature projection * resolve comments in defaults.yaml --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,6 +25,7 @@ type Instance interface {
|
||||
SecurityPolicyAllowedOrigins() []string
|
||||
Block() *bool
|
||||
AuditLogRetention() *time.Duration
|
||||
Features() feature.Features
|
||||
}
|
||||
|
||||
type InstanceVerifier interface {
|
||||
@@ -37,6 +40,7 @@ type instance struct {
|
||||
appID string
|
||||
clientID string
|
||||
orgID string
|
||||
features feature.Features
|
||||
}
|
||||
|
||||
func (i *instance) Block() *bool {
|
||||
@@ -83,6 +87,10 @@ func (i *instance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *instance) Features() feature.Features {
|
||||
return i.features
|
||||
}
|
||||
|
||||
func GetInstance(ctx context.Context) Instance {
|
||||
instance, ok := ctx.Value(instanceKey).(Instance)
|
||||
if !ok {
|
||||
@@ -91,6 +99,10 @@ func GetInstance(ctx context.Context) Instance {
|
||||
return instance
|
||||
}
|
||||
|
||||
func GetFeatures(ctx context.Context) feature.Features {
|
||||
return GetInstance(ctx).Features()
|
||||
}
|
||||
|
||||
func WithInstance(ctx context.Context, instance Instance) context.Context {
|
||||
return context.WithValue(ctx, instanceKey, instance)
|
||||
}
|
||||
@@ -120,3 +132,12 @@ func WithConsole(ctx context.Context, projectID, appID string) context.Context {
|
||||
//i.clientID = clientID
|
||||
return context.WithValue(ctx, instanceKey, i)
|
||||
}
|
||||
|
||||
func WithFeatures(ctx context.Context, f feature.Features) context.Context {
|
||||
i, ok := ctx.Value(instanceKey).(*instance)
|
||||
if !ok {
|
||||
i = new(instance)
|
||||
}
|
||||
i.features = f
|
||||
return context.WithValue(ctx, instanceKey, i)
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
func Test_Instance(t *testing.T) {
|
||||
@@ -17,6 +19,7 @@ func Test_Instance(t *testing.T) {
|
||||
instanceID string
|
||||
projectID string
|
||||
consoleID string
|
||||
features feature.Features
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -56,6 +59,19 @@ func Test_Instance(t *testing.T) {
|
||||
consoleID: "consoleID",
|
||||
},
|
||||
},
|
||||
{
|
||||
"WithFeatures",
|
||||
args{
|
||||
WithFeatures(context.Background(), feature.Features{
|
||||
LoginDefaultOrg: true,
|
||||
}),
|
||||
},
|
||||
res{
|
||||
features: feature.Features{
|
||||
LoginDefaultOrg: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -63,6 +79,7 @@ func Test_Instance(t *testing.T) {
|
||||
assert.Equal(t, tt.res.instanceID, got.InstanceID())
|
||||
assert.Equal(t, tt.res.projectID, got.ProjectID())
|
||||
assert.Equal(t, tt.res.consoleID, got.ConsoleClientID())
|
||||
assert.Equal(t, tt.res.features, got.Features())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -112,3 +129,7 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
@@ -3,13 +3,17 @@ package admin
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
)
|
||||
|
||||
func (s *Server) ActivateFeatureLoginDefaultOrg(ctx context.Context, _ *admin_pb.ActivateFeatureLoginDefaultOrgRequest) (*admin_pb.ActivateFeatureLoginDefaultOrgResponse, error) {
|
||||
details, err := s.command.SetBooleanInstanceFeature(ctx, domain.FeatureLoginDefaultOrg, true)
|
||||
details, err := s.command.SetInstanceFeatures(ctx, &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
71
internal/api/grpc/feature/v2/converter.go
Normal file
71
internal/api/grpc/feature/v2/converter.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
|
||||
return &command.SystemFeatures{
|
||||
LoginDefaultOrg: req.LoginDefaultOrg,
|
||||
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
||||
LegacyIntrospection: req.OidcLegacyIntrospection,
|
||||
}
|
||||
}
|
||||
|
||||
func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse {
|
||||
return &feature_pb.GetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(f.Details),
|
||||
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
||||
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
||||
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
||||
}
|
||||
}
|
||||
|
||||
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures {
|
||||
return &command.InstanceFeatures{
|
||||
LoginDefaultOrg: req.LoginDefaultOrg,
|
||||
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
||||
LegacyIntrospection: req.OidcLegacyIntrospection,
|
||||
}
|
||||
}
|
||||
|
||||
func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse {
|
||||
return &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(f.Details),
|
||||
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
||||
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
||||
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
||||
}
|
||||
}
|
||||
|
||||
func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag {
|
||||
return &feature_pb.FeatureFlag{
|
||||
Enabled: fs.Value,
|
||||
Source: featureLevelToSourcePb(fs.Level),
|
||||
}
|
||||
}
|
||||
|
||||
func featureLevelToSourcePb(level feature.Level) feature_pb.Source {
|
||||
switch level {
|
||||
case feature.LevelUnspecified:
|
||||
return feature_pb.Source_SOURCE_UNSPECIFIED
|
||||
case feature.LevelSystem:
|
||||
return feature_pb.Source_SOURCE_SYSTEM
|
||||
case feature.LevelInstance:
|
||||
return feature_pb.Source_SOURCE_INSTANCE
|
||||
case feature.LevelOrg:
|
||||
return feature_pb.Source_SOURCE_ORGANIZATION
|
||||
case feature.LevelProject:
|
||||
return feature_pb.Source_SOURCE_PROJECT
|
||||
case feature.LevelApp:
|
||||
return feature_pb.Source_SOURCE_APP
|
||||
case feature.LevelUser:
|
||||
return feature_pb.Source_SOURCE_USER
|
||||
default:
|
||||
return feature_pb.Source(level)
|
||||
}
|
||||
}
|
188
internal/api/grpc/feature/v2/converter_test.go
Normal file
188
internal/api/grpc/feature/v2/converter_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
)
|
||||
|
||||
func Test_systemFeaturesToCommand(t *testing.T) {
|
||||
arg := &feature_pb.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
OidcLegacyIntrospection: nil,
|
||||
}
|
||||
want := &command.SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: nil,
|
||||
}
|
||||
got := systemFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_systemFeaturesToPb(t *testing.T) {
|
||||
arg := &query.SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(123, 0),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TriggerIntrospectionProjections: query.FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
LegacyIntrospection: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×tamppb.Timestamp{Seconds: 123},
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
}
|
||||
got := systemFeaturesToPb(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
arg := &feature_pb.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
OidcLegacyIntrospection: nil,
|
||||
}
|
||||
want := &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: nil,
|
||||
}
|
||||
got := instanceFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
arg := &query.InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(123, 0),
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TriggerIntrospectionProjections: query.FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
LegacyIntrospection: query.FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×tamppb.Timestamp{Seconds: 123},
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
}
|
||||
got := instanceFeaturesToPb(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_featureLevelToSourcePb(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level feature.Level
|
||||
want feature_pb.Source
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
level: feature.LevelUnspecified,
|
||||
want: feature_pb.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
name: "system",
|
||||
level: feature.LevelSystem,
|
||||
want: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
{
|
||||
name: "instance",
|
||||
level: feature.LevelInstance,
|
||||
want: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
{
|
||||
name: "org",
|
||||
level: feature.LevelOrg,
|
||||
want: feature_pb.Source_SOURCE_ORGANIZATION,
|
||||
},
|
||||
{
|
||||
name: "project",
|
||||
level: feature.LevelProject,
|
||||
want: feature_pb.Source_SOURCE_PROJECT,
|
||||
},
|
||||
{
|
||||
name: "app",
|
||||
level: feature.LevelApp,
|
||||
want: feature_pb.Source_SOURCE_APP,
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
level: feature.LevelUser,
|
||||
want: feature_pb.Source_SOURCE_USER,
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
level: 99,
|
||||
want: 99,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := featureLevelToSourcePb(tt.level)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
86
internal/api/grpc/feature/v2/feature.go
Normal file
86
internal/api/grpc/feature/v2/feature.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
|
||||
details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.SetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetSystemFeatures(ctx context.Context, req *feature.ResetSystemFeaturesRequest) (_ *feature.ResetSystemFeaturesResponse, err error) {
|
||||
details, err := s.command.ResetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.ResetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFeaturesRequest) (_ *feature.GetSystemFeaturesResponse, err error) {
|
||||
f, err := s.query.GetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return systemFeaturesToPb(f), nil
|
||||
}
|
||||
|
||||
func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) {
|
||||
details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.SetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetInstanceFeatures(ctx context.Context, req *feature.ResetInstanceFeaturesRequest) (_ *feature.ResetInstanceFeaturesResponse, err error) {
|
||||
details, err := s.command.ResetInstanceFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.ResetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetInstanceFeatures(ctx context.Context, req *feature.GetInstanceFeaturesRequest) (_ *feature.GetInstanceFeaturesResponse, err error) {
|
||||
f, err := s.query.GetInstanceFeatures(ctx, req.GetInheritance())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return instanceFeaturesToPb(f), nil
|
||||
}
|
||||
|
||||
func (s *Server) SetOrganizationFeatures(ctx context.Context, req *feature.SetOrganizationFeaturesRequest) (_ *feature.SetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *feature.ResetOrganizationFeaturesRequest) (_ *feature.ResetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) GetOrganizationFeatures(ctx context.Context, req *feature.GetOrganizationFeaturesRequest) (_ *feature.GetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) SetUserFeatures(ctx context.Context, req *feature.SetUserFeatureRequest) (_ *feature.SetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetUserFeatures not implemented")
|
||||
}
|
||||
func (s *Server) ResetUserFeatures(ctx context.Context, req *feature.ResetUserFeaturesRequest) (_ *feature.ResetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResetUserFeatures not implemented")
|
||||
}
|
||||
func (s *Server) GetUserFeatures(ctx context.Context, req *feature.GetUserFeaturesRequest) (_ *feature.GetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserFeatures not implemented")
|
||||
}
|
470
internal/api/grpc/feature/v2/feature_integration_test.go
Normal file
470
internal/api/grpc/feature/v2/feature_integration_test.go
Normal file
@@ -0,0 +1,470 @@
|
||||
//go:build integration
|
||||
|
||||
package feature_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
SystemCTX context.Context
|
||||
IamCTX context.Context
|
||||
OrgCTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client feature.FeatureServiceClient
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, _, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
Tester = integration.NewTester(ctx)
|
||||
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
|
||||
IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
|
||||
OrgCTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
|
||||
|
||||
defer Tester.Done()
|
||||
Client = Tester.Client.FeatureV2
|
||||
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func TestServer_SetSystemFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.SetSystemFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *feature.SetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no changes error",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
want: &feature.SetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
got, err := Client.SetSystemFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResetSystemFeatures(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *feature.ResetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
ctx: IamCTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
ctx: SystemCTX,
|
||||
want: &feature.ResetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResetSystemFeatures(tt.ctx, &feature.ResetSystemFeaturesRequest{})
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetSystemFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.GetSystemFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(t *testing.T)
|
||||
args args
|
||||
want *feature.GetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nothing set",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetSystemFeaturesResponse{},
|
||||
},
|
||||
{
|
||||
name: "some features",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetSystemFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if tt.prepare != nil {
|
||||
tt.prepare(t)
|
||||
}
|
||||
got, err := Client.GetSystemFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
|
||||
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
|
||||
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_SetInstanceFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.SetInstanceFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *feature.SetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: OrgCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no changes error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
want: &feature.SetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
got, err := Client.SetInstanceFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResetInstanceFeatures(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *feature.ResetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
ctx: OrgCTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
ctx: IamCTX,
|
||||
want: &feature.ResetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResetInstanceFeatures(tt.ctx, &feature.ResetInstanceFeaturesRequest{})
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetInstanceFeatures(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
OidcLegacyIntrospection: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.GetInstanceFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(t *testing.T)
|
||||
args args
|
||||
want *feature.GetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: OrgCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "defaults, no inheritance",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{},
|
||||
},
|
||||
{
|
||||
name: "defaults, inheritance",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{
|
||||
Inheritance: true,
|
||||
},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "some features, no inheritance",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one feature, inheritance",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{
|
||||
Inheritance: true,
|
||||
},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if tt.prepare != nil {
|
||||
tt.prepare(t)
|
||||
}
|
||||
got, err := Client.GetInstanceFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
|
||||
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
|
||||
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertFeatureFlag(t *testing.T, expected, actual *feature.FeatureFlag) {
|
||||
t.Helper()
|
||||
assert.Equal(t, expected.GetEnabled(), actual.GetEnabled(), "enabled")
|
||||
assert.Equal(t, expected.GetSource(), actual.GetSource(), "source")
|
||||
}
|
47
internal/api/grpc/feature/v2/server.go
Normal file
47
internal/api/grpc/feature/v2/server.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
feature.UnimplementedFeatureServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
feature.RegisterFeatureServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return feature.FeatureService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return feature.FeatureService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return feature.FeatureService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return feature.RegisterFeatureServiceHandler
|
||||
}
|
@@ -22,9 +22,9 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance {
|
||||
instance.Sequence,
|
||||
instance.CreationDate,
|
||||
instance.ChangeDate,
|
||||
instance.InstanceID(),
|
||||
instance.ID,
|
||||
),
|
||||
Id: instance.InstanceID(),
|
||||
Id: instance.ID,
|
||||
Name: instance.Name,
|
||||
Domains: DomainsToPb(instance.Domains),
|
||||
Version: build.Version(),
|
||||
@@ -38,9 +38,9 @@ func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail {
|
||||
instance.Sequence,
|
||||
instance.CreationDate,
|
||||
instance.ChangeDate,
|
||||
instance.InstanceID(),
|
||||
instance.ID,
|
||||
),
|
||||
Id: instance.InstanceID(),
|
||||
Id: instance.ID,
|
||||
Name: instance.Name,
|
||||
Domains: DomainsToPb(instance.Domains),
|
||||
Version: build.Version(),
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
func Test_hostNameFromContext(t *testing.T) {
|
||||
@@ -208,3 +209,7 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
@@ -22,12 +23,14 @@ func (s *Server) SetInstanceFeature(ctx context.Context, req *system_pb.SetInsta
|
||||
|
||||
func (s *Server) setInstanceFeature(ctx context.Context, req *system_pb.SetInstanceFeatureRequest) (*domain.ObjectDetails, error) {
|
||||
feat := domain.Feature(req.FeatureId)
|
||||
if !feat.IsAFeature() {
|
||||
if feat != domain.FeatureLoginDefaultOrg {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SYST-SGV45", "Errors.Feature.NotExisting")
|
||||
}
|
||||
switch t := req.Value.(type) {
|
||||
case *system_pb.SetInstanceFeatureRequest_Bool:
|
||||
return s.command.SetBooleanInstanceFeature(ctx, feat, t.Bool)
|
||||
return s.command.SetInstanceFeatures(ctx, &command.InstanceFeatures{
|
||||
LoginDefaultOrg: &t.Bool,
|
||||
})
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SYST-dag5g", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
|
@@ -151,7 +151,7 @@ func (s *Server) ListDomains(ctx context.Context, req *system_pb.ListDomainsRequ
|
||||
}
|
||||
|
||||
func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest) (*system_pb.AddDomainResponse, error) {
|
||||
instance, err := s.query.Instance(ctx, true)
|
||||
instance, err := s.query.InstanceByID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
zitadel_http "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
func Test_instanceInterceptor_Handler(t *testing.T) {
|
||||
@@ -343,3 +344,7 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
@@ -95,7 +96,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
|
||||
}
|
||||
roles, err := s.query.SearchProjectRoles(ctx, s.features.TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -23,10 +24,11 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
|
||||
span.EndWithError(err)
|
||||
}()
|
||||
|
||||
if s.features.LegacyIntrospection {
|
||||
features := authz.GetFeatures(ctx)
|
||||
if features.LegacyIntrospection {
|
||||
return s.LegacyServer.Introspect(ctx, r)
|
||||
}
|
||||
if s.features.TriggerIntrospectionProjections {
|
||||
if features.TriggerIntrospectionProjections {
|
||||
// Execute all triggers in one concurrent sweep.
|
||||
query.TriggerIntrospectionProjections(ctx)
|
||||
}
|
||||
|
@@ -42,7 +42,6 @@ type Config struct {
|
||||
DeviceAuth *DeviceAuthorizationConfig
|
||||
DefaultLoginURLV2 string
|
||||
DefaultLogoutURLV2 string
|
||||
Features Features
|
||||
PublicKeyCacheMaxAge time.Duration
|
||||
}
|
||||
|
||||
@@ -62,11 +61,6 @@ type Endpoint struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type Features struct {
|
||||
TriggerIntrospectionProjections bool
|
||||
LegacyIntrospection bool
|
||||
}
|
||||
|
||||
type OPStorage struct {
|
||||
repo repository.Repository
|
||||
command *command.Commands
|
||||
@@ -128,7 +122,6 @@ func NewServer(
|
||||
|
||||
server := &Server{
|
||||
LegacyServer: op.NewLegacyServer(provider, endpoints(config.CustomEndpoints)),
|
||||
features: config.Features,
|
||||
repo: repo,
|
||||
query: query,
|
||||
command: command,
|
||||
|
@@ -21,7 +21,6 @@ import (
|
||||
type Server struct {
|
||||
http.Handler
|
||||
*op.LegacyServer
|
||||
features Features
|
||||
|
||||
repo repository.Repository
|
||||
query *query.Queries
|
||||
|
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
@@ -39,7 +38,6 @@ type Login struct {
|
||||
samlAuthCallbackURL func(context.Context, string) string
|
||||
idpConfigAlg crypto.EncryptionAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
featureCheck feature.Checker
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -76,7 +74,6 @@ func CreateLogin(config Config,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
idpConfigAlg crypto.EncryptionAlgorithm,
|
||||
csrfCookieKey []byte,
|
||||
featureCheck feature.Checker,
|
||||
) (*Login, error) {
|
||||
login := &Login{
|
||||
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
||||
@@ -89,7 +86,6 @@ func CreateLogin(config Config,
|
||||
authRepo: authRepo,
|
||||
idpConfigAlg: idpConfigAlg,
|
||||
userCodeAlg: userCodeAlg,
|
||||
featureCheck: featureCheck,
|
||||
}
|
||||
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
|
||||
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
||||
|
@@ -524,12 +524,12 @@ func (l *Login) getOrgID(r *http.Request, authReq *domain.AuthRequest) string {
|
||||
}
|
||||
|
||||
func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string {
|
||||
defaultID := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
f, err := l.featureCheck.CheckInstanceBooleanFeature(r.Context(), domain.FeatureLoginDefaultOrg)
|
||||
logging.OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg)
|
||||
if !f.Boolean {
|
||||
defaultID = authz.GetInstance(r.Context()).InstanceID()
|
||||
instance := authz.GetInstance(r.Context())
|
||||
defaultID := instance.DefaultOrganisationID()
|
||||
if !instance.Features().LoginDefaultOrg {
|
||||
defaultID = instance.InstanceID()
|
||||
}
|
||||
|
||||
if authReq != nil {
|
||||
return authReq.PrivateLabelingOrgID(defaultID)
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
cache "github.com/zitadel/zitadel/internal/auth_request/repository"
|
||||
@@ -50,8 +49,6 @@ type AuthRequestRepo struct {
|
||||
ApplicationProvider applicationProvider
|
||||
CustomTextProvider customTextProvider
|
||||
|
||||
FeatureCheck feature.Checker
|
||||
|
||||
IdGenerator id.Generator
|
||||
}
|
||||
|
||||
@@ -656,16 +653,15 @@ func (repo *AuthRequestRepo) getLoginPolicyAndIDPProviders(ctx context.Context,
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.AuthRequest) error {
|
||||
instance := authz.GetInstance(ctx)
|
||||
orgID := request.RequestedOrgID
|
||||
if orgID == "" {
|
||||
orgID = request.UserOrgID
|
||||
}
|
||||
if orgID == "" {
|
||||
orgID = authz.GetInstance(ctx).DefaultOrganisationID()
|
||||
f, err := repo.FeatureCheck.CheckInstanceBooleanFeature(ctx, domain.FeatureLoginDefaultOrg)
|
||||
logging.WithFields("authReq", request.ID).OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg)
|
||||
if !f.Boolean {
|
||||
orgID = authz.GetInstance(ctx).InstanceID()
|
||||
if !instance.Features().LoginDefaultOrg {
|
||||
orgID = instance.InstanceID()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,7 +688,7 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A
|
||||
return err
|
||||
}
|
||||
request.LabelPolicy = labelPolicy
|
||||
defaultLoginTranslations, err := repo.getLoginTexts(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
defaultLoginTranslations, err := repo.getLoginTexts(ctx, instance.InstanceID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ package eventsourcing
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/eventstore"
|
||||
auth_handler "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler"
|
||||
auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
@@ -77,7 +76,6 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c
|
||||
ProjectProvider: queryView,
|
||||
ApplicationProvider: queries,
|
||||
CustomTextProvider: queries,
|
||||
FeatureCheck: feature.NewCheck(esV2),
|
||||
IdGenerator: id.SonyFlakeGenerator(),
|
||||
},
|
||||
eventstore.TokenRepo{
|
||||
|
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/limits"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
@@ -110,7 +109,7 @@ type InstanceSetup struct {
|
||||
SMTPConfiguration *smtp.Config
|
||||
OIDCSettings *OIDCSettings
|
||||
Quotas *SetQuotas
|
||||
Features map[domain.Feature]any
|
||||
Features *InstanceFeatures
|
||||
Limits *SetLimits
|
||||
Restrictions *SetRestrictions
|
||||
}
|
||||
@@ -313,9 +312,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
|
||||
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
|
||||
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
|
||||
if err := setupFeatures(c, &validations, setup.Features, instanceID); err != nil {
|
||||
return "", "", nil, nil, err
|
||||
}
|
||||
setupFeatures(&validations, setup.Features, instanceID)
|
||||
setupLimits(c, &validations, limitsAgg, setup.Limits)
|
||||
setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions)
|
||||
|
||||
@@ -368,20 +365,8 @@ func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQ
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupFeatures(commands *Commands, validations *[]preparation.Validation, enableFeatures map[domain.Feature]any, instanceID string) error {
|
||||
for f, value := range enableFeatures {
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*validations = append(*validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, commands.idGenerator))
|
||||
default:
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
func setupFeatures(validations *[]preparation.Validation, features *InstanceFeatures, instanceID string) {
|
||||
*validations = append(*validations, prepareSetFeatures(instanceID, features))
|
||||
}
|
||||
|
||||
func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) {
|
||||
|
@@ -1,63 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func (c *Commands) SetBooleanInstanceFeature(ctx context.Context, f domain.Feature, value bool) (*domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
writeModel, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
|
||||
prepareSetFeature(writeModel, feature.Boolean{Boolean: value}, c.idGenerator))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(&writeModel.FeatureWriteModel.WriteModel), nil
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(pushedEvents), nil
|
||||
}
|
||||
|
||||
func prepareSetFeature[T feature.SetEventType](writeModel *InstanceFeatureWriteModel[T], value T, idGenerator id.Generator) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
if !writeModel.feature.IsAFeature() || writeModel.feature == domain.FeatureUnspecified {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "FEAT-JK3td", "Errors.Feature.NotExisting")
|
||||
}
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
if err = writeModel.Reduce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
writeModel.AggregateID, err = idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
setEvent, err := writeModel.Set(ctx, value)
|
||||
if err != nil || setEvent == nil {
|
||||
return nil, err
|
||||
}
|
||||
return []eventstore.Command{setEvent}, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type FeatureWriteModel[T feature.SetEventType] struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
feature domain.Feature
|
||||
|
||||
Value T
|
||||
}
|
||||
|
||||
func NewFeatureWriteModel[T feature.SetEventType](instanceID, resourceOwner string, feature domain.Feature) (*FeatureWriteModel[T], error) {
|
||||
wm := &FeatureWriteModel[T]{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
InstanceID: instanceID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
feature: feature,
|
||||
}
|
||||
if wm.Value.FeatureType() != feature.Type() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue")
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
func (wm *FeatureWriteModel[T]) Set(ctx context.Context, value T) (event *feature.SetEvent[T], err error) {
|
||||
if wm.Value == value {
|
||||
return nil, nil
|
||||
}
|
||||
return feature.NewSetEvent[T](
|
||||
ctx,
|
||||
&feature.NewAggregate(wm.AggregateID, wm.ResourceOwner).Aggregate,
|
||||
wm.eventType(),
|
||||
value,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(feature.AggregateType).
|
||||
EventTypes(wm.eventType()).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature.SetEvent[T]:
|
||||
wm.Value = e.Value
|
||||
default:
|
||||
return zerrors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) eventType() eventstore.EventType {
|
||||
return feature.EventTypeFromFeature(wm.feature)
|
||||
}
|
||||
|
||||
type InstanceFeatureWriteModel[T feature.SetEventType] struct {
|
||||
FeatureWriteModel[T]
|
||||
}
|
||||
|
||||
func NewInstanceFeatureWriteModel[T feature.SetEventType](instanceID string, feature domain.Feature) (*InstanceFeatureWriteModel[T], error) {
|
||||
wm, err := NewFeatureWriteModel[T](instanceID, instanceID, feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InstanceFeatureWriteModel[T]{
|
||||
FeatureWriteModel: *wm,
|
||||
}, nil
|
||||
}
|
@@ -1,170 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetBooleanInstanceFeature(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f domain.Feature
|
||||
value bool
|
||||
}
|
||||
type res struct {
|
||||
details *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"unknown feature",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureUnspecified,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"wrong type",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
// as there's currently no other [feature.SetEventType] than [feature.Boolean],
|
||||
// we need to use a completely other event type to demonstrate the behaviour
|
||||
instance.NewInstanceAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate,
|
||||
"instance",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"first set",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "featureID"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"update flag",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: false},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: false,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"no change",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.SetBooleanInstanceFeature(tt.args.ctx, tt.args.f, tt.args.value)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.details, got)
|
||||
})
|
||||
}
|
||||
}
|
69
internal/command/instance_features.go
Normal file
69
internal/command/instance_features.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type InstanceFeatures struct {
|
||||
LoginDefaultOrg *bool
|
||||
TriggerIntrospectionProjections *bool
|
||||
LegacyIntrospection *bool
|
||||
}
|
||||
|
||||
func (m *InstanceFeatures) isEmpty() bool {
|
||||
return m.LoginDefaultOrg == nil &&
|
||||
m.TriggerIntrospectionProjections == nil &&
|
||||
m.LegacyIntrospection == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
||||
if f.isEmpty() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vigh1", "Errors.NoChangesFound")
|
||||
}
|
||||
wm := NewInstanceFeaturesWriteModel(authz.GetInstance(ctx).InstanceID())
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds := wm.setCommands(ctx, f)
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
|
||||
func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
wm := NewInstanceFeaturesWriteModel(instanceID)
|
||||
return wm.setCommands(ctx, f), nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) ResetInstanceFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
wm := NewInstanceFeaturesWriteModel(instanceID)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wm.isEmpty() {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
aggregate := feature_v2.NewAggregate(instanceID, instanceID)
|
||||
events, err := c.eventstore.Push(ctx, feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
93
internal/command/instance_features_model.go
Normal file
93
internal/command/instance_features_model.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type InstanceFeaturesWriteModel struct {
|
||||
*eventstore.WriteModel
|
||||
InstanceFeatures
|
||||
}
|
||||
|
||||
func NewInstanceFeaturesWriteModel(instanceID string) *InstanceFeaturesWriteModel {
|
||||
m := &InstanceFeaturesWriteModel{
|
||||
WriteModel: &eventstore.WriteModel{
|
||||
AggregateID: instanceID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v1.SetEvent[feature_v1.Boolean]:
|
||||
err = m.reduceBoolFeature(
|
||||
feature_v1.DefaultLoginInstanceEventToV2(e),
|
||||
)
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v2.InstanceResetEventType,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) reduceReset() {
|
||||
m.LoginDefaultOrg = nil
|
||||
m.TriggerIntrospectionProjections = nil
|
||||
m.LegacyIntrospection = nil
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
_, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
m.LoginDefaultOrg = &event.Value
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
m.TriggerIntrospectionProjections = &event.Value
|
||||
case feature.KeyLegacyIntrospection:
|
||||
m.LegacyIntrospection = &event.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *InstanceFeatures) []eventstore.Command {
|
||||
aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
||||
cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.InstanceLoginDefaultOrgEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.InstanceTriggerIntrospectionProjectionsEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType)
|
||||
return cmds
|
||||
}
|
323
internal/command/instance_features_test.go
Normal file
323
internal/command/instance_features_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f *InstanceFeatures
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "all nil, No Change",
|
||||
eventstore: expectEventstore(),
|
||||
args: args{ctx, &InstanceFeatures{}},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-Vigh1", "Errors.NoChangesFound"),
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg, update from v1",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v1.NewSetEvent[feature_v1.Boolean](
|
||||
ctx, &eventstore.Aggregate{
|
||||
ID: "instance1",
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v1.Boolean{
|
||||
Boolean: false,
|
||||
},
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set TriggerIntrospectionProjections",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LegacyIntrospection",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "set all",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set only updated",
|
||||
eventstore: expectEventstore(
|
||||
// throw in some set events, reset and set again.
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.SetInstanceFeatures(tt.args.ctx, tt.args.f)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ResetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType),
|
||||
),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change after previous reset",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change without previous events",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.ResetInstanceFeatures(ctx)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
@@ -215,6 +216,10 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
||||
func newMockPermissionCheckAllowed() domain.PermissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return nil
|
||||
|
56
internal/command/system_features.go
Normal file
56
internal/command/system_features.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SystemFeatures struct {
|
||||
LoginDefaultOrg *bool
|
||||
TriggerIntrospectionProjections *bool
|
||||
LegacyIntrospection *bool
|
||||
}
|
||||
|
||||
func (m *SystemFeatures) isEmpty() bool {
|
||||
return m.LoginDefaultOrg == nil &&
|
||||
m.TriggerIntrospectionProjections == nil &&
|
||||
m.LegacyIntrospection == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {
|
||||
if f.isEmpty() {
|
||||
return nil, zerrors.ThrowInternal(nil, "COMMAND-Oop8a", "Errors.NoChangesFound")
|
||||
}
|
||||
wm := NewSystemFeaturesWriteModel()
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds := wm.setCommands(ctx, f)
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ResetSystemFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
|
||||
wm := NewSystemFeaturesWriteModel()
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wm.isEmpty() {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
events, err := c.eventstore.Push(ctx, feature_v2.NewResetEvent(ctx, aggregate, feature_v2.SystemResetEventType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
94
internal/command/system_features_model.go
Normal file
94
internal/command/system_features_model.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type SystemFeaturesWriteModel struct {
|
||||
*eventstore.WriteModel
|
||||
SystemFeatures
|
||||
}
|
||||
|
||||
func NewSystemFeaturesWriteModel() *SystemFeaturesWriteModel {
|
||||
m := &SystemFeaturesWriteModel{
|
||||
WriteModel: &eventstore.WriteModel{
|
||||
AggregateID: "SYSTEM",
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v2.SystemResetEventType,
|
||||
feature_v2.SystemLoginDefaultOrgEventType,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.SystemLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) reduceReset() {
|
||||
m.LoginDefaultOrg = nil
|
||||
m.TriggerIntrospectionProjections = nil
|
||||
m.LegacyIntrospection = nil
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
_, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
m.LoginDefaultOrg = &event.Value
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
m.TriggerIntrospectionProjections = &event.Value
|
||||
case feature.KeyLegacyIntrospection:
|
||||
m.LegacyIntrospection = &event.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFeatures) []eventstore.Command {
|
||||
aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
||||
cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.SystemLoginDefaultOrgEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.SystemTriggerIntrospectionProjectionsEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType)
|
||||
return cmds
|
||||
}
|
||||
|
||||
func appendFeatureUpdate[T comparable](ctx context.Context, cmds []eventstore.Command, aggregate *feature_v2.Aggregate, oldValue, newValue *T, eventType eventstore.EventType) []eventstore.Command {
|
||||
if newValue != nil && (oldValue == nil || *oldValue != *newValue) {
|
||||
cmds = append(cmds, feature_v2.NewSetEvent[T](ctx, aggregate, eventType, *newValue))
|
||||
}
|
||||
return cmds
|
||||
}
|
290
internal/command/system_features_test.go
Normal file
290
internal/command/system_features_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f *SystemFeatures
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "all nil, No Change",
|
||||
eventstore: expectEventstore(),
|
||||
args: args{context.Background(), &SystemFeatures{}},
|
||||
wantErr: zerrors.ThrowInternal(nil, "COMMAND-Oop8a", "Errors.NoChangesFound"),
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set TriggerIntrospectionProjections",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LegacyIntrospection",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "set all",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set only updated",
|
||||
eventstore: expectEventstore(
|
||||
// throw in some set events, reset and set again.
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.SetSystemFeatures(tt.args.ctx, tt.args.f)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ResetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewResetEvent(context.Background(), aggregate, feature_v2.SystemResetEventType),
|
||||
),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewResetEvent(context.Background(), aggregate, feature_v2.SystemResetEventType),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change after previous reset",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change without previous events",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.ResetSystemFeatures(context.Background())
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,78 +0,0 @@
|
||||
// Code generated by "enumer -type Feature"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _FeatureName = "FeatureUnspecifiedFeatureLoginDefaultOrg"
|
||||
|
||||
var _FeatureIndex = [...]uint8{0, 18, 40}
|
||||
|
||||
const _FeatureLowerName = "featureunspecifiedfeaturelogindefaultorg"
|
||||
|
||||
func (i Feature) String() string {
|
||||
if i < 0 || i >= Feature(len(_FeatureIndex)-1) {
|
||||
return fmt.Sprintf("Feature(%d)", i)
|
||||
}
|
||||
return _FeatureName[_FeatureIndex[i]:_FeatureIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _FeatureNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[FeatureUnspecified-(0)]
|
||||
_ = x[FeatureLoginDefaultOrg-(1)]
|
||||
}
|
||||
|
||||
var _FeatureValues = []Feature{FeatureUnspecified, FeatureLoginDefaultOrg}
|
||||
|
||||
var _FeatureNameToValueMap = map[string]Feature{
|
||||
_FeatureName[0:18]: FeatureUnspecified,
|
||||
_FeatureLowerName[0:18]: FeatureUnspecified,
|
||||
_FeatureName[18:40]: FeatureLoginDefaultOrg,
|
||||
_FeatureLowerName[18:40]: FeatureLoginDefaultOrg,
|
||||
}
|
||||
|
||||
var _FeatureNames = []string{
|
||||
_FeatureName[0:18],
|
||||
_FeatureName[18:40],
|
||||
}
|
||||
|
||||
// FeatureString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func FeatureString(s string) (Feature, error) {
|
||||
if val, ok := _FeatureNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _FeatureNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Feature values", s)
|
||||
}
|
||||
|
||||
// FeatureValues returns all values of the enum
|
||||
func FeatureValues() []Feature {
|
||||
return _FeatureValues
|
||||
}
|
||||
|
||||
// FeatureStrings returns a slice of all String values of the enum
|
||||
func FeatureStrings() []string {
|
||||
strs := make([]string, len(_FeatureNames))
|
||||
copy(strs, _FeatureNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAFeature returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Feature) IsAFeature() bool {
|
||||
for _, v := range _FeatureValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ObjectDetails struct {
|
||||
Sequence uint64
|
||||
|
@@ -532,6 +532,12 @@ func NewIsNullCond(column string) Condition {
|
||||
}
|
||||
}
|
||||
|
||||
func NewIsNotNullCond(column string) Condition {
|
||||
return func(string) (string, []any) {
|
||||
return column + " IS NOT NULL", nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewTextArrayContainsCond returns a Condition that checks if the column that stores an array of text contains the given value
|
||||
func NewTextArrayContainsCond(column string, value string) Condition {
|
||||
return func(param string) (string, []any) {
|
||||
|
@@ -44,7 +44,6 @@ func (rm *ReadModel) Reduce() error {
|
||||
rm.ChangeDate = rm.Events[len(rm.Events)-1].CreatedAt()
|
||||
rm.ProcessedSequence = rm.Events[len(rm.Events)-1].Sequence()
|
||||
// all events processed and not needed anymore
|
||||
rm.Events = nil
|
||||
rm.Events = []Event{}
|
||||
rm.Events = rm.Events[0:0]
|
||||
return nil
|
||||
}
|
||||
|
30
internal/feature/feature.go
Normal file
30
internal/feature/feature.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package feature
|
||||
|
||||
//go:generate enumer -type Key -transform snake -trimprefix Key
|
||||
type Key int
|
||||
|
||||
const (
|
||||
KeyUnspecified Key = iota
|
||||
KeyLoginDefaultOrg
|
||||
KeyTriggerIntrospectionProjections
|
||||
KeyLegacyIntrospection
|
||||
)
|
||||
|
||||
//go:generate enumer -type Level -transform snake -trimprefix Level
|
||||
type Level int
|
||||
|
||||
const (
|
||||
LevelUnspecified Level = iota
|
||||
LevelSystem
|
||||
LevelInstance
|
||||
LevelOrg
|
||||
LevelProject
|
||||
LevelApp
|
||||
LevelUser
|
||||
)
|
||||
|
||||
type Features struct {
|
||||
LoginDefaultOrg bool `json:"login_default_org,omitempty"`
|
||||
TriggerIntrospectionProjections bool `json:"trigger_introspection_projections,omitempty"`
|
||||
LegacyIntrospection bool `json:"legacy_introspection,omitempty"`
|
||||
}
|
43
internal/feature/feature_test.go
Normal file
43
internal/feature/feature_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKey(t *testing.T) {
|
||||
tests := []string{
|
||||
"unspecified",
|
||||
"login_default_org",
|
||||
"trigger_introspection_projections",
|
||||
"legacy_introspection",
|
||||
}
|
||||
for _, want := range tests {
|
||||
t.Run(want, func(t *testing.T) {
|
||||
feature, err := KeyString(want)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, feature.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevel(t *testing.T) {
|
||||
tests := []string{
|
||||
"unspecified",
|
||||
"system",
|
||||
"instance",
|
||||
"org",
|
||||
"project",
|
||||
"app",
|
||||
"user",
|
||||
}
|
||||
for _, want := range tests {
|
||||
t.Run(want, func(t *testing.T) {
|
||||
level, err := LevelString(want)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, level.String())
|
||||
})
|
||||
}
|
||||
}
|
86
internal/feature/key_enumer.go
Normal file
86
internal/feature/key_enumer.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Code generated by "enumer -type Key -transform snake -trimprefix Key"; DO NOT EDIT.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspection"
|
||||
|
||||
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81}
|
||||
|
||||
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspection"
|
||||
|
||||
func (i Key) String() string {
|
||||
if i < 0 || i >= Key(len(_KeyIndex)-1) {
|
||||
return fmt.Sprintf("Key(%d)", i)
|
||||
}
|
||||
return _KeyName[_KeyIndex[i]:_KeyIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _KeyNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[KeyUnspecified-(0)]
|
||||
_ = x[KeyLoginDefaultOrg-(1)]
|
||||
_ = x[KeyTriggerIntrospectionProjections-(2)]
|
||||
_ = x[KeyLegacyIntrospection-(3)]
|
||||
}
|
||||
|
||||
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection}
|
||||
|
||||
var _KeyNameToValueMap = map[string]Key{
|
||||
_KeyName[0:11]: KeyUnspecified,
|
||||
_KeyLowerName[0:11]: KeyUnspecified,
|
||||
_KeyName[11:28]: KeyLoginDefaultOrg,
|
||||
_KeyLowerName[11:28]: KeyLoginDefaultOrg,
|
||||
_KeyName[28:61]: KeyTriggerIntrospectionProjections,
|
||||
_KeyLowerName[28:61]: KeyTriggerIntrospectionProjections,
|
||||
_KeyName[61:81]: KeyLegacyIntrospection,
|
||||
_KeyLowerName[61:81]: KeyLegacyIntrospection,
|
||||
}
|
||||
|
||||
var _KeyNames = []string{
|
||||
_KeyName[0:11],
|
||||
_KeyName[11:28],
|
||||
_KeyName[28:61],
|
||||
_KeyName[61:81],
|
||||
}
|
||||
|
||||
// KeyString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func KeyString(s string) (Key, error) {
|
||||
if val, ok := _KeyNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _KeyNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Key values", s)
|
||||
}
|
||||
|
||||
// KeyValues returns all values of the enum
|
||||
func KeyValues() []Key {
|
||||
return _KeyValues
|
||||
}
|
||||
|
||||
// KeyStrings returns a slice of all String values of the enum
|
||||
func KeyStrings() []string {
|
||||
strs := make([]string, len(_KeyNames))
|
||||
copy(strs, _KeyNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAKey returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Key) IsAKey() bool {
|
||||
for _, v := range _KeyValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
98
internal/feature/level_enumer.go
Normal file
98
internal/feature/level_enumer.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Code generated by "enumer -type Level -transform snake -trimprefix Level"; DO NOT EDIT.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _LevelName = "unspecifiedsysteminstanceorgprojectappuser"
|
||||
|
||||
var _LevelIndex = [...]uint8{0, 11, 17, 25, 28, 35, 38, 42}
|
||||
|
||||
const _LevelLowerName = "unspecifiedsysteminstanceorgprojectappuser"
|
||||
|
||||
func (i Level) String() string {
|
||||
if i < 0 || i >= Level(len(_LevelIndex)-1) {
|
||||
return fmt.Sprintf("Level(%d)", i)
|
||||
}
|
||||
return _LevelName[_LevelIndex[i]:_LevelIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _LevelNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[LevelUnspecified-(0)]
|
||||
_ = x[LevelSystem-(1)]
|
||||
_ = x[LevelInstance-(2)]
|
||||
_ = x[LevelOrg-(3)]
|
||||
_ = x[LevelProject-(4)]
|
||||
_ = x[LevelApp-(5)]
|
||||
_ = x[LevelUser-(6)]
|
||||
}
|
||||
|
||||
var _LevelValues = []Level{LevelUnspecified, LevelSystem, LevelInstance, LevelOrg, LevelProject, LevelApp, LevelUser}
|
||||
|
||||
var _LevelNameToValueMap = map[string]Level{
|
||||
_LevelName[0:11]: LevelUnspecified,
|
||||
_LevelLowerName[0:11]: LevelUnspecified,
|
||||
_LevelName[11:17]: LevelSystem,
|
||||
_LevelLowerName[11:17]: LevelSystem,
|
||||
_LevelName[17:25]: LevelInstance,
|
||||
_LevelLowerName[17:25]: LevelInstance,
|
||||
_LevelName[25:28]: LevelOrg,
|
||||
_LevelLowerName[25:28]: LevelOrg,
|
||||
_LevelName[28:35]: LevelProject,
|
||||
_LevelLowerName[28:35]: LevelProject,
|
||||
_LevelName[35:38]: LevelApp,
|
||||
_LevelLowerName[35:38]: LevelApp,
|
||||
_LevelName[38:42]: LevelUser,
|
||||
_LevelLowerName[38:42]: LevelUser,
|
||||
}
|
||||
|
||||
var _LevelNames = []string{
|
||||
_LevelName[0:11],
|
||||
_LevelName[11:17],
|
||||
_LevelName[17:25],
|
||||
_LevelName[25:28],
|
||||
_LevelName[28:35],
|
||||
_LevelName[35:38],
|
||||
_LevelName[38:42],
|
||||
}
|
||||
|
||||
// LevelString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func LevelString(s string) (Level, error) {
|
||||
if val, ok := _LevelNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _LevelNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Level values", s)
|
||||
}
|
||||
|
||||
// LevelValues returns all values of the enum
|
||||
func LevelValues() []Level {
|
||||
return _LevelValues
|
||||
}
|
||||
|
||||
// LevelStrings returns a slice of all String values of the enum
|
||||
func LevelStrings() []string {
|
||||
strs := make([]string, len(_LevelNames))
|
||||
copy(strs, _LevelNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsALevel returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Level) IsALevel() bool {
|
||||
for _, v := range _LevelValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
@@ -49,6 +50,7 @@ type Client struct {
|
||||
OrgV2 organisation.OrganizationServiceClient
|
||||
System system.SystemServiceClient
|
||||
ExecutionV3 execution.ExecutionServiceClient
|
||||
FeatureV2 feature.FeatureServiceClient
|
||||
}
|
||||
|
||||
func newClient(cc *grpc.ClientConn) Client {
|
||||
@@ -63,6 +65,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
||||
OrgV2: organisation.NewOrganizationServiceClient(cc),
|
||||
System: system.NewSystemServiceClient(cc),
|
||||
ExecutionV3: execution.NewExecutionServiceClient(cc),
|
||||
FeatureV2: feature.NewFeatureServiceClient(cc),
|
||||
}
|
||||
}
|
||||
|
||||
|
14
internal/query/converter.go
Normal file
14
internal/query/converter.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func readModelToObjectDetails(model *eventstore.ReadModel) *domain.ObjectDetails {
|
||||
return &domain.ObjectDetails{
|
||||
Sequence: model.ProcessedSequence,
|
||||
ResourceOwner: model.ResourceOwner,
|
||||
EventDate: model.ChangeDate,
|
||||
}
|
||||
}
|
@@ -3,6 +3,8 @@ package query
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@@ -94,21 +97,12 @@ type Instance struct {
|
||||
Sequence uint64
|
||||
Name string
|
||||
|
||||
DefaultOrgID string
|
||||
IAMProjectID string
|
||||
ConsoleID string
|
||||
ConsoleAppID string
|
||||
DefaultLang language.Tag
|
||||
Domains []*InstanceDomain
|
||||
host string
|
||||
csp csp
|
||||
block *bool
|
||||
auditLogRetention *time.Duration
|
||||
}
|
||||
|
||||
type csp struct {
|
||||
enabled bool
|
||||
allowedOrigins database.TextArray[string]
|
||||
DefaultOrgID string
|
||||
IAMProjectID string
|
||||
ConsoleID string
|
||||
ConsoleAppID string
|
||||
DefaultLang language.Tag
|
||||
Domains []*InstanceDomain
|
||||
}
|
||||
|
||||
type Instances struct {
|
||||
@@ -116,53 +110,6 @@ type Instances struct {
|
||||
Instances []*Instance
|
||||
}
|
||||
|
||||
func (i *Instance) InstanceID() string {
|
||||
return i.ID
|
||||
}
|
||||
|
||||
func (i *Instance) ProjectID() string {
|
||||
return i.IAMProjectID
|
||||
}
|
||||
|
||||
func (i *Instance) ConsoleClientID() string {
|
||||
return i.ConsoleID
|
||||
}
|
||||
|
||||
func (i *Instance) ConsoleApplicationID() string {
|
||||
return i.ConsoleAppID
|
||||
}
|
||||
|
||||
func (i *Instance) RequestedDomain() string {
|
||||
return strings.Split(i.host, ":")[0]
|
||||
}
|
||||
|
||||
func (i *Instance) RequestedHost() string {
|
||||
return i.host
|
||||
}
|
||||
|
||||
func (i *Instance) DefaultLanguage() language.Tag {
|
||||
return i.DefaultLang
|
||||
}
|
||||
|
||||
func (i *Instance) DefaultOrganisationID() string {
|
||||
return i.DefaultOrgID
|
||||
}
|
||||
|
||||
func (i *Instance) SecurityPolicyAllowedOrigins() []string {
|
||||
if !i.csp.enabled {
|
||||
return nil
|
||||
}
|
||||
return i.csp.allowedOrigins
|
||||
}
|
||||
|
||||
func (i *Instance) Block() *bool {
|
||||
return i.block
|
||||
}
|
||||
|
||||
func (i *Instance) AuditLogRetention() *time.Duration {
|
||||
return i.auditLogRetention
|
||||
}
|
||||
|
||||
type InstanceSearchQueries struct {
|
||||
SearchRequest
|
||||
Queries []SearchQuery
|
||||
@@ -224,7 +171,7 @@ func (q *Queries) Instance(ctx context.Context, shouldTriggerBulk bool) (instanc
|
||||
traceSpan.EndWithError(err)
|
||||
}
|
||||
|
||||
stmt, scan := prepareInstanceDomainQuery(ctx, q.client, authz.GetInstance(ctx).RequestedDomain())
|
||||
stmt, scan := prepareInstanceDomainQuery(ctx, q.client)
|
||||
query, args, err := stmt.Where(sq.Eq{
|
||||
InstanceColumnID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
}).ToSql()
|
||||
@@ -239,28 +186,34 @@ func (q *Queries) Instance(ctx context.Context, shouldTriggerBulk bool) (instanc
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func (q *Queries) InstanceByHost(ctx context.Context, host string) (instance authz.Instance, err error) {
|
||||
var (
|
||||
//go:embed instance_by_domain.sql
|
||||
instanceByDomainQuery string
|
||||
|
||||
//go:embed instance_by_id.sql
|
||||
instanceByIDQuery string
|
||||
)
|
||||
|
||||
func (q *Queries) InstanceByHost(ctx context.Context, host string) (_ authz.Instance, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
stmt, scan := prepareAuthzInstanceQuery(ctx, q.client, host)
|
||||
host = strings.Split(host, ":")[0] //remove possible port
|
||||
query, args, err := stmt.Where(sq.Eq{
|
||||
InstanceDomainDomainCol.identifier(): host,
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-SAfg2", "Errors.Query.SQLStatement")
|
||||
}
|
||||
|
||||
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
|
||||
instance, err = scan(rows)
|
||||
return err
|
||||
}, query, args...)
|
||||
domain := strings.Split(host, ":")[0] // remove possible port
|
||||
instance, scan := scanAuthzInstance(host, domain)
|
||||
err = q.client.QueryRowContext(ctx, scan, instanceByDomainQuery, domain)
|
||||
logging.OnError(err).WithField("host", host).WithField("domain", domain).Warn("instance by host")
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func (q *Queries) InstanceByID(ctx context.Context) (_ authz.Instance, err error) {
|
||||
return q.Instance(ctx, true)
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
instance, scan := scanAuthzInstance("", "")
|
||||
err = q.client.QueryRowContext(ctx, scan, instanceByIDQuery, instanceID)
|
||||
logging.OnError(err).WithField("instance_id", instanceID).Warn("instance by ID")
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag {
|
||||
@@ -268,48 +221,7 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag {
|
||||
if err != nil {
|
||||
return language.Und
|
||||
}
|
||||
return instance.DefaultLanguage()
|
||||
}
|
||||
|
||||
func prepareInstanceQuery(ctx context.Context, db prepareDatabase, host string) (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
|
||||
return sq.Select(
|
||||
InstanceColumnID.identifier(),
|
||||
InstanceColumnCreationDate.identifier(),
|
||||
InstanceColumnChangeDate.identifier(),
|
||||
InstanceColumnSequence.identifier(),
|
||||
InstanceColumnDefaultOrgID.identifier(),
|
||||
InstanceColumnProjectID.identifier(),
|
||||
InstanceColumnConsoleID.identifier(),
|
||||
InstanceColumnConsoleAppID.identifier(),
|
||||
InstanceColumnDefaultLanguage.identifier(),
|
||||
).
|
||||
From(instanceTable.identifier() + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*Instance, error) {
|
||||
var (
|
||||
instance = &Instance{host: host}
|
||||
lang = ""
|
||||
)
|
||||
err := row.Scan(
|
||||
&instance.ID,
|
||||
&instance.CreationDate,
|
||||
&instance.ChangeDate,
|
||||
&instance.Sequence,
|
||||
&instance.DefaultOrgID,
|
||||
&instance.IAMProjectID,
|
||||
&instance.ConsoleID,
|
||||
&instance.ConsoleAppID,
|
||||
&lang,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-5m09s", "Errors.IAM.NotFound")
|
||||
}
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-3j9sf", "Errors.Internal")
|
||||
}
|
||||
instance.DefaultLang = language.Make(lang)
|
||||
return instance, nil
|
||||
}
|
||||
return instance.DefaultLang
|
||||
}
|
||||
|
||||
func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) {
|
||||
@@ -417,7 +329,7 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu
|
||||
}
|
||||
}
|
||||
|
||||
func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase, host string) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) {
|
||||
func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) {
|
||||
return sq.Select(
|
||||
InstanceColumnID.identifier(),
|
||||
InstanceColumnCreationDate.identifier(),
|
||||
@@ -441,7 +353,6 @@ func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase, host st
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) (*Instance, error) {
|
||||
instance := &Instance{
|
||||
host: host,
|
||||
Domains: make([]*InstanceDomain, 0),
|
||||
}
|
||||
lang := ""
|
||||
@@ -499,104 +410,123 @@ func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase, host st
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAuthzInstanceQuery(ctx context.Context, db prepareDatabase, host string) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) {
|
||||
return sq.Select(
|
||||
InstanceColumnID.identifier(),
|
||||
InstanceColumnCreationDate.identifier(),
|
||||
InstanceColumnChangeDate.identifier(),
|
||||
InstanceColumnSequence.identifier(),
|
||||
InstanceColumnName.identifier(),
|
||||
InstanceColumnDefaultOrgID.identifier(),
|
||||
InstanceColumnProjectID.identifier(),
|
||||
InstanceColumnConsoleID.identifier(),
|
||||
InstanceColumnConsoleAppID.identifier(),
|
||||
InstanceColumnDefaultLanguage.identifier(),
|
||||
InstanceDomainDomainCol.identifier(),
|
||||
InstanceDomainIsPrimaryCol.identifier(),
|
||||
InstanceDomainIsGeneratedCol.identifier(),
|
||||
InstanceDomainCreationDateCol.identifier(),
|
||||
InstanceDomainChangeDateCol.identifier(),
|
||||
InstanceDomainSequenceCol.identifier(),
|
||||
SecurityPolicyColumnEnabled.identifier(),
|
||||
SecurityPolicyColumnAllowedOrigins.identifier(),
|
||||
LimitsColumnAuditLogRetention.identifier(),
|
||||
LimitsColumnBlock.identifier(),
|
||||
).
|
||||
From(instanceTable.identifier()).
|
||||
LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)).
|
||||
LeftJoin(join(SecurityPolicyColumnInstanceID, InstanceColumnID)).
|
||||
LeftJoin(join(LimitsColumnInstanceID, InstanceColumnID) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) (*Instance, error) {
|
||||
instance := &Instance{
|
||||
host: host,
|
||||
Domains: make([]*InstanceDomain, 0),
|
||||
}
|
||||
lang := ""
|
||||
for rows.Next() {
|
||||
var (
|
||||
domain sql.NullString
|
||||
isPrimary sql.NullBool
|
||||
isGenerated sql.NullBool
|
||||
changeDate sql.NullTime
|
||||
creationDate sql.NullTime
|
||||
sequence sql.NullInt64
|
||||
securityPolicyEnabled sql.NullBool
|
||||
auditLogRetention database.NullDuration
|
||||
block sql.NullBool
|
||||
)
|
||||
err := rows.Scan(
|
||||
&instance.ID,
|
||||
&instance.CreationDate,
|
||||
&instance.ChangeDate,
|
||||
&instance.Sequence,
|
||||
&instance.Name,
|
||||
&instance.DefaultOrgID,
|
||||
&instance.IAMProjectID,
|
||||
&instance.ConsoleID,
|
||||
&instance.ConsoleAppID,
|
||||
&lang,
|
||||
&domain,
|
||||
&isPrimary,
|
||||
&isGenerated,
|
||||
&changeDate,
|
||||
&creationDate,
|
||||
&sequence,
|
||||
&securityPolicyEnabled,
|
||||
&instance.csp.allowedOrigins,
|
||||
&auditLogRetention,
|
||||
&block,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-d3fas", "Errors.Internal")
|
||||
}
|
||||
if !domain.Valid {
|
||||
continue
|
||||
}
|
||||
instance.Domains = append(instance.Domains, &InstanceDomain{
|
||||
CreationDate: creationDate.Time,
|
||||
ChangeDate: changeDate.Time,
|
||||
Sequence: uint64(sequence.Int64),
|
||||
Domain: domain.String,
|
||||
IsPrimary: isPrimary.Bool,
|
||||
IsGenerated: isGenerated.Bool,
|
||||
InstanceID: instance.ID,
|
||||
})
|
||||
if auditLogRetention.Valid {
|
||||
instance.auditLogRetention = &auditLogRetention.Duration
|
||||
}
|
||||
if block.Valid {
|
||||
instance.block = &block.Bool
|
||||
}
|
||||
instance.csp.enabled = securityPolicyEnabled.Bool
|
||||
}
|
||||
if instance.ID == "" {
|
||||
return nil, zerrors.ThrowNotFound(nil, "QUERY-1kIjX", "Errors.IAM.NotFound")
|
||||
}
|
||||
instance.DefaultLang = language.Make(lang)
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Dfbe2", "Errors.Query.CloseRows")
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
type authzInstance struct {
|
||||
id string
|
||||
iamProjectID string
|
||||
consoleID string
|
||||
consoleAppID string
|
||||
host string
|
||||
domain string
|
||||
defaultLang language.Tag
|
||||
defaultOrgID string
|
||||
csp csp
|
||||
block *bool
|
||||
auditLogRetention *time.Duration
|
||||
features feature.Features
|
||||
}
|
||||
|
||||
type csp struct {
|
||||
enabled bool
|
||||
allowedOrigins database.TextArray[string]
|
||||
}
|
||||
|
||||
func (i *authzInstance) InstanceID() string {
|
||||
return i.id
|
||||
}
|
||||
|
||||
func (i *authzInstance) ProjectID() string {
|
||||
return i.iamProjectID
|
||||
}
|
||||
|
||||
func (i *authzInstance) ConsoleClientID() string {
|
||||
return i.consoleID
|
||||
}
|
||||
|
||||
func (i *authzInstance) ConsoleApplicationID() string {
|
||||
return i.consoleAppID
|
||||
}
|
||||
|
||||
func (i *authzInstance) RequestedDomain() string {
|
||||
return strings.Split(i.host, ":")[0]
|
||||
}
|
||||
|
||||
func (i *authzInstance) RequestedHost() string {
|
||||
return i.host
|
||||
}
|
||||
|
||||
func (i *authzInstance) DefaultLanguage() language.Tag {
|
||||
return i.defaultLang
|
||||
}
|
||||
|
||||
func (i *authzInstance) DefaultOrganisationID() string {
|
||||
return i.defaultOrgID
|
||||
}
|
||||
|
||||
func (i *authzInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
if !i.csp.enabled {
|
||||
return nil
|
||||
}
|
||||
return i.csp.allowedOrigins
|
||||
}
|
||||
|
||||
func (i *authzInstance) Block() *bool {
|
||||
return i.block
|
||||
}
|
||||
|
||||
func (i *authzInstance) AuditLogRetention() *time.Duration {
|
||||
return i.auditLogRetention
|
||||
}
|
||||
|
||||
func (i *authzInstance) Features() feature.Features {
|
||||
return i.features
|
||||
}
|
||||
|
||||
func scanAuthzInstance(host, domain string) (*authzInstance, func(row *sql.Row) error) {
|
||||
instance := &authzInstance{
|
||||
host: host,
|
||||
domain: domain,
|
||||
}
|
||||
return instance, func(row *sql.Row) error {
|
||||
var (
|
||||
lang string
|
||||
securityPolicyEnabled sql.NullBool
|
||||
auditLogRetention database.NullDuration
|
||||
block sql.NullBool
|
||||
features []byte
|
||||
)
|
||||
err := row.Scan(
|
||||
&instance.id,
|
||||
&instance.defaultOrgID,
|
||||
&instance.iamProjectID,
|
||||
&instance.consoleID,
|
||||
&instance.consoleAppID,
|
||||
&lang,
|
||||
&securityPolicyEnabled,
|
||||
&instance.csp.allowedOrigins,
|
||||
&auditLogRetention,
|
||||
&block,
|
||||
&features,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return zerrors.ThrowNotFound(nil, "QUERY-1kIjX", "Errors.IAM.NotFound")
|
||||
}
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "QUERY-d3fas", "Errors.Internal")
|
||||
}
|
||||
instance.defaultLang = language.Make(lang)
|
||||
if auditLogRetention.Valid {
|
||||
instance.auditLogRetention = &auditLogRetention.Duration
|
||||
}
|
||||
if block.Valid {
|
||||
instance.block = &block.Bool
|
||||
}
|
||||
instance.csp.enabled = securityPolicyEnabled.Bool
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err = json.Unmarshal(features, &instance.features); err != nil {
|
||||
return zerrors.ThrowInternal(err, "QUERY-Po8ki", "Errors.Internal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
30
internal/query/instance_by_domain.sql
Normal file
30
internal/query/instance_by_domain.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
with domain as (
|
||||
select instance_id from projections.instance_domains
|
||||
where domain = $1
|
||||
), features as (
|
||||
select instance_id, json_object_agg(
|
||||
coalesce(i.key, s.key),
|
||||
coalesce(i.value, s.value)
|
||||
) features
|
||||
from domain d
|
||||
cross join projections.system_features s
|
||||
full outer join projections.instance_features i using (key, instance_id)
|
||||
group by instance_id
|
||||
)
|
||||
select
|
||||
i.id,
|
||||
i.default_org_id,
|
||||
i.iam_project_id,
|
||||
i.console_client_id,
|
||||
i.console_app_id,
|
||||
i.default_language,
|
||||
s.enabled,
|
||||
s.origins,
|
||||
l.audit_log_retention,
|
||||
l.block,
|
||||
f.features
|
||||
from domain d
|
||||
join projections.instances i on i.id = d.instance_id
|
||||
left join projections.security_policies s on i.id = s.instance_id
|
||||
left join projections.limits l on i.id = l.instance_id
|
||||
left join features f on i.id = f.instance_id;
|
27
internal/query/instance_by_id.sql
Normal file
27
internal/query/instance_by_id.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
with features as (
|
||||
select instance_id, json_object_agg(
|
||||
coalesce(i.key, s.key),
|
||||
coalesce(i.value, s.value)
|
||||
) features
|
||||
from (select $1::text instance_id) x
|
||||
cross join projections.system_features s
|
||||
full outer join projections.instance_features i using (key, instance_id)
|
||||
group by instance_id
|
||||
)
|
||||
select
|
||||
i.id,
|
||||
i.default_org_id,
|
||||
i.iam_project_id,
|
||||
i.console_client_id,
|
||||
i.console_app_id,
|
||||
i.default_language,
|
||||
s.enabled,
|
||||
s.origins,
|
||||
l.audit_log_retention,
|
||||
l.block,
|
||||
f.features
|
||||
from projections.instances i
|
||||
left join projections.security_policies s on i.id = s.instance_id
|
||||
left join projections.limits l on i.id = l.instance_id
|
||||
left join features f on i.id = f.instance_id
|
||||
where i.id = $1;
|
30
internal/query/instance_features.go
Normal file
30
internal/query/instance_features.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
type InstanceFeatures struct {
|
||||
Details *domain.ObjectDetails
|
||||
LoginDefaultOrg FeatureSource[bool]
|
||||
TriggerIntrospectionProjections FeatureSource[bool]
|
||||
LegacyIntrospection FeatureSource[bool]
|
||||
}
|
||||
|
||||
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {
|
||||
var system *SystemFeatures
|
||||
if cascade {
|
||||
system, err = q.GetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m := NewInstanceFeaturesReadModel(ctx, system)
|
||||
if err = q.eventstore.FilterToQueryReducer(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.instance.Details = readModelToObjectDetails(m.ReadModel)
|
||||
return m.instance, nil
|
||||
}
|
109
internal/query/instance_features_model.go
Normal file
109
internal/query/instance_features_model.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type InstanceFeaturesReadModel struct {
|
||||
*eventstore.ReadModel
|
||||
system *SystemFeatures
|
||||
instance *InstanceFeatures
|
||||
}
|
||||
|
||||
func NewInstanceFeaturesReadModel(ctx context.Context, system *SystemFeatures) *InstanceFeaturesReadModel {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
m := &InstanceFeaturesReadModel{
|
||||
ReadModel: &eventstore.ReadModel{
|
||||
AggregateID: instanceID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
instance: new(InstanceFeatures),
|
||||
system: system,
|
||||
}
|
||||
m.populateFromSystem()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v1.SetEvent[feature_v1.Boolean]:
|
||||
err = m.reduceBoolFeature(
|
||||
feature_v1.DefaultLoginInstanceEventToV2(e),
|
||||
)
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.ReadModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v2.InstanceResetEventType,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) reduceReset() {
|
||||
if m.populateFromSystem() {
|
||||
return
|
||||
}
|
||||
m.instance.LoginDefaultOrg = FeatureSource[bool]{}
|
||||
m.instance.TriggerIntrospectionProjections = FeatureSource[bool]{}
|
||||
m.instance.LegacyIntrospection = FeatureSource[bool]{}
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
|
||||
if m.system == nil {
|
||||
return false
|
||||
}
|
||||
m.instance.LoginDefaultOrg = m.system.LoginDefaultOrg
|
||||
m.instance.TriggerIntrospectionProjections = m.system.TriggerIntrospectionProjections
|
||||
m.instance.LegacyIntrospection = m.system.LegacyIntrospection
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
level, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var dst *FeatureSource[bool]
|
||||
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
dst = &m.instance.LoginDefaultOrg
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
dst = &m.instance.TriggerIntrospectionProjections
|
||||
case feature.KeyLegacyIntrospection:
|
||||
dst = &m.instance.LegacyIntrospection
|
||||
}
|
||||
*dst = FeatureSource[bool]{
|
||||
Level: level,
|
||||
Value: event.Value,
|
||||
}
|
||||
return nil
|
||||
}
|
230
internal/query/instance_features_test.go
Normal file
230
internal/query/instance_features_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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/feature/feature_v2"
|
||||
)
|
||||
|
||||
func TestQueries_GetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
|
||||
type args struct {
|
||||
cascade bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *InstanceFeatures
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
args: args{false},
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "system filter error cascaded",
|
||||
args: args{true},
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "no features set, not cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no features set, cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
),
|
||||
args: args{true},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
))),
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
),
|
||||
),
|
||||
args: args{true},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature, cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
))),
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
args: args{true},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature, not cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
args: args{false},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q := &Queries{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := q.GetInstanceFeatures(ctx, tt.args.cascade)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@@ -12,33 +12,9 @@ import (
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
instanceQuery = `SELECT projections.instances.id,` +
|
||||
` projections.instances.creation_date,` +
|
||||
` projections.instances.change_date,` +
|
||||
` projections.instances.sequence,` +
|
||||
` projections.instances.default_org_id,` +
|
||||
` projections.instances.iam_project_id,` +
|
||||
` projections.instances.console_client_id,` +
|
||||
` projections.instances.console_app_id,` +
|
||||
` projections.instances.default_language` +
|
||||
` FROM projections.instances` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`
|
||||
instanceCols = []string{
|
||||
"id",
|
||||
"creation_date",
|
||||
"change_date",
|
||||
"sequence",
|
||||
"default_org_id",
|
||||
"iam_project_id",
|
||||
"console_client_id",
|
||||
"console_app_id",
|
||||
"default_language",
|
||||
}
|
||||
instancesQuery = `SELECT f.count, f.id,` +
|
||||
` projections.instances.creation_date,` +
|
||||
` projections.instances.change_date,` +
|
||||
@@ -93,76 +69,6 @@ func Test_InstancePrepares(t *testing.T) {
|
||||
want want
|
||||
object interface{}
|
||||
}{
|
||||
{
|
||||
name: "prepareInstanceQuery no result",
|
||||
additionalArgs: []reflect.Value{reflect.ValueOf("")},
|
||||
prepare: prepareInstanceQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueriesScanErr(
|
||||
regexp.QuoteMeta(instanceQuery),
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !zerrors.IsNotFound(err) {
|
||||
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*Instance)(nil),
|
||||
},
|
||||
{
|
||||
name: "prepareInstanceQuery found",
|
||||
additionalArgs: []reflect.Value{reflect.ValueOf("")},
|
||||
prepare: prepareInstanceQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQuery(
|
||||
regexp.QuoteMeta(instanceQuery),
|
||||
instanceCols,
|
||||
[]driver.Value{
|
||||
"id",
|
||||
testNow,
|
||||
testNow,
|
||||
uint64(20211108),
|
||||
"global-org-id",
|
||||
"project-id",
|
||||
"client-id",
|
||||
"app-id",
|
||||
"en",
|
||||
},
|
||||
),
|
||||
},
|
||||
object: &Instance{
|
||||
ID: "id",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
Sequence: 20211108,
|
||||
DefaultOrgID: "global-org-id",
|
||||
IAMProjectID: "project-id",
|
||||
ConsoleID: "client-id",
|
||||
ConsoleAppID: "app-id",
|
||||
DefaultLang: language.English,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareInstanceQuery sql err",
|
||||
additionalArgs: []reflect.Value{reflect.ValueOf("")},
|
||||
prepare: prepareInstanceQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueryErr(
|
||||
regexp.QuoteMeta(instanceQuery),
|
||||
sql.ErrConnDone,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrConnDone) {
|
||||
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*Instance)(nil),
|
||||
},
|
||||
{
|
||||
name: "prepareInstancesQuery no result",
|
||||
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) {
|
||||
|
120
internal/query/projection/instance_features.go
Normal file
120
internal/query/projection/instance_features.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
InstanceFeatureTable = "projections.instance_features"
|
||||
|
||||
InstanceFeatureInstanceIDCol = "instance_id"
|
||||
InstanceFeatureKeyCol = "key"
|
||||
InstanceFeatureCreationDateCol = "creation_date"
|
||||
InstanceFeatureChangeDateCol = "change_date"
|
||||
InstanceFeatureSequenceCol = "sequence"
|
||||
InstanceFeatureValueCol = "value"
|
||||
)
|
||||
|
||||
type instanceFeatureProjection struct{}
|
||||
|
||||
func newInstanceFeatureProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(instanceFeatureProjection))
|
||||
}
|
||||
|
||||
func (*instanceFeatureProjection) Name() string {
|
||||
return InstanceFeatureTable
|
||||
}
|
||||
|
||||
func (*instanceFeatureProjection) Init() *old_handler.Check {
|
||||
return handler.NewTableCheck(handler.NewTable(
|
||||
[]*handler.InitColumn{
|
||||
handler.NewColumn(InstanceFeatureInstanceIDCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(InstanceFeatureKeyCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(InstanceFeatureCreationDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(InstanceFeatureChangeDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(InstanceFeatureSequenceCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(InstanceFeatureValueCol, handler.ColumnTypeJSONB),
|
||||
},
|
||||
handler.NewPrimaryKey(InstanceFeatureInstanceIDCol, InstanceFeatureKeyCol),
|
||||
))
|
||||
}
|
||||
|
||||
func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{{
|
||||
Aggregate: feature_v2.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: feature_v1.DefaultLoginInstanceEventType,
|
||||
Reduce: reduceSetDefaultLoginInstance_v1,
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceResetEventType,
|
||||
Reduce: reduceInstanceResetFeatures,
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceLoginDefaultOrgEventType,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func reduceSetDefaultLoginInstance_v1(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v1.SetEvent[feature_v1.Boolean])
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-in2Xo", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
return reduceInstanceSetFeature[bool](
|
||||
feature_v1.DefaultLoginInstanceEventToV2(e),
|
||||
)
|
||||
}
|
||||
|
||||
func reduceInstanceSetFeature[T any](event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.SetEvent[T])
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-uPh8O", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
f, err := e.FeatureJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
columns := []handler.Column{
|
||||
handler.NewCol(InstanceFeatureInstanceIDCol, e.Aggregate().ID),
|
||||
handler.NewCol(InstanceFeatureKeyCol, f.Key.String()),
|
||||
handler.NewCol(InstanceFeatureCreationDateCol, handler.OnlySetValueOnInsert(InstanceFeatureTable, e.CreationDate())),
|
||||
handler.NewCol(InstanceFeatureChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(InstanceFeatureSequenceCol, e.Sequence()),
|
||||
handler.NewCol(InstanceFeatureValueCol, f.Value),
|
||||
}
|
||||
return handler.NewUpsertStatement(e, columns[0:2], columns), nil
|
||||
}
|
||||
|
||||
func reduceInstanceResetFeatures(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.ResetEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-roo6A", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
return handler.NewDeleteStatement(e, []handler.Condition{
|
||||
handler.NewCond(InstanceFeatureInstanceIDCol, e.Aggregate().ID),
|
||||
}), nil
|
||||
}
|
152
internal/query/projection/instance_features_test.go
Normal file
152
internal/query/projection/instance_features_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestInstanceFeaturesProjection_reduces(t *testing.T) {
|
||||
type args struct {
|
||||
event func(t *testing.T) eventstore.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "reduceInstanceSetFeature",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte(`{"value": true}`),
|
||||
), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]),
|
||||
},
|
||||
reduce: reduceInstanceSetFeature[bool],
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.instance_features (instance_id, key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.instance_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"legacy_introspection",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
[]byte("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceSetDefaultLoginInstance_v1",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v1.AggregateType,
|
||||
[]byte(`{"Value":{"Boolean":true}}`),
|
||||
), eventstore.GenericEventMapper[feature_v1.SetEvent[feature_v1.Boolean]]),
|
||||
},
|
||||
reduce: reduceSetDefaultLoginInstance_v1,
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.instance_features (instance_id, key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.instance_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"login_default_org",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
[]byte("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceInstanceResetFeatures",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.InstanceResetEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte{},
|
||||
), eventstore.GenericEventMapper[feature_v2.ResetEvent]),
|
||||
},
|
||||
reduce: reduceInstanceResetFeatures,
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.instance_features WHERE (instance_id = $1)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceInstanceRemoved",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
instance.InstanceRemovedEventType,
|
||||
instance.AggregateType,
|
||||
nil,
|
||||
), instance.InstanceRemovedEventMapper),
|
||||
},
|
||||
reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("instance"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.instance_features WHERE (instance_id = $1)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := baseEvent(t)
|
||||
got, err := tt.reduce(event)
|
||||
if ok := zerrors.IsErrorInvalidArgument(err); !ok {
|
||||
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||
}
|
||||
|
||||
event = tt.args.event(t)
|
||||
got, err = tt.reduce(event)
|
||||
assertReduce(t, got, err, InstanceFeatureTable, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
@@ -72,6 +72,8 @@ var (
|
||||
QuotaProjection *quotaProjection
|
||||
LimitsProjection *handler.Handler
|
||||
RestrictionsProjection *handler.Handler
|
||||
SystemFeatureProjection *handler.Handler
|
||||
InstanceFeatureProjection *handler.Handler
|
||||
)
|
||||
|
||||
type projection interface {
|
||||
@@ -148,6 +150,8 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
|
||||
QuotaProjection = newQuotaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["quotas"]))
|
||||
LimitsProjection = newLimitsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["limits"]))
|
||||
RestrictionsProjection = newRestrictionsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["restrictions"]))
|
||||
SystemFeatureProjection = newSystemFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["system_features"]))
|
||||
InstanceFeatureProjection = newInstanceFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_features"]))
|
||||
newProjectionsList()
|
||||
return nil
|
||||
}
|
||||
@@ -257,5 +261,7 @@ func newProjectionsList() {
|
||||
QuotaProjection.handler,
|
||||
LimitsProjection,
|
||||
RestrictionsProjection,
|
||||
SystemFeatureProjection,
|
||||
InstanceFeatureProjection,
|
||||
}
|
||||
}
|
||||
|
98
internal/query/projection/system_features.go
Normal file
98
internal/query/projection/system_features.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
SystemFeatureTable = "projections.system_features"
|
||||
|
||||
SystemFeatureKeyCol = "key"
|
||||
SystemFeatureCreationDateCol = "creation_date"
|
||||
SystemFeatureChangeDateCol = "change_date"
|
||||
SystemFeatureSequenceCol = "sequence"
|
||||
SystemFeatureValueCol = "value"
|
||||
)
|
||||
|
||||
type systemFeatureProjection struct{}
|
||||
|
||||
func newSystemFeatureProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(systemFeatureProjection))
|
||||
}
|
||||
|
||||
func (*systemFeatureProjection) Name() string {
|
||||
return SystemFeatureTable
|
||||
}
|
||||
|
||||
func (*systemFeatureProjection) Init() *old_handler.Check {
|
||||
return handler.NewTableCheck(handler.NewTable(
|
||||
[]*handler.InitColumn{
|
||||
handler.NewColumn(SystemFeatureKeyCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(SystemFeatureCreationDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(SystemFeatureChangeDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(SystemFeatureSequenceCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(SystemFeatureValueCol, handler.ColumnTypeJSONB),
|
||||
},
|
||||
handler.NewPrimaryKey(SystemFeatureKeyCol),
|
||||
))
|
||||
}
|
||||
|
||||
func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{{
|
||||
Aggregate: feature_v2.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: feature_v2.SystemResetEventType,
|
||||
Reduce: reduceSystemResetFeatures,
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemLoginDefaultOrgEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemTriggerIntrospectionProjectionsEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemLegacyIntrospectionEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func reduceSystemSetFeature[T any](event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.SetEvent[T])
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-uPh8O", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
f, err := e.FeatureJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
columns := []handler.Column{
|
||||
handler.NewCol(SystemFeatureKeyCol, f.Key.String()),
|
||||
handler.NewCol(SystemFeatureCreationDateCol, handler.OnlySetValueOnInsert(SystemFeatureTable, e.CreationDate())),
|
||||
handler.NewCol(SystemFeatureChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(SystemFeatureSequenceCol, e.Sequence()),
|
||||
handler.NewCol(SystemFeatureValueCol, f.Value),
|
||||
}
|
||||
return handler.NewUpsertStatement(e, columns[0:1], columns), nil
|
||||
}
|
||||
|
||||
func reduceSystemResetFeatures(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.ResetEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-roo6A", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
return handler.NewDeleteStatement(e, []handler.Condition{
|
||||
// Hack: need at least one condition or the query builder will throw us an error
|
||||
handler.NewIsNotNullCond(SystemFeatureKeyCol),
|
||||
}), nil
|
||||
}
|
90
internal/query/projection/system_features_test.go
Normal file
90
internal/query/projection/system_features_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestSystemFeaturesProjection_reduces(t *testing.T) {
|
||||
type args struct {
|
||||
event func(t *testing.T) eventstore.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "reduceSystemSetFeature",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.SystemLegacyIntrospectionEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte(`{"value": true}`),
|
||||
), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]),
|
||||
},
|
||||
reduce: reduceSystemSetFeature[bool],
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.system_features (key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.system_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
|
||||
expectedArgs: []interface{}{
|
||||
"legacy_introspection",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
[]byte("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceSystemResetFeatures",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.SystemResetEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte{},
|
||||
), eventstore.GenericEventMapper[feature_v2.ResetEvent]),
|
||||
},
|
||||
reduce: reduceSystemResetFeatures,
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.system_features WHERE (key IS NOT NULL)",
|
||||
expectedArgs: []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := baseEvent(t)
|
||||
got, err := tt.reduce(event)
|
||||
if ok := zerrors.IsErrorInvalidArgument(err); !ok {
|
||||
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||
}
|
||||
|
||||
event = tt.args.event(t)
|
||||
got, err = tt.reduce(event)
|
||||
assertReduce(t, got, err, SystemFeatureTable, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
30
internal/query/system_features.go
Normal file
30
internal/query/system_features.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
type FeatureSource[T any] struct {
|
||||
Level feature.Level
|
||||
Value T
|
||||
}
|
||||
|
||||
type SystemFeatures struct {
|
||||
Details *domain.ObjectDetails
|
||||
|
||||
LoginDefaultOrg FeatureSource[bool]
|
||||
TriggerIntrospectionProjections FeatureSource[bool]
|
||||
LegacyIntrospection FeatureSource[bool]
|
||||
}
|
||||
|
||||
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {
|
||||
m := NewSystemFeaturesReadModel()
|
||||
if err := q.eventstore.FilterToQueryReducer(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.system.Details = readModelToObjectDetails(m.ReadModel)
|
||||
return m.system, nil
|
||||
}
|
82
internal/query/system_features_model.go
Normal file
82
internal/query/system_features_model.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type SystemFeaturesReadModel struct {
|
||||
*eventstore.ReadModel
|
||||
system *SystemFeatures
|
||||
}
|
||||
|
||||
func NewSystemFeaturesReadModel() *SystemFeaturesReadModel {
|
||||
m := &SystemFeaturesReadModel{
|
||||
ReadModel: &eventstore.ReadModel{
|
||||
AggregateID: "SYSTEM",
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
system: new(SystemFeatures),
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) Reduce() error {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err := m.reduceBoolFeature(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return m.ReadModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v2.SystemResetEventType,
|
||||
feature_v2.SystemLoginDefaultOrgEventType,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.SystemLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) reduceReset() {
|
||||
m.system = new(SystemFeatures)
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
level, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var dst *FeatureSource[bool]
|
||||
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
dst = &m.system.LoginDefaultOrg
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
dst = &m.system.TriggerIntrospectionProjections
|
||||
case feature.KeyLegacyIntrospection:
|
||||
dst = &m.system.LegacyIntrospection
|
||||
}
|
||||
|
||||
*dst = FeatureSource[bool]{
|
||||
Level: level,
|
||||
Value: event.Value,
|
||||
}
|
||||
return nil
|
||||
}
|
179
internal/query/system_features_test.go
Normal file
179
internal/query/system_features_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
func TestQueries_GetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *SystemFeatures
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "no features set",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature, not cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q := &Queries{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := q.GetSystemFeatures(context.Background())
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@@ -13,18 +13,3 @@ const (
|
||||
AggregateType = "feature"
|
||||
AggregateVersion = "v1"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, resourceOwner string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
// Package feature implements the v1 feature repository.
|
||||
// DEPRECATED: use ./feature_v2 instead.
|
||||
package feature
|
||||
|
||||
import (
|
||||
@@ -6,14 +8,22 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultLoginInstanceEventType = EventTypeFromFeature(domain.FeatureLoginDefaultOrg)
|
||||
DefaultLoginInstanceEventType = eventTypePrefix + eventstore.EventType(strings.ToLower("FeatureLoginDefaultOrg")) + setSuffix
|
||||
)
|
||||
|
||||
func EventTypeFromFeature(feature domain.Feature) eventstore.EventType {
|
||||
return eventTypePrefix + eventstore.EventType(strings.ToLower(feature.String())) + setSuffix
|
||||
// DefaultLoginInstanceEventToV2 upgrades the SetEvent to a V2 SetEvent so that
|
||||
// the v2 reducers can handle the V1 events.
|
||||
func DefaultLoginInstanceEventToV2(e *SetEvent[Boolean]) *feature_v2.SetEvent[bool] {
|
||||
v2e := &feature_v2.SetEvent[bool]{
|
||||
BaseEvent: e.BaseEvent,
|
||||
Value: e.Value.Boolean,
|
||||
}
|
||||
v2e.BaseEvent.EventType = feature_v2.InstanceLoginDefaultOrgEventType
|
||||
return v2e
|
||||
}
|
||||
|
||||
type SetEvent[T SetEventType] struct {
|
||||
|
25
internal/repository/feature/feature_v2/aggregate.go
Normal file
25
internal/repository/feature/feature_v2/aggregate.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "feature"
|
||||
AggregateVersion = "v2"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, resourceOwner string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
16
internal/repository/feature/feature_v2/eventstore.go
Normal file
16
internal/repository/feature/feature_v2/eventstore.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemResetEventType, eventstore.GenericEventMapper[ResetEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
}
|
132
internal/repository/feature/feature_v2/feature.go
Normal file
132
internal/repository/feature/feature_v2/feature.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
SystemResetEventType = resetEventTypeFromFeature(feature.LevelSystem)
|
||||
SystemLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginDefaultOrg)
|
||||
SystemTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTriggerIntrospectionProjections)
|
||||
SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection)
|
||||
|
||||
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
|
||||
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
|
||||
InstanceTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTriggerIntrospectionProjections)
|
||||
InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection)
|
||||
)
|
||||
|
||||
const (
|
||||
resetSuffix = "reset"
|
||||
setSuffix = "set"
|
||||
)
|
||||
|
||||
func resetEventTypeFromFeature(level feature.Level) eventstore.EventType {
|
||||
return eventstore.EventType(strings.Join([]string{AggregateType, level.String(), resetSuffix}, "."))
|
||||
}
|
||||
|
||||
func setEventTypeFromFeature(level feature.Level, key feature.Key) eventstore.EventType {
|
||||
return eventstore.EventType(strings.Join([]string{AggregateType, level.String(), key.String(), setSuffix}, "."))
|
||||
}
|
||||
|
||||
type ResetEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *ResetEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = b
|
||||
}
|
||||
|
||||
func (e *ResetEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ResetEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewResetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *Aggregate,
|
||||
eventType eventstore.EventType,
|
||||
) *ResetEvent {
|
||||
return &ResetEvent{
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx, &aggregate.Aggregate, eventType),
|
||||
}
|
||||
}
|
||||
|
||||
type SetEvent[T any] struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Value T
|
||||
}
|
||||
|
||||
func (e *SetEvent[T]) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = b
|
||||
}
|
||||
|
||||
func (e *SetEvent[T]) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SetEvent[T]) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
type FeatureJSON struct {
|
||||
Key feature.Key
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// FeatureJSON prepares converts the event to a key-value pair with a JSON value payload.
|
||||
func (e *SetEvent[T]) FeatureJSON() (*FeatureJSON, error) {
|
||||
_, key, err := e.FeatureInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jsonValue, err := json.Marshal(e.Value)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternalf(err, "FEAT-go9Ji", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
return &FeatureJSON{
|
||||
Key: key,
|
||||
Value: jsonValue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FeatureInfo extracts a feature's level and key from the event.
|
||||
func (e *SetEvent[T]) FeatureInfo() (feature.Level, feature.Key, error) {
|
||||
ss := strings.Split(string(e.EventType), ".")
|
||||
if len(ss) != 4 {
|
||||
return 0, 0, zerrors.ThrowInternalf(nil, "FEAT-Ahs4m", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
level, err := feature.LevelString(ss[1])
|
||||
if err != nil {
|
||||
return 0, 0, zerrors.ThrowInternalf(err, "FEAT-Boo2i", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
key, err := feature.KeyString(ss[2])
|
||||
if err != nil {
|
||||
return 0, 0, zerrors.ThrowInternalf(err, "FEAT-eir0M", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
return level, key, nil
|
||||
}
|
||||
|
||||
func NewSetEvent[T any](
|
||||
ctx context.Context,
|
||||
aggregate *Aggregate,
|
||||
eventType eventstore.EventType,
|
||||
value T,
|
||||
) *SetEvent[T] {
|
||||
return &SetEvent[T]{
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx, &aggregate.Aggregate, eventType),
|
||||
value,
|
||||
}
|
||||
}
|
118
internal/repository/feature/feature_v2/feature_test.go
Normal file
118
internal/repository/feature/feature_v2/feature_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestSetEvent_FeatureJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
e *SetEvent[float64] // using float so it's easy to create marshal errors
|
||||
want *FeatureJSON
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid key error",
|
||||
e: &SetEvent[float64]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "feature.system.foo_bar.some_feat",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-eir0M", "reduce.wrong.event.type %s", "feature.system.foo_bar.some_feat"),
|
||||
},
|
||||
{
|
||||
name: "marshal error",
|
||||
e: &SetEvent[float64]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: SystemLoginDefaultOrgEventType,
|
||||
},
|
||||
Value: math.NaN(),
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-go9Ji", "reduce.wrong.event.type %s", SystemLoginDefaultOrgEventType),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
e: &SetEvent[float64]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: SystemLoginDefaultOrgEventType,
|
||||
},
|
||||
Value: 555,
|
||||
},
|
||||
want: &FeatureJSON{
|
||||
Key: feature.KeyLoginDefaultOrg,
|
||||
Value: []byte(`555`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.e.FeatureJSON()
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEvent_FeatureInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
e *SetEvent[bool]
|
||||
want feature.Level
|
||||
want1 feature.Key
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "format error",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "foo.bar",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-Ahs4m", "reduce.wrong.event.type %s", "foo.bar"),
|
||||
},
|
||||
{
|
||||
name: "level error",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "feature.foo.bar.something",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-Boo2i", "reduce.wrong.event.type %s", "feature.foo.bar.something"),
|
||||
},
|
||||
{
|
||||
name: "key error",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "feature.system.bar.something",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-eir0M", "reduce.wrong.event.type %s", "feature.system.bar.something"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: SystemLoginDefaultOrgEventType,
|
||||
},
|
||||
},
|
||||
want: feature.LevelSystem,
|
||||
want1: feature.KeyLoginDefaultOrg,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := tt.e.FeatureInfo()
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
assert.Equal(t, tt.want1, got1)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user