Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added lib/iris/dashboard/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions lib/iris/dashboard/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineConfig({
templateParameters: {
title: 'Iris Dashboard',
},
favicon: './favicon.ico',
},
server: {
proxy: {
Expand Down
31 changes: 11 additions & 20 deletions lib/iris/src/iris/cluster/controller/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import logging
import os
from collections.abc import Callable
from http.cookies import SimpleCookie
from urllib.parse import urlparse

Expand All @@ -39,7 +38,15 @@

from iris.cluster.controller.actor_proxy import PROXY_ROUTE, ActorProxy
from iris.cluster.controller.service import ControllerServiceImpl
from iris.cluster.dashboard_common import html_shell, on_shutdown, static_files_mount
from iris.cluster.dashboard_common import (
_AUTH_POLICY_ATTR,
favicon_route,
html_shell,
on_shutdown,
public,
requires_auth,
static_files_mount,
)
from iris.log_server.client import LogServiceProxy
from iris.log_server.server import LogServiceImpl
from iris.rpc.auth import SESSION_COOKIE, NullAuthInterceptor, TokenVerifier, extract_bearer_token, resolve_auth
Expand All @@ -52,24 +59,6 @@

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Route auth policy annotations
# ---------------------------------------------------------------------------

_AUTH_POLICY_ATTR = "_auth_policy"


def public(fn: Callable) -> Callable:
"""Mark a route handler as publicly accessible (no auth required)."""
setattr(fn, _AUTH_POLICY_ATTR, "public")
return fn


def requires_auth(fn: Callable) -> Callable:
"""Mark a route handler as requiring authentication via session cookie or Bearer token."""
setattr(fn, _AUTH_POLICY_ATTR, "requires_auth")
return fn


def _extract_token_from_scope(scope: Scope) -> str | None:
"""Extract auth token from ASGI scope (cookie or Authorization header)."""
Expand Down Expand Up @@ -318,6 +307,7 @@ async def _proxy_actor_rpc(request: Request) -> Response:

routes = [
Route("/", self._dashboard),
favicon_route(),
Route("/auth/session_bootstrap", self._session_bootstrap),
Route("/auth/config", self._auth_config),
Route("/auth/session", self._auth_session, methods=["POST"]),
Expand Down Expand Up @@ -484,6 +474,7 @@ def app(self) -> Starlette:
def _create_app(self) -> Starlette:
routes = [
Route("/", self._dashboard),
favicon_route(),
Route("/job/{job_id:path}", self._job_detail_page),
Route("/worker/{worker_id:path}", self._worker_detail_page),
Route("/bundles/{bundle_id:str}.zip", self._proxy_bundle),
Expand Down
43 changes: 41 additions & 2 deletions lib/iris/src/iris/cluster/dashboard_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,32 @@
from typing import Any

from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Mount
from starlette.requests import Request
from starlette.responses import PlainTextResponse, Response
from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles
from starlette.types import ASGIApp, Receive, Scope, Send

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Route auth policy annotations
# ---------------------------------------------------------------------------

_AUTH_POLICY_ATTR = "_auth_policy"


def public(fn: Callable) -> Callable:
"""Mark a route handler as publicly accessible (no auth required)."""
setattr(fn, _AUTH_POLICY_ATTR, "public")
return fn


def requires_auth(fn: Callable) -> Callable:
"""Mark a route handler as requiring authentication via session cookie or Bearer token."""
setattr(fn, _AUTH_POLICY_ATTR, "requires_auth")
return fn


def on_shutdown(
*callbacks: Callable[[], Awaitable[None]],
Expand Down Expand Up @@ -115,6 +134,26 @@ def static_files_mount() -> Mount:
return Mount("/static", app=_LazyStaticFiles(), name="static")


@public
def _favicon(_request: Request) -> Response:
dist = _vue_dist_dir()
if dist is None:
return Response(status_code=404)
favicon_path = dist / "favicon.ico"
if not favicon_path.exists():
return Response(status_code=404)
return Response(
content=favicon_path.read_bytes(),
media_type="image/x-icon",
headers={"cache-control": f"public, max-age={STATIC_MAX_AGE_SECONDS}"},
)


def favicon_route() -> Route:
"""Route for serving favicon.ico from the Vue dashboard dist root."""
return Route("/favicon.ico", _favicon)
Comment on lines +152 to +154
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark favicon route as public for auth-enabled controller

The new Route("/favicon.ico", _favicon) endpoint is unannotated, and _RouteAuthMiddleware default-denies any matched Route without @public/@requires_auth when dashboard auth is enabled. In clusters with auth configured, requests to /favicon.ico will therefore return 401 (not the icon), so the feature does not work in the primary authenticated deployment path; this route should be explicitly marked public (or otherwise skipped by policy) like other unauthenticated UI assets.

Useful? React with 👍 / 👎.

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.

Should be fixed now.



_NOT_BUILT_HTML = """\
<!doctype html>
<html><body>
Expand Down
3 changes: 2 additions & 1 deletion lib/iris/src/iris/cluster/worker/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from starlette.routing import Mount, Route

from iris.cluster.worker.service import WorkerServiceImpl
from iris.cluster.dashboard_common import html_shell, static_files_mount
from iris.cluster.dashboard_common import favicon_route, html_shell, static_files_mount
from iris.rpc.worker_connect import WorkerServiceWSGIApplication


Expand Down Expand Up @@ -43,6 +43,7 @@ def _create_app(self) -> Starlette:
routes = [
Route("/health", self._health),
Route("/", self._dashboard),
favicon_route(),
Route("/task/{task_id:path}", self._task_detail_page),
Route("/status", self._status_page),
static_files_mount(),
Expand Down
Loading