feat: implement user schema management (#7416)

This PR adds the functionality to manage user schemas through the new user schema service.
It includes the possibility to create a basic JSON schema and also provides a way on defining permissions (read, write) for owner and self context with an annotation.

Further annotations for OIDC claims and SAML attribute mappings will follow.

A guide on how to create a schema and assign permissions has been started. It will be extended though out the process of implementing the schema and users based on those.

Note:
This feature is in an early stage and therefore not enabled by default. To test it out, please enable the UserSchema feature flag on your instance / system though the feature service.
This commit is contained in:
Livio Spring
2024-03-12 14:50:13 +01:00
committed by GitHub
parent 2a39cc16f5
commit 0e181b218c
61 changed files with 3614 additions and 35 deletions

View File

@@ -15,12 +15,14 @@ type InstanceFeatures struct {
LoginDefaultOrg *bool
TriggerIntrospectionProjections *bool
LegacyIntrospection *bool
UserSchema *bool
}
func (m *InstanceFeatures) isEmpty() bool {
return m.LoginDefaultOrg == nil &&
m.TriggerIntrospectionProjections == nil &&
m.LegacyIntrospection == nil
m.LegacyIntrospection == nil &&
m.UserSchema == nil
}
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {

View File

@@ -54,6 +54,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceLoginDefaultOrgEventType,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
feature_v2.InstanceLegacyIntrospectionEventType,
feature_v2.InstanceUserSchemaEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -62,6 +63,7 @@ func (m *InstanceFeaturesWriteModel) reduceReset() {
m.LoginDefaultOrg = nil
m.TriggerIntrospectionProjections = nil
m.LegacyIntrospection = nil
m.UserSchema = nil
}
func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
@@ -78,6 +80,8 @@ func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEven
m.TriggerIntrospectionProjections = &event.Value
case feature.KeyLegacyIntrospection:
m.LegacyIntrospection = &event.Value
case feature.KeyUserSchema:
m.UserSchema = &event.Value
}
return nil
}
@@ -88,5 +92,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
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)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType)
return cmds
}

View File

@@ -131,6 +131,24 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
ResourceOwner: "instance1",
},
},
{
name: "set UserSchema",
eventstore: expectEventstore(
expectFilter(),
expectPush(
feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, true,
),
),
),
args: args{ctx, &InstanceFeatures{
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
},
},
{
name: "push error",
eventstore: expectEventstore(
@@ -164,12 +182,17 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, true,
),
feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, true,
),
),
),
args: args{ctx, &InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",

View File

@@ -12,12 +12,14 @@ type SystemFeatures struct {
LoginDefaultOrg *bool
TriggerIntrospectionProjections *bool
LegacyIntrospection *bool
UserSchema *bool
}
func (m *SystemFeatures) isEmpty() bool {
return m.LoginDefaultOrg == nil &&
m.TriggerIntrospectionProjections == nil &&
m.LegacyIntrospection == nil
m.LegacyIntrospection == nil &&
m.UserSchema == nil
}
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {

View File

@@ -49,6 +49,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemLoginDefaultOrgEventType,
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
feature_v2.SystemLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -57,6 +58,7 @@ func (m *SystemFeaturesWriteModel) reduceReset() {
m.LoginDefaultOrg = nil
m.TriggerIntrospectionProjections = nil
m.LegacyIntrospection = nil
m.UserSchema = nil
}
func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
@@ -73,6 +75,8 @@ func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[
m.TriggerIntrospectionProjections = &event.Value
case feature.KeyLegacyIntrospection:
m.LegacyIntrospection = &event.Value
case feature.KeyUserSchema:
m.UserSchema = &event.Value
}
return nil
}
@@ -83,6 +87,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
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)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType)
return cmds
}

View File

