Merge branch 'refs/heads/main' into next-rc

This commit is contained in:
Livio Spring
2024-07-04 10:26:55 +02:00
56 changed files with 2460 additions and 297 deletions

View File

@@ -109,6 +109,10 @@ func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED
case feature.ImprovedPerformanceTypeOrgByID:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID
case feature.ImprovedPerformanceTypeProjectGrant:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT
case feature.ImprovedPerformanceTypeProject:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT
default:
return feature_pb.ImprovedPerformance(typ)
}
@@ -133,6 +137,10 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp
return feature.ImprovedPerformanceTypeUnknown
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID:
return feature.ImprovedPerformanceTypeOrgByID
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT:
return feature.ImprovedPerformanceTypeProjectGrant
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT:
return feature.ImprovedPerformanceTypeProject
default:
return feature.ImprovedPerformanceTypeUnknown
}

View File

@@ -216,6 +216,8 @@ func idpTypeToPb(idpType domain.IDPType) settings.IdentityProviderType {
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED
case domain.IDPTypeGoogle:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE
case domain.IDPTypeSAML:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML
default:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
}

View File

@@ -466,6 +466,10 @@ func Test_idpTypeToPb(t *testing.T) {
args: args{domain.IDPTypeGoogle},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE,
},
{
args: args{domain.IDPTypeSAML},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML,
},
{
args: args{99},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED,

View File

@@ -555,7 +555,7 @@ func (s *Server) authResponseToken(authReq *AuthRequest, authorizer op.Authorize
authReq.AuthTime,
authReq.GetNonce(),
authReq.PreferredLanguage,
authReq.BrowserInfo.ToUserAgent(),
authReq.ToUserAgent(),
domain.TokenReasonAuthRequest,
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),

View File

@@ -81,7 +81,7 @@ func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.A
authReq.AuthTime,
authReq.GetNonce(),
authReq.PreferredLanguage,
authReq.BrowserInfo.ToUserAgent(),
authReq.ToUserAgent(),
domain.TokenReasonAuthRequest,
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),

View File

