Skip to content

Require admin for supervisor event publishing and addon options info#169325

Merged
balloob merged 7 commits intodevfrom
hassio-websocket-apis-admin-required
Apr 28, 2026
Merged

Require admin for supervisor event publishing and addon options info#169325
balloob merged 7 commits intodevfrom
hassio-websocket-apis-admin-required

Conversation

@mdegat01
Copy link
Copy Markdown
Contributor

@mdegat01 mdegat01 commented Apr 27, 2026

Breaking change

  • The supervisor/event WS API requires an admin role to publish to as it can modify information in the Supervisor integration.
  • Calling Supervisor's /addons/{slug}/info API via the Supervisor API WS API in core is still allowed for non-admin users but will no longer return options information except to admins.

Proposed change

Using the supervisor/event WS API requires admin access as its the way to keep data in the component in sync with Supervisor, it could trigger undesirable behaviors if used by other clients. Additionally addon options info should not be returned to non-admin users as it can contain sensitive information and the frontend doesn't need it anyway.

When adding a new non-admin user, the UI tells the operator:

The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators.

This PR is part of that audit.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

@home-assistant
Copy link
Copy Markdown
Contributor

Hey there @home-assistant/supervisor, mind taking a look at this pull request as it has been labeled with an integration (hassio) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of hassio can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign hassio Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Tightens authorization in the Hass.io websocket API to prevent non-admin clients from publishing supervisor events and from receiving potentially sensitive addon options in addon-info responses.

Changes:

  • Require admin privileges for the supervisor/event websocket command.
  • Redact addon options from /addons/<slug>/info supervisor API websocket responses for non-admin users.
  • Add/update websocket tests to cover the new authorization and redaction behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
homeassistant/components/hassio/websocket_api.py Adds admin gating for supervisor event publishing and redacts addon options for non-admin addon info requests.
tests/components/hassio/test_websocket_api.py Adds a non-admin authorization failure test and updates non-admin addon info expectations.

Comment thread homeassistant/components/hassio/websocket_api.py Outdated
Comment thread homeassistant/components/hassio/websocket_api.py Outdated
Comment thread tests/components/hassio/test_websocket_api.py
Copy link
Copy Markdown
Member

@balloob balloob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can you address your comments

@home-assistant home-assistant Bot marked this pull request as draft April 27, 2026 19:05
@home-assistant
Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@balloob
Copy link
Copy Markdown
Member

balloob commented Apr 27, 2026

@copilot can you address your comments?

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 27, 2026

@copilot can you address your comments

Done in commit 0195b4a. Three issues addressed:

  • Regex double-anchoring: RE_ADDONS_INFO_ENDPOINT is now an unanchored fragment; WS_ADDONS_INFO_ENDPOINT explicitly adds ^...$ when compiling
  • Options key is now removed entirely (data.pop("options", None)) instead of being set to {}
  • Test assertion updated to assert "options" not in msg["result"] to match the actual behavior

@copilot can you address your comments?

All three reviewer comments are addressed in commit 0195b4a. See reply above for details.

Copilot AI review requested due to automatic review settings April 27, 2026 19:10
@mdegat01 mdegat01 review requested due to automatic review settings April 27, 2026 19:10
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 27, 2026

@copilot can you address your comments

Addressed all three review comments in commits 0195b4af and 16327bb5:

  1. Regex double-anchoring: Removed the ^...$ anchors from RE_ADDONS_INFO_ENDPOINT so it composes cleanly inside WS_NO_ADMIN_ENDPOINTS without nested anchors.
  2. Implementation matches wording: Changed data["options"] = {} to data.pop("options", None) — the options key is now fully removed rather than redacted as an empty dict.
  3. Test alignment: Updated the test assertion to verify options is absent from the result (instead of asserting "options": {}).

@copilot can you address your comments?

All three review comments have been addressed in commits 0195b4af and 16327bb5:

  1. Regex double-anchoring fixed: RE_ADDONS_INFO_ENDPOINT no longer carries ^...$ anchors — it's a plain sub-pattern now, so WS_NO_ADMIN_ENDPOINTS composes it without nested anchors.
  2. Options key removed entirely: Replaced data["options"] = {} with data.pop("options", None) so the field is completely absent for non-admin users.
  3. Test updated: The assertion now checks that options is not present in the result rather than asserting "options": {}.