@@ -99,6 +99,24 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
ResourceOwner: "SYSTEM",
},
},
{
name: "set UserSchema",
eventstore: expectEventstore(
expectFilter(),
expectPush(
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, true,
),
),
),
args: args{context.Background(), &SystemFeatures{
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",
},
},
{
name: "push error",
eventstore: expectEventstore(
@@ -132,12 +150,17 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, true,
),
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, true,
),
),
),
args: args{context.Background(), &SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",
@@ -178,12 +201,17 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
),
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, true,
),
),
),
args: args{context.Background(), &SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",

View File

@@ -0,0 +1,180 @@
package command
import (
"bytes"
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/domain"
domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
"github.com/zitadel/zitadel/internal/repository/user/schema"
"github.com/zitadel/zitadel/internal/zerrors"
)
type CreateUserSchema struct {
ResourceOwner string
Type string
Schema json.RawMessage
PossibleAuthenticators []domain.AuthenticatorType
}
func (s *CreateUserSchema) Valid() error {
if s.Type == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-DGFj3", "Errors.UserSchema.Type.Missing")
}
if err := validateUserSchema(s.Schema); err != nil {
return err
}
for _, authenticator := range s.PossibleAuthenticators {
if authenticator == domain.AuthenticatorTypeUnspecified {
return zerrors.ThrowInvalidArgument(nil, "COMMA-Gh652", "Errors.UserSchema.Authenticator.Invalid")
}
}
return nil
}
type UpdateUserSchema struct {
ID string
ResourceOwner string
Type *string
Schema json.RawMessage
PossibleAuthenticators []domain.AuthenticatorType
}
func (s *UpdateUserSchema) Valid() error {
if s.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing")
}
if s.Type != nil && *s.Type == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-G43gn", "Errors.UserSchema.Type.Missing")
}
if err := validateUserSchema(s.Schema); err != nil {
return err
}
for _, authenticator := range s.PossibleAuthenticators {
if authenticator == domain.AuthenticatorTypeUnspecified {
return zerrors.ThrowInvalidArgument(nil, "COMMA-WF4hg", "Errors.UserSchema.Authenticator.Invalid")
}
}
return nil
}
func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) (string, *domain.ObjectDetails, error) {
if err := userSchema.Valid(); err != nil {
return "", nil, err
}
if userSchema.ResourceOwner == "" {
return "", nil, zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing")
}
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
writeModel := NewUserSchemaWriteModel(id, userSchema.ResourceOwner)
err = c.pushAppendAndReduce(ctx, writeModel,
schema.NewCreatedEvent(ctx,
UserSchemaAggregateFromWriteModel(&writeModel.WriteModel),
userSchema.Type, userSchema.Schema, userSchema.PossibleAuthenticators,
),
)
if err != nil {
return "", nil, err
}
return id, writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserSchema) (*domain.ObjectDetails, error) {
if err := userSchema.Valid(); err != nil {
return nil, err
}
writeModel := NewUserSchemaWriteModel(userSchema.ID, userSchema.ResourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive")
}
updatedEvent := writeModel.NewUpdatedEvent(
ctx,
UserSchemaAggregateFromWriteModel(&writeModel.WriteModel),
userSchema.Type,
userSchema.Schema,
userSchema.PossibleAuthenticators,
)
if updatedEvent == nil {
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
if err := c.pushAppendAndReduce(ctx, writeModel, updatedEvent); err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) DeactivateUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive")
}
err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewDeactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) ReactivateUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive")
}
err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewReactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) DeleteUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if !writeModel.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists")
}
// TODO: check for users based on that schema; this is only possible with / after https://github.com/zitadel/zitadel/issues/7308
err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewDeletedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel), writeModel.SchemaType),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func validateUserSchema(userSchema json.RawMessage) error {
_, err := domain_schema.NewSchema(0, bytes.NewReader(userSchema))
if err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid")
}
return nil
}

View File

@@ -0,0 +1,112 @@
package command
import (
"bytes"
"context"
"encoding/json"
"golang.org/x/exp/slices"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user/schema"
)
type UserSchemaWriteModel struct {
eventstore.WriteModel
SchemaType string
Schema json.RawMessage
PossibleAuthenticators []domain.AuthenticatorType
State domain.UserSchemaState
}
func NewUserSchemaWriteModel(schemaID, resourceOwner string) *UserSchemaWriteModel {
return &UserSchemaWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: schemaID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *UserSchemaWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *schema.CreatedEvent:
wm.SchemaType = e.SchemaType
wm.Schema = e.Schema
wm.PossibleAuthenticators = e.PossibleAuthenticators
wm.State = domain.UserSchemaStateActive
case *schema.UpdatedEvent:
if e.SchemaType != nil {
wm.SchemaType = *e.SchemaType
}
if len(e.Schema) > 0 {
wm.Schema = e.Schema
}
if len(e.PossibleAuthenticators) > 0 {
wm.PossibleAuthenticators = e.PossibleAuthenticators
}
case *schema.DeactivatedEvent:
wm.State = domain.UserSchemaStateInactive
case *schema.ReactivatedEvent:
wm.State = domain.UserSchemaStateActive
case *schema.DeletedEvent:
wm.State = domain.UserSchemaStateDeleted
}
}
return wm.WriteModel.Reduce()
}
func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(schema.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
schema.CreatedType,
schema.UpdatedType,
schema.DeactivatedType,
schema.ReactivatedType,
schema.DeletedType,
).
Builder()
}
func (wm *UserSchemaWriteModel) NewUpdatedEvent(
ctx context.Context,
agg *eventstore.Aggregate,
schemaType *string,
userSchema json.RawMessage,
possibleAuthenticators []domain.AuthenticatorType,
) *schema.UpdatedEvent {
changes := make([]schema.Changes, 0)
if schemaType != nil && wm.SchemaType != *schemaType {
changes = append(changes, schema.ChangeSchemaType(wm.SchemaType, *schemaType))
}
if !bytes.Equal(wm.Schema, userSchema) {
changes = append(changes, schema.ChangeSchema(userSchema))
}
if len(possibleAuthenticators) > 0 && slices.Compare(wm.PossibleAuthenticators, possibleAuthenticators) != 0 {
changes = append(changes, schema.ChangePossibleAuthenticators(possibleAuthenticators))
}
if len(changes) == 0 {
return nil
}
return schema.NewUpdatedEvent(ctx, agg, changes)
}
func UserSchemaAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: wm.AggregateID,
Type: schema.AggregateType,
ResourceOwner: wm.ResourceOwner,
InstanceID: wm.InstanceID,
Version: schema.AggregateVersion,
}
}
func (wm *UserSchemaWriteModel) Exists() bool {
return wm.State != domain.UserSchemaStateUnspecified && wm.State != domain.UserSchemaStateDeleted
}

