Skip to content

Commit

Permalink
satellite/console/dbcleanup: make chore clean up webapp sessions
Browse files Browse the repository at this point in the history
The console DB cleanup chore has been extended to remove expired webapp
session records.

Resolves #5893

Change-Id: I455b4933552cfde86817a2ef8f9879dd7b0a121d
  • Loading branch information
jewharton authored and Storj Robot committed Jun 29, 2023
1 parent 0d0e8cc commit faf5b96
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 3 deletions.
4 changes: 3 additions & 1 deletion satellite/console/consoleauth/sessions.go
Expand Up @@ -23,7 +23,9 @@ type WebappSessions interface {
// DeleteAllByUserID deletes all webapp sessions by user ID.
DeleteAllByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
// UpdateExpiration updates the expiration time of the session.
UpdateExpiration(ctx context.Context, sessionID uuid.UUID, expiresAt time.Time) (err error)
UpdateExpiration(ctx context.Context, sessionID uuid.UUID, expiresAt time.Time) error
// DeleteExpired deletes all sessions that have expired before the provided timestamp.
DeleteExpired(ctx context.Context, now time.Time, asOfSystemTimeInterval time.Duration, pageSize int) error
}

// WebappSession represents a session on the satellite web app.
Expand Down
6 changes: 6 additions & 0 deletions satellite/console/dbcleanup/chore.go
Expand Up @@ -54,6 +54,12 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
if err != nil {
chore.log.Error("Error deleting unverified users", zap.Error(err))
}

err = chore.db.WebappSessions().DeleteExpired(ctx, time.Now(), chore.config.AsOfSystemTimeInterval, chore.config.PageSize)
if err != nil {
chore.log.Error("Error deleting expired webapp sessions", zap.Error(err))
}

return nil
})
}
Expand Down
2 changes: 1 addition & 1 deletion satellite/satellitedb/consoledb.go
Expand Up @@ -78,7 +78,7 @@ func (db *ConsoleDB) ResetPasswordTokens() console.ResetPasswordTokens {

// WebappSessions is a getter for WebappSessions repository.
func (db *ConsoleDB) WebappSessions() consoleauth.WebappSessions {
return &webappSessions{db.methods}
return &webappSessions{db.db}
}

// AccountFreezeEvents is a getter for AccountFreezeEvents repository.
Expand Down
73 changes: 72 additions & 1 deletion satellite/satellitedb/webappsessions.go
Expand Up @@ -5,6 +5,8 @@ package satellitedb

import (
"context"
"database/sql"
"errors"
"time"

"storj.io/common/uuid"
Expand All @@ -16,7 +18,7 @@ import (
var _ consoleauth.WebappSessions = (*webappSessions)(nil)

type webappSessions struct {
db dbx.Methods
db *satelliteDB
}

// Create creates a webapp session and returns the session info.
Expand Down Expand Up @@ -91,6 +93,75 @@ func (db *webappSessions) DeleteAllByUserID(ctx context.Context, userID uuid.UUI
return db.db.Delete_WebappSession_By_UserId(ctx, dbx.WebappSession_UserId(userID.Bytes()))
}

// DeleteExpired deletes all sessions that have expired before the provided timestamp.
func (db *webappSessions) DeleteExpired(ctx context.Context, now time.Time, asOfSystemTimeInterval time.Duration, pageSize int) (err error) {
defer mon.Task()(&ctx)(&err)

if pageSize <= 0 {
return Error.New("expected page size to be positive; got %d", pageSize)
}

var pageCursor, pageEnd uuid.UUID
aost := db.db.impl.AsOfSystemInterval(asOfSystemTimeInterval)
for {
// Select the ID beginning this page of records
err := db.db.QueryRowContext(ctx, `
SELECT id FROM webapp_sessions
`+aost+`
WHERE id > $1 AND expires_at < $2
ORDER BY id LIMIT 1
`, pageCursor, now).Scan(&pageCursor)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return Error.Wrap(err)
}

// Select the ID ending this page of records
err = db.db.QueryRowContext(ctx, `
SELECT id FROM webapp_sessions
`+aost+`
WHERE id > $1
ORDER BY id LIMIT 1 OFFSET $2
`, pageCursor, pageSize).Scan(&pageEnd)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return Error.Wrap(err)
}
// Since this is the last page, we want to return all remaining records
_, err = db.db.ExecContext(ctx, `
DELETE FROM webapp_sessions
WHERE id IN (
SELECT id FROM webapp_sessions
`+aost+`
WHERE id >= $1 AND expires_at < $2
ORDER BY id
)
`, pageCursor, now)
return Error.Wrap(err)
}

// Delete all expired records in the range between the beginning and ending IDs
_, err = db.db.ExecContext(ctx, `
DELETE FROM webapp_sessions
WHERE id IN (
SELECT id FROM webapp_sessions
`+aost+`
WHERE id BETWEEN $1 AND $2
AND expires_at < $3
ORDER BY id
)
`, pageCursor, pageEnd, now)
if err != nil {
return Error.Wrap(err)
}

// Advance the cursor to the next page
pageCursor = pageEnd
}
}

func getSessionFromDBX(dbxSession *dbx.WebappSession) (consoleauth.WebappSession, error) {
id, err := uuid.FromBytes(dbxSession.Id)
if err != nil {
Expand Down
24 changes: 24 additions & 0 deletions satellite/satellitedb/webappsessions_test.go
Expand Up @@ -4,6 +4,7 @@
package satellitedb_test

import (
"database/sql"
"testing"
"time"

Expand Down Expand Up @@ -186,3 +187,26 @@ func TestWebappSessionsDeleteAllByUserID(t *testing.T) {
require.Len(t, allSessions, 0)
})
}

func TestDeleteExpired(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
sessionsDB := db.Console().WebappSessions()
now := time.Now()

// Only positive page sizes should be allowed.
require.Error(t, sessionsDB.DeleteExpired(ctx, time.Time{}, 0, 0))
require.Error(t, sessionsDB.DeleteExpired(ctx, time.Time{}, 0, -1))

newSession, err := sessionsDB.Create(ctx, testrand.UUID(), testrand.UUID(), "", "", now.Add(time.Second))
require.NoError(t, err)
oldSession, err := sessionsDB.Create(ctx, testrand.UUID(), testrand.UUID(), "", "", now.Add(-time.Second))
require.NoError(t, err)
require.NoError(t, sessionsDB.DeleteExpired(ctx, now, 0, 1))

// Ensure that the old session record was deleted and the other remains.
_, err = sessionsDB.GetBySessionID(ctx, oldSession.ID)
require.ErrorIs(t, err, sql.ErrNoRows)
_, err = sessionsDB.GetBySessionID(ctx, newSession.ID)
require.NoError(t, err)
})
}

0 comments on commit faf5b96

Please sign in to comment.