From 4ce4bb6271b252d80b20af772c763884a624bd15 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Thu, 2 Nov 2023 11:28:07 -0700 Subject: [PATCH] client/web: limit authorization checks to API calls This completes the migration to setting up authentication state in the client first before fetching any node data or rendering the client view. Notable changes: - `authorizeRequest` is now only enforced on `/api/*` calls (with the exception of /api/auth, which is handled early because it's needed to initially setup auth, particularly for synology) - re-separate the App and WebClient components to ensure that auth is completed before moving on - refactor platform auth (synology and QNAP) to fit into this new structure. Synology no longer returns redirect for auth, but returns authResponse instructing the client to fetch a SynoToken Updates tailscale/corp#14335 Signed-off-by: Will Norris --- client/web/qnap.go | 15 +++---- client/web/src/components/app.tsx | 29 +++++++++++--- client/web/src/hooks/auth.ts | 23 ++++++----- client/web/synology.go | 51 ++++++++---------------- client/web/web.go | 65 +++++++++++++++++-------------- client/web/web_test.go | 8 +--- 6 files changed, 97 insertions(+), 94 deletions(-) diff --git a/client/web/qnap.go b/client/web/qnap.go index 5ccd76798..07145a77c 100644 --- a/client/web/qnap.go +++ b/client/web/qnap.go @@ -9,6 +9,7 @@ package web import ( "crypto/tls" "encoding/xml" + "errors" "fmt" "io" "log" @@ -18,21 +19,17 @@ import ( // authorizeQNAP authenticates the logged-in QNAP user and verifies that they // are authorized to use the web client. -// It reports true if the request is authorized to continue, and false otherwise. -// authorizeQNAP manages writing out any relevant authorization errors to the -// ResponseWriter itself. -func authorizeQNAP(w http.ResponseWriter, r *http.Request) (ok bool) { +// If the user is not authorized to use the client, an error is returned. +func authorizeQNAP(r *http.Request) (ar authResponse, err error) { _, resp, err := qnapAuthn(r) if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return false + return ar, err } if resp.IsAdmin == 0 { - http.Error(w, "user is not an admin", http.StatusForbidden) - return false + return ar, errors.New("user is not an admin") } - return true + return authResponse{OK: true}, nil } type qnapAuthResponse struct { diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx index 8b10ae475..edbd885aa 100644 --- a/client/web/src/components/app.tsx +++ b/client/web/src/components/app.tsx @@ -3,17 +3,36 @@ import React from "react" import LegacyClientView from "src/components/views/legacy-client-view" import LoginClientView from "src/components/views/login-client-view" import ReadonlyClientView from "src/components/views/readonly-client-view" -import useAuth from "src/hooks/auth" +import useAuth, { AuthResponse } from "src/hooks/auth" import useNodeData from "src/hooks/node-data" import ManagementClientView from "./views/management-client-view" export default function App() { - const { data, refreshData, updateNode } = useNodeData() const { data: auth, loading: loadingAuth, waitOnAuth } = useAuth() return (
- {!data || loadingAuth ? ( + {loadingAuth ? ( +
Loading...
// TODO(sonia): add a loading view + ) : ( + + )} +
+ ) +} + +function WebClient({ + auth, + waitOnAuth, +}: { + auth?: AuthResponse + waitOnAuth: () => Promise +}) { + const { data, refreshData, updateNode } = useNodeData() + + return ( + <> + {!data ? (
Loading...
// TODO(sonia): add a loading view ) : data?.Status === "NeedsLogin" || data?.Status === "NoState" ? ( // Client not on a tailnet, render login. @@ -35,8 +54,8 @@ export default function App() { updateNode={updateNode} /> )} - {data && !loadingAuth &&