@@ -162,7 +162,7 @@ func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) {
action := mux.Vars(r)["action"]
switch action {
case deviceAuthAllowed:
_, err = l.command.ApproveDeviceAuth(r.Context(), authDev.DeviceCode, authReq.UserID, authReq.UserOrgID, authReq.UserAuthMethodTypes(), authReq.AuthTime, authReq.PreferredLanguage, authReq.BrowserInfo.ToUserAgent())
_, err = l.command.ApproveDeviceAuth(r.Context(), authDev.DeviceCode, authReq.UserID, authReq.UserOrgID, authReq.UserAuthMethodTypes(), authReq.AuthTime, authReq.PreferredLanguage, authReq.ToUserAgent())
case deviceAuthDenied:
_, err = l.command.CancelDeviceAuth(r.Context(), authDev.DeviceCode, domain.DeviceAuthCanceledDenied)
default:

View File

@@ -21,7 +21,7 @@ LDAP:
SelectAccount:
Title: Konto auswählen
Description: Wähle dein Konto aus.
TitleLinking: Konto auswählen um zu verknüpfen
TitleLinking: Konto für die Benutzerverknüpfung auswählen
DescriptionLinking: Wähle dein Konto, um dieses mit deinem externen Benutzerkonto zu verbinden.
OtherUser: Anderer Benutzer
SessionState0: aktiv
@@ -88,7 +88,7 @@ InitUserDone:
InitMFAPrompt:
Title: Zweitfaktor hinzufügen
Description: Zwei-Faktor-Authentifizierung gibt dir eine zusätzliche Sicherheit für dein Benutzerkonto. Damit stellst du sicher, dass nur du Zugriff auf dein Konto hast.
Description: Die Zwei-Faktor-Authentifizierung gibt dir eine zusätzliche Sicherheit für dein Benutzerkonto. Damit stellst du sicher, dass nur du Zugriff auf dein Konto hast.
Provider0: Authentifizierungs-App (z.B. Google/Microsoft Authenticator, Authy)
Provider1: Geräte-gebunden (z.B. FaceID, Windows Hello, Fingerprint)
Provider3: Einmalpasswort per SMS
@@ -97,7 +97,7 @@ InitMFAPrompt:
SkipButtonText: Überspringen
InitMFAOTP:
Title: Zwei-Faktor-Anmeldung
Title: Zwei-Faktor-Authentifizierung
Description: Erstelle deinen Zweitfaktor. Installiere eine Authentifizierungs-App, wenn du noch keine hast.
OTPDescription: Scanne den Code mit einer Authentifizierungs-App (z.B. Google/Mircorsoft Authenticator, Authy) oder kopiere das Secret und gib anschliessend den Code ein.
SecretLabel: Secret
@@ -120,12 +120,12 @@ InitMFAU2F:
Description: Ein Sicherheitsschlüssel ist eine Verifizierungsmethode, die in deinem Telefon integriert sein kann, oder ein externes Gerät, das über Bluetooth oder USB mit deinem Computer verbunden wird.
TokenNameLabel: Name des Geräts
NotSupported: WebAuthN wird durch deinen Browser nicht unterstützt. Stelle sicher, dass du die aktuelle Version installiert hast oder nutze eine anderen (z.B. Chrome, Safari, Firefox)
RegisterTokenButtonText: Sicherschlüssel hinzufügen
RegisterTokenButtonText: Sicherheitsschlüssel hinzufügen
ErrorRetry: Versuche es erneut, erstelle eine neue Abfrage oder wähle einen andere Methode.
InitMFADone:
Title: Sicherheitsschlüssel eingerichtet
Description: Großartig! Du hast gerade erfolgreich deinen Zweitfaktor eingerichtet und dein Konto viel sicherer gemacht. Der Zweitfaktor muss bei jeder Anmeldung verwendet werden.
Description: Großartig! Du hast gerade erfolgreich deinen Zweitfaktor eingerichtet und dein Konto viel sicherer gemacht. Der Zweitfaktor muss ab sofort bei jeder Anmeldung verwendet werden.
NextButtonText: Weiter
CancelButtonText: Abbrechen
@@ -189,7 +189,7 @@ PasswordlessRegistrationDone:
PasswordChange:
Title: Passwort ändern
Description: Ändere dein Passwort indem du dein altes und dann dein neues Passwort eingibst.
Description: Ändere dein Passwort, indem du dein altes und dann dein neues Passwort eingibst.
ExpiredDescription: Dein Passwort ist abgelaufen und muss geändert werden. Gib dein altes und neues Passwort ein.
OldPasswordLabel: Altes Passwort
NewPasswordLabel: Neues Passwort
@@ -204,12 +204,12 @@ PasswordChangeDone:
PasswordResetDone:
Title: Resetlink versendet
Description: Prüfe dein E-Mail-Postfach, um ein neues Passwort zu setzen.
Description: Prüfe dein E-Mail-Postfach, um ein neues Passwort festzulegen.
NextButtonText: Weiter
EmailVerification:
Title: E-Mail-Verifizierung
Description: Du hast eine E-Mail zur Verifizierung deiner E-Mail-Adresse bekommen. Gib den Code im untenstehenden Formular ein. Mit erneut versenden, wird dir eine neue E-Mail zugestellt.
Description: Du hast eine E-Mail zur Verifizierung deiner E-Mail-Adresse bekommen. Gib den Code im untenstehenden Feld ein. Mit erneut versenden, wird dir eine neue E-Mail gesendet.
CodeLabel: Code
NextButtonText: Weiter
ResendButtonText: Code erneut senden
@@ -222,7 +222,7 @@ EmailVerificationDone:
LoginButtonText: Anmelden
RegisterOption:
Title: Registrationsmöglichkeiten
Title: Registrieren
Description: Wähle aus, wie du dich registrieren möchtest.
RegisterUsernamePasswordButtonText: Mit Benutzername/Passwort
ExternalLoginDescription: oder registriere dich mit einem externen Benutzerkonto
@@ -304,7 +304,7 @@ ExternalRegistrationUserOverview:
RegistrationOrg:
Title: Organisation registrieren
Description: Gib deinen Organisationsnamen und deine Benutzerdaten an.
OrgNameLabel: Organisationsname
OrgNameLabel: Name der Organisation
EmailLabel: E-Mail
UsernameLabel: Benutzername
FirstnameLabel: Vorname
@@ -320,7 +320,7 @@ RegistrationOrg:
LoginSuccess:
Title: Erfolgreich angemeldet
AutoRedirectDescription: Du wirst automatisch zurück in die Anwendung geleitet. Danach kannst du dieses Fenster schließen.
AutoRedirectDescription: Du wirst automatisch zu der Anwendung weitergeleitet. Wenn nicht, klicke auf die Schaltfläche unten. Danach kannst Du das Fenster schließen.
RedirectedDescription: Du kannst dieses Fenster nun schließen.
NextButtonText: Weiter
@@ -331,19 +331,19 @@ LogoutDone:
LinkingUserPrompt:
Title: Vorhandener Benutzer gefunden
Description: "Möchten Sie Ihr bestehendes Konto verknüpfen:"
Description: "Möchten Sie Ihr bestehendes Konto verknüpfen?"
LinkButtonText: Verknüpfen
OtherButtonText: Andere Optionen
LinkingUsersDone:
Title: Benutzerkonto verknpüfen
Description: Benuzterkonto verknpüft.
Title: Benutzerkonto verknüpfen
Description: Das Benutzerkonto wurde erfolgreich verknüpft.
CancelButtonText: Abbrechen
NextButtonText: Weiter
ExternalNotFound:
Title: Externes Benutzerkonto nicht gefunden
Description: Externes Benutzerkonto konnte nicht gefunden werden. Willst du deinen Benutzer mit einem bestehenden verknüpfen oder diesen als neuen Benutzer registrieren.
Description: Externes Benutzerkonto konnte nicht gefunden werden. Möchtest du deinen Benutzer mit einem bestehenden Benutzer verknüpfen oder ihn als neuen Benutzer registrieren?
LinkButtonText: Verknüpfen
AutoRegisterButtonText: Registrieren
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
@@ -367,18 +367,18 @@ ExternalNotFound:
Dutch: Nederlands
Swedish: Svenska
DeviceAuth:
Title: Gerätezulassung
Title: Gerät verbinden
UserCode:
Label: Benutzercode
Description: Gib den auf dem Gerät angezeigten Benutzercode ein
Label: Gerätecode
Description: Gib den auf dem Gerät angezeigten Code ein
ButtonNext: Weiter
Action:
Description: Gerätezugriff erlauben
GrantDevice: Du bist dabei, das Gerät zu erlauben
AccessToScopes: Zugriff auf die folgenden Daten
GrantDevice: Möchtest du dem Gerät
AccessToScopes: Zugriff auf die folgenden Daten erlauben?
Button:
Allow: Erlauben
Deny: Verweigern
Deny: Ablehnen
Done:
Description: Abgeschlossen
Approved: Gerätezulassung genehmigt. Sie können jetzt zum Gerät zurückkehren.
@@ -406,11 +406,11 @@ Errors:
NotFound: Benutzer konnte nicht gefunden werden
AlreadyExists: Benutzer existiert bereits
Inactive: Benutzer ist inaktiv
NotFoundOnOrg: Benutzer konnte in der gewünschten Organisation nicht gefunden werden
NotFoundOnOrg: Benutzer konnte nicht in der gewünschten Organisation gefunden werden
NotAllowedOrg: Benutzer gehört nicht der benötigten Organisation an
NotMatchingUserID: Benutzer stimmt nicht mit Benutzer in Auth Request überein
UserIDMissing: UserID ist leer
Invalid: Userdaten sind ungültig
NotMatchingUserID: Benutzer stimmt nicht mit Benutzer im Auth Request überein
UserIDMissing: User-ID ist leer
Invalid: Benutzerdaten sind ungültig
DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet werden
NotAllowedToLink: Der Benutzer darf nicht mit einem externen Benutzerkonto verknüpft werden
Profile:
@@ -423,7 +423,7 @@ Errors:
Email:
NotFound: E-Mail-Adresse nicht gefunden
Invalid: E-Mail-Adresse ist ungültig
AlreadyVerified: E-Mail-Adresse ist bereits verifiziert
AlreadyVerified: E-Mail-Adresse wurde bereits verifiziert
NotChanged: E-Mail-Adresse wurde nicht geändert
Empty: E-Mail-Adresse ist leer
IDMissing: E-Mail ID fehlt
@@ -449,10 +449,10 @@ Errors:
UsernameOrPassword:
Invalid: Benutzername oder Passwort ist ungültig
PasswordComplexityPolicy:
NotFound: Passwortpolicy konnte nicht gefunden werden
NotFound: Passwortrichtlinie konnte nicht gefunden werden
MinLength: Passwort ist zu kurz
HasLower: Passwort beinhaltet keinen Kleinbuchstaben
HasUpper: Passwort beinhaltet keinen Großbuchstaben
HasLower: Passwort beinhaltet keine Kleinbuchstaben
HasUpper: Passwort beinhaltet keine Großbuchstaben
HasNumber: Passwort beinhaltet keine Zahl
HasSymbol: Passwort beinhaltet kein Symbol
Code:
@@ -463,11 +463,11 @@ Errors:
NotFound: Code konnte nicht gefunden werden
GeneratorAlgNotSupported: Generator-Algorithmus wird nicht unterstützt
EmailVerify:
UserIDEmpty: UserID ist leer
UserIDEmpty: User-ID ist leer
ExternalData:
CouldNotRead: Externe Daten konnten nicht korrekt gelesen werden
MFA:
NoProviders: Es stehen keine Multifaktorprovider zur Verfügung
NoProviders: Es steht kein Multifaktorprovider zur Verfügung
OTP:
AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet
NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht
@@ -481,21 +481,21 @@ Errors:
NotAllowed: Externer Login Provider ist nicht erlaubt
IDPConfigIDEmpty: Identity Provider ID ist leer
ExternalUserIDEmpty: Externe User ID ist leer
UserDisplayNameEmpty: Benutzer Anzeige Name ist leer
NoExternalUserData: Keine externe User Daten erhalten
UserDisplayNameEmpty: Anzeige-Name des Benutzers ist leer
NoExternalUserData: Keine externen User-Daten erhalten
CreationNotAllowed: Erstellen eines neuen Benutzers mit diesem Provider ist nicht erlaubt
LinkingNotAllowed: Verknüpfen eines Benutzers mit diesem Provider ist nicht erlaubt
GrantRequired: Die Anmeldung an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte melde dich bei deinem Administrator.
ProjectRequired: Die Anmeldung an dieser Applikation ist nicht möglich. Die Organisation des Benutzer benötigt Berechtigung auf das Projekt. Bitte melde dich bei deinem Administrator.
GrantRequired: Die Anmeldung an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte wende dich an deinen Administrator.
ProjectRequired: Die Anmeldung an dieser Applikation ist nicht möglich. Die Organisation des Benutzer benötigt Berechtigung auf das Projekt. Bitte wende dich an deinen Administrator.
IdentityProvider:
InvalidConfig: Konfiguration des Identitätsproviders ist ungültig
IAM:
LockoutPolicy:
NotExisting: Lockout Policy existiert nicht
NotExisting: Aussperrungs-Richtlinie existiert nicht
Org:
LoginPolicy:
RegistrationNotAllowed: Registrierung ist nicht erlaubt
DeviceAuth:
NotExisting: Benutzercode existiert nicht
NotExisting: Gerätecode existiert nicht
optional: (optional)

View File

@@ -443,6 +443,7 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation {
if a.ID == instance.DefaultOrganisationID() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-wG9p1", "Errors.Org.DefaultOrgNotDeletable")
}
err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID)
// if there is no error, the ZITADEL project was found on the org to be deleted
if err == nil {

View File

@@ -6,9 +6,12 @@ import (
"github.com/zitadel/logging"
"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/feature"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -165,16 +168,51 @@ func (c *Commands) getProjectByID(ctx context.Context, projectID, resourceOwner
return projectWriteModelToProject(projectWriteModel), nil
}
func (c *Commands) projectAggregateByID(ctx context.Context, projectID, resourceOwner string) (*eventstore.Aggregate, domain.ProjectState, error) {
result, err := c.projectState(ctx, projectID, resourceOwner)
if err != nil {
return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-NDQoF", "Errors.Project.NotFound")
}
if len(result) == 0 {
_ = projection.ProjectGrantFields.Trigger(ctx)
result, err = c.projectState(ctx, projectID, resourceOwner)
if err != nil || len(result) == 0 {
return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-U1nza", "Errors.Project.NotFound")
}
}
var state domain.ProjectState
err = result[0].Value.Unmarshal(&state)
if err != nil {
return nil, state, zerrors.ThrowNotFound(err, "COMMA-o4n6F", "Errors.Project.NotFound")
}
return &result[0].Aggregate, state, nil
}
func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner string) ([]*eventstore.SearchResult, error) {
return c.eventstore.Search(
ctx,
map[eventstore.FieldType]any{
eventstore.FieldTypeObjectType: project.ProjectSearchType,
eventstore.FieldTypeObjectID: projectID,
eventstore.FieldTypeObjectRevision: project.ProjectObjectRevision,
eventstore.FieldTypeFieldName: project.ProjectStateSearchField,
eventstore.FieldTypeResourceOwner: resourceOwner,
},
)
}
func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return err
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProject) {
return c.checkProjectExistsOld(ctx, projectID, resourceOwner)
}
if projectWriteModel.State == domain.ProjectStateUnspecified || projectWriteModel.State == domain.ProjectStateRemoved {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound")
_, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner)
if err != nil || !state.Valid() {
return zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound")
}
return nil
}
@@ -184,6 +222,10 @@ func (c *Commands) ChangeProject(ctx context.Context, projectChange *domain.Proj
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid")
}
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProject) {
return c.changeProjectOld(ctx, projectChange, resourceOwner)
}
existingProject, err := c.getProjectWriteModelByID(ctx, projectChange.AggregateID, resourceOwner)
if err != nil {
return nil, err
@@ -223,6 +265,27 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-88iF0", "Errors.Project.ProjectIDMissing")
}
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProject) {
return c.deactivateProjectOld(ctx, projectID, resourceOwner)
}
projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
}
if state == domain.ProjectStateUnspecified || state == domain.ProjectStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-112M9", "Errors.Project.NotFound")
}
if state != domain.ProjectStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive")
}
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectDeactivatedEvent(ctx, projectAgg))
if err != nil {
return nil, err
}
existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
@@ -234,16 +297,11 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive")
}
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectDeactivatedEvent(ctx, projectAgg))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingProject, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingProject.WriteModel), nil
return &domain.ObjectDetails{
ResourceOwner: pushedEvents[0].Aggregate().ResourceOwner,
Sequence: pushedEvents[0].Sequence(),
EventDate: pushedEvents[0].CreatedAt(),
}, nil
}
func (c *Commands) ReactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) {
@@ -251,6 +309,23 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3ihsF", "Errors.Project.ProjectIDMissing")
}
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProject) {
return c.reactivateProjectOld(ctx, projectID, resourceOwner)
}
projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
}
if state == domain.ProjectStateUnspecified || state == domain.ProjectStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
}
if state != domain.ProjectStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive")
}
existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
@@ -262,16 +337,16 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive")
}
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectReactivatedEvent(ctx, projectAgg))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingProject, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingProject.WriteModel), nil
return &domain.ObjectDetails{
ResourceOwner: pushedEvents[0].Aggregate().ResourceOwner,
Sequence: pushedEvents[0].Sequence(),
EventDate: pushedEvents[0].CreatedAt(),
}, nil
}
func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner string, cascadingUserGrantIDs ...string) (*domain.ObjectDetails, error) {
@@ -279,6 +354,10 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing")
}
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProject) {
return c.removeProjectOld(ctx, projectID, resourceOwner)
}
existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err

View File

@@ -6,8 +6,11 @@ import (
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -143,10 +146,12 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI
if grantID == "" || projectID == "" {
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing")
}
err = c.checkProjectExists(ctx, projectID, resourceOwner)
if err != nil {
return details, err
return nil, err
}
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner)
if err != nil {
return details, err
@@ -171,10 +176,12 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI
if grantID == "" || projectID == "" {
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing")
}
err = c.checkProjectExists(ctx, projectID, resourceOwner)
if err != nil {
return details, err
return nil, err
}
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner)
if err != nil {
return details, err
@@ -198,10 +205,12 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r
if grantID == "" || projectID == "" {
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing")
}
err = c.checkProjectExists(ctx, projectID, resourceOwner)
if err != nil {
return details, zerrors.ThrowPreconditionFailed(err, "PROJECT-6mf9s", "Errors.Project.NotFound")
return nil, err
}
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner)
if err != nil {
return details, err
@@ -247,18 +256,76 @@ func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, proj
}
func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectGrant *domain.ProjectGrant) error {
preConditions := NewProjectGrantPreConditionReadModel(projectGrant.AggregateID, projectGrant.GrantedOrgID)
err := c.eventstore.FilterToQueryReducer(ctx, preConditions)
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProjectGrant) {
return c.checkProjectGrantPreConditionOld(ctx, projectGrant)
}
results, err := c.eventstore.Search(
ctx,
// project state query
map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: projectGrant.AggregateID,
eventstore.FieldTypeFieldName: project.ProjectStateSearchField,
eventstore.FieldTypeObjectType: project.ProjectSearchType,
},
// granted org query
map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: org.AggregateType,
eventstore.FieldTypeAggregateID: projectGrant.GrantedOrgID,
eventstore.FieldTypeFieldName: org.OrgStateSearchField,
eventstore.FieldTypeObjectType: org.OrgSearchType,
},
// role query
map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: projectGrant.AggregateID,
eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField,
eventstore.FieldTypeObjectType: project.ProjectRoleSearchType,
},
)
if err != nil {
return err
}
if !preConditions.ProjectExists {
var (
existsProject bool
existsGrantedOrg bool
existingRoleKeys []string
)
for _, result := range results {
switch result.Object.Type {
case project.ProjectRoleSearchType:
var role string
err := result.Value.Unmarshal(&role)
if err != nil {
return err
}
existingRoleKeys = append(existingRoleKeys, role)
case org.OrgSearchType:
var state domain.OrgState
err := result.Value.Unmarshal(&state)
if err != nil {
return err
}
existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved
case project.ProjectSearchType:
var state domain.ProjectState
err := result.Value.Unmarshal(&state)
if err != nil {
return err
}
existsProject = state.Valid() && state != domain.ProjectStateRemoved
}
}
if !existsProject {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound")
}
if !preConditions.GrantedOrgExists {
if !existsGrantedOrg {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
}
if projectGrant.HasInvalidRoles(preConditions.ExistingRoleKeys) {
if projectGrant.HasInvalidRoles(existingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound")
}
return nil

View File

@@ -0,0 +1,180 @@
package command
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) checkProjectExistsOld(ctx context.Context, projectID, resourceOwner string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return err
}
if projectWriteModel.State == domain.ProjectStateUnspecified || projectWriteModel.State == domain.ProjectStateRemoved {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound")
}
return nil
}
func (c *Commands) changeProjectOld(ctx context.Context, projectChange *domain.Project, resourceOwner string) (*domain.Project, error) {
if !projectChange.IsValid() || projectChange.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid")
}
existingProject, err := c.getProjectWriteModelByID(ctx, projectChange.AggregateID, resourceOwner)
if err != nil {
return nil, err
}
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
}
//nolint: contextcheck
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
changedEvent, hasChanged, err := existingProject.NewChangedEvent(
ctx,
projectAgg,
projectChange.Name,
projectChange.ProjectRoleAssertion,
projectChange.ProjectRoleCheck,
projectChange.HasProjectCheck,
projectChange.PrivateLabelingSetting)
if err != nil {
return nil, err
}
if !hasChanged {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.NoChangesFound")
}
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingProject, pushedEvents...)
if err != nil {
return nil, err
}
return projectWriteModelToProject(existingProject), nil
}
func (c *Commands) deactivateProjectOld(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) {
existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
}
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-112M9", "Errors.Project.NotFound")
}
if existingProject.State != domain.ProjectStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive")
}
//nolint: contextcheck
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectDeactivatedEvent(ctx, projectAgg))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingProject, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingProject.WriteModel), nil
}
func (c *Commands) reactivateProjectOld(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) {
existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
}
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
}
if existingProject.State != domain.ProjectStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive")
}
//nolint: contextcheck
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectReactivatedEvent(ctx, projectAgg))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingProject, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingProject.WriteModel), nil
}
func (c *Commands) removeProjectOld(ctx context.Context, projectID, resourceOwner string, cascadingUserGrantIDs ...string) (*domain.ObjectDetails, error) {
existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
}
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
}
samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
}
uniqueConstraints := make([]*eventstore.UniqueConstraint, len(samlEntityIDsAgg.EntityIDs))
for i, entityID := range samlEntityIDsAgg.EntityIDs {
uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID)
}
//nolint: contextcheck
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
events := []eventstore.Command{
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints),
}
for _, grantID := range cascadingUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, grantID, "", true)
if err != nil {
logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
}
events = append(events, event)
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingProject, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingProject.WriteModel), nil
}
func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, projectGrant *domain.ProjectGrant) error {
preConditions := NewProjectGrantPreConditionReadModel(projectGrant.AggregateID, projectGrant.GrantedOrgID)
err := c.eventstore.FilterToQueryReducer(ctx, preConditions)
if err != nil {
return err
}
if !preConditions.ProjectExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound")
}
if !preConditions.GrantedOrgExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
}
if projectGrant.HasInvalidRoles(preConditions.ExistingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound")
}
return nil
}

View File

