Skip to content

VIEWER API key with only READ_ANY can execute notification mutations #2065

Description

Environment

Unraid OS Version: 7.2.4
Unraid API: 4.29.2+c39b0b26
Authentication: API key, role VIEWER, granular permissions, READ_ANY only
Transport: local GraphQL endpoint

Are you using a reverse proxy? No — tested against the server directly.

No hostnames, addresses, API-key values, or registration data are included.

Pre-submission Checklist

  • I have verified that my Unraid OS is up to date
  • I have tested this issue by accessing my server directly (not through a reverse proxy)
  • This is not an Unraid Connect related issue

Issue Description

An API key holding role VIEWER and only READ_ANY permissions can successfully
execute notification mutations. Every other resource correctly rejects mutations
for the same key, so this looks like a missing authorization guard on the notification
mutation resolvers rather than a problem with the permission model itself.

Key scope, as reported by the API when queried with the key under test:

query { me { roles permissions { resource actions } } }
roles: ["VIEWER"]
permissions: 17 resources, every one ["READ_ANY"]
  ARRAY, DISK, DOCKER, LOGS, VARS, NETWORK, INFO, NOTIFICATIONS,
  SERVICES, SHARE, VMS, OS, ONLINE, REGISTRATION, FLASH, DISPLAY, CONFIG

No CREATE_*, UPDATE_*, or DELETE_* action is granted on any resource. The
NOTIFICATIONS entry is exactly {"resource": "NOTIFICATIONS", "actions": ["READ_ANY"]}.

Steps to Reproduce

  1. Create an API key with role VIEWER and granular permissions including
    NOTIFICATIONS:READ_ANY. Grant no write action on any resource.

  2. Confirm the key's scope contains only READ_ANY entries:

    query { me { roles permissions { resource actions } } }
  3. Issue a notification mutation with that key:

    mutation {
      createNotification(input: {
        title: "scope-test"
        subject: "scope-test"
        description: "permission probe"
        importance: INFO
      }) { id }
    }

    HTTP 200:

    { "data": { "createNotification": { "id": "..." } } }

    The notification is created and appears in the WebGUI.

  4. Contrast with a correctly-guarded mutation using the same key:

    mutation { array { setState(input: { desiredState: STOP }) { state } } }
    { "errors": [ { "message": "Forbidden resource",
                    "extensions": { "code": "FORBIDDEN",
                                    "originalError": { "statusCode": 403 } } } ],
      "data": null }

Expected Behavior

All mutations are rejected with Forbidden resource, consistent with a key that holds
only READ_ANY.

Actual Behavior

Notification mutations succeed. Mutations on every other resource tested are correctly
refused.

Mutation Expected Actual
createNotification Forbidden Succeeds
archiveAll Forbidden Succeeds
deleteArchivedNotifications Forbidden Succeeds
array { setState } Forbidden Forbidden (correct)
docker { start } Forbidden Forbidden (correct)
vm { start } Forbidden Forbidden (correct)
parityCheck { start } Forbidden Forbidden (correct)
apiKey { create } Forbidden Forbidden (correct)
updateSettings Forbidden Forbidden (correct)

Results were reproduced across two separate runs.

Impact

This breaks the security guarantee of the VIEWER role. A key documented and intended
as read-only can write to the notification subsystem.

The deletion path is the more meaningful half. deleteArchivedNotifications and
archiveAll let a read-only key destroy or hide notification history, including
warnings a user has not yet seen (SMART warnings, parity errors). That reads as an
alert-suppression / audit-integrity issue rather than a cosmetic one.

This is becoming more relevant as third-party integrations standardise on VIEWER
keys for monitoring — Home Assistant bridges, MCP servers, dashboards. Users granting
a VIEWER key to an external tool reasonably expect that tool cannot mutate anything.

Suggested Fix

Apply the authorization guard already used by the array / docker / vm / settings
mutation resolvers to the notification mutation resolvers, requiring an appropriate
write action (NOTIFICATIONS:CREATE_ANY / UPDATE_ANY / DELETE_ANY) rather than
being satisfied by READ_ANY.

Additional Context

Found while configuring a read-only integration against the GraphQL API. Any test
notification created during verification was archived and deleted afterwards; no other
server state was modified.

Possibly related, same authorization subsystem: #2060.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions