Files
upstream-ory-kratos/persistence/sql/persister_session.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

725 lines
25 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package sql
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"github.com/ory/herodot"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/events"
"github.com/ory/pop/v6"
"github.com/ory/x/dbal"
"github.com/ory/x/otelx"
"github.com/ory/x/pagination/keysetpagination"
"github.com/ory/x/popx"
"github.com/ory/x/sqlcon"
"github.com/ory/x/stringsx"
)
var _ session.Persister = new(Persister)
const (
SessionDeviceUserAgentMaxLength = 512
SessionDeviceLocationMaxLength = 512
paginationMaxItemsSize = 1000
paginationDefaultItemsSize = 250
)
func (p *Persister) GetSession(ctx context.Context, sid uuid.UUID, expandables session.Expandables) (_ *session.Session, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetSession")
defer otelx.End(span, &err)
var s session.Session
s.Devices = make([]session.Device, 0)
nid := p.NetworkID(ctx)
q := p.GetConnection(ctx).Q()
// if len(expandables) > 0 {
if expandables.Has(session.ExpandSessionDevices) {
q = q.Eager(expandables.ToEager()...)
}
if err := q.Where("id = ? AND nid = ?", sid, nid).First(&s); err != nil {
return nil, sqlcon.HandleError(err)
}
if expandables.Has(session.ExpandSessionIdentity) {
// This is needed because of how identities are fetched from the store (if we use eager not all fields are
// available!).
i, err := p.GetIdentity(ctx, s.IdentityID, identity.ExpandDefault)
if err != nil {
return nil, err
}
s.Identity = i
}
s.Active = s.IsActive()
return &s, nil
}
func (p *Persister) ListSessions(ctx context.Context, active *bool, paginatorOpts []keysetpagination.Option, expandables session.Expandables) (_ []session.Session, _ *keysetpagination.Paginator, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListSessions")
defer otelx.End(span, &err)
var s []session.Session
nid := p.NetworkID(ctx)
paginatorOpts = append(paginatorOpts, keysetpagination.WithDefaultSize(paginationDefaultItemsSize))
paginatorOpts = append(paginatorOpts, keysetpagination.WithMaxSize(paginationMaxItemsSize))
paginatorOpts = append(paginatorOpts, keysetpagination.WithDefaultToken(new(session.Session).DefaultPageToken()))
paginatorOpts = append(paginatorOpts, keysetpagination.WithColumn("created_at", "DESC"))
paginator := keysetpagination.GetPaginator(paginatorOpts...)
if _, err := uuid.FromString(paginator.Token().Parse("id")["id"]); err != nil {
return nil, nil, errors.WithStack(x.PageTokenInvalid)
}
if err := p.listWithinReadCommittedReadOnlyTx(ctx, func(ctx context.Context, c *pop.Connection) error {
s = make([]session.Session, 0)
q := c.Where("nid = ?", nid)
if active != nil {
if *active {
q.Where("active = ? AND expires_at >= ?", *active, time.Now().UTC())
} else {
q.Where("(active = ? OR expires_at < ?)", *active, time.Now().UTC())
}
}
if len(expandables) > 0 {
q = q.EagerPreload(expandables.ToEager()...)
}
if err := q.Scope(keysetpagination.Paginate[session.Session](paginator)).All(&s); err != nil {
return sqlcon.HandleError(err)
}
return nil
}); err != nil {
return nil, nil, err
}
for k := range s {
if s[k].Identity == nil {
continue
}
if err := p.InjectTraitsSchemaURL(ctx, s[k].Identity); err != nil {
return nil, nil, err
}
}
s, nextPage := keysetpagination.Result(s, paginator)
return s, nextPage, nil
}
// ListSessionsByIdentity retrieves sessions for an identity from the store.
func (p *Persister) ListSessionsByIdentity(
ctx context.Context,
iID uuid.UUID,
active *bool,
page, perPage int,
except uuid.UUID,
expandables session.Expandables,
) (_ []session.Session, _ int64, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListSessionsByIdentity")
defer otelx.End(span, &err)
var s []session.Session
t := int64(0)
nid := p.NetworkID(ctx)
if err := p.listWithinReadCommittedReadOnlyTx(ctx, func(ctx context.Context, c *pop.Connection) error {
s = make([]session.Session, 0)
q := c.Where("identity_id = ? AND nid = ?", iID, nid)
if except != uuid.Nil {
q = q.Where("id != ?", except)
}
if active != nil {
if *active {
q.Where("active = ? AND expires_at >= ?", *active, time.Now().UTC())
} else {
q.Where("(active = ? OR expires_at < ?)", *active, time.Now().UTC())
}
}
if len(expandables) > 0 {
q = q.EagerPreload(expandables.ToEager()...)
}
total, err := q.Count(new(session.Session))
if err != nil {
return sqlcon.HandleError(err)
}
t = int64(total)
q.Order("created_at DESC")
if err := q.Paginate(page, perPage).All(&s); err != nil {
return sqlcon.HandleError(err)
}
return nil
}); err != nil {
return nil, 0, err
}
return s, t, nil
}
// ExtendSession updates the expiry of a session.
func (p *Persister) ExtendSession(ctx context.Context, sessionID uuid.UUID) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ExtendSession")
defer otelx.End(span, &err)
nid := p.NetworkID(ctx)
s := new(session.Session)
var didRefresh bool
if err := errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) {
lockBehavior := ""
if tx.Dialect.Name() == dbal.DriverCockroachDB {
// SKIP LOCKED returns no rows if the row is locked by another transaction.
lockBehavior = "FOR UPDATE SKIP LOCKED"
}
if err := tx.
Where(
// We make use of the fact that CRDB supports FOR UPDATE as part of the WHERE clause.
fmt.Sprintf("id = ? AND nid = ? %s", lockBehavior),
sessionID, nid,
).First(s); err != nil {
// This is a special case for CockroachDB. If the row is locked, we do not see the session. Therefor we return
// a 404 not found error indicating to the user that the session might already be updated by someone else.
if errors.Is(err, sqlcon.ErrNoRows()) && tx.Dialect.Name() == dbal.DriverCockroachDB {
return errors.WithStack(herodot.ErrNotFound().WithReason("The session you are trying to extend is already being extended by another request or does not exist."))
}
return sqlcon.HandleError(err)
}
if !s.CanBeRefreshed(ctx, p.r.Config()) {
// This prevents excessive writes to the database.
return nil
}
didRefresh = true
s = s.Refresh(ctx, p.r.Config())
if _, err := tx.Where("id = ? AND nid = ?", sessionID, nid).UpdateQuery(s, "expires_at"); err != nil {
return sqlcon.HandleError(err)
}
return nil
})); err != nil {
return err
}
if didRefresh {
trace.SpanFromContext(ctx).AddEvent(events.NewSessionLifespanExtended(ctx, s.ID, s.IdentityID, s.ExpiresAt))
}
return nil
}
// UpsertSession creates a session if not found else updates.
// This operation also inserts Session device records when a session is being created.
// The update operation skips updating Session device records since only one record would need to be updated in this case.
func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpsertSession")
defer otelx.End(span, &err)
s.NID = p.NetworkID(ctx)
if s.Identity != nil {
s.IdentityID = s.Identity.ID
} else if s.IdentityID.IsNil() {
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("cannot upsert session without an identity or identity ID set"))
}
var updated bool
defer func() {
if err != nil {
return
}
if updated {
trace.SpanFromContext(ctx).AddEvent(events.NewSessionChanged(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID))
} else {
trace.SpanFromContext(ctx).AddEvent(events.NewSessionIssued(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID))
}
}()
return errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) {
updated = false
exists := false
if !s.ID.IsNil() {
exists, err = tx.Where("id = ? AND nid = ?", s.ID, s.NID).Exists(new(session.Session))
if err != nil {
return sqlcon.HandleError(err)
}
}
if exists {
// This must not be eager or identities will be created / updated
// Only update session and not corresponding session device records
if err := tx.Update(s, "issued_at", "identity_id", "nid"); err != nil {
return sqlcon.HandleError(err)
}
updated = true
return nil
}
// This must not be eager or identities will be created / updated
if err := sqlcon.HandleError(tx.Create(s)); err != nil {
return err
}
for i := range s.Devices {
device := &(s.Devices[i])
device.SessionID = s.ID
device.NID = s.NID
device.IdentityID = new(s.IdentityID)
if device.Location != nil {
device.Location = new(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength))
}
if device.UserAgent != nil {
device.UserAgent = new(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength))
}
if err := p.CreateDevice(ctx, device); err != nil {
return err
}
}
return nil
}))
}
// DeleteSession permanently deletes a single session. Returns
// sqlcon.ErrNoRows() when no matching session is found in the caller's
// network.
func (p *Persister) DeleteSession(ctx context.Context, sid uuid.UUID) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSession")
defer otelx.End(span, &err)
n, err := p.DeleteSessionsByIDs(ctx, []uuid.UUID{sid})
if err != nil {
return err
}
if n == 0 {
return errors.WithStack(sqlcon.ErrNoRows())
}
return nil
}
func (p *Persister) DeleteSessionsByIdentity(ctx context.Context, identityID uuid.UUID) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionsByIdentity")
defer otelx.End(span, &err)
//#nosec G201 -- TableName is static
count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf(
"DELETE FROM %s WHERE identity_id = ? AND nid = ?",
session.Session{}.TableName(),
),
identityID,
p.NetworkID(ctx),
).ExecWithCount()
if err != nil {
return sqlcon.HandleError(err)
}
if count == 0 {
return errors.WithStack(sqlcon.ErrNoRows())
}
return nil
}
func (p *Persister) GetSessionByToken(ctx context.Context, token string, expand session.Expandables, identityExpand identity.Expandables) (res *session.Session, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetSessionByToken")
defer otelx.End(span, &err)
var s session.Session
s.Devices = make([]session.Device, 0)
nid := p.NetworkID(ctx)
con := p.GetConnection(ctx)
if err := con.Where("token = ? AND nid = ?", token, nid).First(&s); err != nil {
return nil, sqlcon.HandleError(err)
}
var (
i *identity.Identity
sd []session.Device
)
eg, ctx := errgroup.WithContext(ctx)
if expand.Has(session.ExpandSessionDevices) {
eg.Go(func() error {
return sqlcon.HandleError(con.WithContext(ctx).
Where("session_id = ? AND nid = ?", s.ID, nid).All(&sd))
})
}
// This is needed because of how identities are fetched from the store (if we use eager not all fields are
// available!).
if expand.Has(session.ExpandSessionIdentity) {
eg.Go(func() (err error) {
i, err = p.GetIdentity(ctx, s.IdentityID, identityExpand)
return err
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
s.Identity = i
s.Devices = sd
return &s, nil
}
func (p *Persister) DeleteSessionByToken(ctx context.Context, token string) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionByToken")
defer otelx.End(span, &err)
//#nosec G201 -- TableName is static
count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf(
"DELETE FROM %s WHERE token = ? AND nid = ?",
session.Session{}.TableName(),
),
token,
p.NetworkID(ctx),
).ExecWithCount()
if err != nil {
return sqlcon.HandleError(err)
}
if count == 0 {
return errors.WithStack(sqlcon.ErrNoRows())
}
return nil
}
// RevokeSessionByToken marks the session with the given token inactive and
// returns its IDs so the caller can attach them to observability events without
// a separate GetSessionByToken round trip.
// Returns sqlcon.ErrNoRows() when no matching session exists in the caller's
// network; the returned RevokedSession is the zero value in that case.
func (p *Persister) RevokeSessionByToken(ctx context.Context, token string) (revoked session.RevokedSession, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionByToken")
defer otelx.End(span, &err)
con := p.GetConnection(ctx)
nid := p.NetworkID(ctx)
var dst struct {
ID uuid.UUID `db:"id"`
IdentityID uuid.UUID `db:"identity_id"`
}
if dbal.IsPostgresCompatible(con.Dialect.Name()) {
// CTE: identify the row by (token, nid), conditionally flip active=false
// only when currently true, and return the matched row's identifiers in
// one round trip. See revokeMatchingSessions for the contention rationale.
const query = `WITH found AS (SELECT id, identity_id FROM sessions WHERE token = ? AND nid = ?),
upd AS (UPDATE sessions SET active = false FROM found WHERE sessions.id = found.id AND sessions.active = true RETURNING 1)
SELECT id, identity_id FROM found`
err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error {
return c.RawQuery(query, token, nid).First(&dst)
})
} else {
// SQLite and MySQL: data-modifying CTEs are not portable here, so issue
// a separate SELECT followed by the legacy UPDATE. Same two-statement
// shape as today's caller (GetSessionByToken + RevokeSessionByToken),
// so no regression on these dialects.
err = con.RawQuery("SELECT id, identity_id FROM sessions WHERE token = ? AND nid = ?", token, nid).First(&dst)
if err == nil {
err = con.RawQuery("UPDATE sessions SET active = false WHERE token = ? AND nid = ?", token, nid).Exec()
}
}
if err != nil {
return session.RevokedSession{}, sqlcon.HandleError(err)
}
return session.RevokedSession{ID: dst.ID, IdentityID: dst.IdentityID}, nil
}
// RevokeSessionById revokes a given session. Returns sqlcon.ErrNoRows() when
// no matching session is found in the caller's network.
func (p *Persister) RevokeSessionById(ctx context.Context, sID uuid.UUID) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionById")
defer otelx.End(span, &err)
count, err := p.revokeMatchingSessions(ctx, "id = ? AND nid = ?", sID, p.NetworkID(ctx))
if err != nil {
return err
}
if count == 0 {
return errors.WithStack(sqlcon.ErrNoRows())
}
return nil
}
// RevokeSession revokes a given session. If the session does not exist or was not modified,
// it effectively has been revoked already, and therefore that case does not return an error.
func (p *Persister) RevokeSession(ctx context.Context, iID, sID uuid.UUID) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSession")
defer otelx.End(span, &err)
return p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error {
return c.RawQuery(
"UPDATE sessions SET active = false WHERE id = ? AND identity_id = ? AND nid = ? AND active = true",
sID, iID, p.NetworkID(ctx),
).Exec()
})
}
// RevokeSessionsIdentityExcept marks all except the given session of an identity inactive.
func (p *Persister) RevokeSessionsIdentityExcept(ctx context.Context, iID, sID uuid.UUID) (res int, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionsIdentityExcept")
defer otelx.End(span, &err)
return p.revokeMatchingSessions(ctx, "identity_id = ? AND id != ? AND nid = ?", iID, sID, p.NetworkID(ctx))
}
// RevokeSessionsByIdentities marks all currently active sessions inactive for the given identity IDs.
func (p *Persister) RevokeSessionsByIdentities(ctx context.Context, identityIDs []uuid.UUID) (count int, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionsByIdentities")
defer otelx.End(span, &err)
if len(identityIDs) == 0 {
return 0, nil
}
err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error {
var inner error
count, inner = c.RawQuery(
"UPDATE sessions SET active = false WHERE identity_id IN (?) AND active = true AND nid = ?",
identityIDs, p.NetworkID(ctx),
).ExecWithCount()
return inner
})
if err != nil {
return 0, sqlcon.HandleError(err)
}
return count, nil
}
// RevokeSessionsByIDs marks the listed sessions inactive (only ones currently active).
func (p *Persister) RevokeSessionsByIDs(ctx context.Context, sessionIDs []uuid.UUID) (count int, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeSessionsByIDs")
defer otelx.End(span, &err)
if len(sessionIDs) == 0 {
return 0, nil
}
err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error {
var inner error
count, inner = c.RawQuery(
"UPDATE sessions SET active = false WHERE id IN (?) AND active = true AND nid = ?",
sessionIDs, p.NetworkID(ctx),
).ExecWithCount()
return inner
})
if err != nil {
return 0, sqlcon.HandleError(err)
}
return count, nil
}
// RevokeAllSessions deactivates up to `limit` currently-active sessions in
// the caller's network in a single SQL statement and returns the number of
// rows actually updated (in the range [0, limit]).
func (p *Persister) RevokeAllSessions(ctx context.Context, limit int) (count int, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeAllSessions")
defer otelx.End(span, &err)
err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error {
var inner error
count, inner = c.RawQuery(
"UPDATE sessions SET active = false WHERE id IN (SELECT id FROM (SELECT id FROM sessions c WHERE active = true AND nid = ? LIMIT ?) AS s)",
p.NetworkID(ctx), limit,
).ExecWithCount()
return inner
})
if err != nil {
return 0, sqlcon.HandleError(err)
}
return count, nil
}
// revokeMatchingSessions flips active=false on every sessions row matching the
// given WHERE clause and returns the number of rows that matched the clause
// before the update (irrespective of whether they were already inactive). Used
// by the revoke entry points whose caller relies on the matched-count
// distinction — either to map count==0 to sqlcon.ErrNoRows() or to surface
// the count as an API response field.
//
// On CockroachDB and PostgreSQL, the update is expressed as a CTE so that
// only rows whose current active=true get rewritten. CockroachDB's sessions
// table is LOCALITY GLOBAL: every write commits at a future timestamp and
// any concurrent write to the same key inside that closed-timestamp window
// raises WriteTooOldError + a serializable refresh. Suppressing the
// redundant rewrite of already-inactive rows eliminates the dominant
// same-fingerprint contention pattern observed in production (concurrent
// revokes for the same token from logout retries / multiple clients).
// SQLite and MySQL retain the single-statement UPDATE; neither is subject
// to the GLOBAL-table closed-timestamp contention.
func (p *Persister) revokeMatchingSessions(ctx context.Context, predicate string, args ...any) (int, error) {
con := p.GetConnection(ctx)
var (
count int
err error
)
if dbal.IsPostgresCompatible(con.Dialect.Name()) {
//#nosec G201 -- predicate is a static persister-internal constant, not user input
query := fmt.Sprintf(`WITH found AS (SELECT id FROM sessions WHERE %s),
upd AS (UPDATE sessions SET active = false FROM found WHERE sessions.id = found.id AND sessions.active = true RETURNING 1)
SELECT count(*) FROM found`, predicate)
err = p.runInReadCommittedOnCRDB(ctx, func(c *pop.Connection) error {
return c.RawQuery(query, args...).First(&count)
})
} else {
//#nosec G201 -- predicate is a static persister-internal constant, not user input
count, err = con.RawQuery(
fmt.Sprintf("UPDATE sessions SET active = false WHERE %s", predicate),
args...,
).ExecWithCount()
}
if err != nil {
return 0, sqlcon.HandleError(err)
}
return count, nil
}
// runInReadCommittedOnCRDB invokes fn against a connection bound to a READ
// COMMITTED transaction on CockroachDB, and against the bare persister
// connection on every other dialect. CRDB needs the explicit isolation
// downgrade so the cluster transparently advances the statement read
// timestamp on WriteTooOldError instead of surfacing the serializable
// retry. Postgres already runs single statements at READ COMMITTED by
// default; SQLite and MySQL are not subject to the GLOBAL-table
// closed-timestamp contention. YugabyteDB does not get CockroachDB's isolation
// override and relies on TransactionWithOptions retrying serialization
// failures instead.
func (p *Persister) runInReadCommittedOnCRDB(ctx context.Context, fn func(*pop.Connection) error) error {
con := p.GetConnection(ctx)
if con.Dialect.Name() != dbal.DriverCockroachDB {
return fn(con)
}
return popx.TransactionWithOptions(ctx, con,
&sql.TxOptions{Isolation: sql.LevelReadCommitted},
func(ctx context.Context, tx *pop.Connection) error { return fn(tx) },
)
}
// listWithinReadCommittedReadOnlyTx invokes fn, which must only read, inside a READ
// COMMITTED read-only transaction on CockroachDB and PostgreSQL, and inside a
// regular transaction on every other dialect. The paginated session list
// queries run more than one statement per transaction (count + page, or page
// + eager preloads); under SERIALIZABLE a concurrent write to the scanned
// rows — a login upsert, extension, or revocation — between those statements
// invalidates the transaction's read timestamp and surfaces as a client-side
// RETRY_SERIALIZABLE retry. READ COMMITTED gives each statement its own read
// timestamp so CockroachDB absorbs those conflicts server-side. READ
// COMMITTED is already the PostgreSQL default; there the options make the
// read-only intent explicit and let the server skip write-path bookkeeping.
// SQLite and MySQL keep the plain transaction.
func (p *Persister) listWithinReadCommittedReadOnlyTx(ctx context.Context, fn func(ctx context.Context, c *pop.Connection) error) error {
con := p.GetConnection(ctx)
if dbal.IsPostgresCompatible(con.Dialect.Name()) {
return popx.TransactionWithOptions(ctx, con,
&sql.TxOptions{Isolation: sql.LevelReadCommitted, ReadOnly: true},
fn,
)
}
return p.Transaction(ctx, fn)
}
// DeleteSessionsByIdentities permanently deletes all sessions belonging to the given identity IDs.
func (p *Persister) DeleteSessionsByIdentities(ctx context.Context, identityIDs []uuid.UUID) (count int, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionsByIdentities")
defer otelx.End(span, &err)
if len(identityIDs) == 0 {
return 0, nil
}
//#nosec G201 -- TableName is static.
count, err = p.GetConnection(ctx).RawQuery(fmt.Sprintf(
"DELETE FROM %s WHERE identity_id IN (?) AND nid = ?",
session.Session{}.TableName(),
), identityIDs, p.NetworkID(ctx)).ExecWithCount()
if err != nil {
return 0, sqlcon.HandleError(err)
}
return count, nil
}
// DeleteSessionsByIDs permanently deletes the listed sessions.
func (p *Persister) DeleteSessionsByIDs(ctx context.Context, sessionIDs []uuid.UUID) (count int, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteSessionsByIDs")
defer otelx.End(span, &err)
if len(sessionIDs) == 0 {
return 0, nil
}
//#nosec G201 -- TableName is static.
count, err = p.GetConnection(ctx).RawQuery(fmt.Sprintf(
"DELETE FROM %s WHERE id IN (?) AND nid = ?",
session.Session{}.TableName(),
), sessionIDs, p.NetworkID(ctx)).ExecWithCount()
if err != nil {
return 0, sqlcon.HandleError(err)
}
return count, nil
}
// DeleteAllSessions permanently deletes up to `limit` sessions in the
// caller's network in a single SQL statement and returns the number of rows
// actually deleted (in the range [0, limit]).
func (p *Persister) DeleteAllSessions(ctx context.Context, limit int) (count int, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteAllSessions")
defer otelx.End(span, &err)
//#nosec G201 -- TableName is static
count, err = p.GetConnection(ctx).RawQuery(fmt.Sprintf(
"DELETE FROM %[1]s WHERE id IN (SELECT id FROM (SELECT id FROM %[1]s c WHERE nid = ? LIMIT ?) AS s)",
session.Session{}.TableName(),
), p.NetworkID(ctx), limit).ExecWithCount()
if err != nil {
return 0, sqlcon.HandleError(err)
}
return count, nil
}
func (p *Persister) DeleteExpiredSessions(ctx context.Context, expiresAt time.Time, limit int) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteExpiredSessions")
defer otelx.End(span, &err)
//#nosec G201 -- TableName is static
err = p.GetConnection(ctx).RawQuery(fmt.Sprintf(
"DELETE FROM %[1]s WHERE id in (SELECT id FROM (SELECT id FROM %[1]s c WHERE expires_at <= ? and nid = ? ORDER BY expires_at ASC LIMIT ?) AS s)",
session.Session{}.TableName(),
),
expiresAt,
p.NetworkID(ctx),
limit,
).Exec()
return sqlcon.HandleError(err)
}