From 0484c54919ff071e21e1db3197060fc8277213e0 Mon Sep 17 00:00:00 2001 From: user01010111 <12504630+user01010111@users.noreply.github.com> Date: Wed, 30 Sep 2026 03:27:11 +1300 Subject: [PATCH] fix(hub): require system access for alert subscriptions and delivery (#2455) Co-authored-by: hank --- internal/alerts/alerts.go | 4 + internal/alerts/alerts_access_test.go | 361 ++++++++++++++++++ internal/alerts/alerts_cache.go | 5 +- internal/alerts/alerts_history.go | 5 + internal/alerts/alerts_network_monitors.go | 6 +- .../alerts/alerts_network_monitors_test.go | 5 +- internal/hub/collections.go | 19 + internal/hub/collections_test.go | 6 +- .../0_collections_snapshot_0_20_0.go | 8 +- 9 files changed, 404 insertions(+), 15 deletions(-) create mode 100644 internal/alerts/alerts_access_test.go diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index 589ec8363..496a11608 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -212,6 +212,10 @@ func (am *AlertManager) IsNotificationSilenced(userID, systemID string) bool { // SendAlert sends an alert to the user func (am *AlertManager) SendAlert(data AlertMessageData) error { + // Stored subscriptions and queued notifications may outlive system access. + if data.SystemID != "" && !userHasSystem(am.hub, data.UserID, data.SystemID) { + return nil + } // Check if alert is silenced if am.IsNotificationSilenced(data.UserID, data.SystemID) { am.hub.Logger().Info("Notification silenced", "user", data.UserID, "system", data.SystemID, "title", data.Title) diff --git a/internal/alerts/alerts_access_test.go b/internal/alerts/alerts_access_test.go new file mode 100644 index 000000000..03f35b306 --- /dev/null +++ b/internal/alerts/alerts_access_test.go @@ -0,0 +1,361 @@ +//go:build testing + +package alerts_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "testing/synctest" + "time" + + "github.com/henrygd/beszel/internal/alerts" + "github.com/henrygd/beszel/internal/entities/monitor" + "github.com/henrygd/beszel/internal/entities/system" + beszelTests "github.com/henrygd/beszel/internal/tests" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func alertAPIRouter(t *testing.T, hub *beszelTests.TestHub) http.Handler { + t.Helper() + router, err := apis.NewRouter(hub) + require.NoError(t, err) + require.NoError(t, hub.OnServe().Trigger(&core.ServeEvent{App: hub, Router: router})) + mux, err := router.BuildMux() + require.NoError(t, err) + return mux +} + +func alertAPIRequest(t *testing.T, handler http.Handler, token, method, path string, body any, status int) map[string]any { + t.Helper() + req := httptest.NewRequest(method, path, jsonReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", token) + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + assert.Equal(t, status, res.Code, "%s %s: %s", method, path, res.Body.String()) + var result map[string]any + require.NoError(t, json.Unmarshal(res.Body.Bytes(), &result)) + return result +} + +func TestAlertRecordSystemAccess(t *testing.T) { + for _, shareAll := range []string{"false", "true"} { + t.Run("share_all="+shareAll, func(t *testing.T) { + t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", shareAll) + hub, err := beszelTests.NewTestHub(t.TempDir()) + require.NoError(t, err) + defer hub.Cleanup() + hub.StartHub() + user, err := beszelTests.CreateUser(hub, "subscriber@example.com", "password") + require.NoError(t, err) + other, err := beszelTests.CreateUser(hub, "owner@example.com", "password") + require.NoError(t, err) + token, err := user.NewAuthToken() + require.NoError(t, err) + own, err := beszelTests.CreateSystems(hub, 2, user.Id, "paused") + require.NoError(t, err) + foreign, err := beszelTests.CreateSystems(hub, 1, other.Id, "paused") + require.NoError(t, err) + handler := alertAPIRouter(t, hub) + const records = "/api/collections/alerts/records" + // Collection rules reject inaccessible creates as 400 and hide inaccessible updates as 404. + readStatus, createStatus, updateStatus := 404, 400, 404 + if shareAll == "true" { + readStatus, createStatus, updateStatus = 200, 200, 200 + } + alertAPIRequest(t, handler, token, "GET", "/api/collections/systems/records/"+own[0].Id, nil, 200) + alertAPIRequest(t, handler, token, "GET", "/api/collections/systems/records/"+foreign[0].Id, nil, readStatus) + collection, err := hub.FindCollectionByNameOrId("alerts") + require.NoError(t, err) + for _, name := range collection.Fields.GetByName("name").(*core.SelectField).Values { + t.Run(name, func(t *testing.T) { + // Scalar and array representations both pass through PocketBase's relation binding. + for _, relation := range []any{foreign[0].Id, []string{foreign[0].Id}} { + result := alertAPIRequest(t, handler, token, "POST", records, map[string]any{ + "name": name, "user": user.Id, "system": relation, "value": 50, + }, createStatus) + if id, ok := result["id"].(string); ok { + record, err := hub.FindRecordById("alerts", id) + require.NoError(t, err) + require.NoError(t, hub.Delete(record)) + } + } + result := alertAPIRequest(t, handler, token, "POST", records, map[string]any{ + "name": name, "user": user.Id, "system": own[0].Id, "value": 50, + }, 200) + id := result["id"].(string) + defer func() { + record, err := hub.FindRecordById("alerts", id) + require.NoError(t, err) + require.NoError(t, hub.Delete(record)) + }() + alertAPIRequest(t, handler, token, "PATCH", records+"/"+id, map[string]any{"value": 51}, 200) + // The system of an existing alert is immutable through the API, even between accessible systems. + for _, body := range []map[string]any{ + {"system": foreign[0].Id}, + {"system+": foreign[0].Id}, + {"system": []string{own[1].Id}}, + } { + alertAPIRequest(t, handler, token, "PATCH", records+"/"+id, body, 404) + } + saved, err := hub.FindRecordById("alerts", id) + require.NoError(t, err) + assert.Equal(t, own[0].Id, saved.GetString("system")) + // Internal saves can still move an alert, which must remove its previous cache binding. + saved.Set("system", own[1].Id) + require.NoError(t, hub.Save(saved)) + cache := hub.GetAlertManager().GetSystemAlertsCache() + assert.Empty(t, cache.GetSystemAlerts(own[0].Id), "moving an alert must remove its previous cache binding") + assert.Len(t, cache.GetSystemAlerts(own[1].Id), 1) + }) + } + alertAPIRequest(t, handler, token, "POST", records, map[string]any{"name": "CPU", "user": other.Id, "system": own[0].Id}, 400) + alertAPIRequest(t, handler, token, "POST", records, map[string]any{"name": "CPU", "user": user.Id, "system": "missing00000000"}, 400) + alertAPIRequest(t, handler, "", "POST", records, map[string]any{"name": "CPU", "user": user.Id, "system": own[0].Id}, 400) + // An old subscription must still be checked when PATCH omits the relation. + oldAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{ + "name": "CPU", "user": user.Id, "system": foreign[0].Id, "value": 50, + }) + require.NoError(t, err) + alertAPIRequest(t, handler, token, "PATCH", records+"/"+oldAlert.Id, map[string]any{"value": 51}, updateStatus) + require.NoError(t, hub.Delete(oldAlert)) + otherAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{ + "name": "CPU", "user": other.Id, "system": foreign[0].Id, + }) + require.NoError(t, err) + alertAPIRequest(t, handler, token, "PATCH", records+"/"+otherAlert.Id, map[string]any{"system": own[0].Id}, 404) + require.NoError(t, hub.Delete(otherAlert)) + // Alerts cannot be handed to another user. + ownAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{ + "name": "CPU", "user": user.Id, "system": own[0].Id, + }) + require.NoError(t, err) + alertAPIRequest(t, handler, token, "PATCH", records+"/"+ownAlert.Id, map[string]any{"user": other.Id}, 404) + ownAlert, err = hub.FindRecordById("alerts", ownAlert.Id) + require.NoError(t, err) + assert.Equal(t, user.Id, ownAlert.GetString("user")) + require.NoError(t, hub.Delete(ownAlert)) + // Readonly users retain their own alert preferences; superusers retain their bypass. + user.Set("role", "readonly") + require.NoError(t, hub.Save(user)) + result := alertAPIRequest(t, handler, token, "POST", records, map[string]any{"name": "CPU", "user": user.Id, "system": own[0].Id}, 200) + alertAPIRequest(t, handler, token, "PATCH", records+"/"+result["id"].(string), map[string]any{"name": "Memory"}, 200) + alertAPIRequest(t, handler, token, "PATCH", records+"/"+result["id"].(string), map[string]any{"name": "NetworkMonitorLoss"}, 400) + record, err := hub.FindRecordById("alerts", result["id"].(string)) + require.NoError(t, err) + require.NoError(t, hub.Delete(record)) + superuser, err := beszelTests.CreateSuperuser(hub, "superuser@example.com", "password123") + require.NoError(t, err) + superToken, err := superuser.NewAuthToken() + require.NoError(t, err) + result = alertAPIRequest(t, handler, superToken, "POST", records, map[string]any{"name": "CPU", "user": other.Id, "system": foreign[0].Id}, 200) + record, err = hub.FindRecordById("alerts", result["id"].(string)) + require.NoError(t, err) + require.NoError(t, hub.Delete(record)) + // Bulk requests continue to skip inaccessible systems and accept accessible ones. + alertAPIRequest(t, handler, token, "POST", "/api/beszel/user-alerts", map[string]any{ + "name": "CPU", "systems": []string{own[0].Id, foreign[0].Id}, "value": 50, + }, 200) + count, err := hub.CountRecords("alerts") + require.NoError(t, err) + expectedCount := 1 + if shareAll == "true" { + expectedCount = 2 + } + assert.EqualValues(t, expectedCount, count) + }) + } +} + +func TestAlertRecordForeignSystemDelivery(t *testing.T) { + t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "false") + hub, err := beszelTests.NewTestHub(t.TempDir()) + require.NoError(t, err) + defer hub.Cleanup() + hub.StartHub() + user, err := beszelTests.CreateUser(hub, "subscriber@example.com", "password") + require.NoError(t, err) + owner, err := beszelTests.CreateUser(hub, "owner@example.com", "password") + require.NoError(t, err) + for _, recipient := range []*core.Record{user, owner} { + _, err := beszelTests.CreateRecord(hub, "user_settings", map[string]any{ + "user": recipient.Id, "settings": map[string]any{"emails": []string{recipient.GetString("email")}, "webhooks": []string{}}, + }) + require.NoError(t, err) + } + foreign, err := beszelTests.CreateSystems(hub, 1, owner.Id, "paused") + require.NoError(t, err) + foreign[0].Set("name", "private-system") + require.NoError(t, hub.Save(foreign[0])) + handler := alertAPIRouter(t, hub) + for _, recipient := range []*core.Record{user, owner} { + token, err := recipient.NewAuthToken() + require.NoError(t, err) + status := 200 + if recipient.Id == user.Id { + status = 400 + } + alertAPIRequest(t, handler, token, "POST", "/api/collections/alerts/records", map[string]any{ + "name": "CPU", "user": recipient.Id, "system": foreign[0].Id, "min": 1, "value": 50, + }, status) + } + synctest.Test(t, func(t *testing.T) { + require.NoError(t, hub.GetAlertManager().HandleSystemAlerts(foreign[0], &system.CombinedData{Info: system.Info{Cpu: 91}})) + synctest.Wait() + messages := hub.TestMailer.Messages() + assert.Len(t, messages, 1, "only the authorised subscriber should receive telemetry") + for _, message := range messages { + assert.Equal(t, "owner@example.com", message.To[0].Address) + assert.Contains(t, message.Subject, "private-system CPU above threshold") + assert.Contains(t, message.Text, "91.00%") + t.Logf("captured synthetic email: recipient=%s subject=%q body=%q", message.To[0].Address, message.Subject, message.Text) + } + history, err := hub.FindAllRecords("alerts_history", dbx.HashExp{"system": foreign[0].Id}) + require.NoError(t, err) + assert.Len(t, history, 1) + for _, record := range history { + assert.Equal(t, owner.Id, record.GetString("user")) + } + t.Logf("captured synthetic history entries=%d", len(history)) + }) +} + +func TestAlertStoredSubscriptionAccess(t *testing.T) { + for _, mode := range []string{"foreign", "revoked", "shared", "share_all", "revoked_active"} { + t.Run(mode, func(t *testing.T) { + t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "false") + if mode == "share_all" { + t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "true") + } + hub, err := beszelTests.NewTestHub(t.TempDir()) + require.NoError(t, err) + defer hub.Cleanup() + hub.StartHub() + owner, err := beszelTests.CreateUser(hub, "owner@example.com", "password") + require.NoError(t, err) + subscriber, err := beszelTests.CreateUser(hub, "subscriber@example.com", "password") + require.NoError(t, err) + systems, err := beszelTests.CreateSystems(hub, 1, owner.Id, "paused") + require.NoError(t, err) + systemRecord := systems[0] + if mode == "revoked" || mode == "shared" || mode == "revoked_active" { + systemRecord.Set("users", []string{owner.Id, subscriber.Id}) + require.NoError(t, hub.Save(systemRecord)) + } + for _, user := range []*core.Record{owner, subscriber} { + _, err = beszelTests.CreateRecord(hub, "user_settings", map[string]any{ + "user": user.Id, "settings": map[string]any{"emails": []string{user.GetString("email")}, "webhooks": []string{}}, + }) + require.NoError(t, err) + // Direct saves model records already stored before the request-hook fix. + _, err = beszelTests.CreateRecord(hub, "alerts", map[string]any{ + "name": "CPU", "user": user.Id, "system": systemRecord.Id, "value": 50, "min": 1, + }) + require.NoError(t, err) + } + require.NoError(t, hub.GetAlertManager().GetSystemAlertsCache().PopulateFromDB(true)) + if mode == "revoked" { + systemRecord.Set("users", []string{owner.Id}) + require.NoError(t, hub.Save(systemRecord)) + } + synctest.Test(t, func(t *testing.T) { + require.NoError(t, hub.GetAlertManager().HandleSystemAlerts(systemRecord, &system.CombinedData{Info: system.Info{Cpu: 91}})) + synctest.Wait() + }) + allowed := mode == "shared" || mode == "share_all" || mode == "revoked_active" + want := 1 + if allowed { + want = 2 + } + assert.Equal(t, want, hub.TestMailer.TotalSend()) + for _, message := range hub.TestMailer.Messages() { + if !allowed { + assert.Equal(t, "owner@example.com", message.To[0].Address) + } + t.Logf("%s captured recipient=%s body=%q", mode, message.To[0].Address, message.Text) + } + history, err := hub.FindAllRecords("alerts_history") + require.NoError(t, err) + assert.Len(t, history, want) + for _, record := range history { + if !allowed { + assert.Equal(t, owner.Id, record.GetString("user")) + } + } + count, err := hub.CountRecords("alerts") + require.NoError(t, err) + assert.EqualValues(t, 2, count, "stored subscriptions are preserved") + if mode == "revoked_active" { + systemRecord.Set("users", []string{owner.Id}) + require.NoError(t, hub.Save(systemRecord)) + synctest.Test(t, func(t *testing.T) { + require.NoError(t, hub.GetAlertManager().HandleSystemAlerts(systemRecord, &system.CombinedData{Info: system.Info{Cpu: 40}})) + synctest.Wait() + }) + assert.Equal(t, 3, hub.TestMailer.TotalSend(), "only the owner should receive recovery after revocation") + previous, err := hub.FindFirstRecordByFilter("alerts_history", "user={:user}", dbx.Params{"user": subscriber.Id}) + require.NoError(t, err) + assert.True(t, previous.GetDateTime("resolved").IsZero(), "revoked subscribers must not learn recovery timing through history") + } + }) + } +} + +func TestAlertPendingStatusAccessRevoked(t *testing.T) { + t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "false") + hub, err := beszelTests.NewTestHub(t.TempDir()) + require.NoError(t, err) + defer hub.Cleanup() + hub.StartHub() + user, err := beszelTests.CreateUser(hub, "subscriber@example.com", "password") + require.NoError(t, err) + owner, err := beszelTests.CreateUser(hub, "owner@example.com", "password") + require.NoError(t, err) + _, err = beszelTests.CreateRecord(hub, "user_settings", map[string]any{ + "user": user.Id, "settings": map[string]any{"emails": []string{"subscriber@example.com"}, "webhooks": []string{}}, + }) + require.NoError(t, err) + systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "paused") + require.NoError(t, err) + _, err = beszelTests.CreateRecord(hub, "alerts", map[string]any{ + "name": "Status", "user": user.Id, "system": systems[0].Id, "min": 1, + }) + require.NoError(t, err) + synctest.Test(t, func(t *testing.T) { + defer hub.GetAlertManager().Stop() + require.NoError(t, hub.GetAlertManager().HandleStatusAlerts("down", systems[0])) + require.Equal(t, 1, hub.GetAlertManager().GetPendingAlertsCount()) + systems[0].Set("users", []string{owner.Id}) + require.NoError(t, hub.Save(systems[0])) + time.Sleep(61 * time.Second) + synctest.Wait() + assert.Zero(t, hub.TestMailer.TotalSend()) + count, err := hub.CountRecords("alerts_history") + require.NoError(t, err) + assert.Zero(t, count) + }) +} + +func TestAlertStoredNetworkMonitorAccess(t *testing.T) { + t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "false") + hub, systemRecord, _, monitors := networkAlertSetup(t) + other, err := beszelTests.CreateUser(hub, "foreign@example.com", "password") + require.NoError(t, err) + foreign, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{ + "name": "NetworkMonitorLoss", "user": other.Id, "system": systemRecord.Id, "value": 5, + }) + require.NoError(t, err) + am := alerts.NewTestAlertManagerWithoutWorker(hub) + require.NoError(t, am.HandleNetworkMonitorAlerts(systemRecord, map[string]monitor.Result{monitors[0].Id: monitorResult(10)})) + history, err := hub.FindAllRecords("alerts_history") + require.NoError(t, err) + require.Len(t, history, 1, "foreign subscriptions must not block the authorised incident transaction") + assert.NotEqual(t, foreign.Id, history[0].GetString("alert_id")) + assert.Equal(t, 1, hub.TestMailer.TotalSend()) +} diff --git a/internal/alerts/alerts_cache.go b/internal/alerts/alerts_cache.go index 58550ff02..facc401b3 100644 --- a/internal/alerts/alerts_cache.go +++ b/internal/alerts/alerts_cache.go @@ -63,7 +63,10 @@ func NewAlertsCache(app core.App) *AlertsCache { // bindEvents sets up event listeners to keep the cache in sync with database changes. func (c *AlertsCache) bindEvents() *AlertsCache { c.app.OnRecordAfterUpdateSuccess("alerts").BindFunc(func(e *core.RecordEvent) error { - // c.Delete(e.Record.Original()) // this would be needed if the system field on an existing alert was changed, however we don't currently allow that in the UI so we'll leave it commented out + // Remove the previous binding if the system changed (only possible through superuser or internal saves). + if original := e.Record.Original(); original.GetString("system") != e.Record.GetString("system") { + c.Delete(original) + } c.Update(e.Record) return e.Next() }) diff --git a/internal/alerts/alerts_history.go b/internal/alerts/alerts_history.go index c29475f6f..f6b1bd4d2 100644 --- a/internal/alerts/alerts_history.go +++ b/internal/alerts/alerts_history.go @@ -39,6 +39,11 @@ func updateHistoryOnAlertUpdate(e *core.RecordEvent) error { return e.Next() } + // History is visible to the subscriber, including after system access is removed. + if !userHasSystem(e.App, new.GetString("user"), new.GetString("system")) { + return e.Next() + } + // if new state is triggered, create new alert history record if newTriggered { _, _ = createAlertHistoryRecord(e.App, new) diff --git a/internal/alerts/alerts_network_monitors.go b/internal/alerts/alerts_network_monitors.go index 6b69c3ccf..fb7723eb0 100644 --- a/internal/alerts/alerts_network_monitors.go +++ b/internal/alerts/alerts_network_monitors.go @@ -32,9 +32,6 @@ func (am *AlertManager) bindNetworkMonitorAlertEvents() { return e.BadRequestError("Delete and recreate the alert to change its type or system", nil) } if e.Record.GetString("name") == alertNameNetworkMonitorLoss { - if !e.HasSuperuserAuth() && (e.Auth == nil || !userHasSystem(e.App, e.Auth.Id, e.Record.GetString("system"))) { - return e.ForbiddenError("You do not have access to this system", nil) - } e.Record.Set("triggered", e.Record.Original().GetBool("triggered")) value := e.Record.GetFloat("value") if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 || value >= 100 { @@ -136,6 +133,9 @@ func (am *AlertManager) evaluateNetworkMonitorAlerts(app core.App, systemID stri } now := time.Now() for _, alert := range alerts { + if !userHasSystem(tx, alert.GetString("user"), systemID) { + continue + } var state networkMonitorAlertState if err := alert.UnmarshalJSONField("state", &state); err != nil { return err diff --git a/internal/alerts/alerts_network_monitors_test.go b/internal/alerts/alerts_network_monitors_test.go index aa02cf430..0331d822c 100644 --- a/internal/alerts/alerts_network_monitors_test.go +++ b/internal/alerts/alerts_network_monitors_test.go @@ -248,7 +248,7 @@ func TestNetworkMonitorAlertAPI(t *testing.T) { {name: "negative threshold", value: -1, status: 400}, {name: "unreachable threshold", value: 100, status: 400}, {name: "bulk inaccessible system", value: 5, denied: true, status: 200}, - {name: "direct inaccessible system", value: 5, direct: true, denied: true, status: 403}, + {name: "direct inaccessible system", value: 5, direct: true, denied: true, status: 400}, {name: "direct invalid threshold", value: -1, direct: true, status: 400}, {name: "direct private state", value: 5, direct: true, status: 200}, {name: "patch preserves state", value: 10, direct: true, patch: true, status: 200}, @@ -287,9 +287,6 @@ func TestNetworkMonitorAlertAPI(t *testing.T) { if tc.status == 400 { content = `"status":400` } - if tc.status == 403 { - content = `"status":403` - } scenario := beszelTests.ApiScenario{ Name: tc.name, Method: method, URL: url, Body: jsonReader(body), Headers: map[string]string{"Authorization": token}, ExpectedStatus: tc.status, ExpectedContent: []string{content}, diff --git a/internal/hub/collections.go b/internal/hub/collections.go index 3a0c3e85f..41a7d989d 100644 --- a/internal/hub/collections.go +++ b/internal/hub/collections.go @@ -118,6 +118,25 @@ func setCollectionAuthSettings(app core.App) error { return err } + // Alerts belong to their user and may only reference systems the user can access. + // The user and system of an existing alert cannot be changed through the API. + // Readonly users can still manage their own alerts, so these build on the read rule. + alertsOwnerRule := authenticatedRule + " && user = @request.auth.id" + alertsCreateRule := alertsOwnerRule + alertsUpdateRule := alertsOwnerRule + " && @request.body.user:changed = false && @request.body.system:changed = false" + if shareAllSystems != "true" { + alertsCreateRule += " && system.users.id ?= @request.auth.id" + alertsUpdateRule += " && system.users.id ?= @request.auth.id" + } + if err := applyCollectionRules(app, []string{"alerts"}, collectionRules{ + list: &alertsOwnerRule, + create: &alertsCreateRule, + update: &alertsUpdateRule, + delete: &alertsOwnerRule, + }); err != nil { + return err + } + if err := applyCollectionRules(app, []string{"system_details"}, collectionRules{ list: &systemScopedReadRule, view: &systemScopedReadRule, diff --git a/internal/hub/collections_test.go b/internal/hub/collections_test.go index 969b27828..280a583bc 100644 --- a/internal/hub/collections_test.go +++ b/internal/hub/collections_test.go @@ -47,8 +47,8 @@ func TestCollectionRulesDefault(t *testing.T) { require.NoError(t, err, "Failed to find alerts collection") assert.Equal(t, isUserMatchesUser, *alertsCollection.ListRule) assert.Nil(t, alertsCollection.ViewRule) - assert.Equal(t, isUserMatchesUser, *alertsCollection.CreateRule) - assert.Equal(t, isUserMatchesUser, *alertsCollection.UpdateRule) + assert.Equal(t, isUserMatchesUser+` && system.users.id ?= @request.auth.id`, *alertsCollection.CreateRule) + assert.Equal(t, isUserMatchesUser+` && @request.body.user:changed = false && @request.body.system:changed = false && system.users.id ?= @request.auth.id`, *alertsCollection.UpdateRule) assert.Equal(t, isUserMatchesUser, *alertsCollection.DeleteRule) alertNames := alertsCollection.Fields.GetByName("name").(*core.SelectField).Values for _, name := range []string{"CPUIOWait", "CPUSteal"} { @@ -183,7 +183,7 @@ func TestCollectionRulesShareAllSystems(t *testing.T) { assert.Equal(t, isUserMatchesUser, *alertsCollection.ListRule) assert.Nil(t, alertsCollection.ViewRule) assert.Equal(t, isUserMatchesUser, *alertsCollection.CreateRule) - assert.Equal(t, isUserMatchesUser, *alertsCollection.UpdateRule) + assert.Equal(t, isUserMatchesUser+` && @request.body.user:changed = false && @request.body.system:changed = false`, *alertsCollection.UpdateRule) assert.Equal(t, isUserMatchesUser, *alertsCollection.DeleteRule) // alerts_history collection diff --git a/internal/migrations/0_collections_snapshot_0_20_0.go b/internal/migrations/0_collections_snapshot_0_20_0.go index 293c2f799..93043b751 100644 --- a/internal/migrations/0_collections_snapshot_0_20_0.go +++ b/internal/migrations/0_collections_snapshot_0_20_0.go @@ -11,11 +11,11 @@ func init() { jsonData := `[ { "id": "elngm8x1l60zi2v", - "listRule": "@request.auth.id != \"\" && user = @request.auth.id", + "listRule": null, "viewRule": null, - "createRule": "@request.auth.id != \"\" && user = @request.auth.id", - "updateRule": "@request.auth.id != \"\" && user = @request.auth.id", - "deleteRule": "@request.auth.id != \"\" && user = @request.auth.id", + "createRule": null, + "updateRule": null, + "deleteRule": null, "name": "alerts", "type": "base", "fields": [