added App.DeleteAllExternalAuthsByRecord
This commit is contained in:
@@ -502,6 +502,11 @@ type App interface {
|
||||
// ExternalAuth model that satisfies the non-nil expression.
|
||||
FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalAuth, error)
|
||||
|
||||
// DeleteAllExternalAuthsByRecord deletes all ExternalAuth models associated with the provided record.
|
||||
//
|
||||
// Returns a combined error with the failed deletes.
|
||||
DeleteAllExternalAuthsByRecord(authRecord *Record) error
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// FindAllMFAsByRecord returns all MFA models linked to the provided auth record.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
@@ -59,3 +61,25 @@ func (app *BaseApp) FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalA
|
||||
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// DeleteAllExternalAuthsByRecord deletes all ExternalAuth models associated with the provided record.
|
||||
//
|
||||
// Returns a combined error with the failed deletes.
|
||||
func (app *BaseApp) DeleteAllExternalAuthsByRecord(authRecord *Record) error {
|
||||
models, err := app.FindAllExternalAuthsByRecord(authRecord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, m := range models {
|
||||
if err := app.Delete(m); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package core_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -174,3 +175,68 @@ func TestFindFirstExternalAuthByExpr(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAllExternalAuthsByRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testApp, _ := tests.NewTestApp()
|
||||
defer testApp.Cleanup()
|
||||
|
||||
demo1, err := testApp.FindRecordById("demo1", "84nmscqy84lsi1t")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user1, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client1, err := testApp.FindAuthRecordByEmail("clients", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client2, err := testApp.FindAuthRecordByEmail("clients", "test2@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
record *core.Record
|
||||
deletedIds []string
|
||||
}{
|
||||
{demo1, nil}, // non-auth record
|
||||
{user1, []string{"dlmflokuq1xl342", "clmflokuq1xl341"}},
|
||||
{client1, []string{"f1z5b3843pzc964"}},
|
||||
{client2, nil},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Collection().Name, s.record.Id), func(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
deletedIds := []string{}
|
||||
app.OnRecordDelete().BindFunc(func(e *core.RecordEvent) error {
|
||||
deletedIds = append(deletedIds, e.Record.Id)
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
err := app.DeleteAllExternalAuthsByRecord(s.record)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(deletedIds) != len(s.deletedIds) {
|
||||
t.Fatalf("Expected deleted ids\n%v\ngot\n%v", s.deletedIds, deletedIds)
|
||||
}
|
||||
|
||||
for _, id := range s.deletedIds {
|
||||
if !slices.Contains(deletedIds, id) {
|
||||
t.Errorf("Expected to find deleted id %q in %v", id, deletedIds)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,22 +1427,31 @@ func onRecordValidate(e *RecordEvent) error {
|
||||
}
|
||||
|
||||
func onRecordSaveExecute(e *RecordEvent) error {
|
||||
var needToDeleteExternalAuths bool
|
||||
|
||||
if e.Record.Collection().IsAuth() {
|
||||
// ensure that the token key is regenerated on password change or email change
|
||||
// auth resets to prevent (pre)hijacking vulnerabilities
|
||||
if !e.Record.IsNew() {
|
||||
lastSavedRecord, err := e.App.FindRecordById(e.Record.Collection(), e.Record.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure that the token key is regenerated on password change or email change
|
||||
if lastSavedRecord.TokenKey() == e.Record.TokenKey() &&
|
||||
(lastSavedRecord.Get(FieldNamePassword) != e.Record.Get(FieldNamePassword) ||
|
||||
lastSavedRecord.Email() != e.Record.Email()) {
|
||||
e.Record.RefreshTokenKey()
|
||||
}
|
||||
|
||||
// in case upgrading from "unverified" -> "verified" mark all pre-existing OAuth2 links
|
||||
// for deletion since there is no reliable way to verify that they weren't created by an attacker
|
||||
if !lastSavedRecord.Verified() && e.Record.Verified() {
|
||||
needToDeleteExternalAuths = true
|
||||
}
|
||||
}
|
||||
|
||||
// cross-check that the auth record id is unique across all auth collections.
|
||||
// cross-check that the auth record id is unique across all auth collections
|
||||
authCollections, err := e.App.FindAllCollections(CollectionTypeAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch the auth collections for cross-id unique check: %w", err)
|
||||
@@ -1460,16 +1469,45 @@ func onRecordSaveExecute(e *RecordEvent) error {
|
||||
}
|
||||
}
|
||||
|
||||
err := e.Next()
|
||||
if err == nil {
|
||||
return nil
|
||||
finalizer := func() error {
|
||||
err := e.Next()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validators.NormalizeUniqueIndexError(
|
||||
err,
|
||||
e.Record.Collection().Name,
|
||||
e.Record.Collection().Fields.FieldNames(),
|
||||
)
|
||||
}
|
||||
|
||||
return validators.NormalizeUniqueIndexError(
|
||||
err,
|
||||
e.Record.Collection().Name,
|
||||
e.Record.Collection().Fields.FieldNames(),
|
||||
)
|
||||
if needToDeleteExternalAuths {
|
||||
originalApp := e.App
|
||||
|
||||
return e.App.RunInTransaction(func(txApp App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = originalApp }()
|
||||
|
||||
externalAuths, err := txApp.FindAllExternalAuthsByRecord(e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(externalAuths) > 0 {
|
||||
// delete all pre-existing external auths
|
||||
if err := txApp.DeleteAllExternalAuthsByRecord(e.Record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// force refresh tokens reset (if not already)
|
||||
e.Record.RefreshTokenKey()
|
||||
}
|
||||
|
||||
return finalizer()
|
||||
})
|
||||
}
|
||||
|
||||
return finalizer()
|
||||
}
|
||||
|
||||
func onRecordDeleteExecute(e *RecordEvent) error {
|
||||
|
||||
Reference in New Issue
Block a user