View File

@@ -0,0 +1,912 @@
package command
import (
"context"
"encoding/json"
"testing"
"github.com/muhlemmer/gu"
"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/user/schema"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommands_CreateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
ctx context.Context
userSchema *CreateUserSchema
}
type res struct {
id string
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"no type, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-DGFj3", "Errors.UserSchema.Type.Missing"),
},
},
{
"no schema, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
Type: "type",
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"invalid authenticator, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
Type: "type",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUnspecified,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-Gh652", "Errors.UserSchema.Authenticator.Invalid"),
},
},
{
"no resourceOwner, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
Type: "type",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing"),
},
},
{
"empty user schema created",
fields{
eventstore: expectEventstore(
expectPush(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"user schema created",
fields{
eventstore: expectEventstore(
expectPush(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"user schema with invalid permission, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": true
}
}
}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"user schema with permission created",
fields{
eventstore: expectEventstore(
expectPush(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
id: "id1",
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,
}
gotID, gotDetails, err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema)
assert.Equal(t, tt.res.id, gotID)
assert.Equal(t, tt.res.details, gotDetails)
assert.ErrorIs(t, err, tt.res.err)
})
}
}
func TestCommands_UpdateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
userSchema *UpdateUserSchema
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing"),
},
},
{
"empty type, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Type: gu.Ptr(""),
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-G43gn", "Errors.UserSchema.Type.Missing"),
},
},
{
"no schema, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"invalid schema, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{
"properties": {
"name": {
"type": "string",
"required": true,
}
}
}`),
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"invalid authenticator, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUnspecified,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-WF4hg", "Errors.UserSchema.Authenticator.Invalid"),
},
},
{
"not active / exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Type: gu.Ptr("type"),
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive"),
},
},
{
"no changes",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Type: gu.Ptr("type"),
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"update type",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewUpdatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
[]schema.Changes{schema.ChangeSchemaType("type", "newType")},
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
Type: gu.Ptr("newType"),
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"update schema",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewUpdatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
[]schema.Changes{schema.ChangeSchema(json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`))},
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
Type: nil,
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"update possible authenticators",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewUpdatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
[]schema.Changes{schema.ChangePossibleAuthenticators([]domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
domain.AuthenticatorTypePassword,
})},
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
domain.AuthenticatorTypePassword,
},
},
},
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),
}
got, err := c.UpdateUserSchema(tt.args.ctx, tt.args.userSchema)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}
func TestCommands_DeactivateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "",
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing"),
},
},
{
"not active / exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive"),
},
},
{
"deactivate ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewDeactivatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
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),
}
got, err := c.DeactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}
func TestCommands_ReactivateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "",
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing"),
},
},
{
"not deactivated / exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive"),
},
},
{
"reactivate ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
eventFromEventPusher(
schema.NewDeactivatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
),
),
),
expectPush(
schema.NewReactivatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
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),
}
got, err := c.ReactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}
func TestCommands_DeleteUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "",
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing"),
},
},
{
"not exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists"),
},
},
{
"delete ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewDeletedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
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),
}
got, err := c.DeleteUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}