diff --git a/CHANGELOG.md b/CHANGELOG.md index acb90c0b..4ebe8666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ > [!CAUTION] > **This is a prerelease intended for test and experimental purposes only!** +- Fixed realtime 403 API error on resubscribe ([#5674](https://github.com/pocketbase/pocketbase/issues/5674)). + - Fixed the auto OAuth2 avatar mapped field assignment when the OAuth2 provider doesn't return an avatar URL ([#5673](https://github.com/pocketbase/pocketbase/pull/5673)). _In case the avatar retrieval fails and the mapped record field "Required" option is not set, the error is silenced and only logged with WARN level._ diff --git a/apis/realtime.go b/apis/realtime.go index 9262ba6b..dab452ff 100644 --- a/apis/realtime.go +++ b/apis/realtime.go @@ -185,10 +185,9 @@ func realtimeSetSubscriptions(e *core.RequestEvent) error { return e.NotFoundError("Missing or invalid client id.", err) } - // check if the previous request was authorized - oldAuthId := extractAuthIdFromGetter(client) - newAuthId := extractAuthIdFromGetter(e) - if oldAuthId != "" && oldAuthId != newAuthId { + // for now allow only guest->auth upgrades and any other auth change is forbidden + clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record) + if clientAuth != nil && !isSameAuth(clientAuth, e.Auth) { return e.ForbiddenError("The current and the previous request authorization don't match.", nil) } @@ -535,8 +534,8 @@ func realtimeBroadcastRecord(app core.App, action string, record *core.Record, d // ignore the auth record email visibility checks // for auth owner, superuser or manager if collection.IsAuth() { - authId := extractAuthIdFromGetter(client) - if authId == cleanRecord.Id || + clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record) + if isSameAuth(clientAuth, cleanRecord) || realtimeCanAccessRecord(app, cleanRecord, requestInfo, collection.ManageRule) { cleanRecord.IgnoreEmailVisibility(true) } @@ -681,17 +680,16 @@ func realtimeUnsetDryCachedRecord(app core.App, action string, record *core.Reco return group.Wait() } -type getter interface { - Get(string) any -} - -func extractAuthIdFromGetter(val getter) string { - record, _ := val.Get(RealtimeClientAuthKey).(*core.Record) - if record != nil { - return record.Id +func isSameAuth(authA, authB *core.Record) bool { + if authA == nil { + return authB == nil } - return "" + if authB == nil { + return false + } + + return authA.Id == authB.Id && authA.Collection().Id == authB.Collection().Id } // realtimeCanAccessRecord checks if the subscription client has access to the specified record model. diff --git a/apis/realtime_test.go b/apis/realtime_test.go index 414e1579..f7c920f8 100644 --- a/apis/realtime_test.go +++ b/apis/realtime_test.go @@ -233,7 +233,7 @@ func TestRealtimeSubscribe(t *testing.T) { }, }, { - Name: "existing client - authorized superuser", + Name: "existing client - guest -> authorized superuser", Method: http.MethodPost, URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), @@ -257,7 +257,7 @@ func TestRealtimeSubscribe(t *testing.T) { }, }, { - Name: "existing client - authorized regular record", + Name: "existing client - guest -> authorized regular auth record", Method: http.MethodPost, URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), @@ -280,6 +280,38 @@ func TestRealtimeSubscribe(t *testing.T) { resetClient() }, }, + { + Name: "existing client - same auth", + Method: http.MethodPost, + URL: "/api/realtime", + Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRealtimeSubscribeRequest": 1, + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // the same user as the auth token + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + client.Set(apis.RealtimeClientAuthKey, user) + + app.SubscriptionsBroker().Register(client) + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + authRecord, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record) + if authRecord == nil { + t.Errorf("Expected auth record model, got nil") + } + resetClient() + }, + }, { Name: "existing client - mismatched auth", Method: http.MethodPost,