Merge branch 'next' into next-rc

This commit is contained in:
Livio Spring 2024-11-27 15:42:08 +01:00
commit e857e6794a
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
19 changed files with 296 additions and 36 deletions

View File

@ -211,11 +211,11 @@ Caches:
# host:port address.
Addr: localhost:6379
# ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
ClientName: ZITADEL_cache
ClientName: ""
# Use the specified Username to authenticate the current connection
# with one of the connections defined in the ACL list when connecting
# to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
Username: zitadel
Username: ""
# Optional password. Must match the password specified in the
# requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower),
# or the User Password when connecting to a Redis 6.0 instance, or greater,

View File

@ -5,13 +5,16 @@ import (
"database/sql"
_ "embed"
"fmt"
"slices"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/zitadel/logging"
cryptoDatabase "github.com/zitadel/zitadel/internal/crypto/database"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/database/dialect"
"github.com/zitadel/zitadel/internal/query/projection"
)
func verifyCmd() *cobra.Command {
@ -98,12 +101,22 @@ func getViews(ctx context.Context, dest *database.DB, schema string) (tables []s
}
func countEntries(ctx context.Context, client *database.DB, table string) (count int) {
instanceClause := instanceClause()
noInstanceIDColumn := []string{
projection.InstanceProjectionTable,
projection.SystemFeatureTable,
cryptoDatabase.EncryptionKeysTable,
}
if slices.Contains(noInstanceIDColumn, table) {
instanceClause = ""
}
err := client.QueryRowContext(
ctx,
func(r *sql.Row) error {
return r.Scan(&count)
},
fmt.Sprintf("SELECT COUNT(*) FROM %s %s", table, instanceClause()),
fmt.Sprintf("SELECT COUNT(*) FROM %s %s", table, instanceClause),
)
logging.WithFields("table", table, "db", client.DatabaseName()).OnError(err).Error("unable to count")

View File

@ -42,7 +42,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest
}
orgs := make([]*admin_pb.DataOrg, len(queriedOrgs.Orgs))
processedOrgs := make([]string, len(queriedOrgs.Orgs))
processedOrgs := make([]string, 0, len(queriedOrgs.Orgs))
processedProjects := make([]string, 0)
processedGrants := make([]string, 0)
processedUsers := make([]string, 0)

View File

@ -8,9 +8,11 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"github.com/crewjam/saml"
"github.com/gorilla/mux"
"github.com/muhlemmer/gu"
"github.com/zitadel/logging"
http_utils "github.com/zitadel/zitadel/internal/api/http"
@ -49,6 +51,7 @@ const (
paramError = "error"
paramErrorDescription = "error_description"
varIDPID = "idpid"
paramInternalUI = "internalUI"
)
type Handler struct {
@ -187,21 +190,8 @@ func (h *Handler) handleMetadata(w http.ResponseWriter, r *http.Request) {
}
metadata := sp.ServiceProvider.Metadata()
for i, spDesc := range metadata.SPSSODescriptors {
spDesc.AssertionConsumerServices = append(
spDesc.AssertionConsumerServices,
saml.IndexedEndpoint{
Binding: saml.HTTPPostBinding,
Location: h.loginSAMLRootURL(ctx),
Index: len(spDesc.AssertionConsumerServices) + 1,
}, saml.IndexedEndpoint{
Binding: saml.HTTPArtifactBinding,
Location: h.loginSAMLRootURL(ctx),
Index: len(spDesc.AssertionConsumerServices) + 2,
},
)
metadata.SPSSODescriptors[i] = spDesc
}
internalUI, _ := strconv.ParseBool(r.URL.Query().Get(paramInternalUI))
h.assertionConsumerServices(ctx, metadata, internalUI)
buf, _ := xml.MarshalIndent(metadata, "", " ")
w.Header().Set("Content-Type", "application/samlmetadata+xml")
@ -212,6 +202,48 @@ func (h *Handler) handleMetadata(w http.ResponseWriter, r *http.Request) {
}
}
func (h *Handler) assertionConsumerServices(ctx context.Context, metadata *saml.EntityDescriptor, internalUI bool) {
if !internalUI {
for i, spDesc := range metadata.SPSSODescriptors {
spDesc.AssertionConsumerServices = append(
spDesc.AssertionConsumerServices,
saml.IndexedEndpoint{
Binding: saml.HTTPPostBinding,
Location: h.loginSAMLRootURL(ctx),
Index: len(spDesc.AssertionConsumerServices) + 1,
}, saml.IndexedEndpoint{
Binding: saml.HTTPArtifactBinding,
Location: h.loginSAMLRootURL(ctx),
Index: len(spDesc.AssertionConsumerServices) + 2,
},
)
metadata.SPSSODescriptors[i] = spDesc
}
return
}
for i, spDesc := range metadata.SPSSODescriptors {
acs := make([]saml.IndexedEndpoint, 0, len(spDesc.AssertionConsumerServices)+2)
acs = append(acs,
saml.IndexedEndpoint{
Binding: saml.HTTPPostBinding,
Location: h.loginSAMLRootURL(ctx),
Index: 0,
IsDefault: gu.Ptr(true),
},
saml.IndexedEndpoint{
Binding: saml.HTTPArtifactBinding,
Location: h.loginSAMLRootURL(ctx),
Index: 1,
})
for i := 0; i < len(spDesc.AssertionConsumerServices); i++ {
spDesc.AssertionConsumerServices[i].Index = 2 + i
acs = append(acs, spDesc.AssertionConsumerServices[i])
}
spDesc.AssertionConsumerServices = acs
metadata.SPSSODescriptors[i] = spDesc
}
}
func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
data := parseSAMLRequest(r)

View File

@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
saml_xml "github.com/zitadel/saml/pkg/provider/xml"
"github.com/zitadel/saml/pkg/provider/xml/md"
"golang.org/x/crypto/bcrypt"
http_util "github.com/zitadel/zitadel/internal/api/http"
@ -111,13 +112,15 @@ func TestServer_SAMLMetadata(t *testing.T) {
oauthIdpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id)
type args struct {
ctx context.Context
idpID string
ctx context.Context
idpID string
internalUI bool
}
tests := []struct {
name string
args args
want int
name string
args args
want int
wantACS []md.IndexedEndpointType
}{
{
name: "saml metadata, invalid idp",
@ -142,11 +145,115 @@ func TestServer_SAMLMetadata(t *testing.T) {
idpID: samlRedirectIdpID,
},
want: http.StatusOK,
wantACS: []md.IndexedEndpointType{
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "1",
IsDefault: "",
Binding: saml.HTTPPostBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs",
ResponseLocation: "",
},
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "2",
IsDefault: "",
Binding: saml.HTTPArtifactBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs",
ResponseLocation: "",
},
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "3",
IsDefault: "",
Binding: saml.HTTPPostBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs",
ResponseLocation: "",
},
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "4",
IsDefault: "",
Binding: saml.HTTPArtifactBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs",
ResponseLocation: "",
},
},
},
{
name: "saml metadata, ok (internalUI)",
args: args{
ctx: CTX,
idpID: samlRedirectIdpID,
internalUI: true,
},
want: http.StatusOK,
wantACS: []md.IndexedEndpointType{
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "0",
IsDefault: "true",
Binding: saml.HTTPPostBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs",
ResponseLocation: "",
},
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "1",
IsDefault: "",
Binding: saml.HTTPArtifactBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs",
ResponseLocation: "",
},
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "2",
IsDefault: "",
Binding: saml.HTTPPostBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs",
ResponseLocation: "",
},
{
XMLName: xml.Name{
Space: "urn:oasis:names:tc:SAML:2.0:metadata",
Local: "AssertionConsumerService",
},
Index: "3",
IsDefault: "",
Binding: saml.HTTPArtifactBinding,
Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs",
ResponseLocation: "",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metadataURL := http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + tt.args.idpID + "/saml/metadata"
if tt.args.internalUI {
metadataURL = metadataURL + "?internalUI=true"
}
resp, err := http.Get(metadataURL)
assert.NoError(t, err)
assert.Equal(t, tt.want, resp.StatusCode)
@ -155,10 +262,11 @@ func TestServer_SAMLMetadata(t *testing.T) {
defer resp.Body.Close()
assert.NoError(t, err)
_, err = saml_xml.ParseMetadataXmlIntoStruct(b)
metadata, err := saml_xml.ParseMetadataXmlIntoStruct(b)
assert.NoError(t, err)
}
assert.Equal(t, metadata.SPSSODescriptor.AssertionConsumerService, tt.wantACS)
}
})
}
}

