diff --git a/internal/domain/authn_key.go b/internal/domain/authn_key.go index 9130dcc90a..df3d37fcbe 100644 --- a/internal/domain/authn_key.go +++ b/internal/domain/authn_key.go @@ -27,7 +27,7 @@ type authNKey interface { type AuthNKeyType int32 const ( - AuthNKeyTypeNONE = iota + AuthNKeyTypeNONE AuthNKeyType = iota AuthNKeyTypeJSON keyCount diff --git a/internal/query/projection/authn_key.go b/internal/query/projection/authn_key.go new file mode 100644 index 0000000000..da2eb2107c --- /dev/null +++ b/internal/query/projection/authn_key.go @@ -0,0 +1,195 @@ +package projection + +import ( + "context" + "time" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" +) + +const ( + AuthNKeyTable = "zitadel.projections.authn_keys" + AuthNKeyIDCol = "id" + AuthNKeyCreationDateCol = "creation_date" + AuthNKeyResourceOwnerCol = "resource_owner" + AuthNKeyAggregateIDCol = "aggregate_id" + AuthNKeySequenceCol = "sequence" + AuthNKeyObjectIDCol = "object_id" + AuthNKeyExpirationCol = "expiration" + AuthNKeyIdentifierCol = "identifier" + AuthNKeyPublicKeyCol = "public_key" + AuthNKeyTypeCol = "type" + AuthNKeyEnabledCol = "enabled" +) + +type AuthNKeyProjection struct { + crdb.StatementHandler +} + +func NewAuthNKeyProjection(ctx context.Context, config crdb.StatementHandlerConfig) *AuthNKeyProjection { + p := &AuthNKeyProjection{} + config.ProjectionName = AuthNKeyTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *AuthNKeyProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: project.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: project.ApplicationKeyAddedEventType, + Reduce: p.reduceAuthNKeyAdded, + }, + { + Event: project.ApplicationKeyRemovedEventType, + Reduce: p.reduceAuthNKeyRemoved, + }, + { + Event: project.APIConfigChangedType, + Reduce: p.reduceAuthNKeyEnabledChanged, + }, + { + Event: project.OIDCConfigChangedType, + Reduce: p.reduceAuthNKeyEnabledChanged, + }, + { + Event: project.ApplicationRemovedType, + Reduce: p.reduceAuthNKeyRemoved, + }, + { + Event: project.ProjectRemovedType, + Reduce: p.reduceAuthNKeyRemoved, + }, + }, + }, + { + Aggregate: user.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: user.MachineKeyAddedEventType, + Reduce: p.reduceAuthNKeyAdded, + }, + { + Event: user.MachineKeyRemovedEventType, + Reduce: p.reduceAuthNKeyRemoved, + }, + { + Event: user.UserRemovedType, + Reduce: p.reduceAuthNKeyRemoved, + }, + }, + }, + } +} + +func (p *AuthNKeyProjection) reduceAuthNKeyAdded(event eventstore.EventReader) (*handler.Statement, error) { + var authNKeyEvent struct { + eventstore.BaseEvent + keyID string + objectID string + expiration time.Time + identifier string + publicKey []byte + keyType domain.AuthNKeyType + } + switch e := event.(type) { + case *project.ApplicationKeyAddedEvent: + authNKeyEvent.BaseEvent = e.BaseEvent + authNKeyEvent.keyID = e.KeyID + authNKeyEvent.objectID = e.AppID + authNKeyEvent.expiration = e.ExpirationDate + authNKeyEvent.identifier = e.ClientID + authNKeyEvent.publicKey = e.PublicKey + authNKeyEvent.keyType = e.KeyType + case *user.MachineKeyAddedEvent: + authNKeyEvent.BaseEvent = e.BaseEvent + authNKeyEvent.keyID = e.KeyID + authNKeyEvent.objectID = e.Aggregate().ID + authNKeyEvent.expiration = e.ExpirationDate + authNKeyEvent.identifier = e.Aggregate().ID + authNKeyEvent.publicKey = e.PublicKey + authNKeyEvent.keyType = e.KeyType + default: + logging.LogWithFields("PROJE-Dbr3g", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{project.ApplicationKeyAddedEventType, user.MachineKeyAddedEventType}).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-Dgb32", "reduce.wrong.event.type") + } + return crdb.NewMultiStatement( + &authNKeyEvent, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(AuthNKeyIDCol, authNKeyEvent.keyID), + handler.NewCol(AuthNKeyCreationDateCol, authNKeyEvent.CreationDate()), + handler.NewCol(AuthNKeyResourceOwnerCol, authNKeyEvent.Aggregate().ResourceOwner), + handler.NewCol(AuthNKeyAggregateIDCol, authNKeyEvent.Aggregate().ID), + handler.NewCol(AuthNKeySequenceCol, authNKeyEvent.Sequence()), + handler.NewCol(AuthNKeyObjectIDCol, authNKeyEvent.objectID), + handler.NewCol(AuthNKeyExpirationCol, authNKeyEvent.expiration), + handler.NewCol(AuthNKeyIdentifierCol, authNKeyEvent.identifier), + handler.NewCol(AuthNKeyPublicKeyCol, authNKeyEvent.publicKey), + handler.NewCol(AuthNKeyTypeCol, authNKeyEvent.keyType), + }, + ), + ), nil +} + +func (p *AuthNKeyProjection) reduceAuthNKeyEnabledChanged(event eventstore.EventReader) (*handler.Statement, error) { + var appID string + var enabled bool + switch e := event.(type) { + case *project.APIConfigChangedEvent: + if e.AuthMethodType == nil { + return crdb.NewNoOpStatement(event), nil + } + appID = e.AppID + enabled = *e.AuthMethodType == domain.APIAuthMethodTypePrivateKeyJWT + case *project.OIDCConfigChangedEvent: + if e.AuthMethodType == nil { + return crdb.NewNoOpStatement(event), nil + } + appID = e.AppID + enabled = *e.AuthMethodType == domain.OIDCAuthMethodTypePrivateKeyJWT + default: + logging.LogWithFields("PROJE-Db5u3", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{project.APIConfigChangedType, project.OIDCConfigChangedType}).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-Dbrt1", "reduce.wrong.event.type") + } + return crdb.NewUpdateStatement( + event, + []handler.Column{handler.NewCol(AuthNKeyEnabledCol, enabled)}, + []handler.Condition{handler.NewCond(AuthNKeyObjectIDCol, appID)}, + ), nil +} + +func (p *AuthNKeyProjection) reduceAuthNKeyRemoved(event eventstore.EventReader) (*handler.Statement, error) { + var condition handler.Condition + switch e := event.(type) { + case *project.ApplicationKeyRemovedEvent: + condition = handler.NewCond(AuthNKeyIDCol, e.KeyID) + case *project.ApplicationRemovedEvent: + condition = handler.NewCond(AuthNKeyObjectIDCol, e.AppID) + case *project.ProjectRemovedEvent: + condition = handler.NewCond(AuthNKeyAggregateIDCol, e.Aggregate().ID) + case *user.MachineKeyRemovedEvent: + condition = handler.NewCond(AuthNKeyIDCol, e.KeyID) + case *user.UserRemovedEvent: + condition = handler.NewCond(AuthNKeyAggregateIDCol, e.Aggregate().ID) + default: + logging.LogWithFields("PROJE-Sfdg3", "seq", event.Sequence(), "expectedTypes", + []eventstore.EventType{project.ApplicationKeyRemovedEventType, project.ApplicationRemovedType, project.ProjectRemovedType, user.MachineKeyRemovedEventType, user.UserRemovedType}).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-BGge42", "reduce.wrong.event.type") + } + return crdb.NewDeleteStatement( + event, + []handler.Condition{condition}, + ), nil +} diff --git a/internal/query/projection/authn_key_test.go b/internal/query/projection/authn_key_test.go new file mode 100644 index 0000000000..d4229127a3 --- /dev/null +++ b/internal/query/projection/authn_key_test.go @@ -0,0 +1,452 @@ +package projection + +import ( + "testing" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestAuthNKeyProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.EventReader + } + tests := []struct { + name string + args args + reduce func(event eventstore.EventReader) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceAuthNKeyAdded app", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationKeyAddedEventType), + project.AggregateType, + []byte(`{"applicationId": "appId", "clientId":"clientId","keyId": "keyId", "type": 1, "expirationDate": "2021-11-30T15:00:00Z", "publicKey": "cHVibGljS2V5"}`), + ), project.ApplicationKeyAddedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyAdded, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.authn_keys (id, creation_date, resource_owner, aggregate_id, sequence, object_id, expiration, identifier, public_key, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedArgs: []interface{}{ + "keyId", + anyArg{}, + "ro-id", + "agg-id", + uint64(15), + "appId", + anyArg{}, + "clientId", + []byte("publicKey"), + domain.AuthNKeyTypeJSON, + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyAdded user", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineKeyAddedEventType), + user.AggregateType, + []byte(`{"keyId": "keyId", "type": 1, "expirationDate": "2021-11-30T15:00:00Z", "publicKey": "cHVibGljS2V5"}`), + ), user.MachineKeyAddedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyAdded, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("user"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.authn_keys (id, creation_date, resource_owner, aggregate_id, sequence, object_id, expiration, identifier, public_key, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedArgs: []interface{}{ + "keyId", + anyArg{}, + "ro-id", + "agg-id", + uint64(15), + "agg-id", + anyArg{}, + "agg-id", + []byte("publicKey"), + domain.AuthNKeyTypeJSON, + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyRemoved app key", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationKeyRemovedEventType), + project.AggregateType, + []byte(`{"keyId": "keyId"}`), + ), project.ApplicationKeyRemovedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyRemoved, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.authn_keys WHERE (id = $1)", + expectedArgs: []interface{}{ + "keyId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyEnabledChanged api no change", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.APIConfigChangedType), + project.AggregateType, + []byte(`{"appId": "appId"}`), + ), project.APIConfigChangedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyEnabledChanged, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{}, + }, + }, + }, + { + name: "reduceAuthNKeyEnabledChanged api config basic", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.APIConfigChangedType), + project.AggregateType, + []byte(`{"appId": "appId", "authMethodType": 0}`), + ), project.APIConfigChangedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyEnabledChanged, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.authn_keys SET (enabled) = ($1) WHERE (object_id = $2)", + expectedArgs: []interface{}{ + false, + "appId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyEnabledChanged api config jwt", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.APIConfigChangedType), + project.AggregateType, + []byte(`{"appId": "appId", "authMethodType": 1}`), + ), project.APIConfigChangedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyEnabledChanged, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.authn_keys SET (enabled) = ($1) WHERE (object_id = $2)", + expectedArgs: []interface{}{ + true, + "appId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyRemoved app key", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineKeyRemovedEventType), + user.AggregateType, + []byte(`{"keyId": "keyId"}`), + ), user.MachineKeyRemovedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyRemoved, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("user"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.authn_keys WHERE (id = $1)", + expectedArgs: []interface{}{ + "keyId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyEnabledChanged oidc no change", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.OIDCConfigChangedType), + project.AggregateType, + []byte(`{"appId": "appId"}`), + ), project.OIDCConfigChangedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyEnabledChanged, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{}, + }, + }, + }, + { + name: "reduceAuthNKeyEnabledChanged oidc config basic", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.OIDCConfigChangedType), + project.AggregateType, + []byte(`{"appId": "appId", "authMethodType": 0}`), + ), project.OIDCConfigChangedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyEnabledChanged, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.authn_keys SET (enabled) = ($1) WHERE (object_id = $2)", + expectedArgs: []interface{}{ + false, + "appId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyEnabledChanged oidc config jwt", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.OIDCConfigChangedType), + project.AggregateType, + []byte(`{"appId": "appId", "authMethodType": 3}`), + ), project.OIDCConfigChangedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyEnabledChanged, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.authn_keys SET (enabled) = ($1) WHERE (object_id = $2)", + expectedArgs: []interface{}{ + true, + "appId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyRemoved app key removed", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationKeyRemovedEventType), + project.AggregateType, + []byte(`{"keyId": "keyId"}`), + ), project.ApplicationKeyRemovedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyRemoved, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.authn_keys WHERE (id = $1)", + expectedArgs: []interface{}{ + "keyId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyRemoved app removed", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationRemovedType), + project.AggregateType, + []byte(`{"appId": "appId"}`), + ), project.ApplicationRemovedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyRemoved, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.authn_keys WHERE (object_id = $1)", + expectedArgs: []interface{}{ + "appId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyRemoved project removed", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ProjectRemovedType), + project.AggregateType, + nil, + ), project.ProjectRemovedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyRemoved, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.authn_keys WHERE (aggregate_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyRemoved machine key removed", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineKeyRemovedEventType), + user.AggregateType, + []byte(`{"keyId": "keyId"}`), + ), user.MachineKeyRemovedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyRemoved, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("user"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.authn_keys WHERE (id = $1)", + expectedArgs: []interface{}{ + "keyId", + }, + }, + }, + }, + }, + }, + { + name: "reduceAuthNKeyRemoved user removed", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserRemovedType), + user.AggregateType, + []byte(`{"keyId": "keyId"}`), + ), user.UserRemovedEventMapper), + }, + reduce: (&AuthNKeyProjection{}).reduceAuthNKeyRemoved, + want: wantReduce{ + projection: AuthNKeyTable, + aggregateType: eventstore.AggregateType("user"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.authn_keys WHERE (aggregate_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 !errors.IsErrorInvalidArgument(err) { + 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, tt.want) + }) + } +} diff --git a/internal/query/projection/executer_test.go b/internal/query/projection/executer_test.go index 9b25f814c2..dc4f52e19e 100644 --- a/internal/query/projection/executer_test.go +++ b/internal/query/projection/executer_test.go @@ -2,9 +2,10 @@ package projection import ( "database/sql" - "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/caos/zitadel/internal/errors" ) @@ -47,9 +48,7 @@ func (e *testExecuter) Validate(t *testing.T) { if _, ok := execution.expectedArgs[i].(anyArg); ok { continue } - if !reflect.DeepEqual(execution.expectedArgs[i], execution.gottenArgs[i]) { - t.Errorf("wrong argument at index %d: got: %v want: %v", i, execution.gottenArgs[i], execution.expectedArgs[i]) - } + assert.Equal(t, execution.expectedArgs[i], execution.gottenArgs[i], "wrong argument at index %d", i) } } if execution.gottenStmt != execution.expectedStmt { diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 7597440ec8..20b81fd37e 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -64,6 +64,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co NewProjectMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_members"])) NewProjectGrantMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_members"])) _, err := NewKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), defaults.KeyConfig) + NewAuthNKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["authn_keys"])) return err } diff --git a/migrations/cockroach/V1.101__authn_keys.sql b/migrations/cockroach/V1.101__authn_keys.sql new file mode 100644 index 0000000000..97433b5b1f --- /dev/null +++ b/migrations/cockroach/V1.101__authn_keys.sql @@ -0,0 +1,16 @@ +CREATE TABLE zitadel.projections.authn_keys( + id STRING + , creation_date TIMESTAMPTZ NOT NULL + , resource_owner STRING NOT NULL + , aggregate_id STRING NOT NULL + , sequence INT8 NOT NULL + + , object_id STRING NOT NULL + , expiration TIMESTAMPTZ NOT NULL + , identifier STRING NOT NULL + , public_key BYTES NOT NULL + , enabled BOOLEAN NOT NULL DEFAULT true + , type INT2 NOT NULL DEFAULT 0 + + , PRIMARY KEY (id) +);