From 7f9e128ae7480b8e643656b52d3349c9045561c9 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 12 Dec 2025 22:45:41 +0100 Subject: [PATCH] Require HA admin user to finish setup on Ingress (#2801) --- music_assistant/controllers/webserver/auth.py | 2 +- .../controllers/webserver/controller.py | 43 ++++- .../webserver/helpers/auth_providers.py | 5 +- music_assistant/helpers/resources/error.html | 149 ++++++++++++++++++ 4 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 music_assistant/helpers/resources/error.html diff --git a/music_assistant/controllers/webserver/auth.py b/music_assistant/controllers/webserver/auth.py index 4c2d948e..cec3e403 100644 --- a/music_assistant/controllers/webserver/auth.py +++ b/music_assistant/controllers/webserver/auth.py @@ -95,7 +95,7 @@ class AuthenticationManager: # Setup login providers based on config await self._setup_login_providers(allow_self_registration) - self._has_users = await self.database.get_count("users") > 0 + self._has_users = await self._has_non_system_users() self.logger.info( "Authentication manager initialized (providers=%d)", len(self.login_providers) diff --git a/music_assistant/controllers/webserver/controller.py b/music_assistant/controllers/webserver/controller.py index f4b491b8..8881f273 100644 --- a/music_assistant/controllers/webserver/controller.py +++ b/music_assistant/controllers/webserver/controller.py @@ -52,7 +52,7 @@ from .helpers.auth_middleware import ( is_request_from_ingress, set_current_user, ) -from .helpers.auth_providers import BuiltinLoginProvider +from .helpers.auth_providers import BuiltinLoginProvider, get_ha_user_role from .remote_access import RemoteAccessManager from .websocket_client import WebsocketClientHandler @@ -598,10 +598,41 @@ class WebserverController(CoreController): swagger_html_path = str(RESOURCES_DIR.joinpath("swagger_ui.html")) return await self._server.serve_static(swagger_html_path, request) + async def _render_error_page(self, error_message: str, status: int = 403) -> web.Response: + """Render a user-friendly error page with the given message. + + :param error_message: The error message to display to the user. + :param status: HTTP status code for the response. + """ + error_html_path = str(RESOURCES_DIR.joinpath("error.html")) + async with aiofiles.open(error_html_path) as f: + html_content = await f.read() + # Replace placeholder with the actual error message (escape to prevent XSS) + html_content = html_content.replace("{{ERROR_MESSAGE}}", html.escape(error_message)) + return web.Response(text=html_content, content_type="text/html", status=status) + async def _handle_index(self, request: web.Request) -> web.StreamResponse: """Handle request for index page (Vue frontend).""" # If not yet onboarded, redirect to setup - if not self.auth.has_users and not is_request_from_ingress(request): + if ( + not self.auth.has_users + and not self.mass.config.onboard_done + and is_request_from_ingress(request) + ): + # a non-admin user tries to access the index via HA ingress + # while we're not yet onboarded, prevent that as it leads to a bad UX + ingress_user_id = request.headers.get("X-Remote-User-ID", "") + role = await get_ha_user_role(self.mass, ingress_user_id) + if role != UserRole.ADMIN: + return await self._render_error_page( + "Administrator permissions are required to complete the initial setup. " + "Please ask a Home Assistant administrator to complete the setup first." + ) + # NOTE: For ingress admin user, + # we allow access to index, user will be auto created and then forwarded to the + # frontend (which will take care of onboarding) + if not self.auth.has_users: + # non ingress request and no users yet, redirect to setup return web.Response(status=302, headers={"Location": "setup"}) # Serve the Vue frontend index.html return await self._server.serve_static(self._index_path, request) @@ -930,11 +961,11 @@ class WebserverController(CoreController): if not is_valid: return web.Response(status=400, text="Invalid return_url") else: - return_url = "/login" - # check if setup is already completed + return_url = "/" + if self.auth.has_users: - # Setup already completed, redirect to login (or provided return_url) - return web.Response(status=302, headers={"Location": return_url}) + # this should not happen, but guard anyways + return await self._render_error_page("Setup has already been completed.") setup_html_path = str(RESOURCES_DIR.joinpath("setup.html")) async with aiofiles.open(setup_html_path) as f: diff --git a/music_assistant/controllers/webserver/helpers/auth_providers.py b/music_assistant/controllers/webserver/helpers/auth_providers.py index 51442527..2600e15b 100644 --- a/music_assistant/controllers/webserver/helpers/auth_providers.py +++ b/music_assistant/controllers/webserver/helpers/auth_providers.py @@ -49,8 +49,7 @@ async def get_ha_user_role(mass: MusicAssistant, ha_user_id: str) -> UserRole: try: hass_prov = mass.get_provider("hass") if hass_prov is None or not hass_prov.available: - LOGGER.debug("HA provider not available, returning USER role") - return UserRole.USER + raise RuntimeError("Home Assistant provider not available") hass_prov = cast("HomeAssistantProvider", hass_prov) # Query HA for user list to check admin status @@ -64,7 +63,7 @@ async def get_ha_user_role(mass: MusicAssistant, ha_user_id: str) -> UserRole: return UserRole.ADMIN break except Exception as err: - LOGGER.debug("Failed to check HA admin status: %s", err) + LOGGER.error("Failed to check HA admin status: %s", err) return UserRole.USER diff --git a/music_assistant/helpers/resources/error.html b/music_assistant/helpers/resources/error.html new file mode 100644 index 00000000..5aa506d6 --- /dev/null +++ b/music_assistant/helpers/resources/error.html @@ -0,0 +1,149 @@ + + + + + + Music Assistant - Error + + + + +
+ + +
+ + + +
+ +

Access Denied

+

{{ERROR_MESSAGE}}

+ + Go to Home + +

+ If you believe this is an error, please contact your administrator. +

+
+ + -- 2.34.1