View File

@ -600,6 +600,7 @@ func (s *Server) authResponseToken(authReq *AuthRequest, authorizer op.Authorize
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),
authReq.SessionID,
authReq.oidc().ResponseType,
)
if err != nil {
op.AuthRequestError(w, r, authReq, err, authorizer)

View File

@ -47,6 +47,7 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ
nil,
false,
"",
domain.OIDCResponseTypeUnspecified,
)
if err != nil {
return nil, err

View File

@ -87,6 +87,7 @@ func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.A
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),
authReq.SessionID,
authReq.oidc().ResponseType,
)
if err != nil {
return nil, err

View File

@ -300,6 +300,7 @@ func (s *Server) createExchangeAccessToken(
actor,
slices.Contains(scope, oidc.ScopeOfflineAccess),
"",
domain.OIDCResponseTypeUnspecified,
)
if err != nil {
return "", "", "", 0, err
@ -346,6 +347,7 @@ func (s *Server) createExchangeJWT(
actor,
slices.Contains(scope, oidc.ScopeOfflineAccess),
"",
domain.OIDCResponseTypeUnspecified,
)
accessToken, err = s.createJWT(ctx, client, session, getUserInfo, roleAssertion, getSigner)
if err != nil {

View File

@ -57,6 +57,7 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr
nil,
false,
"",
domain.OIDCResponseTypeUnspecified,
)
if err != nil {
return nil, err

View File

@ -69,6 +69,7 @@ func (s *Server) refreshTokenV1(ctx context.Context, client *Client, r *op.Clien
refreshToken.Actor,
true,
"",
domain.OIDCResponseTypeUnspecified,
)
if err != nil {
return nil, err

View File

@ -147,6 +147,7 @@ func (c *Commands) CreateOIDCSession(ctx context.Context,
actor *domain.TokenActor,
needRefreshToken bool,
sessionID string,
responseType domain.OIDCResponseType,
) (session *OIDCSession, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -164,8 +165,10 @@ func (c *Commands) CreateOIDCSession(ctx context.Context,
cmd.AddSession(ctx, userID, resourceOwner, sessionID, clientID, audience, scope, authMethods, authTime, nonce, preferredLanguage, userAgent)
cmd.RegisterLogout(ctx, sessionID, userID, clientID, backChannelLogoutURI)
if err = cmd.AddAccessToken(ctx, scope, userID, resourceOwner, reason, actor); err != nil {
return nil, err
if responseType != domain.OIDCResponseTypeIDToken {
if err = cmd.AddAccessToken(ctx, scope, userID, resourceOwner, reason, actor); err != nil {
return nil, err
}
}
if needRefreshToken {
if err = cmd.AddRefreshToken(ctx, userID); err != nil {

View File

@ -749,6 +749,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
actor *domain.TokenActor
needRefreshToken bool
sessionID string
responseType domain.OIDCResponseType
}
tests := []struct {
name string
@ -788,6 +789,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
Issuer: "foo.com",
},
needRefreshToken: false,
responseType: domain.OIDCResponseTypeUnspecified,
},
wantErr: io.ErrClosedPipe,
},
@ -844,6 +846,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
Issuer: "foo.com",
},
needRefreshToken: false,
responseType: domain.OIDCResponseTypeUnspecified,
},
wantErr: zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive"),
},
@ -918,6 +921,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
Issuer: "foo.com",
},
needRefreshToken: false,
responseType: domain.OIDCResponseTypeUnspecified,
},
want: &OIDCSession{
TokenID: "V2_oidcSessionID-at_accessTokenID",
@ -943,6 +947,87 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
},
},
},
{
name: "ID token only",
fields: fields{
eventstore: expectEventstore(
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
&user.NewAggregate("userID", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.Afrikaans,
domain.GenderUnspecified,
"email",
false,
),
),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
),
),
),
idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID"),
defaultAccessTokenLifetime: time.Hour,
defaultRefreshTokenLifetime: 7 * 24 * time.Hour,
defaultRefreshTokenIdleLifetime: 24 * time.Hour,
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
userID: "userID",
resourceOwner: "org1",
clientID: "clientID",
audience: []string{"audience"},
scope: []string{"openid", "offline_access"},
authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
authTime: testNow,
nonce: "nonce",
preferredLanguage: &language.Afrikaans,
userAgent: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
reason: domain.TokenReasonAuthRequest,
actor: &domain.TokenActor{
UserID: "user2",
Issuer: "foo.com",
},
needRefreshToken: false,
responseType: domain.OIDCResponseTypeIDToken,
},
want: &OIDCSession{
ClientID: "clientID",
UserID: "userID",
Audience: []string{"audience"},
Scope: []string{"openid", "offline_access"},
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
AuthTime: testNow,
Nonce: "nonce",
PreferredLanguage: &language.Afrikaans,
UserAgent: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
},
},
{
name: "disable user token event",
fields: fields{
@ -1018,6 +1103,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
Issuer: "foo.com",
},
needRefreshToken: false,
responseType: domain.OIDCResponseTypeUnspecified,
},
want: &OIDCSession{
TokenID: "V2_oidcSessionID-at_accessTokenID",
@ -1115,6 +1201,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
Issuer: "foo.com",
},
needRefreshToken: true,
responseType: domain.OIDCResponseTypeUnspecified,
},
want: &OIDCSession{
TokenID: "V2_oidcSessionID-at_accessTokenID",
@ -1213,6 +1300,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
},
needRefreshToken: false,
sessionID: "sessionID",
responseType: domain.OIDCResponseTypeUnspecified,
},
want: &OIDCSession{
TokenID: "V2_oidcSessionID-at_accessTokenID",
@ -1594,6 +1682,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
Issuer: "foo.com",
},
needRefreshToken: false,
responseType: domain.OIDCResponseTypeUnspecified,
},
wantErr: zerrors.ThrowPermissionDenied(nil, "test", "test"),
},
@ -1675,6 +1764,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
Issuer: "foo.com",
},
needRefreshToken: false,
responseType: domain.OIDCResponseTypeUnspecified,
},
want: &OIDCSession{
TokenID: "V2_oidcSessionID-at_accessTokenID",
@ -1729,6 +1819,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
tt.args.actor,
tt.args.needRefreshToken,
tt.args.sessionID,
tt.args.responseType,
)
require.ErrorIs(t, err, tt.wantErr)
if got != nil {

View File

@ -2,7 +2,7 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/org"
@ -47,7 +47,7 @@ func (c *Commands) SetTriggerActions(ctx context.Context, flowType domain.FlowTy
if err != nil {
return nil, err
}
if reflect.DeepEqual(existingFlow.Triggers[triggerType], actionIDs) {
if slices.Equal(existingFlow.Triggers[triggerType], actionIDs) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nfh52", "Errors.Flow.NoChanges")
}
if len(actionIDs) > 0 {

View File

@ -448,7 +448,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.
return nil, nil, err
}
if existing.UserState != domain.UserStateUnspecified {
if existing.UserState.Exists() {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-ziuna", "Errors.User.AlreadyExisting")
}
}

View File

@ -79,7 +79,8 @@ const (
type OIDCResponseType int32
const (
OIDCResponseTypeCode OIDCResponseType = iota
OIDCResponseTypeUnspecified OIDCResponseType = iota - 1 // Negative offset not to break existing configs.
OIDCResponseTypeCode
OIDCResponseTypeIDToken
OIDCResponseTypeIDTokenToken
)

View File

@ -18,6 +18,7 @@ type StepStates struct {
// Query implements eventstore.QueryReducer.
func (*StepStates) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
InstanceID(""). // to make sure we can use an appropriate index
AddQuery().
AggregateTypes(SystemAggregate).
AggregateIDs(SystemAggregateID).

View File

@ -168,6 +168,7 @@ func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.Sel
).
From(flowsTriggersTable.name).
LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))).
OrderBy(FlowsTriggersColumnTriggerSequence.identifier()).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) ([]*Action, error) {
actions := make([]*Action, 0)
@ -220,6 +221,7 @@ func prepareFlowQuery(ctx context.Context, db prepareDatabase, flowType domain.F
).
From(flowsTriggersTable.name).
LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))).
OrderBy(FlowsTriggersColumnTriggerSequence.identifier()).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Flow, error) {
flow := &Flow{

View File

@ -33,8 +33,9 @@ var (
` projections.flow_triggers3.sequence,` +
` projections.flow_triggers3.resource_owner` +
` FROM projections.flow_triggers3` +
` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id`
// ` AS OF SYSTEM TIME '-1 ms'`
` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` +
` AS OF SYSTEM TIME '-1 ms'` +
` ORDER BY projections.flow_triggers3.trigger_sequence`
prepareFlowCols = []string{
"id",
"creation_date",
@ -66,8 +67,9 @@ var (
` projections.actions3.allowed_to_fail,` +
` projections.actions3.timeout` +
` FROM projections.flow_triggers3` +
` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id`
// ` AS OF SYSTEM TIME '-1 ms'`
` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` +
` AS OF SYSTEM TIME '-1 ms'` +
` ORDER BY projections.flow_triggers3.trigger_sequence`
prepareTriggerActionCols = []string{
"id",