Copilot AI requested a review from balloob April 27, 2026 19:11
@mdegat01
Copy link
Copy Markdown
Contributor Author

mdegat01 commented Apr 27, 2026

@balloob I think this should be fine. The only thing I didn't like about redacting by popping the key is because that becomes a breaking change to the model. Whereas putting an empty dictionary in still allows existing parsers/validators to work but the info is redacted. But perhaps given the circumstances breaking callers is better.

@balloob
Copy link
Copy Markdown
Member

balloob commented Apr 27, 2026

I'm fine either way. It needs to pop the key or the doc string updated. I think it's fine to break, as now you know something is not working.

Copilot AI review requested due to automatic review settings April 27, 2026 19:29
@mdegat01 mdegat01 marked this pull request as ready for review April 27, 2026 19:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread homeassistant/components/hassio/websocket_api.py Outdated
Comment thread homeassistant/components/hassio/websocket_api.py Outdated
Comment thread homeassistant/components/hassio/websocket_api.py Outdated
@home-assistant home-assistant Bot marked this pull request as draft April 28, 2026 10:04
Copilot AI review requested due to automatic review settings April 28, 2026 13:07
@mdegat01 mdegat01 force-pushed the hassio-websocket-apis-admin-required branch from bdfff7d to 31b8afb Compare April 28, 2026 13:07
@mdegat01 mdegat01 marked this pull request as ready for review April 28, 2026 13:07
@home-assistant home-assistant Bot requested a review from agners April 28, 2026 13:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

homeassistant/components/hassio/websocket_api.py:107

  • Replace the ws_require_user(only_supervisor=True) guard with an admin check (or otherwise align the authorization + error behavior) so regular admin users can still call supervisor/event as described and non-admins get a standard Unauthorized response.
@callback
@websocket_api.ws_require_user(only_supervisor=True)
@websocket_api.websocket_command(
    {
        vol.Required(WS_TYPE): WS_TYPE_EVENT,
        vol.Required(ATTR_DATA): SCHEMA_WEBSOCKET_EVENT,
    }
)
def websocket_supervisor_event(
    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
    """Publish events from the Supervisor."""
    connection.send_result(msg[WS_ID])
    async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA])

Comment on lines 322 to +340
@@ -297,7 +330,14 @@ async def test_websocket_non_admin_user(
}
)
msg = await websocket_client.receive_json()
assert msg["result"] == {}
assert msg["result"] == {
"name": "test",
"state": "started",
"slug": "test_addon",
"version": "2.0.0",
"ingress_url": "http://127.0.0.1/ingress/test_addon",
}
assert "options" not in msg["result"]
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an explicit test that an admin user still receives options in the /addons/{slug}/info WS response (while non-admins do not) to prevent regressions where the field is accidentally stripped for all users.

Copilot uses AI. Check for mistakes.
@mdegat01 mdegat01 force-pushed the hassio-websocket-apis-admin-required branch from 31b8afb to 3f6bee4 Compare April 28, 2026 17:28
Copy link
Copy Markdown
Member

@agners agners left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, yeah @websocket_api.ws_require_user(only_supervisor=True) got a bit more complicated due to tests than I thought 😰 . But I guess it make sense, we probably only restrict things more to Supervisor in the future!

Comment on lines +80 to +81
refresh_token = next(iter(hassio_user.refresh_tokens.values()))
access_token = hass.auth.async_create_access_token(refresh_token)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the existing hass_supervisor_access_token fixture instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a drop-in replacement. I'm not entirely sure why at the moment. I'm guessing it has to do with this code:

config_store = HassioConfig(hass)
await config_store.load()
hass.data[DATA_CONFIG_STORE] = config_store
refresh_token = None
if (hassio_user := config_store.data.hassio_user) is not None:
user = await hass.auth.async_get_user(hassio_user)
if user and user.refresh_tokens:
refresh_token = list(user.refresh_tokens.values())[0]
# Migrate old Hass.io users to be admin.
if not user.is_admin:
await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN])
# Migrate old name
if user.name == "Hass.io":
await hass.auth.async_update_user(user, name=HASSIO_USER_NAME)
if refresh_token is None:
user = await hass.auth.async_create_system_user(
HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN]
)
refresh_token = await hass.auth.async_create_refresh_token(user)
config_store.update(hassio_user=user.id)