@@ -41,7 +41,7 @@ func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.Proje
func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOwner string, projectRoles []*domain.ProjectRole) (details *domain.ObjectDetails, err error) {
err = c.checkProjectExists(ctx, projectID, resourceOwner)
if err != nil {
return details, err
return nil, err
}
roleWriteModel := NewProjectRoleWriteModel(projectID, resourceOwner)

View File

@@ -23,14 +23,15 @@ func BrowserInfoFromRequest(r *net_http.Request) *BrowserInfo {
}
}
func (b *BrowserInfo) ToUserAgent() *UserAgent {
if b == nil {
return nil
func (a *AuthRequest) ToUserAgent() *UserAgent {
agent := &UserAgent{
FingerprintID: &a.AgentID,
}
return &UserAgent{
FingerprintID: &b.UserAgent,
IP: b.RemoteIP,
Description: &b.UserAgent,
Header: b.Header,
if a.BrowserInfo == nil {
return agent
}
agent.IP = a.BrowserInfo.RemoteIP
agent.Description = &a.BrowserInfo.UserAgent
agent.Header = a.BrowserInfo.Header
return agent
}

View File

@@ -36,4 +36,10 @@ const (
OrgStateActive
OrgStateInactive
OrgStateRemoved
orgStateMax
)
func (s OrgState) Valid() bool {
return s > OrgStateUnspecified && s < orgStateMax
}

View File

@@ -8,6 +8,7 @@ type Config struct {
PushTimeout time.Duration
MaxRetries uint32
Pusher Pusher
Querier Querier
Pusher Pusher
Querier Querier
Searcher Searcher
}

View File

@@ -31,6 +31,8 @@ type Command interface {
Payload() any
// UniqueConstraints should be added for unique attributes of an event, if nil constraints will not be checked
UniqueConstraints() []*UniqueConstraint
// Fields should be added for fields which should be indexed for lookup, if nil fields will not be indexed
Fields() []*FieldOperation
}
// Event is a stored activity

View File

@@ -123,3 +123,7 @@ func NewBaseEventForPush(ctx context.Context, aggregate *Aggregate, typ EventTyp
EventType: typ,
}
}
func (*BaseEvent) Fields() []*FieldOperation {
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/zerrors"
)
// Eventstore abstracts all functions needed to store valid events
@@ -19,8 +20,9 @@ type Eventstore struct {
PushTimeout time.Duration
maxRetries int
pusher Pusher
querier Querier
pusher Pusher
querier Querier
searcher Searcher
instances []string
lastInstanceQuery time.Time
@@ -62,8 +64,9 @@ func NewEventstore(config *Config) *Eventstore {
PushTimeout: config.PushTimeout,
maxRetries: int(config.MaxRetries),
pusher: config.Pusher,
querier: config.Querier,
pusher: config.Pusher,
querier: config.Querier,
searcher: config.Searcher,
instancesMu: sync.Mutex{},
}
@@ -127,6 +130,20 @@ func (es *Eventstore) AggregateTypes() []string {
return aggregateTypes
}
// FillFields implements the [Searcher] interface
func (es *Eventstore) FillFields(ctx context.Context, events ...FillFieldsEvent) error {
return es.searcher.FillFields(ctx, events...)
}
// Search implements the [Searcher] interface
func (es *Eventstore) Search(ctx context.Context, conditions ...map[FieldType]any) ([]*SearchResult, error) {
if len(conditions) == 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "V3-5Xbr1", "no search conditions")
}
return es.searcher.Search(ctx, conditions...)
}
// Filter filters the stored events based on the searchQuery
// and maps the events to the defined event structs
//
@@ -262,6 +279,22 @@ type Pusher interface {
Push(ctx context.Context, commands ...Command) (_ []Event, err error)
}
type FillFieldsEvent interface {
Event
Fields() []*FieldOperation
}
type Searcher interface {
// Search allows to search for specific fields of objects
// The instance id is taken from the context
// The list of conditions are combined with AND
// The search fields are combined with OR
// At least one must be defined
Search(ctx context.Context, conditions ...map[FieldType]any) (result []*SearchResult, err error)
// FillFields is to insert the fields of previously stored events
FillFields(ctx context.Context, events ...FillFieldsEvent) error
}
func appendEventType(typ EventType) {
i := sort.SearchStrings(eventTypes, string(typ))
if i < len(eventTypes) && eventTypes[i] == string(typ) {

View File

@@ -0,0 +1,140 @@
package eventstore
// FieldOperation if the definition of the operation to be executed on the field
type FieldOperation struct {
// Set a field in the field table
// if [SearchField.UpsertConflictFields] are set the field will be updated if the conflict fields match
// if no [SearchField.UpsertConflictFields] are set the field will be inserted
Set *Field
// Remove fields using the map as `AND`ed conditions
Remove map[FieldType]any
}
type SearchResult struct {
Aggregate Aggregate
Object Object
FieldName string
// Value represents the stored value
// use the Unmarshal method to parse the value to the desired type
Value interface {
// Unmarshal parses the value to ptr
Unmarshal(ptr any) error
}
}
// // NumericResultValue marshals the value to the given type
type Object struct {
// Type of the object
Type string
// ID of the object
ID string
// Revision of the object, if an object evolves the revision should be increased
// analog to current projection versioning
Revision uint8
}
type Field struct {
Aggregate *Aggregate
Object Object
UpsertConflictFields []FieldType
FieldName string
Value Value
}
type Value struct {
Value any
// MustBeUnique defines if the field must be unique
// This field will replace unique constraints in the future
// If MustBeUnique is true the value must be a primitive type
MustBeUnique bool
// ShouldIndex defines if the field should be indexed
// If the field is not indexed it can not be used in search queries
// If ShouldIndex is true the value must be a primitive type
ShouldIndex bool
}
type SearchValueType int8
const (
SearchValueTypeString SearchValueType = iota
SearchValueTypeNumeric
)
// SetSearchField sets the field based on the defined parameters
// if conflictFields are set the field will be updated if the conflict fields match
func SetField(aggregate *Aggregate, object Object, fieldName string, value *Value, conflictFields ...FieldType) *FieldOperation {
return &FieldOperation{
Set: &Field{
Aggregate: aggregate,
Object: object,
UpsertConflictFields: conflictFields,
FieldName: fieldName,
Value: *value,
},
}
}
// RemoveSearchFields removes fields using the map as `AND`ed conditions
func RemoveSearchFields(clause map[FieldType]any) *FieldOperation {
return &FieldOperation{
Remove: clause,
}
}
// RemoveSearchFieldsByAggregate removes fields using the aggregate as `AND`ed conditions
func RemoveSearchFieldsByAggregate(aggregate *Aggregate) *FieldOperation {
return &FieldOperation{
Remove: map[FieldType]any{
FieldTypeInstanceID: aggregate.InstanceID,
FieldTypeResourceOwner: aggregate.ResourceOwner,
FieldTypeAggregateType: aggregate.Type,
FieldTypeAggregateID: aggregate.ID,
},
}
}
// RemoveSearchFieldsByAggregateAndObject removes fields using the aggregate and object as `AND`ed conditions
func RemoveSearchFieldsByAggregateAndObject(aggregate *Aggregate, object Object) *FieldOperation {
return &FieldOperation{
Remove: map[FieldType]any{
FieldTypeInstanceID: aggregate.InstanceID,
FieldTypeResourceOwner: aggregate.ResourceOwner,
FieldTypeAggregateType: aggregate.Type,
FieldTypeAggregateID: aggregate.ID,
FieldTypeObjectType: object.Type,
FieldTypeObjectID: object.ID,
FieldTypeObjectRevision: object.Revision,
},
}
}
// RemoveSearchFieldsByAggregateAndObjectAndField removes fields using the aggregate, object and field as `AND`ed conditions
func RemoveSearchFieldsByAggregateAndObjectAndField(aggregate *Aggregate, object Object, field string) *FieldOperation {
return &FieldOperation{
Remove: map[FieldType]any{
FieldTypeInstanceID: aggregate.InstanceID,
FieldTypeResourceOwner: aggregate.ResourceOwner,
FieldTypeAggregateType: aggregate.Type,
FieldTypeAggregateID: aggregate.ID,
FieldTypeObjectType: object.Type,
FieldTypeObjectID: object.ID,
FieldTypeObjectRevision: object.Revision,
FieldTypeFieldName: field,
},
}
}
type FieldType int8
const (
FieldTypeAggregateType FieldType = iota
FieldTypeAggregateID
FieldTypeInstanceID
FieldTypeResourceOwner
FieldTypeObjectType
FieldTypeObjectID
FieldTypeObjectRevision
FieldTypeFieldName
FieldTypeValue
)

View File

@@ -0,0 +1,205 @@
package handler
import (
"context"
"database/sql"
"errors"
"sync"
"time"
"github.com/jackc/pgx/v5/pgconn"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
type FieldHandler struct {
Handler
}
type fieldProjection struct {
name string
}
// Name implements Projection.
func (f *fieldProjection) Name() string {
return f.name
}
// Reducers implements Projection.
func (f *fieldProjection) Reducers() []AggregateReducer {
return nil
}
var _ Projection = (*fieldProjection)(nil)
func NewFieldHandler(config *Config, name string, eventTypes map[eventstore.AggregateType][]eventstore.EventType) *FieldHandler {
return &FieldHandler{
Handler: Handler{
projection: &fieldProjection{name: name},
client: config.Client,
es: config.Eventstore,
bulkLimit: config.BulkLimit,
eventTypes: eventTypes,
requeueEvery: config.RequeueEvery,
handleActiveInstances: config.HandleActiveInstances,
now: time.Now,
maxFailureCount: config.MaxFailureCount,
retryFailedAfter: config.RetryFailedAfter,
triggeredInstancesSync: sync.Map{},
triggerWithoutEvents: config.TriggerWithoutEvents,
txDuration: config.TransactionDuration,
},
}
}
func (h *FieldHandler) Trigger(ctx context.Context, opts ...TriggerOpt) (err error) {
config := new(triggerConfig)
for _, opt := range opts {
opt(config)
}
cancel := h.lockInstance(ctx, config)
if cancel == nil {
return nil
}
defer cancel()
for i := 0; ; i++ {
additionalIteration, err := h.processEvents(ctx, config)
h.log().OnError(err).Info("process events failed")
h.log().WithField("iteration", i).Debug("trigger iteration")
if !additionalIteration || err != nil {
return err
}
}
}
func (h *FieldHandler) processEvents(ctx context.Context, config *triggerConfig) (additionalIteration bool, err error) {
defer func() {
pgErr := new(pgconn.PgError)
if errors.As(err, &pgErr) {
// error returned if the row is currently locked by another connection
if pgErr.Code == "55P03" {
h.log().Debug("state already locked")
err = nil
additionalIteration = false
}
}
}()
txCtx := ctx
if h.txDuration > 0 {
var cancel, cancelTx func()
// add 100ms to store current state if iteration takes too long
txCtx, cancelTx = context.WithTimeout(ctx, h.txDuration+100*time.Millisecond)
defer cancelTx()
ctx, cancel = context.WithTimeout(ctx, h.txDuration)
defer cancel()
}
ctx, spanBeginTx := tracing.NewNamedSpan(ctx, "db.BeginTx")
tx, err := h.client.BeginTx(txCtx, nil)
spanBeginTx.EndWithError(err)
if err != nil {
return false, err
}
defer func() {
if err != nil && !errors.Is(err, &executionError{}) {
rollbackErr := tx.Rollback()
h.log().OnError(rollbackErr).Debug("unable to rollback tx")
return
}
commitErr := tx.Commit()
if err == nil {
err = commitErr
}
}()
currentState, err := h.currentState(ctx, tx, config)
if err != nil {
if errors.Is(err, errJustUpdated) {
return false, nil
}
return additionalIteration, err
}
// stop execution if currentState.eventTimestamp >= config.maxCreatedAt
if config.maxPosition != 0 && currentState.position >= config.maxPosition {
return false, nil
}
events, additionalIteration, err := h.fetchEvents(ctx, tx, currentState)
if err != nil {
return additionalIteration, err
}
if len(events) == 0 {
err = h.setState(tx, currentState)
return additionalIteration, err
}
err = h.es.FillFields(ctx, events...)
if err != nil {
return false, err
}
err = h.setState(tx, currentState)
return additionalIteration, err
}
func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState *state) (_ []eventstore.FillFieldsEvent, additionalIteration bool, err error) {
events, err := h.es.Filter(ctx, h.eventQuery(currentState).SetTx(tx))
if err != nil {
h.log().WithError(err).Debug("filter eventstore failed")
return nil, false, err
}
eventAmount := len(events)
idx, offset := skipPreviouslyReducedEvents(events, currentState)
if currentState.position == events[len(events)-1].Position() {
offset += currentState.offset
}
currentState.position = events[len(events)-1].Position()
currentState.offset = offset
currentState.aggregateID = events[len(events)-1].Aggregate().ID
currentState.aggregateType = events[len(events)-1].Aggregate().Type
currentState.sequence = events[len(events)-1].Sequence()
currentState.eventTimestamp = events[len(events)-1].CreatedAt()
if idx+1 == len(events) {
return nil, false, nil
}
events = events[idx+1:]
additionalIteration = eventAmount == int(h.bulkLimit)
fillFieldsEvents := make([]eventstore.FillFieldsEvent, len(events))
highestPosition := events[len(events)-1].Position()
for i, event := range events {
if event.Position() == highestPosition {
offset++
}
fillFieldsEvents[i] = event.(eventstore.FillFieldsEvent)
}
return fillFieldsEvents, additionalIteration, nil
}
func skipPreviouslyReducedEvents(events []eventstore.Event, currentState *state) (index int, offset uint32) {
var position float64
for i, event := range events {
if event.Position() != position {
offset = 0
position = event.Position()
}
offset++
if event.Position() == currentState.position &&
event.Aggregate().ID == currentState.aggregateID &&
event.Aggregate().Type == currentState.aggregateType &&
event.Sequence() == currentState.sequence {
return i, offset
}
}
return -1, 0
}

View File

@@ -28,6 +28,7 @@ type EventStore interface {
FilterToQueryReducer(ctx context.Context, reducer eventstore.QueryReducer) error
Filter(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error)
Push(ctx context.Context, cmds ...eventstore.Command) ([]eventstore.Event, error)
FillFields(ctx context.Context, events ...eventstore.FillFieldsEvent) error
}
type Config struct {
@@ -542,7 +543,7 @@ func (h *Handler) generateStatements(ctx context.Context, tx *sql.Tx, currentSta
return []*Statement{stmt}, false, nil
}
events, err := h.es.Filter(ctx, h.eventQuery(currentState))
events, err := h.es.Filter(ctx, h.eventQuery(currentState).SetTx(tx))
if err != nil {
h.log().WithError(err).Debug("filter eventstore failed")
return nil, false, err
@@ -554,7 +555,7 @@ func (h *Handler) generateStatements(ctx context.Context, tx *sql.Tx, currentSta
return nil, false, err
}
idx := skipPreviouslyReduced(statements, currentState)
idx := skipPreviouslyReducedStatements(statements, currentState)
if idx+1 == len(statements) {
currentState.position = statements[len(statements)-1].Position
currentState.offset = statements[len(statements)-1].offset
@@ -576,7 +577,7 @@ func (h *Handler) generateStatements(ctx context.Context, tx *sql.Tx, currentSta
return statements, additionalIteration, nil
}
func skipPreviouslyReduced(statements []*Statement, currentState *state) int {
func skipPreviouslyReducedStatements(statements []*Statement, currentState *state) int {
for i, statement := range statements {
if statement.Position == currentState.position &&
statement.AggregateID == currentState.aggregateID &&
@@ -655,12 +656,11 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder
}
for aggregateType, eventTypes := range h.eventTypes {
query := builder.
builder = builder.
AddQuery().
AggregateTypes(aggregateType).
EventTypes(eventTypes...)
builder = query.Builder()
EventTypes(eventTypes...).
Builder()
}
return builder

View File

@@ -120,3 +120,7 @@ func (e *Event) Payload() any {
func (e *Event) UniqueConstraints() []*eventstore.UniqueConstraint {
return e.Constraints
}
func (e *Event) Fields() []*eventstore.FieldOperation {
return nil
}

View File

@@ -0,0 +1,367 @@
package eventstore
import (
"context"
"database/sql"
_ "embed"
"encoding/json"
"reflect"
"slices"
"strconv"
"strings"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type fieldValue struct {
value []byte
}
func (value *fieldValue) Unmarshal(ptr any) error {
return json.Unmarshal(value.value, ptr)
}
func (es *Eventstore) FillFields(ctx context.Context, events ...eventstore.FillFieldsEvent) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.End()
tx, err := es.client.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
return
}
err = tx.Commit()
}()
return handleFieldFillEvents(ctx, tx, events)
}
// Search implements the [eventstore.Search] method
func (es *Eventstore) Search(ctx context.Context, conditions ...map[eventstore.FieldType]any) (result []*eventstore.SearchResult, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
var builder strings.Builder
args := buildSearchStatement(ctx, &builder, conditions...)
err = es.client.QueryContext(
ctx,
func(rows *sql.Rows) error {
for rows.Next() {
var (
res eventstore.SearchResult
value fieldValue
)
err = rows.Scan(
&res.Aggregate.InstanceID,
&res.Aggregate.ResourceOwner,
&res.Aggregate.Type,
&res.Aggregate.ID,
&res.Object.Type,
&res.Object.ID,
&res.Object.Revision,
&res.FieldName,
&value.value,
)
if err != nil {
return err
}
res.Value = &value
result = append(result, &res)
}
return nil
},
builder.String(),
args...,
)
if err != nil {
return nil, err
}
return result, nil
}
const searchQueryPrefix = `SELECT instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value FROM eventstore.fields WHERE instance_id = $1`
func buildSearchStatement(ctx context.Context, builder *strings.Builder, conditions ...map[eventstore.FieldType]any) []any {
args := make([]any, 0, len(conditions)*4+1)
args = append(args, authz.GetInstance(ctx).InstanceID())
builder.WriteString(searchQueryPrefix)
builder.WriteString(" AND ")
if len(conditions) > 1 {
builder.WriteRune('(')
}
for i, condition := range conditions {
if i > 0 {
builder.WriteString(" OR ")
}
if len(condition) > 1 {
builder.WriteRune('(')
}
args = append(args, buildSearchCondition(builder, len(args)+1, condition)...)
if len(condition) > 1 {
builder.WriteRune(')')
}
}
if len(conditions) > 1 {
builder.WriteRune(')')
}
return args
}
func buildSearchCondition(builder *strings.Builder, index int, conditions map[eventstore.FieldType]any) []any {
args := make([]any, 0, len(conditions))
orderedCondition := make([]eventstore.FieldType, 0, len(conditions))
for field := range conditions {
orderedCondition = append(orderedCondition, field)
}
slices.Sort(orderedCondition)
for _, field := range orderedCondition {
if len(args) > 0 {
builder.WriteString(" AND ")
}
builder.WriteString(fieldNameByType(field, conditions[field]))
builder.WriteString(" = $")
builder.WriteString(strconv.Itoa(index + len(args)))
args = append(args, conditions[field])
}
return args
}
func handleFieldCommands(ctx context.Context, tx *sql.Tx, commands []eventstore.Command) error {
for _, command := range commands {
if len(command.Fields()) > 0 {
if err := handleFieldOperations(ctx, tx, command.Fields()); err != nil {
return err
}
}
}
return nil
}
func handleFieldFillEvents(ctx context.Context, tx *sql.Tx, events []eventstore.FillFieldsEvent) error {
for _, event := range events {
if len(event.Fields()) > 0 {
if err := handleFieldOperations(ctx, tx, event.Fields()); err != nil {
return err
}
}
}
return nil
}
func handleFieldOperations(ctx context.Context, tx *sql.Tx, operations []*eventstore.FieldOperation) error {
for _, operation := range operations {
if operation.Set != nil {
if err := handleFieldSet(ctx, tx, operation.Set); err != nil {
return err
}
continue
}
if operation.Remove != nil {
if err := handleSearchDelete(ctx, tx, operation.Remove); err != nil {
return err
}
}
}
return nil
}
func handleFieldSet(ctx context.Context, tx *sql.Tx, field *eventstore.Field) error {
if len(field.UpsertConflictFields) == 0 {
return handleSearchInsert(ctx, tx, field)
}
return handleSearchUpsert(ctx, tx, field)
}
const (
insertField = `INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`
)
func handleSearchInsert(ctx context.Context, tx *sql.Tx, field *eventstore.Field) error {
value, err := json.Marshal(field.Value.Value)
if err != nil {
return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value")
}
_, err = tx.ExecContext(
ctx,
insertField,
field.Aggregate.InstanceID,
field.Aggregate.ResourceOwner,
field.Aggregate.Type,
field.Aggregate.ID,
field.Object.Type,
field.Object.ID,
field.Object.Revision,
field.FieldName,
value,
field.Value.MustBeUnique,
field.Value.ShouldIndex,
)
return err
}
const (
fieldsUpsertPrefix = `WITH upsert AS (UPDATE eventstore.fields SET (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) WHERE `
fieldsUpsertSuffix = ` RETURNING * ) INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 WHERE NOT EXISTS (SELECT 1 FROM upsert)`
)
func handleSearchUpsert(ctx context.Context, tx *sql.Tx, field *eventstore.Field) error {
value, err := json.Marshal(field.Value.Value)
if err != nil {
return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value")
}
_, err = tx.ExecContext(
ctx,
writeUpsertField(field.UpsertConflictFields),
field.Aggregate.InstanceID,
field.Aggregate.ResourceOwner,
field.Aggregate.Type,
field.Aggregate.ID,
field.Object.Type,
field.Object.ID,
field.Object.Revision,
field.FieldName,
value,
field.Value.MustBeUnique,
field.Value.ShouldIndex,
)
return err
}
func writeUpsertField(fields []eventstore.FieldType) string {
var builder strings.Builder
builder.WriteString(fieldsUpsertPrefix)
for i, fieldName := range fields {
if i > 0 {
builder.WriteString(" AND ")
}
name, index := searchFieldNameAndIndexByTypeForPush(fieldName)
builder.WriteString(name)
builder.WriteString(" = ")
builder.WriteString(index)
}
builder.WriteString(fieldsUpsertSuffix)
return builder.String()
}
const removeSearch = `DELETE FROM eventstore.fields WHERE `
func handleSearchDelete(ctx context.Context, tx *sql.Tx, clauses map[eventstore.FieldType]any) error {
if len(clauses) == 0 {
return zerrors.ThrowInvalidArgument(nil, "V3-oqlBZ", "no conditions")
}
stmt, args := writeDeleteField(clauses)
_, err := tx.ExecContext(ctx, stmt, args...)
return err
}
func writeDeleteField(clauses map[eventstore.FieldType]any) (string, []any) {
var (
builder strings.Builder
args = make([]any, 0, len(clauses))
)
builder.WriteString(removeSearch)
orderedCondition := make([]eventstore.FieldType, 0, len(clauses))
for field := range clauses {
orderedCondition = append(orderedCondition, field)
}
slices.Sort(orderedCondition)
for _, fieldName := range orderedCondition {
if len(args) > 0 {
builder.WriteString(" AND ")
}
builder.WriteString(fieldNameByType(fieldName, clauses[fieldName]))
builder.WriteString(" = $")
builder.WriteString(strconv.Itoa(len(args) + 1))
args = append(args, clauses[fieldName])
}
return builder.String(), args
}
func fieldNameByType(typ eventstore.FieldType, value any) string {
switch typ {
case eventstore.FieldTypeAggregateID:
return "aggregate_id"
case eventstore.FieldTypeAggregateType:
return "aggregate_type"
case eventstore.FieldTypeInstanceID:
return "instance_id"
case eventstore.FieldTypeResourceOwner:
return "resource_owner"
case eventstore.FieldTypeFieldName:
return "field_name"
case eventstore.FieldTypeObjectType:
return "object_type"
case eventstore.FieldTypeObjectID:
return "object_id"
case eventstore.FieldTypeObjectRevision:
return "object_revision"
case eventstore.FieldTypeValue:
return valueColumn(value)
}
return ""
}
func searchFieldNameAndIndexByTypeForPush(typ eventstore.FieldType) (string, string) {
switch typ {
case eventstore.FieldTypeInstanceID:
return "instance_id", "$1"
case eventstore.FieldTypeResourceOwner:
return "resource_owner", "$2"
case eventstore.FieldTypeAggregateType:
return "aggregate_type", "$3"
case eventstore.FieldTypeAggregateID:
return "aggregate_id", "$4"
case eventstore.FieldTypeObjectType:
return "object_type", "$5"
case eventstore.FieldTypeObjectID:
return "object_id", "$6"
case eventstore.FieldTypeObjectRevision:
return "object_revision", "$7"
case eventstore.FieldTypeFieldName:
return "field_name", "$8"
case eventstore.FieldTypeValue:
return "value", "$9"
}
return "", ""
}
func valueColumn(value any) string {
//nolint: exhaustive
switch reflect.TypeOf(value).Kind() {
case reflect.Bool:
return "bool_value"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
return "number_value"
case reflect.String:
return "text_value"
}
return ""
}

View File

@@ -0,0 +1,260 @@
package eventstore
import (
"context"
_ "embed"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
)
func Test_handleSearchDelete(t *testing.T) {
type args struct {
clauses map[eventstore.FieldType]any
}
type want struct {
stmt string
args []any
}
tests := []struct {
name string
args args
want want
}{
{
name: "1 condition",
args: args{
clauses: map[eventstore.FieldType]any{
eventstore.FieldTypeInstanceID: "i_id",
},
},
want: want{
stmt: "DELETE FROM eventstore.fields WHERE instance_id = $1",
args: []any{"i_id"},
},
},
{
name: "2 conditions",
args: args{
clauses: map[eventstore.FieldType]any{
eventstore.FieldTypeInstanceID: "i_id",
eventstore.FieldTypeAggregateID: "a_id",
},
},
want: want{
stmt: "DELETE FROM eventstore.fields WHERE aggregate_id = $1 AND instance_id = $2",
args: []any{"a_id", "i_id"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, args := writeDeleteField(tt.args.clauses)
if stmt != tt.want.stmt {
t.Errorf("handleSearchDelete() stmt = %q, want %q", stmt, tt.want.stmt)
}
assert.Equal(t, tt.want.args, args)
})
}
}
func Test_writeUpsertField(t *testing.T) {
type args struct {
fields []eventstore.FieldType
}
tests := []struct {
name string
args args
want string
}{
{
name: "1 field",
args: args{
fields: []eventstore.FieldType{
eventstore.FieldTypeInstanceID,
},
},
want: "WITH upsert AS (UPDATE eventstore.fields SET (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) WHERE instance_id = $1 RETURNING * ) INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 WHERE NOT EXISTS (SELECT 1 FROM upsert)",
},
{
name: "2 fields",
args: args{
fields: []eventstore.FieldType{
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeAggregateType,
},
},
want: "WITH upsert AS (UPDATE eventstore.fields SET (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) WHERE instance_id = $1 AND aggregate_type = $3 RETURNING * ) INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 WHERE NOT EXISTS (SELECT 1 FROM upsert)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := writeUpsertField(tt.args.fields); got != tt.want {
t.Errorf("writeUpsertField() = %q, want %q", got, tt.want)
}
})
}
}
func Test_buildSearchCondition(t *testing.T) {
type args struct {
index int
conditions map[eventstore.FieldType]any
}
type want struct {
stmt string
args []any
}
tests := []struct {
name string
args args
want want
}{
{
name: "1 condition",
args: args{
index: 1,
conditions: map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateID: "a_id",
},
},
want: want{
stmt: "aggregate_id = $1",
args: []any{"a_id"},
},
},
{
name: "3 condition",
args: args{
index: 1,
conditions: map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateID: "a_id",
eventstore.FieldTypeInstanceID: "i_id",
eventstore.FieldTypeAggregateType: "a_type",
},
},
want: want{
stmt: "aggregate_type = $1 AND aggregate_id = $2 AND instance_id = $3",
args: []any{"a_type", "a_id", "i_id"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var builder strings.Builder
if got := buildSearchCondition(&builder, tt.args.index, tt.args.conditions); !reflect.DeepEqual(got, tt.want.args) {
t.Errorf("buildSearchCondition() = %v, want %v", got, tt.want)
}
if tt.want.stmt != builder.String() {
t.Errorf("buildSearchCondition() stmt = %q, want %q", builder.String(), tt.want.stmt)
}
})
}
}
func Test_buildSearchStatement(t *testing.T) {
type args struct {
index int
conditions []map[eventstore.FieldType]any
}
type want struct {
stmt string
args []any
}
tests := []struct {
name string
args args
want want
}{
{
name: "1 condition with 1 field",
args: args{
index: 1,
conditions: []map[eventstore.FieldType]any{
{
eventstore.FieldTypeAggregateID: "a_id",
},
},
},
want: want{
stmt: "SELECT instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value FROM eventstore.fields WHERE instance_id = $1 AND aggregate_id = $2",
args: []any{"a_id"},
},
},
{
name: "1 condition with 3 fields",
args: args{
index: 1,
conditions: []map[eventstore.FieldType]any{
{
eventstore.FieldTypeAggregateID: "a_id",
eventstore.FieldTypeInstanceID: "i_id",
eventstore.FieldTypeAggregateType: "a_type",
},
},
},
want: want{
stmt: "SELECT instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value FROM eventstore.fields WHERE instance_id = $1 AND (aggregate_type = $2 AND aggregate_id = $3 AND instance_id = $4)",
args: []any{"a_type", "a_id", "i_id"},
},
},
{
name: "2 condition with 1 field",
args: args{
index: 1,
conditions: []map[eventstore.FieldType]any{
{
eventstore.FieldTypeAggregateID: "a_id",
},
{
eventstore.FieldTypeAggregateType: "a_type",
},
},
},
want: want{
stmt: "SELECT instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value FROM eventstore.fields WHERE instance_id = $1 AND (aggregate_id = $2 OR aggregate_type = $3)",
args: []any{"a_id", "a_type"},
},
},
{
name: "2 condition with 2 fields",
args: args{
index: 1,
conditions: []map[eventstore.FieldType]any{
{
eventstore.FieldTypeAggregateID: "a_id1",
eventstore.FieldTypeAggregateType: "a_type1",
},
{
eventstore.FieldTypeAggregateID: "a_id2",
eventstore.FieldTypeAggregateType: "a_type2",
},
},
},
want: want{
stmt: "SELECT instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value FROM eventstore.fields WHERE instance_id = $1 AND ((aggregate_type = $2 AND aggregate_id = $3) OR (aggregate_type = $4 AND aggregate_id = $5))",
args: []any{"a_type1", "a_id1", "a_type2", "a_id2"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var builder strings.Builder
tt.want.args = append([]any{"i_id"}, tt.want.args...)
ctx := authz.WithInstanceID(context.Background(), "i_id")
if got := buildSearchStatement(ctx, &builder, tt.args.conditions...); !reflect.DeepEqual(got, tt.want.args) {
t.Errorf("buildSearchStatement() = %v, want %v", got, tt.want)
}
if tt.want.stmt != builder.String() {
t.Errorf("buildSearchStatement() stmt = %q, want %q", builder.String(), tt.want.stmt)
}
})
}
}

View File

@@ -42,6 +42,10 @@ func (m *mockCommand) UniqueConstraints() []*eventstore.UniqueConstraint {
return m.constraints
}
func (e *mockCommand) Fields() []*eventstore.FieldOperation {
return nil
}
func mockEvent(aggregate *eventstore.Aggregate, sequence uint64, payload Payload) eventstore.Event {
return &event{
aggregate: aggregate,

View File

@@ -41,7 +41,20 @@ func (es *Eventstore) Push(ctx context.Context, commands ...eventstore.Command)
return err
}
return handleUniqueConstraints(ctx, tx, commands)
if err = handleUniqueConstraints(ctx, tx, commands); err != nil {
return err
}
// CockroachDB by default does not allow multiple modifications of the same table using ON CONFLICT
// Thats why we enable it manually
if es.client.Type() == "cockroach" {
_, err = tx.Exec("SET enable_multiple_modifications_of_table = on")
if err != nil {
return err
}
}
return handleFieldCommands(ctx, tx, commands)
})
if err != nil {

View File

@@ -42,6 +42,8 @@ type ImprovedPerformanceType int32
const (
ImprovedPerformanceTypeUnknown = iota
ImprovedPerformanceTypeOrgByID
ImprovedPerformanceTypeProjectGrant
ImprovedPerformanceTypeProject
)
func (f Features) ShouldUseImprovedPerformance(typ ImprovedPerformanceType) bool {

View File

@@ -0,0 +1,19 @@
package projection
import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
)
func newFillProjectGrantFields(config handler.Config) *handler.FieldHandler {
return handler.NewFieldHandler(
&config,
"project_grant_fields",
map[eventstore.AggregateType][]eventstore.EventType{
org.AggregateType: nil,
project.AggregateType: nil,
},
)
}

View File

@@ -49,3 +49,7 @@ func (m *mockEventStore) Push(ctx context.Context, cmds ...eventstore.Command) (
m.pushCounter++
return m.pushResponse[m.pushCounter-1], nil
}
func (m *mockEventStore) FillFields(ctx context.Context, events ...eventstore.FillFieldsEvent) error {
return nil
}

View File

@@ -77,6 +77,8 @@ var (
TargetProjection *handler.Handler
ExecutionProjection *handler.Handler
UserSchemaProjection *handler.Handler
ProjectGrantFields *handler.FieldHandler
)
type projection interface {
@@ -158,6 +160,9 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
TargetProjection = newTargetProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["targets"]))
ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"]))
UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"]))
ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations["project_grant_fields"]))
newProjectionsList()
return nil
}

View File

@@ -68,7 +68,7 @@ func NewMembershipUserIDQuery(userID string) (SearchQuery, error) {
}
func NewMembershipOrgIDQuery(value string) (SearchQuery, error) {
return NewTextQuery(membershipOrgID, value, TextEquals)
return NewTextQuery(OrgMemberOrgID, value, TextEquals)
}
func NewMembershipResourceOwnersSearchQuery(ids ...string) (SearchQuery, error) {
@@ -84,15 +84,15 @@ func NewMembershipGrantedOrgIDSearchQuery(id string) (SearchQuery, error) {
}
func NewMembershipProjectIDQuery(value string) (SearchQuery, error) {
return NewTextQuery(membershipProjectID, value, TextEquals)
return NewTextQuery(ProjectMemberProjectID, value, TextEquals)
}
func NewMembershipProjectGrantIDQuery(value string) (SearchQuery, error) {
return NewTextQuery(membershipGrantID, value, TextEquals)
return NewTextQuery(ProjectGrantMemberGrantID, value, TextEquals)
}
func NewMembershipIsIAMQuery() (SearchQuery, error) {
return NewNotNullQuery(membershipIAMID)
return NewNotNullQuery(InstanceMemberIAMID)
}
func (q *MembershipSearchQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
@@ -357,7 +357,7 @@ func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) {
).From(orgMemberTable.identifier())
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == orgMemberTable.name {
builder = q.toQuery(builder)
}
}
@@ -380,7 +380,7 @@ func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) {
).From(instanceMemberTable.identifier())
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == instanceMemberTable.name {
builder = q.toQuery(builder)
}
}
@@ -403,7 +403,7 @@ func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{})
).From(projectMemberTable.identifier())
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name {
builder = q.toQuery(builder)
}
}
@@ -427,7 +427,7 @@ func prepareProjectGrantMember(query *MembershipSearchQuery) (string, []interfac
).From(projectGrantMemberTable.identifier())
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name || q.Col().table.name == projectGrantMemberTable.name {
builder = q.toQuery(builder)
}
}

View File

@@ -17,6 +17,10 @@ const (
OrgDeactivatedEventType = orgEventTypePrefix + "deactivated"
OrgReactivatedEventType = orgEventTypePrefix + "reactivated"
OrgRemovedEventType = orgEventTypePrefix + "removed"
OrgSearchType = "org"
OrgNameSearchField = "name"
OrgStateSearchField = "state"
)
func NewAddOrgNameUniqueConstraint(orgName string) *eventstore.UniqueConstraint {
@@ -46,6 +50,43 @@ func (e *OrgAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewAddOrgNameUniqueConstraint(e.Name)}
}
func (e *OrgAddedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
orgSearchObject(e.Aggregate().ID),
OrgNameSearchField,
&eventstore.Value{
Value: e.Name,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
eventstore.SetField(
e.Aggregate(),
orgSearchObject(e.Aggregate().ID),
OrgStateSearchField,
&eventstore.Value{
Value: domain.OrgStateActive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewOrgAddedEvent(ctx context.Context, aggregate *eventstore.Aggregate, name string) *OrgAddedEvent {
return &OrgAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -87,6 +128,28 @@ func (e *OrgChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
}
}
func (e *OrgChangedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
orgSearchObject(e.Aggregate().ID),
OrgNameSearchField,
&eventstore.Value{
Value: e.Name,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewOrgChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, oldName, newName string) *OrgChangedEvent {
return &OrgChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -123,6 +186,28 @@ func (e *OrgDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint
return nil
}
func (e *OrgDeactivatedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
orgSearchObject(e.Aggregate().ID),
OrgStateSearchField,
&eventstore.Value{
Value: domain.OrgStateInactive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewOrgDeactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *OrgDeactivatedEvent {
return &OrgDeactivatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -143,6 +228,28 @@ type OrgReactivatedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *OrgReactivatedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
orgSearchObject(e.Aggregate().ID),
OrgStateSearchField,
&eventstore.Value{
Value: domain.OrgStateActive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func (e *OrgReactivatedEvent) Payload() interface{} {
return e
}
@@ -200,6 +307,29 @@ func (e *OrgRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return constraints
}
func (e *OrgRemovedEvent) Fields() []*eventstore.FieldOperation {
// TODO: project grants are currently not removed because we don't have the relationship between the granted org and the grant
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
orgSearchObject(e.Aggregate().ID),
OrgStateSearchField,
&eventstore.Value{
Value: domain.OrgStateRemoved,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewOrgRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, name string, usernames []string, loginMustBeDomain bool, domains []string, externalIDPs []*domain.UserIDPLink, samlEntityIDs []string) *OrgRemovedEvent {
return &OrgRemovedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -221,3 +351,11 @@ func OrgRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
func orgSearchObject(id string) eventstore.Object {
return eventstore.Object{
Type: OrgSearchType,
Revision: 1,
ID: id,
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -17,6 +18,13 @@ var (
GrantDeactivatedType = grantEventTypePrefix + "deactivated"
GrantReactivatedType = grantEventTypePrefix + "reactivated"
GrantRemovedType = grantEventTypePrefix + "removed"
ProjectGrantSearchType = "project_grant"
ProjectGrantGrantIDSearchField = "grant_id"
ProjectGrantGrantedOrgIDSearchField = "granted_org_id"
ProjectGrantStateSearchField = "state"
ProjectGrantRoleKeySearchField = "role_key"
ProjectGrantObjectRevision = uint8(1)
)
func NewAddProjectGrantUniqueConstraint(grantedOrgID, projectID string) *eventstore.UniqueConstraint {
@@ -48,6 +56,76 @@ func (e *GrantAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewAddProjectGrantUniqueConstraint(e.GrantedOrgID, e.Aggregate().ID)}
}
func (e *GrantAddedEvent) Fields() []*eventstore.FieldOperation {
fields := make([]*eventstore.FieldOperation, 0, len(e.RoleKeys)+3)
fields = append(fields,
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantGrantIDSearchField,
&eventstore.Value{
Value: e.GrantID,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantGrantedOrgIDSearchField,
&eventstore.Value{
Value: e.GrantedOrgID,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantStateSearchField,
&eventstore.Value{
Value: domain.ProjectGrantStateActive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
)
for _, roleKey := range e.RoleKeys {
fields = append(fields,
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantRoleKeySearchField,
&eventstore.Value{
Value: roleKey,
ShouldIndex: true,
},
),
)
}
return fields
}
func NewGrantAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -95,6 +173,37 @@ func (e *GrantChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *GrantChangedEvent) Fields() []*eventstore.FieldOperation {
fields := make([]*eventstore.FieldOperation, 0, len(e.RoleKeys)+1)
fields = append(fields,
eventstore.RemoveSearchFieldsByAggregateAndObjectAndField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantRoleKeySearchField,
),
)
for _, roleKey := range e.RoleKeys {
fields = append(fields,
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantRoleKeySearchField,
&eventstore.Value{
Value: roleKey,
ShouldIndex: true,
},
),
)
}
return fields
}
func NewGrantChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -140,6 +249,43 @@ func (e *GrantCascadeChangedEvent) UniqueConstraints() []*eventstore.UniqueConst
return nil
}
func (e *GrantCascadeChangedEvent) Fields() []*eventstore.FieldOperation {
fields := make([]*eventstore.FieldOperation, 0, len(e.RoleKeys)+1)
fields = append(fields,
eventstore.RemoveSearchFieldsByAggregateAndObjectAndField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantRoleKeySearchField,
),
)
for _, roleKey := range e.RoleKeys {
fields = append(fields,
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantRoleKeySearchField,
&eventstore.Value{
Value: roleKey,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
)
}
return fields
}
func NewGrantCascadeChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -184,6 +330,29 @@ func (e *GrantDeactivateEvent) UniqueConstraints() []*eventstore.UniqueConstrain
return nil
}
func (e *GrantDeactivateEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantStateSearchField,
&eventstore.Value{
Value: domain.ProjectGrantStateInactive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewGrantDeactivateEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -226,6 +395,29 @@ func (e *GrantReactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstrai
return nil
}
func (e *GrantReactivatedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantStateSearchField,
&eventstore.Value{
Value: domain.ProjectGrantStateActive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewGrantReactivatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -269,6 +461,29 @@ func (e *GrantRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewRemoveProjectGrantUniqueConstraint(e.grantedOrgID, e.Aggregate().ID)}
}
func (e *GrantRemovedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
grantSearchObject(e.GrantID),
ProjectGrantStateSearchField,
&eventstore.Value{
Value: domain.ProjectGrantStateRemoved,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewGrantRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -298,3 +513,11 @@ func GrantRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return e, nil
}
func grantSearchObject(id string) eventstore.Object {
return eventstore.Object{
Type: ProjectGrantSearchType,
Revision: 1,
ID: id,
}
}

View File

@@ -16,6 +16,11 @@ const (
ProjectDeactivatedType = projectEventTypePrefix + "deactivated"
ProjectReactivatedType = projectEventTypePrefix + "reactivated"
ProjectRemovedType = projectEventTypePrefix + "removed"
ProjectSearchType = "project"
ProjectObjectRevision = uint8(1)
ProjectNameSearchField = "name"
ProjectStateSearchField = "state"
)
func NewAddProjectNameUniqueConstraint(projectName, resourceOwner string) *eventstore.UniqueConstraint {
@@ -49,6 +54,45 @@ func (e *ProjectAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewAddProjectNameUniqueConstraint(e.Name, e.Aggregate().ResourceOwner)}
}
func (e *ProjectAddedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
projectSearchObject(e.Aggregate().ID),
ProjectNameSearchField,
&eventstore.Value{
Value: e.Name,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeObjectRevision,
eventstore.FieldTypeFieldName,
),
eventstore.SetField(
e.Aggregate(),
projectSearchObject(e.Aggregate().ID),
ProjectStateSearchField,
&eventstore.Value{
Value: domain.ProjectStateActive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeObjectRevision,
eventstore.FieldTypeFieldName,
),
}
}
func NewProjectAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -110,6 +154,30 @@ func (e *ProjectChangeEvent) UniqueConstraints() []*eventstore.UniqueConstraint
return nil
}
func (e *ProjectChangeEvent) Fields() []*eventstore.FieldOperation {
if e.Name == nil {
return nil
}
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
projectSearchObject(e.Aggregate().ID),
ProjectNameSearchField,
&eventstore.Value{
Value: *e.Name,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewProjectChangeEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -190,6 +258,28 @@ func (e *ProjectDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstr
return nil
}
func (e *ProjectDeactivatedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
projectSearchObject(e.Aggregate().ID),
ProjectStateSearchField,
&eventstore.Value{
Value: domain.ProjectStateInactive,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewProjectDeactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *ProjectDeactivatedEvent {
return &ProjectDeactivatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -218,6 +308,28 @@ func (e *ProjectReactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstr
return nil
}
func (e *ProjectReactivatedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
projectSearchObject(e.Aggregate().ID),
ProjectStateSearchField,
&eventstore.Value{
Value: domain.ProjectStateRemoved,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewProjectReactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *ProjectReactivatedEvent {
return &ProjectReactivatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -255,6 +367,12 @@ func (e *ProjectRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint
return constraints
}
func (e *ProjectRemovedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.RemoveSearchFieldsByAggregate(e.Aggregate()),
}
}
func NewProjectRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -277,3 +395,11 @@ func ProjectRemovedEventMapper(event eventstore.Event) (eventstore.Event, error)
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
func projectSearchObject(id string) eventstore.Object {
return eventstore.Object{
Type: ProjectSearchType,
Revision: ProjectObjectRevision,
ID: id,
}
}

View File

@@ -14,6 +14,12 @@ var (
RoleAddedType = roleEventTypePrefix + "added"
RoleChangedType = roleEventTypePrefix + "changed"
RoleRemovedType = roleEventTypePrefix + "removed"
ProjectRoleSearchType = "project_role"
ProjectRoleRevision = uint8(1)
ProjectRoleKeySearchField = "key"
ProjectRoleDisplayNameSearchField = "display_name"
ProjectRoleGroupSearchField = "group"
)
func NewAddProjectRoleUniqueConstraint(roleKey, projectID string) *eventstore.UniqueConstraint {
@@ -45,6 +51,59 @@ func (e *RoleAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewAddProjectRoleUniqueConstraint(e.Key, e.Aggregate().ID)}
}
func (e *RoleAddedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.SetField(
e.Aggregate(),
projectRoleSearchObject(e.Key),
ProjectRoleKeySearchField,
&eventstore.Value{
Value: e.Key,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
eventstore.SetField(
e.Aggregate(),
projectRoleSearchObject(e.Key),
ProjectRoleDisplayNameSearchField,
&eventstore.Value{
Value: e.DisplayName,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
eventstore.SetField(
e.Aggregate(),
projectRoleSearchObject(e.Key),
ProjectRoleGroupSearchField,
&eventstore.Value{
Value: e.Group,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
),
}
}
func NewRoleAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -93,6 +152,50 @@ func (e *RoleChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *RoleChangedEvent) Fields() []*eventstore.FieldOperation {
operations := make([]*eventstore.FieldOperation, 0, 2)
if e.DisplayName != nil {
operations = append(operations, eventstore.SetField(
e.Aggregate(),
projectRoleSearchObject(e.Key),
ProjectRoleDisplayNameSearchField,
&eventstore.Value{
Value: *e.DisplayName,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
))
}
if e.Group != nil {
operations = append(operations, eventstore.SetField(
e.Aggregate(),
projectRoleSearchObject(e.Key),
ProjectRoleGroupSearchField,
&eventstore.Value{
Value: *e.Group,
ShouldIndex: true,
},
eventstore.FieldTypeInstanceID,
eventstore.FieldTypeResourceOwner,
eventstore.FieldTypeAggregateType,
eventstore.FieldTypeAggregateID,
eventstore.FieldTypeObjectType,
eventstore.FieldTypeObjectID,
eventstore.FieldTypeFieldName,
))
}
return operations
}
func NewRoleChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -162,6 +265,15 @@ func (e *RoleRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewRemoveProjectRoleUniqueConstraint(e.Key, e.Aggregate().ID)}
}
func (e *RoleRemovedEvent) Fields() []*eventstore.FieldOperation {
return []*eventstore.FieldOperation{
eventstore.RemoveSearchFieldsByAggregateAndObject(
e.Aggregate(),
projectRoleSearchObject(e.Key),
),
}
}
func NewRoleRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -188,3 +300,11 @@ func RoleRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return e, nil
}
func projectRoleSearchObject(id string) eventstore.Object {
return eventstore.Object{
Type: ProjectRoleSearchType,
Revision: ProjectRoleRevision,
ID: id,
}
}

View File

@@ -22,6 +22,6 @@ FROM auth.user_sessions s
LEFT JOIN projections.users13 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users13_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
WHERE (s.user_agent_id = $1)
WHERE (s.user_agent_id = $1 and s.user_agent_id <> '')
AND (s.instance_id = $2)
;