mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-06 14:22:15 +00:00
feat(relational): implement session repository (#11109)
# Which Problems Are Solved As part of going to a relation storage model, we need to implement the repository for handling sessions and their events. # How the Problems Are Solved - Adds a migration step to create the necessary tables and types in the database. - Adds the repository implementation for sessions. - Implements a projection handler for session events to handle the events written by the old command handlers. - Updates the `Session` type and `SessionRepository` interface: - added `CreatorID` - Adjusted `Delete` method to return amount of affected columns - Removed sub-repository clumns from the main interface - Added `CreatorIDCondition`, `ExpirationCondition`, `ExistsFactor` and `ExistsMetadata` - Removed `SetUserAgent` - Methods of `SessionFactor` and `SessionChallenge` have been made public - fixed enumer on `SessionFactorType` - Adds a `JSON` type to the repository package to easy can JSON columns, resp. jsonb object inside a property of a `JSONArray` object. - Adds a `NoChange` interface, which can be implemented to signal that a `Change` interface implementation is not writing any changes to the statement builder, e.g. `CTEChange` # Additional Changes - Fixes the operators enums. `TextOperations` had wrong comments and `NumberOperations` additionally also had from comparisons. - Fixes downgrade of migration step 008. # Additional Context - closes https://github.com/zitadel/zitadel/issues/10212
This commit is contained in:
@@ -17,6 +17,12 @@ type Change interface {
|
||||
IsOnColumn(col Column) bool
|
||||
}
|
||||
|
||||
// NoChange can be implemented by Changes that might not result in any actual change,
|
||||
// resp. when the change has no set statement, e.g. [CTEChange] with no change function.
|
||||
type NoChange interface {
|
||||
NoChange() bool
|
||||
}
|
||||
|
||||
type change[V Value] struct {
|
||||
column Column
|
||||
value V
|
||||
@@ -88,15 +94,23 @@ func (c Changes) IsOnColumn(col Column) bool {
|
||||
}
|
||||
|
||||
// Write implements [Change].
|
||||
func (m Changes) Write(builder *StatementBuilder) error {
|
||||
for i, change := range m {
|
||||
if i > 0 {
|
||||
func (c Changes) Write(builder *StatementBuilder) error {
|
||||
var hadChanges bool
|
||||
for _, change := range c {
|
||||
ch, ok := change.(NoChange)
|
||||
hasChanges := !ok || !ch.NoChange()
|
||||
// if the previous change actually wrote a change to the builder
|
||||
// and this change has changes, add a comma
|
||||
if hadChanges && hasChanges {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
err := change.Write(builder)
|
||||
if err != nil {
|
||||
if err := change.Write(builder); err != nil {
|
||||
return err
|
||||
}
|
||||
// if the change had changes, mark that we had changes for the next iteration
|
||||
if hasChanges {
|
||||
hadChanges = hasChanges
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -123,6 +137,21 @@ func (c Changes) String() string {
|
||||
return "database.Changes"
|
||||
}
|
||||
|
||||
// NoChange implements [NoChange].
|
||||
// It returns false if any of the changes writes to the builder.
|
||||
func (c Changes) NoChange() bool {
|
||||
for _, change := range c {
|
||||
no, ok := change.(NoChange)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if no.NoChange() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var _ Change = Changes(nil)
|
||||
|
||||
func NewChangeToNull(col Column) Change {
|
||||
@@ -319,7 +348,7 @@ func (c *cteChange) Matches(x any) bool {
|
||||
return slices.Equal(expectedCTEBuilder.Args(), actualCTEBuilder.Args())
|
||||
}
|
||||
|
||||
// Name implements [CTEChange].
|
||||
// SetName implements [CTEChange].
|
||||
func (c *cteChange) SetName(name string) {
|
||||
c.name = name
|
||||
}
|
||||
@@ -342,3 +371,9 @@ func (c *cteChange) Write(builder *StatementBuilder) error {
|
||||
func (c *cteChange) WriteCTE(builder *StatementBuilder) {
|
||||
c.cte(builder)
|
||||
}
|
||||
|
||||
// NoChange implements [NoChange].
|
||||
// It returns true if there is no change function defined.
|
||||
func (c *cteChange) NoChange() bool {
|
||||
return c.change == nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
DROP TYPE zitadel.project_grant_state;
|
||||
DROP TYPE IF EXISTS zitadel.project_grant_state CASCADE ;
|
||||
DROP TABLE IF EXISTS zitadel.project_grant_roles;
|
||||
DROP TABLE IF EXISTS zitadel.project_grants;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 011_session_table/up.sql
|
||||
up011SessionTable string
|
||||
//go:embed 011_session_table/down.sql
|
||||
down011SessionTable string
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerSQLMigration(11, up011SessionTable, down011SessionTable)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
DROP TABLE IF EXISTS zitadel.session_metadata CASCADE;
|
||||
DROP TABLE IF EXISTS zitadel.session_user_agents CASCADE;
|
||||
DROP TYPE IF EXISTS zitadel.session_factor_type CASCADE;
|
||||
DROP TABLE IF EXISTS zitadel.session_factors CASCADE;
|
||||
DROP TABLE IF EXISTS zitadel.sessions CASCADE;
|
||||
DROP FUNCTION IF EXISTS zitadel.update_expiration();
|
||||
@@ -0,0 +1,93 @@
|
||||
CREATE TABLE zitadel.session_user_agents (
|
||||
instance_id TEXT NOT NULL
|
||||
, fingerprint_id TEXT NOT NULL CHECK (fingerprint_id <> '')
|
||||
, ip INET
|
||||
, description TEXT
|
||||
, headers JSONB
|
||||
|
||||
, PRIMARY KEY (instance_id, fingerprint_id)
|
||||
);
|
||||
|
||||
CREATE TABLE zitadel.sessions (
|
||||
instance_id TEXT NOT NULL
|
||||
, id TEXT NOT NULL CHECK (id <> '')
|
||||
, token_id TEXT
|
||||
, user_agent_id TEXT
|
||||
, lifetime INTERVAL
|
||||
, expiration TIMESTAMPTZ
|
||||
, user_id TEXT -- this column is used for referential integrity
|
||||
, creator_id TEXT
|
||||
, created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
|
||||
, PRIMARY KEY (instance_id, id)
|
||||
, FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id)
|
||||
-- , FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
|
||||
, FOREIGN KEY (instance_id, user_agent_id) REFERENCES zitadel.session_user_agents(instance_id, fingerprint_id) ON DELETE SET NULL (user_agent_id)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION zitadel.update_expiration()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.expiration := NEW.updated_at + NEW.lifetime;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER set_expiration
|
||||
BEFORE INSERT OR UPDATE OF lifetime ON zitadel.sessions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION zitadel.update_expiration();
|
||||
|
||||
CREATE TYPE zitadel.session_factor_type AS ENUM (
|
||||
'user',
|
||||
'password',
|
||||
'passkey', -- is also a challenge
|
||||
'identity_provider_intent',
|
||||
'totp',
|
||||
'otp_sms', -- is also a challenge
|
||||
'otp_email', -- is also a challenge
|
||||
'recovery_code'
|
||||
);
|
||||
|
||||
CREATE TABLE zitadel.session_factors (
|
||||
instance_id TEXT NOT NULL
|
||||
, session_id TEXT NOT NULL
|
||||
, type zitadel.session_factor_type NOT NULL
|
||||
, last_challenged_at TIMESTAMPTZ -- this is only set if the type is a challenge
|
||||
, challenged_payload JSONB
|
||||
, last_verified_at TIMESTAMPTZ
|
||||
, verified_payload JSONB
|
||||
|
||||
, PRIMARY KEY (instance_id, session_id, type)
|
||||
, FOREIGN KEY (instance_id, session_id) REFERENCES zitadel.sessions(instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE zitadel.session_metadata (
|
||||
instance_id TEXT NOT NULL
|
||||
, session_id TEXT NOT NULL
|
||||
, key TEXT NOT NULL CHECK (key <> '')
|
||||
, value BYTEA NOT NULL
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
|
||||
, PRIMARY KEY (instance_id, session_id, key)
|
||||
|
||||
, CONSTRAINT fk_session_metadata_session FOREIGN KEY (instance_id, session_id) REFERENCES zitadel.sessions (instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_session_metadata_key ON zitadel.session_metadata (key);
|
||||
CREATE INDEX idx_session_metadata_value ON zitadel.session_metadata (sha256(value));
|
||||
|
||||
-- TODO(adlerhurst): these indexes can currently not be used by Postgres, because of the type conversion
|
||||
-- the value can be a json but doesn't have to be.
|
||||
-- CREATE INDEX idx_session_metadata_value_number ON zitadel.session_metadata ((value::NUMERIC)) WHERE jsonb_typeof(value) = 'number';
|
||||
-- CREATE INDEX idx_session_metadata_value_string ON zitadel.session_metadata ((value#>>'{}')) WHERE jsonb_typeof(value) = 'string';
|
||||
-- CREATE INDEX idx_session_metadata_value_boolean ON zitadel.session_metadata ((value::BOOLEAN)) WHERE jsonb_typeof(value) = 'boolean';
|
||||
|
||||
CREATE TRIGGER trg_set_updated_at_session_metadata
|
||||
BEFORE INSERT OR UPDATE ON zitadel.session_metadata
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.updated_at IS NULL)
|
||||
EXECUTE FUNCTION zitadel.set_updated_at();
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
@@ -51,9 +52,10 @@ var (
|
||||
SystemClient system.SystemServiceClient
|
||||
OrgClient v2beta_org.OrganizationServiceClient
|
||||
ProjectClient v2beta_project.ProjectServiceClient
|
||||
SessionClient session.SessionServiceClient
|
||||
UserClient user.UserServiceClient
|
||||
AdminClient admin.AdminServiceClient
|
||||
MgmtClient mgmt.ManagementServiceClient
|
||||
UserClient user_v2.UserServiceClient
|
||||
AuthorizationClient authorization.AuthorizationServiceClient
|
||||
)
|
||||
|
||||
@@ -71,9 +73,10 @@ func TestMain(m *testing.M) {
|
||||
SystemClient = integration.SystemClient()
|
||||
OrgClient = Instance.Client.OrgV2beta
|
||||
ProjectClient = Instance.Client.Projectv2Beta
|
||||
SessionClient = Instance.Client.SessionV2
|
||||
UserClient = Instance.Client.UserV2
|
||||
AdminClient = Instance.Client.Admin
|
||||
MgmtClient = Instance.Client.Mgmt
|
||||
UserClient = Instance.Client.UserV2
|
||||
AuthorizationClient = Instance.Client.AuthorizationV2
|
||||
|
||||
defer func() {
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
//go:build integration
|
||||
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
zdomain "github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func TestServer_SessionReduces(t *testing.T) {
|
||||
instanceID := Instance.ID()
|
||||
sessionRepo := repository.SessionRepository()
|
||||
|
||||
email := integration.Email()
|
||||
testUser := Instance.CreateUserTypeHuman(CTX, email)
|
||||
_, err := UserClient.VerifyEmail(CTX, &user.VerifyEmailRequest{
|
||||
UserId: testUser.GetId(),
|
||||
VerificationCode: testUser.GetEmailCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = UserClient.SetPassword(CTX, &user.SetPasswordRequest{
|
||||
UserId: testUser.GetId(),
|
||||
NewPassword: &user.Password{
|
||||
Password: integration.UserPassword,
|
||||
},
|
||||
Verification: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
totpSecret := registerTOTP(CTX, t, testUser.GetId())
|
||||
Instance.RegisterUserPasskey(CTX, testUser.GetId())
|
||||
registerOTPEmail(CTX, t, testUser.GetId())
|
||||
_, err = UserClient.SetPhone(CTX, &user.SetPhoneRequest{
|
||||
UserId: testUser.GetId(),
|
||||
Phone: integration.Phone(),
|
||||
Verification: &user.SetPhoneRequest_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
registerOTPSMS(CTX, t, testUser.GetId())
|
||||
|
||||
idpID := Instance.AddGenericOAuthProvider(CTX, integration.IDPName()).GetId()
|
||||
idpUserID := integration.ID()
|
||||
_, err = UserClient.AddIDPLink(CTX, &user.AddIDPLinkRequest{
|
||||
UserId: testUser.GetId(),
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
UserId: idpUserID,
|
||||
UserName: integration.Username(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
recoveryCodes := createRecoveryCodes(CTX, t, testUser.GetId())
|
||||
|
||||
lifetime := time.Hour
|
||||
creatorID := "tester"
|
||||
userAgent := &domain.SessionUserAgent{
|
||||
FingerprintID: gu.Ptr(integration.ID()),
|
||||
Description: gu.Ptr("description"),
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{"ZITADEL-Integration-Test"},
|
||||
},
|
||||
}
|
||||
createdSession, err := SessionClient.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: nil,
|
||||
Metadata: nil,
|
||||
Challenges: nil,
|
||||
UserAgent: &session.UserAgent{
|
||||
FingerprintId: userAgent.FingerprintID,
|
||||
Ip: gu.Ptr(userAgent.IP.String()),
|
||||
Description: userAgent.Description,
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"User-Agent": {Values: []string{"ZITADEL-Integration-Test"}},
|
||||
},
|
||||
},
|
||||
Lifetime: durationpb.New(lifetime),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Second)
|
||||
var updatedSession *session.SetSessionResponse
|
||||
|
||||
t.Run("create session reduces", func(t *testing.T) {
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: createdSession.GetDetails().GetChangeDate().AsTime().Add(lifetime),
|
||||
UserID: "",
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: nil,
|
||||
Challenges: nil,
|
||||
Metadata: nil,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
metadata := []*domain.SessionMetadata{
|
||||
{
|
||||
Metadata: domain.Metadata{
|
||||
InstanceID: instanceID,
|
||||
Key: "key1",
|
||||
Value: []byte("value1"),
|
||||
},
|
||||
SessionID: createdSession.GetSessionId(),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("metadata set reduces", func(t *testing.T) {
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Checks: nil,
|
||||
Metadata: map[string][]byte{"key1": []byte("value1")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: createdSession.GetDetails().GetChangeDate().AsTime().Add(lifetime),
|
||||
UserID: "",
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: nil,
|
||||
Challenges: nil,
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
var lifetimeUpdated time.Time
|
||||
|
||||
t.Run("lifetime set reduces", func(t *testing.T) {
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Lifetime: durationpb.New(lifetime),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
lifetimeUpdated = updatedSession.GetDetails().GetChangeDate().AsTime()
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: "",
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: nil,
|
||||
Challenges: nil,
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
var firstFactorCheckTime time.Time
|
||||
|
||||
t.Run("checks set reduces", func(t *testing.T) {
|
||||
_, err := url.Parse(Instance.CreateIntent(CTX, idpID).GetAuthUrl())
|
||||
require.NoError(t, err)
|
||||
expiry := time.Now().Add(1 * time.Hour)
|
||||
intentID, intentToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, testUser.GetId(), expiry)
|
||||
require.NoError(t, err)
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_LoginName{
|
||||
LoginName: email,
|
||||
},
|
||||
},
|
||||
Password: &session.CheckPassword{
|
||||
Password: integration.UserPassword,
|
||||
},
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentToken: intentToken,
|
||||
},
|
||||
Totp: &session.CheckTOTP{
|
||||
Code: func() string {
|
||||
code, err := totp.GenerateCode(totpSecret, time.Now())
|
||||
require.NoError(t, err)
|
||||
return code
|
||||
}(),
|
||||
},
|
||||
RecoveryCode: &session.CheckRecoveryCode{
|
||||
Code: recoveryCodes[0],
|
||||
},
|
||||
},
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
Domain: Instance.Domain,
|
||||
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
},
|
||||
OtpEmail: &session.RequestChallenges_OTPEmail{
|
||||
DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{
|
||||
ReturnCode: &session.RequestChallenges_OTPEmail_ReturnCode{},
|
||||
},
|
||||
},
|
||||
OtpSms: &session.RequestChallenges_OTPSMS{
|
||||
ReturnCode: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
firstFactorCheckTime = updatedSession.GetDetails().GetChangeDate().AsTime()
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: testUser.GetId(),
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: domain.SessionFactors{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: testUser.GetId(),
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPassword{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorIdentityProviderIntent{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorTOTP{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorRecoveryCode{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
},
|
||||
Challenges: domain.SessionChallenges{
|
||||
&domain.SessionChallengePasskey{
|
||||
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Challenge: "challenge", // placeholder
|
||||
AllowedCredentialIDs: [][]byte{Instance.WebAuthN.KeyID()},
|
||||
UserVerification: zdomain.UserVerificationRequirementRequired,
|
||||
RPID: Instance.Domain,
|
||||
},
|
||||
&domain.SessionChallengeOTPEmail{
|
||||
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "",
|
||||
KeyID: "",
|
||||
Crypted: nil,
|
||||
},
|
||||
Expiry: 0,
|
||||
CodeReturned: true,
|
||||
URLTemplate: "",
|
||||
TriggeredAtOrigin: "",
|
||||
},
|
||||
&domain.SessionChallengeOTPSMS{
|
||||
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "",
|
||||
KeyID: "",
|
||||
Crypted: nil,
|
||||
},
|
||||
Expiry: 0,
|
||||
CodeReturned: true,
|
||||
GeneratorID: "",
|
||||
TriggeredAtOrigin: "",
|
||||
},
|
||||
},
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("checks with challenges set reduces", func(t *testing.T) {
|
||||
assertionData, err := Instance.WebAuthN.CreateAssertionResponse(updatedSession.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
WebAuthN: &session.CheckWebAuthN{
|
||||
CredentialAssertionData: assertionData,
|
||||
},
|
||||
OtpEmail: &session.CheckOTP{
|
||||
Code: updatedSession.GetChallenges().GetOtpEmail(),
|
||||
},
|
||||
OtpSms: &session.CheckOTP{
|
||||
Code: updatedSession.GetChallenges().GetOtpSms(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: testUser.GetId(),
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: domain.SessionFactors{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: testUser.GetId(),
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPassword{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorIdentityProviderIntent{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPasskey{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UserVerified: true,
|
||||
},
|
||||
&domain.SessionFactorTOTP{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorOTPEmail{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorOTPSMS{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorRecoveryCode{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
},
|
||||
Challenges: domain.SessionChallenges{},
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("password changed reduces", func(t *testing.T) {
|
||||
passwordSet, err := UserClient.SetPassword(CTX, &user.SetPasswordRequest{
|
||||
UserId: testUser.GetId(),
|
||||
NewPassword: &user.Password{
|
||||
Password: integration.UserPassword + integration.RandString(5),
|
||||
},
|
||||
Verification: &user.SetPasswordRequest_CurrentPassword{
|
||||
CurrentPassword: integration.UserPassword,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: testUser.GetId(),
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: passwordSet.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: domain.SessionFactors{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: testUser.GetId(),
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
// password factor removed
|
||||
&domain.SessionFactorIdentityProviderIntent{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPasskey{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UserVerified: true,
|
||||
},
|
||||
&domain.SessionFactorTOTP{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorOTPEmail{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorOTPSMS{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorRecoveryCode{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
},
|
||||
Challenges: domain.SessionChallenges{},
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("terminated reduces", func(t *testing.T) {
|
||||
_, err := SessionClient.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
SessionToken: gu.Ptr(updatedSession.GetSessionToken()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.ErrorIs(collect, err, new(database.NoRowFoundError))
|
||||
assert.Nil(collect, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
}
|
||||
|
||||
func assertSessionsEqual(t *assert.CollectT, expected, actual *domain.Session) {
|
||||
t.Helper()
|
||||
assert.Equal(t, expected.InstanceID, actual.InstanceID)
|
||||
assert.Equal(t, expected.ID, actual.ID)
|
||||
assert.NotNil(t, actual.TokenID)
|
||||
assert.Equal(t, expected.Lifetime, actual.Lifetime)
|
||||
assert.Equal(t, expected.Expiration.UTC(), actual.Expiration.UTC())
|
||||
assert.Equal(t, expected.UserID, actual.UserID)
|
||||
assert.Equal(t, expected.CreatorID, actual.CreatorID)
|
||||
assert.NotNil(t, actual.CreatedAt)
|
||||
assert.NotNil(t, actual.UpdatedAt)
|
||||
assertSessionFactorsEqual(t, expected.Factors, actual.Factors)
|
||||
assertSessionChallengesEqual(t, expected.Challenges, actual.Challenges)
|
||||
assertSessionMetadataEqual(t, expected.Metadata, actual.Metadata)
|
||||
assert.Equal(t, expected.UserAgent, actual.UserAgent)
|
||||
}
|
||||
|
||||
func assertSessionMetadataEqual(t *assert.CollectT, expected, actual []*domain.SessionMetadata) {
|
||||
t.Helper()
|
||||
assert.Len(t, actual, len(expected))
|
||||
for _, exp := range expected {
|
||||
found := false
|
||||
for _, act := range actual {
|
||||
if exp.InstanceID == act.InstanceID &&
|
||||
exp.SessionID == act.SessionID &&
|
||||
exp.Key == act.Key &&
|
||||
bytes.Equal(exp.Value, act.Value) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(t, found, "expected metadata not found: %+v", exp)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSessionFactorsEqual(t *assert.CollectT, expected, actual domain.SessionFactors) {
|
||||
t.Helper()
|
||||
assert.Len(t, actual, len(expected))
|
||||
for _, exp := range expected {
|
||||
found := false
|
||||
for _, act := range actual {
|
||||
if exp.SessionFactorType() == act.SessionFactorType() {
|
||||
switch expTyped := exp.(type) {
|
||||
case *domain.SessionFactorUser:
|
||||
actTyped := act.(*domain.SessionFactorUser)
|
||||
assert.Equal(t, expTyped.UserID, actTyped.UserID)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorPassword:
|
||||
actTyped := act.(*domain.SessionFactorPassword)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorIdentityProviderIntent:
|
||||
actTyped := act.(*domain.SessionFactorIdentityProviderIntent)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorPasskey:
|
||||
actTyped := act.(*domain.SessionFactorPasskey)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
assert.Equal(t, expTyped.UserVerified, actTyped.UserVerified)
|
||||
case *domain.SessionFactorTOTP:
|
||||
actTyped := act.(*domain.SessionFactorTOTP)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorOTPEmail:
|
||||
actTyped := act.(*domain.SessionFactorOTPEmail)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorOTPSMS:
|
||||
actTyped := act.(*domain.SessionFactorOTPSMS)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorRecoveryCode:
|
||||
actTyped := act.(*domain.SessionFactorRecoveryCode)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(t, found, "expected factor not found: %+v", exp)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSessionChallengesEqual(t *assert.CollectT, expected, actual domain.SessionChallenges) {
|
||||
t.Helper()
|
||||
assert.Len(t, actual, len(expected))
|
||||
for _, exp := range expected {
|
||||
found := false
|
||||
for _, act := range actual {
|
||||
if exp.SessionChallengeType() == act.SessionChallengeType() {
|
||||
switch expTyped := exp.(type) {
|
||||
case *domain.SessionChallengePasskey:
|
||||
actTyped := act.(*domain.SessionChallengePasskey)
|
||||
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
|
||||
assert.NotEmpty(t, actTyped.Challenge)
|
||||
assert.Equal(t, expTyped.AllowedCredentialIDs, actTyped.AllowedCredentialIDs)
|
||||
assert.Equal(t, expTyped.RPID, actTyped.RPID)
|
||||
assert.Equal(t, expTyped.UserVerification, actTyped.UserVerification)
|
||||
case *domain.SessionChallengeOTPEmail:
|
||||
actTyped := act.(*domain.SessionChallengeOTPEmail)
|
||||
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
|
||||
assert.Equal(t, expTyped.Code.CryptoType, actTyped.Code.CryptoType)
|
||||
case *domain.SessionChallengeOTPSMS:
|
||||
actTyped := act.(*domain.SessionChallengeOTPSMS)
|
||||
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
|
||||
assert.Equal(t, expTyped.Code.CryptoType, actTyped.Code.CryptoType)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(t, found, "expected challenge not found: %+v", exp)
|
||||
}
|
||||
}
|
||||
|
||||
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
|
||||
resp, err := UserClient.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
secret = resp.GetSecret()
|
||||
code, err := totp.GenerateCode(secret, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = UserClient.VerifyTOTPRegistration(ctx, &user.VerifyTOTPRegistrationRequest{
|
||||
UserId: userID,
|
||||
Code: code,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return secret
|
||||
}
|
||||
|
||||
func registerOTPSMS(ctx context.Context, t *testing.T, userID string) {
|
||||
_, err := UserClient.AddOTPSMS(ctx, &user.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func registerOTPEmail(ctx context.Context, t *testing.T, userID string) {
|
||||
_, err := UserClient.AddOTPEmail(ctx, &user.AddOTPEmailRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func createRecoveryCodes(ctx context.Context, t *testing.T, userID string) (secret []string) {
|
||||
resp, err := UserClient.GenerateRecoveryCodes(ctx, &user.GenerateRecoveryCodesRequest{
|
||||
UserId: userID,
|
||||
Count: 5,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return resp.GetRecoveryCodes()
|
||||
}
|
||||
@@ -60,9 +60,9 @@ const (
|
||||
|
||||
// TextOperationContainsIgnoreCase checks if the first string contains the second ignoring case
|
||||
TextOperationContainsIgnoreCase // LIKE
|
||||
// TextOperationEndsWith checks if the first string ends with the second ignoring case
|
||||
// TextOperationEndsWithIgnoreCase checks if the first string ends with the second ignoring case
|
||||
TextOperationEndsWithIgnoreCase // LIKE
|
||||
// TextOperationStartsWith checks if the first string starts with the second ignoring case
|
||||
// TextOperationStartsWithIgnoreCase checks if the first string starts with the second ignoring case
|
||||
TextOperationStartsWithIgnoreCase // LIKE
|
||||
// TextOperationEqualIgnoreCase compares two strings for equality ignoring case
|
||||
TextOperationEqualIgnoreCase // =
|
||||
@@ -133,11 +133,11 @@ const (
|
||||
// NumberOperationLessThan compares two numbers to check if the first is less than the second.
|
||||
NumberOperationLessThan // <
|
||||
// NumberOperationLessThanOrEqual compares two numbers to check if the first is less than or equal to the second.
|
||||
NumberOperationAtLeast // <=
|
||||
NumberOperationLessThanOrEqual // <=
|
||||
// NumberOperationGreaterThan compares two numbers to check if the first is greater than the second.
|
||||
NumberOperationGreaterThan // >
|
||||
// NumberOperationGreaterThanOrEqual compares two numbers to check if the first is greater than or equal to the second.
|
||||
NumberOperationAtMost // >=
|
||||
NumberOperationGreaterThanOrEqual // >=
|
||||
)
|
||||
|
||||
func writeNumberOperation[T Number](builder *StatementBuilder, col Column, op NumberOperation, value any) {
|
||||
|
||||
@@ -28,12 +28,12 @@ func _NumberOperationNoOp() {
|
||||
_ = x[NumberOperationEqual-(1)]
|
||||
_ = x[NumberOperationNotEqual-(2)]
|
||||
_ = x[NumberOperationLessThan-(3)]
|
||||
_ = x[NumberOperationAtLeast-(4)]
|
||||
_ = x[NumberOperationLessThanOrEqual-(4)]
|
||||
_ = x[NumberOperationGreaterThan-(5)]
|
||||
_ = x[NumberOperationAtMost-(6)]
|
||||
_ = x[NumberOperationGreaterThanOrEqual-(6)]
|
||||
}
|
||||
|
||||
var _NumberOperationValues = []NumberOperation{NumberOperationEqual, NumberOperationNotEqual, NumberOperationLessThan, NumberOperationAtLeast, NumberOperationGreaterThan, NumberOperationAtMost}
|
||||
var _NumberOperationValues = []NumberOperation{NumberOperationEqual, NumberOperationNotEqual, NumberOperationLessThan, NumberOperationLessThanOrEqual, NumberOperationGreaterThan, NumberOperationGreaterThanOrEqual}
|
||||
|
||||
var _NumberOperationNameToValueMap = map[string]NumberOperation{
|
||||
_NumberOperationName[0:1]: NumberOperationEqual,
|
||||
@@ -42,12 +42,12 @@ var _NumberOperationNameToValueMap = map[string]NumberOperation{
|
||||
_NumberOperationLowerName[1:3]: NumberOperationNotEqual,
|
||||
_NumberOperationName[3:4]: NumberOperationLessThan,
|
||||
_NumberOperationLowerName[3:4]: NumberOperationLessThan,
|
||||
_NumberOperationName[4:6]: NumberOperationAtLeast,
|
||||
_NumberOperationLowerName[4:6]: NumberOperationAtLeast,
|
||||
_NumberOperationName[4:6]: NumberOperationLessThanOrEqual,
|
||||
_NumberOperationLowerName[4:6]: NumberOperationLessThanOrEqual,
|
||||
_NumberOperationName[6:7]: NumberOperationGreaterThan,
|
||||
_NumberOperationLowerName[6:7]: NumberOperationGreaterThan,
|
||||
_NumberOperationName[7:9]: NumberOperationAtMost,
|
||||
_NumberOperationLowerName[7:9]: NumberOperationAtMost,
|
||||
_NumberOperationName[7:9]: NumberOperationGreaterThanOrEqual,
|
||||
_NumberOperationLowerName[7:9]: NumberOperationGreaterThanOrEqual,
|
||||
}
|
||||
|
||||
var _NumberOperationNames = []string{
|
||||
|
||||
@@ -181,7 +181,7 @@ func Test_writeOperation(t *testing.T) {
|
||||
{
|
||||
name: "number less than or equal",
|
||||
write: func(builder *StatementBuilder) {
|
||||
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationAtLeast, 123)
|
||||
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationLessThanOrEqual, 123)
|
||||
},
|
||||
want: want{
|
||||
stmt: "table.column <= $1",
|
||||
@@ -201,7 +201,7 @@ func Test_writeOperation(t *testing.T) {
|
||||
{
|
||||
name: "number greater than or equal",
|
||||
write: func(builder *StatementBuilder) {
|
||||
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationAtMost, 123)
|
||||
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationGreaterThanOrEqual, 123)
|
||||
},
|
||||
want: want{
|
||||
stmt: "table.column >= $1",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type JSON[T any] struct {
|
||||
Value T
|
||||
}
|
||||
|
||||
func (j *JSON[T]) Scan(src any) (err error) {
|
||||
var rawJSON []byte
|
||||
switch s := src.(type) {
|
||||
case string:
|
||||
rawJSON = []byte(s)
|
||||
case []byte:
|
||||
rawJSON = s
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return ErrScanSource
|
||||
}
|
||||
if err = j.UnmarshalJSON(rawJSON); err != nil {
|
||||
return database.NewScanError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON allows encoding/json to decode nested JSON into JSON[T] fields.
|
||||
// This makes [json.Unmarshal] work for fields inside aggregated JSON (e.g. JSON arrays).
|
||||
func (j *JSON[T]) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || string(data) == "null" {
|
||||
return nil
|
||||
}
|
||||
var v T
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
j.Value = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the wrapped value or null if nil.
|
||||
func (j JSON[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(j.Value)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
)
|
||||
|
||||
func TestJSON_Scan(t *testing.T) {
|
||||
type want struct {
|
||||
err error
|
||||
res *testObject
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
src any
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
src: nil,
|
||||
want: want{
|
||||
err: nil,
|
||||
res: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "[]byte with data",
|
||||
src: []byte(`{"id":"1","number":1,"active":true}`),
|
||||
want: want{
|
||||
err: nil,
|
||||
res: &testObject{ID: "1", Number: 1, Active: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "[]byte without data",
|
||||
src: []byte(`{}`),
|
||||
want: want{
|
||||
err: nil,
|
||||
res: &testObject{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil []byte",
|
||||
src: []byte(nil),
|
||||
want: want{
|
||||
err: nil,
|
||||
res: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string with data",
|
||||
src: `{"id":"2","number":2,"active":false}`,
|
||||
want: want{
|
||||
err: nil,
|
||||
res: &testObject{ID: "2", Number: 2, Active: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string without data",
|
||||
src: `{}`,
|
||||
want: want{
|
||||
err: nil,
|
||||
res: &testObject{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
src: ``,
|
||||
want: want{
|
||||
err: nil,
|
||||
res: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported type",
|
||||
src: 12345,
|
||||
want: want{
|
||||
err: repository.ErrScanSource,
|
||||
res: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
src: []byte(`this is not json`),
|
||||
want: want{
|
||||
err: new(database.ScanError),
|
||||
res: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var a repository.JSON[testObject]
|
||||
gotErr := a.Scan(tt.src)
|
||||
require.ErrorIs(t, gotErr, tt.want.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// repository
|
||||
// -------------------------------------------------------------
|
||||
|
||||
var _ domain.SessionRepository = (*session)(nil)
|
||||
|
||||
type session struct {
|
||||
factorRepo sessionFactor
|
||||
metadataRepo sessionMetadata
|
||||
userAgentRepo sessionUserAgent
|
||||
}
|
||||
|
||||
func (s session) qualifiedTableName() string {
|
||||
return "zitadel.sessions"
|
||||
}
|
||||
|
||||
func (s session) unqualifiedTableName() string {
|
||||
return "sessions"
|
||||
}
|
||||
|
||||
func SessionRepository() domain.SessionRepository {
|
||||
return new(session)
|
||||
}
|
||||
|
||||
const querySessionStmt = `
|
||||
SELECT
|
||||
sessions.instance_id
|
||||
, sessions.id
|
||||
, sessions.token_id
|
||||
, sessions.lifetime
|
||||
, sessions.expiration
|
||||
, sessions.user_id
|
||||
, sessions.creator_id
|
||||
, sessions.created_at
|
||||
, sessions.updated_at
|
||||
, jsonb_agg(distinct
|
||||
jsonb_build_object(
|
||||
'instanceId', session_factors.instance_id
|
||||
, 'sessionId', session_factors.session_id
|
||||
, 'type', session_factors.type
|
||||
, 'lastChallengedAt', session_factors.last_challenged_at
|
||||
, 'challengedPayload', session_factors.challenged_payload
|
||||
, 'lastVerifiedAt', session_factors.last_verified_at
|
||||
, 'verifiedPayload', session_factors.verified_payload
|
||||
)
|
||||
)
|
||||
FILTER (WHERE session_factors.session_id IS NOT NULL) AS factors
|
||||
, jsonb_agg(distinct
|
||||
jsonb_build_object(
|
||||
'instanceId', session_metadata.instance_id
|
||||
, 'sessionId', session_metadata.session_id
|
||||
, 'key', session_metadata.key
|
||||
, 'value', encode(session_metadata.value, 'base64')
|
||||
, 'createdAt', session_metadata.created_at
|
||||
, 'updatedAt', session_metadata.updated_at
|
||||
)
|
||||
)
|
||||
FILTER (WHERE session_metadata.session_id IS NOT NULL) AS metadata
|
||||
, CASE WHEN session_user_agents.fingerprint_id IS NOT NULL THEN
|
||||
jsonb_build_object(
|
||||
'fingerprintId', session_user_agents.fingerprint_id
|
||||
, 'description', session_user_agents.description
|
||||
, 'ip', session_user_agents.ip
|
||||
, 'headers', session_user_agents.headers
|
||||
)
|
||||
END as user_agent
|
||||
FROM zitadel.sessions`
|
||||
|
||||
// Get implements [domain.SessionRepository].
|
||||
func (s session) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Session, error) {
|
||||
opts = append(opts,
|
||||
s.joinFactors(),
|
||||
s.joinMetadata(),
|
||||
s.joinUserAgent(),
|
||||
database.WithGroupBy(
|
||||
s.InstanceIDColumn(),
|
||||
s.IDColumn(),
|
||||
s.userAgentRepo.fingerprintIDColumn(),
|
||||
s.userAgentRepo.descriptionColumn(),
|
||||
s.userAgentRepo.ipColumn(),
|
||||
s.userAgentRepo.headersColumn(),
|
||||
),
|
||||
)
|
||||
|
||||
options := new(database.QueryOpts)
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
if !options.Condition.IsRestrictingColumn(s.InstanceIDColumn()) {
|
||||
return nil, database.NewMissingConditionError(s.InstanceIDColumn())
|
||||
}
|
||||
|
||||
var builder database.StatementBuilder
|
||||
builder.WriteString(querySessionStmt)
|
||||
options.Write(&builder)
|
||||
|
||||
return scanSession(ctx, client, &builder)
|
||||
}
|
||||
|
||||
// List implements [domain.SessionRepository].
|
||||
func (s session) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Session, error) {
|
||||
opts = append(opts,
|
||||
s.joinFactors(),
|
||||
s.joinMetadata(),
|
||||
s.joinUserAgent(),
|
||||
database.WithGroupBy(
|
||||
s.InstanceIDColumn(),
|
||||
s.IDColumn(),
|
||||
s.userAgentRepo.fingerprintIDColumn(),
|
||||
s.userAgentRepo.descriptionColumn(),
|
||||
s.userAgentRepo.ipColumn(),
|
||||
s.userAgentRepo.headersColumn(),
|
||||
),
|
||||
)
|
||||
|
||||
options := new(database.QueryOpts)
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
if options.Condition == nil || !options.Condition.IsRestrictingColumn(s.InstanceIDColumn()) {
|
||||
return nil, database.NewMissingConditionError(s.InstanceIDColumn())
|
||||
}
|
||||
|
||||
var builder database.StatementBuilder
|
||||
builder.WriteString(querySessionStmt)
|
||||
options.Write(&builder)
|
||||
|
||||
return scanSessions(ctx, client, &builder)
|
||||
}
|
||||
|
||||
const upsertSessionUserAgentStmt = `
|
||||
WITH user_agent AS (
|
||||
INSERT INTO zitadel.session_user_agents(
|
||||
instance_id, fingerprint_id, description, ip, headers
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (instance_id, fingerprint_id)
|
||||
DO UPDATE SET description = EXCLUDED.description, ip = EXCLUDED.ip, headers = excluded.headers
|
||||
) `
|
||||
|
||||
// Create implements [domain.SessionRepository].
|
||||
func (s session) Create(ctx context.Context, client database.QueryExecutor, session *domain.Session) error {
|
||||
var createdAt any = database.DefaultInstruction
|
||||
if !session.CreatedAt.IsZero() {
|
||||
createdAt = session.CreatedAt
|
||||
}
|
||||
var updatedAt = createdAt
|
||||
if !session.UpdatedAt.IsZero() {
|
||||
updatedAt = session.UpdatedAt
|
||||
}
|
||||
|
||||
builder := new(database.StatementBuilder)
|
||||
var fingerprintID *string
|
||||
if session.UserAgent != nil {
|
||||
builder = database.NewStatementBuilder(upsertSessionUserAgentStmt,
|
||||
session.InstanceID,
|
||||
session.UserAgent.FingerprintID,
|
||||
session.UserAgent.Description,
|
||||
session.UserAgent.IP,
|
||||
session.UserAgent.Header,
|
||||
)
|
||||
fingerprintID = session.UserAgent.FingerprintID
|
||||
}
|
||||
builder.WriteString(`INSERT INTO ` + s.qualifiedTableName() + ` (instance_id, id, lifetime, creator_id, user_agent_id, created_at, updated_at) VALUES ( `)
|
||||
builder.WriteArgs(session.InstanceID, session.ID, session.Lifetime, session.CreatorID, fingerprintID, createdAt, updatedAt)
|
||||
builder.WriteString(` ) RETURNING created_at, updated_at`)
|
||||
return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&session.CreatedAt, &session.UpdatedAt)
|
||||
}
|
||||
|
||||
// Update implements [domain.SessionRepository].
|
||||
func (s session) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
|
||||
if len(changes) == 0 {
|
||||
return 0, database.ErrNoChanges
|
||||
}
|
||||
if !condition.IsRestrictingColumn(s.InstanceIDColumn()) {
|
||||
return 0, database.NewMissingConditionError(s.InstanceIDColumn())
|
||||
}
|
||||
if !database.Changes(changes).IsOnColumn(s.UpdatedAtColumn()) {
|
||||
changes = append(changes, database.NewChange(s.UpdatedAtColumn(), database.DefaultInstruction))
|
||||
}
|
||||
|
||||
var builder database.StatementBuilder
|
||||
builder.WriteString("WITH existing_session AS (SELECT * FROM zitadel.sessions ")
|
||||
writeCondition(&builder, condition)
|
||||
builder.WriteString(") ")
|
||||
for i, change := range changes {
|
||||
sessionCTE(change, i, 0, &builder)
|
||||
}
|
||||
builder.WriteString("UPDATE zitadel.sessions SET ")
|
||||
if err := database.Changes(changes).Write(&builder); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
writeCondition(&builder, condition)
|
||||
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
}
|
||||
|
||||
func sessionCTE(change database.Change, i, j int, builder *database.StatementBuilder) {
|
||||
if multi, ok := change.(database.Changes); ok {
|
||||
for j, ch := range multi {
|
||||
sessionCTE(ch, i, j, builder)
|
||||
}
|
||||
}
|
||||
if cte, ok := change.(database.CTEChange); ok {
|
||||
name := fmt.Sprintf("cte_%d_%d", i, j)
|
||||
fmt.Fprintf(builder, ", %s as (", name)
|
||||
cte.SetName(name)
|
||||
cte.WriteCTE(builder)
|
||||
builder.WriteString(") ")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete implements [domain.SessionRepository].
|
||||
func (s session) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
||||
if !condition.IsRestrictingColumn(s.InstanceIDColumn()) {
|
||||
return 0, database.NewMissingConditionError(s.InstanceIDColumn())
|
||||
}
|
||||
var builder database.StatementBuilder
|
||||
builder.WriteString("DELETE FROM zitadel.sessions")
|
||||
writeCondition(&builder, condition)
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetUpdatedAt implements [domain.sessionChanges].
|
||||
func (s session) SetUpdatedAt(updatedAt time.Time) database.Change {
|
||||
return database.NewChange(s.UpdatedAtColumn(), updatedAt)
|
||||
}
|
||||
|
||||
// SetToken implements [domain.sessionChanges].
|
||||
func (s session) SetToken(token string) database.Change {
|
||||
return database.NewChange(s.TokenIDColumn(), token)
|
||||
}
|
||||
|
||||
// SetLifetime implements [domain.sessionChanges].
|
||||
func (s session) SetLifetime(lifetime time.Duration) database.Change {
|
||||
return database.NewChange(s.LifetimeColumn(), lifetime)
|
||||
}
|
||||
|
||||
// SetChallenge implements [domain.sessionChanges].
|
||||
func (s session) SetChallenge(challenge domain.SessionChallenge) database.Change {
|
||||
switch c := challenge.(type) {
|
||||
case *domain.SessionChallengePasskey:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_challenged_at, challenged_payload) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypePasskey, c.LastChallengedAt, c)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_challenged_at = EXCLUDED.last_challenged_at, challenged_payload = EXCLUDED.challenged_payload")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionChallengeOTPSMS:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_challenged_at, challenged_payload) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeOTPSMS, c.LastChallengedAt, c)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_challenged_at = EXCLUDED.last_challenged_at, challenged_payload = EXCLUDED.challenged_payload")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionChallengeOTPEmail:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_challenged_at, challenged_payload) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeOTPEmail, c.LastChallengedAt, c)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_challenged_at = EXCLUDED.last_challenged_at, challenged_payload = EXCLUDED.challenged_payload")
|
||||
}, nil,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFactor implements [domain.sessionChanges].
|
||||
func (s session) SetFactor(factor domain.SessionFactor) database.Change {
|
||||
switch f := factor.(type) {
|
||||
case *domain.SessionFactorUser:
|
||||
return database.NewChanges(
|
||||
database.NewChange(s.UserIDColumn(), f.UserID),
|
||||
database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at, verified_payload) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeUser, f.LastVerifiedAt, f)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at, verified_payload = EXCLUDED.verified_payload")
|
||||
}, nil,
|
||||
),
|
||||
)
|
||||
case *domain.SessionFactorPassword:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypePassword, f.LastVerifiedAt)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionFactorIdentityProviderIntent:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeIdentityProviderIntent, f.LastVerifiedAt)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionFactorPasskey:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at, verified_payload) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypePasskey, f.LastVerifiedAt, f)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at, verified_payload = EXCLUDED.verified_payload")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionFactorTOTP:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeTOTP, f.LastVerifiedAt)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionFactorOTPSMS:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeOTPSMS, f.LastVerifiedAt)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionFactorOTPEmail:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeOTPEmail, f.LastVerifiedAt)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
|
||||
}, nil,
|
||||
)
|
||||
case *domain.SessionFactorRecoveryCode:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(domain.SessionFactorTypeRecoveryCode, f.LastVerifiedAt)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
|
||||
}, nil,
|
||||
)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ClearFactor implements [domain.sessionChanges].
|
||||
func (s session) ClearFactor(factorType domain.SessionFactorType) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("UPDATE zitadel.session_factors sf SET last_verified_at = NULL FROM existing_session es WHERE sf.instance_id = es.instance_id AND sf.session_id = es.id AND sf.type = ")
|
||||
builder.WriteArg(factorType)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// SetMetadata implements [domain.sessionChanges].
|
||||
func (s session) SetMetadata(metadata []*domain.SessionMetadata) database.Change {
|
||||
changes := make([]database.Change, len(metadata)+1)
|
||||
keys := make([]any, len(metadata))
|
||||
for i, md := range metadata {
|
||||
keys[i] = md.Key
|
||||
changes[i] = database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.session_metadata (instance_id, session_id, key, value) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(md.Key, md.Value)
|
||||
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, key) DO UPDATE SET value = EXCLUDED.value")
|
||||
}, nil,
|
||||
)
|
||||
}
|
||||
changes[len(metadata)] = database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM zitadel.session_metadata WHERE instance_id = (SELECT instance_id FROM existing_session) AND session_id = (SELECT id from existing_session) AND key NOT IN (")
|
||||
builder.WriteArgs(keys...)
|
||||
builder.WriteString(")")
|
||||
}, nil)
|
||||
return database.NewChanges(changes...)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyCondition implements [domain.sessionConditions].
|
||||
func (s session) PrimaryKeyCondition(instanceID, sessionID string) database.Condition {
|
||||
return database.And(
|
||||
s.InstanceIDCondition(instanceID),
|
||||
s.IDCondition(sessionID),
|
||||
)
|
||||
}
|
||||
|
||||
// InstanceIDCondition implements [domain.sessionConditions].
|
||||
func (s session) InstanceIDCondition(instanceID string) database.Condition {
|
||||
return database.NewTextCondition(s.InstanceIDColumn(), database.TextOperationEqual, instanceID)
|
||||
}
|
||||
|
||||
// IDCondition implements [domain.sessionConditions].
|
||||
func (s session) IDCondition(sessionID string) database.Condition {
|
||||
return database.NewTextCondition(s.IDColumn(), database.TextOperationEqual, sessionID)
|
||||
}
|
||||
|
||||
// UserAgentIDCondition implements [domain.sessionConditions].
|
||||
func (s session) UserAgentIDCondition(userAgentID string) database.Condition {
|
||||
return database.NewTextCondition(s.userAgentIDColumn(), database.TextOperationEqual, userAgentID)
|
||||
}
|
||||
|
||||
// UserIDCondition implements [domain.sessionConditions].
|
||||
func (s session) UserIDCondition(userID string) database.Condition {
|
||||
return database.NewTextCondition(s.UserIDColumn(), database.TextOperationEqual, userID)
|
||||
}
|
||||
|
||||
// CreatorIDCondition implements [domain.sessionConditions].
|
||||
func (s session) CreatorIDCondition(creatorID string) database.Condition {
|
||||
return database.NewTextCondition(s.CreatorIDColumn(), database.TextOperationEqual, creatorID)
|
||||
}
|
||||
|
||||
// ExpirationCondition implements [domain.sessionConditions].
|
||||
func (s session) ExpirationCondition(op database.NumberOperation, expiration time.Time) database.Condition {
|
||||
return database.NewNumberCondition(s.ExpirationColumn(), op, expiration)
|
||||
}
|
||||
|
||||
// CreatedAtCondition implements [domain.sessionConditions].
|
||||
func (s session) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
|
||||
return database.NewNumberCondition(s.CreatedAtColumn(), op, createdAt)
|
||||
}
|
||||
|
||||
// UpdatedAtCondition implements [domain.sessionConditions].
|
||||
func (s session) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
|
||||
return database.NewNumberCondition(s.UpdatedAtColumn(), op, updatedAt)
|
||||
}
|
||||
|
||||
// ExistsFactor implements [domain.sessionConditions].
|
||||
func (s session) ExistsFactor(cond database.Condition) database.Condition {
|
||||
return database.Exists(
|
||||
s.factorRepo.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(s.InstanceIDColumn(), s.factorRepo.instanceIDColumn()),
|
||||
database.NewColumnCondition(s.IDColumn(), s.factorRepo.sessionIDColumn()),
|
||||
cond,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// FactorConditions implements [domain.sessionConditions].
|
||||
func (s session) FactorConditions() domain.SessionFactorConditions {
|
||||
return s.factorRepo
|
||||
}
|
||||
|
||||
// ExistsMetadata implements [domain.sessionConditions].
|
||||
func (s session) ExistsMetadata(cond database.Condition) database.Condition {
|
||||
return database.Exists(
|
||||
s.metadataRepo.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(s.InstanceIDColumn(), s.metadataRepo.instanceIDColumn()),
|
||||
database.NewColumnCondition(s.IDColumn(), s.metadataRepo.sessionIDColumn()),
|
||||
cond,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// MetadataConditions implements [domain.sessionConditions].
|
||||
func (s session) MetadataConditions() domain.SessionMetadataConditions {
|
||||
return s.metadataRepo
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.Repository].
|
||||
func (s session) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
s.InstanceIDColumn(),
|
||||
s.IDColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
// InstanceIDColumn implements [domain.sessionColumns].
|
||||
func (s session) InstanceIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
// IDColumn implements [domain.sessionColumns].
|
||||
func (s session) IDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "id")
|
||||
}
|
||||
|
||||
// TokenIDColumn implements [domain.sessionColumns].
|
||||
func (s session) TokenIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "token_id")
|
||||
}
|
||||
|
||||
// LifetimeColumn implements [domain.sessionColumns].
|
||||
func (s session) LifetimeColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "lifetime")
|
||||
}
|
||||
|
||||
// ExpirationColumn implements [domain.sessionColumns].
|
||||
func (s session) ExpirationColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "expiration")
|
||||
}
|
||||
|
||||
// UserIDColumn implements [domain.sessionColumns].
|
||||
func (s session) UserIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "user_id")
|
||||
}
|
||||
|
||||
// UserAgentIDColumn implements [domain.sessionColumns].
|
||||
func (s session) userAgentIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "user_agent_id")
|
||||
}
|
||||
|
||||
// CreatorIDColumn implements [domain.sessionColumns].
|
||||
func (s session) CreatorIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "creator_id")
|
||||
}
|
||||
|
||||
// CreatedAtColumn implements [domain.sessionColumns].
|
||||
func (s session) CreatedAtColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "created_at")
|
||||
}
|
||||
|
||||
// UpdatedAtColumn implements [domain.sessionColumns].
|
||||
func (s session) UpdatedAtColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "updated_at")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// scanners
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type rawSession struct {
|
||||
*domain.Session
|
||||
TokenID *string `json:"tokenID" db:"token_id"`
|
||||
Lifetime *time.Duration `json:"lifetime" db:"lifetime"`
|
||||
Expiration *time.Time `json:"expiration" db:"expiration"`
|
||||
UserID *string `json:"userID" db:"user_id"`
|
||||
CreatorID *string `json:"creatorID" db:"creator_id"`
|
||||
Factors JSONArray[rawFactor] `json:"factors,omitempty" db:"factors"`
|
||||
Metadata JSONArray[domain.SessionMetadata] `json:"metadata,omitempty" db:"metadata"`
|
||||
}
|
||||
|
||||
func scanSession(ctx context.Context, querier database.Querier, builder *database.StatementBuilder) (*domain.Session, error) {
|
||||
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw := new(rawSession)
|
||||
if err := rows.(database.CollectableRows).CollectExactlyOneRow(raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rawSessionToDomain(raw)
|
||||
}
|
||||
|
||||
func scanSessions(ctx context.Context, querier database.Querier, builder *database.StatementBuilder) ([]*domain.Session, error) {
|
||||
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sessions []*rawSession
|
||||
if err := rows.(database.CollectableRows).Collect(&sessions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*domain.Session, len(sessions))
|
||||
for i, session := range sessions {
|
||||
result[i], err = rawSessionToDomain(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func rawSessionToDomain(raw *rawSession) (*domain.Session, error) {
|
||||
raw.Session.TokenID = gu.Value(raw.TokenID)
|
||||
raw.Session.Lifetime = gu.Value(raw.Lifetime)
|
||||
raw.Session.Expiration = gu.Value(raw.Expiration)
|
||||
raw.Session.UserID = gu.Value(raw.UserID)
|
||||
raw.Session.CreatorID = gu.Value(raw.CreatorID)
|
||||
|
||||
for _, factor := range raw.Factors {
|
||||
f, ch, err := factor.ToDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
raw.Session.Factors.AppendTo(f)
|
||||
}
|
||||
if ch != nil {
|
||||
raw.Challenges.AppendTo(ch)
|
||||
}
|
||||
}
|
||||
raw.Session.Metadata = raw.Metadata
|
||||
return raw.Session, nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// sub repositories
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (s session) joinUserAgent() database.QueryOption {
|
||||
columns := make([]database.Condition, 0, 3)
|
||||
columns = append(columns,
|
||||
database.NewColumnCondition(s.InstanceIDColumn(), s.userAgentRepo.instanceIDColumn()),
|
||||
database.NewColumnCondition(s.userAgentIDColumn(), s.userAgentRepo.fingerprintIDColumn()),
|
||||
)
|
||||
return database.WithLeftJoin(
|
||||
s.userAgentRepo.qualifiedTableName(),
|
||||
database.And(columns...),
|
||||
)
|
||||
}
|
||||
|
||||
func (s session) joinFactors() database.QueryOption {
|
||||
columns := make([]database.Condition, 0, 3)
|
||||
columns = append(columns,
|
||||
database.NewColumnCondition(s.InstanceIDColumn(), s.factorRepo.instanceIDColumn()),
|
||||
database.NewColumnCondition(s.IDColumn(), s.factorRepo.sessionIDColumn()),
|
||||
)
|
||||
return database.WithLeftJoin(
|
||||
s.factorRepo.qualifiedTableName(),
|
||||
database.And(columns...),
|
||||
)
|
||||
}
|
||||
|
||||
func (s session) joinMetadata() database.QueryOption {
|
||||
columns := make([]database.Condition, 0, 3)
|
||||
columns = append(columns,
|
||||
database.NewColumnCondition(s.InstanceIDColumn(), s.metadataRepo.instanceIDColumn()),
|
||||
database.NewColumnCondition(s.IDColumn(), s.metadataRepo.sessionIDColumn()),
|
||||
)
|
||||
return database.WithLeftJoin(
|
||||
s.metadataRepo.qualifiedTableName(),
|
||||
database.And(columns...),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type sessionFactor struct {
|
||||
}
|
||||
|
||||
func (s sessionFactor) qualifiedTableName() string {
|
||||
return "zitadel.session_factors"
|
||||
}
|
||||
|
||||
func (s sessionFactor) unqualifiedTableName() string {
|
||||
return "session_factors"
|
||||
}
|
||||
|
||||
func (s sessionFactor) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
s.instanceIDColumn(),
|
||||
s.sessionIDColumn(),
|
||||
s.typeColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s sessionFactor) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (s sessionFactor) sessionIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "session_id")
|
||||
}
|
||||
|
||||
func (s sessionFactor) typeColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "type")
|
||||
}
|
||||
|
||||
func (s sessionFactor) lastVerifiedAtColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "last_verified_at")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (s sessionFactor) FactorTypeCondition(factorType domain.SessionFactorType) database.Condition {
|
||||
return database.NewNumberCondition(s.typeColumn(), database.NumberOperationEqual, factorType)
|
||||
}
|
||||
|
||||
func (s sessionFactor) LastVerifiedBeforeCondition(lastVerifiedAt time.Time) database.Condition {
|
||||
return database.NewNumberCondition(s.lastVerifiedAtColumn(), database.NumberOperationLessThan, lastVerifiedAt)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// scanners
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type rawFactor struct {
|
||||
Type string `json:"type" db:"type"`
|
||||
LastChallengedAt time.Time `json:"lastChallengedAt" db:"last_challenged_at"`
|
||||
ChallengedPayload JSON[json.RawMessage] `json:"challengedPayload" db:"challenged_payload"`
|
||||
LastVerifiedAt time.Time `json:"lastVerifiedAt" db:"last_verified_at"`
|
||||
VerifiedPayload JSON[json.RawMessage] `json:"verifiedPayload" db:"verified_payload"`
|
||||
}
|
||||
|
||||
func (f *rawFactor) ToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
|
||||
factorType, err := domain.SessionFactorTypeString(f.Type)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
switch factorType {
|
||||
case domain.SessionFactorTypeUser:
|
||||
return f.userFactorToDomain()
|
||||
case domain.SessionFactorTypePassword:
|
||||
return f.passwordFactorToDomain()
|
||||
case domain.SessionFactorTypePasskey:
|
||||
return f.passkeyFactorToDomain()
|
||||
case domain.SessionFactorTypeIdentityProviderIntent:
|
||||
return f.identityProviderIntentFactorToDomain()
|
||||
case domain.SessionFactorTypeTOTP:
|
||||
return f.totpFactorToDomain()
|
||||
case domain.SessionFactorTypeOTPSMS:
|
||||
return f.otpSMSFactorToDomain()
|
||||
case domain.SessionFactorTypeOTPEmail:
|
||||
return f.otpEmailFactorToDomain()
|
||||
case domain.SessionFactorTypeRecoveryCode:
|
||||
return f.recoveryCodeFactorToDomain()
|
||||
case domain.SessionFactorTypeUnknown:
|
||||
return nil, nil, nil
|
||||
default:
|
||||
return nil, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *rawFactor) userFactorToDomain() (factor domain.SessionFactor, _ domain.SessionChallenge, err error) {
|
||||
if f.LastVerifiedAt.IsZero() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
factor = new(domain.SessionFactorUser)
|
||||
if err := json.Unmarshal(f.VerifiedPayload.Value, factor); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return factor, nil, nil
|
||||
}
|
||||
|
||||
func (f *rawFactor) passwordFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
|
||||
if f.LastVerifiedAt.IsZero() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return &domain.SessionFactorPassword{
|
||||
LastVerifiedAt: f.LastVerifiedAt,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (f *rawFactor) passkeyFactorToDomain() (factor domain.SessionFactor, challenge domain.SessionChallenge, err error) {
|
||||
if !f.LastChallengedAt.IsZero() && f.LastChallengedAt.After(f.LastVerifiedAt) {
|
||||
challenge = new(domain.SessionChallengePasskey)
|
||||
if err := json.Unmarshal(f.ChallengedPayload.Value, &challenge); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if !f.LastVerifiedAt.IsZero() {
|
||||
factor = new(domain.SessionFactorPasskey)
|
||||
if err := json.Unmarshal(f.VerifiedPayload.Value, factor); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return factor, challenge, nil
|
||||
}
|
||||
|
||||
func (f *rawFactor) identityProviderIntentFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
|
||||
if f.LastVerifiedAt.IsZero() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return &domain.SessionFactorIdentityProviderIntent{
|
||||
LastVerifiedAt: f.LastVerifiedAt,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (f *rawFactor) totpFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
|
||||
if f.LastVerifiedAt.IsZero() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return &domain.SessionFactorTOTP{
|
||||
LastVerifiedAt: f.LastVerifiedAt,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (f *rawFactor) otpSMSFactorToDomain() (factor domain.SessionFactor, challenge domain.SessionChallenge, err error) {
|
||||
if !f.LastChallengedAt.IsZero() && f.LastChallengedAt.After(f.LastVerifiedAt) {
|
||||
challenge = new(domain.SessionChallengeOTPSMS)
|
||||
if err := json.Unmarshal(f.ChallengedPayload.Value, &challenge); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if !f.LastVerifiedAt.IsZero() {
|
||||
factor = &domain.SessionFactorOTPSMS{
|
||||
LastVerifiedAt: f.LastVerifiedAt,
|
||||
}
|
||||
}
|
||||
return factor, challenge, nil
|
||||
}
|
||||
|
||||
func (f *rawFactor) otpEmailFactorToDomain() (factor domain.SessionFactor, challenge domain.SessionChallenge, err error) {
|
||||
if !f.LastChallengedAt.IsZero() && f.LastChallengedAt.After(f.LastVerifiedAt) {
|
||||
challenge = new(domain.SessionChallengeOTPEmail)
|
||||
if err := json.Unmarshal(f.ChallengedPayload.Value, &challenge); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if !f.LastVerifiedAt.IsZero() {
|
||||
factor = &domain.SessionFactorOTPEmail{
|
||||
LastVerifiedAt: f.LastVerifiedAt,
|
||||
}
|
||||
}
|
||||
return factor, challenge, nil
|
||||
}
|
||||
|
||||
func (f *rawFactor) recoveryCodeFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
|
||||
if f.LastVerifiedAt.IsZero() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return &domain.SessionFactorRecoveryCode{
|
||||
LastVerifiedAt: f.LastVerifiedAt,
|
||||
}, nil, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package repository
|
||||
|
||||
import "github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
|
||||
type sessionMetadata struct {
|
||||
}
|
||||
|
||||
func (s sessionMetadata) qualifiedTableName() string {
|
||||
return "zitadel.session_metadata"
|
||||
}
|
||||
|
||||
func (s sessionMetadata) unqualifiedTableName() string {
|
||||
return "session_metadata"
|
||||
}
|
||||
|
||||
func (s sessionMetadata) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
s.instanceIDColumn(),
|
||||
s.sessionIDColumn(),
|
||||
s.keyColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s sessionMetadata) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (s sessionMetadata) sessionIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "session_id")
|
||||
}
|
||||
|
||||
func (s sessionMetadata) keyColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "key")
|
||||
}
|
||||
|
||||
func (s sessionMetadata) valueColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "value")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyCondition implements [domain.SessionMetadataRepository].
|
||||
func (s sessionMetadata) PrimaryKeyCondition(instanceID string, sessionID string, key string) database.Condition {
|
||||
return database.And(
|
||||
s.InstanceIDCondition(instanceID),
|
||||
s.SessionIDCondition(sessionID),
|
||||
s.KeyCondition(database.TextOperationEqual, key),
|
||||
)
|
||||
}
|
||||
|
||||
// InstanceIDCondition implements [domain.SessionMetadataRepository].
|
||||
func (s sessionMetadata) InstanceIDCondition(instanceID string) database.Condition {
|
||||
return database.NewTextCondition(s.instanceIDColumn(), database.TextOperationEqual, instanceID)
|
||||
}
|
||||
|
||||
// SessionIDCondition implements [domain.SessionMetadataRepository].
|
||||
func (s sessionMetadata) SessionIDCondition(sessionID string) database.Condition {
|
||||
return database.NewTextCondition(s.sessionIDColumn(), database.TextOperationEqual, sessionID)
|
||||
}
|
||||
|
||||
// KeyCondition implements [domain.SessionMetadataRepository].
|
||||
func (s sessionMetadata) KeyCondition(op database.TextOperation, key string) database.Condition {
|
||||
return database.NewTextCondition(s.keyColumn(), op, key)
|
||||
}
|
||||
|
||||
// ValueCondition implements [domain.SessionMetadataRepository].
|
||||
func (s sessionMetadata) ValueCondition(op database.BytesOperation, value []byte) database.Condition {
|
||||
return database.NewBytesCondition[[]byte](database.SHA256Column(s.valueColumn()), op, database.SHA256Value(value))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type sessionUserAgent struct {
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) qualifiedTableName() string {
|
||||
return "zitadel.session_user_agents"
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) unqualifiedTableName() string {
|
||||
return "session_user_agents"
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
s.instanceIDColumn(),
|
||||
s.fingerprintIDColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) fingerprintIDColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "fingerprint_id")
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) descriptionColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "description")
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) ipColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "ip")
|
||||
}
|
||||
|
||||
func (s sessionUserAgent) headersColumn() database.Column {
|
||||
return database.NewColumn(s.unqualifiedTableName(), "headers")
|
||||
}
|
||||
Reference in New Issue
Block a user