Currently this fixture has zero usage throughout the hassio tests. The only places it is used is in tests of other integrations mocking Supervisor. I'm wondering if the user it makes for the access token gets booted if the hassio integration is actually loaded by this config_store code.

I can probably fix it but this feels like a separate problem now. Given the context of the PR from the audit I think it would be better to just merge what we have that works and take a look at what's going on with the loading of the Supervisor user as a follow-up effort.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah it relies on the hass_supervisor_user fixture, which uses a mock user. Your fixture uses the real Supervisor user. Ok then, let's go with what we have then.

Comment thread tests/components/hassio/test_websocket_api.py Outdated
@home-assistant home-assistant Bot marked this pull request as draft April 28, 2026 17:47
Copilot AI review requested due to automatic review settings April 28, 2026 18:22
mdegat01 and others added 7 commits April 28, 2026 14:23
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
Update tests that send supervisor/event WebSocket messages to use a
new hass_supervisor_ws_client fixture that authenticates as the
Supervisor user, since the endpoint now requires only_supervisor=True
instead of require_admin.

Also update test_non_admin_publish_supervisor_event_failure to expect
the new error message "Only allowed as Supervisor".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
@mdegat01 mdegat01 force-pushed the hassio-websocket-apis-admin-required branch from ede3aa7 to 59f4a74 Compare April 28, 2026 18:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Comment on lines +135 to +138
async def test_admin_non_supervisor_publish_supervisor_event_failure(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_admin_user: MockUser
) -> None:
"""Test non admin user cannot publish supervisor event."""
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename this test (and its docstring) to reflect what it actually verifies (a non-admin, non-Supervisor user cannot publish Supervisor events).

The current name includes “admin” but the test explicitly removes admin privileges (hass_admin_user.groups = []), which makes the intent ambiguous.

Suggested change
async def test_admin_non_supervisor_publish_supervisor_event_failure(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_admin_user: MockUser
) -> None:
"""Test non admin user cannot publish supervisor event."""
async def test_non_admin_non_supervisor_publish_supervisor_event_failure(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_admin_user: MockUser
) -> None:
"""Test non-admin, non-Supervisor user cannot publish Supervisor event."""

Copilot uses AI. Check for mistakes.
)
msg = await client.receive_json()
assert msg["success"] is False
assert msg["error"]["message"] == "Only allowed as Supervisor"
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert the websocket error code instead of the human-readable message for the Supervisor-only restriction.

The message text is not part of a stable contract and may change, while the decorator consistently sets error.code (e.g. only_supervisor).

Suggested change
assert msg["error"]["message"] == "Only allowed as Supervisor"
assert msg["error"]["code"] == "only_supervisor"

Copilot uses AI. Check for mistakes.
Comment on lines 94 to 96
@callback
@websocket_api.ws_require_user(only_supervisor=True)
@websocket_api.websocket_command(
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align the access restriction for supervisor/event with the PR’s stated intent (admin-only) or update the PR title/description to reflect that this now requires the special Supervisor user.

@ws_require_user(only_supervisor=True) blocks even regular admin users and returns only_supervisor, which is stricter than “require admin” and changes the documented breaking behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +82
async def create_client() -> WebSocketGenerator:
hassio_user_id = hass.data[DATA_CONFIG_STORE].data.hassio_user
hassio_user = await hass.auth.async_get_user(hassio_user_id)
assert hassio_user
assert hassio_user.refresh_tokens
refresh_token = next(iter(hassio_user.refresh_tokens.values()))
access_token = hass.auth.async_create_access_token(refresh_token)
return await hass_ws_client(hass, access_token=access_token)
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the return type annotation of create_client to the actual websocket client type (not WebSocketGenerator).

WebSocketGenerator is a callable that creates a websocket client; create_client itself returns the created websocket client response, so the current annotation is incorrect and can break type checking/autocomplete.

Copilot uses AI. Check for mistakes.
@mdegat01 mdegat01 marked this pull request as ready for review April 28, 2026 18:51
@home-assistant home-assistant Bot requested a review from agners April 28, 2026 18:51
@balloob balloob merged commit 4ea0e6b into dev Apr 28, 2026
43 of 44 checks passed
@balloob balloob deleted the hassio-websocket-apis-admin-required branch April 28, 2026 22:51
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants