Skip to content

Commit

Permalink
client/web: add new readonly mode
Browse files Browse the repository at this point in the history
The new read-only mode is only accessible when running `tailscale web`
by passing a new `-readonly` flag. This new mode is identical to the
existing login mode with two exceptions:

 - the management client in tailscaled is not started (though if it is
   already running, it is left alone)

 - the client does not prompt the user to login or switch to the
   management client. Instead, a message is shown instructing the user
   to use other means to manage the device.

Updates tailscale#10979

Signed-off-by: Will Norris <will@tailscale.com>
  • Loading branch information
willnorris committed Feb 8, 2024
1 parent 9f0eaa4 commit 128c99d
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 14 deletions.
14 changes: 13 additions & 1 deletion client/web/src/components/login-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,19 @@ function LoginPopoverContent({
</div>
{!auth.canManageNode && (
<>
{!auth.viewerIdentity ? (
{auth.serverMode === "readonly" ? (
<p className="text-gray-500 text-xs">
This web interface is running in read-only mode.{" "}
<a
href="https://tailscale.com/s/web-client-read-only"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
) : !auth.viewerIdentity ? (
// User is not connected over Tailscale.
// These states are only possible on the login client.
<>
Expand Down
2 changes: 1 addition & 1 deletion client/web/src/hooks/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export enum AuthType {
export type AuthResponse = {
authNeeded?: AuthType
canManageNode: boolean
serverMode: "login" | "manage"
serverMode: "login" | "readonly" | "manage"
viewerIdentity?: {
loginName: string
nodeName: string
Expand Down
20 changes: 16 additions & 4 deletions client/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ const (
// In this mode, API calls are authenticated via platform auth.
LoginServerMode ServerMode = "login"

// ReadOnlyServerMode is identical to LoginServerMode,
// but does not present a login button to switch to manage mode,
// even if the management client is running and reachable.
//
// This is designed for platforms where the device is configured by other means,
// such as Home Assistant's declarative YAML configuration.
ReadOnlyServerMode ServerMode = "readonly"

// ManageServerMode serves a management client for editing tailscale
// settings of a node.
//
Expand Down Expand Up @@ -154,7 +162,7 @@ type ServerOpts struct {
// and not the lifespan of the web server.
func NewServer(opts ServerOpts) (s *Server, err error) {
switch opts.Mode {
case LoginServerMode, ManageServerMode:
case LoginServerMode, ReadOnlyServerMode, ManageServerMode:
// valid types
case "":
return nil, fmt.Errorf("must specify a Mode")
Expand Down Expand Up @@ -207,10 +215,14 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
// The client is secured by limiting the interface it listens on,
// or by authenticating requests before they reach the web client.
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
if s.mode == LoginServerMode {
switch s.mode {
case LoginServerMode:
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
metric = "web_login_client_initialization"
} else {
case ReadOnlyServerMode:
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
metric = "web_readonly_client_initialization"
case ManageServerMode:
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
metric = "web_client_initialization"
}
Expand Down Expand Up @@ -483,7 +495,7 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {

// First verify platform auth.
// If platform auth is needed, this should happen first.
if s.mode == LoginServerMode {
if s.mode == LoginServerMode || s.mode == ReadOnlyServerMode {
switch distro.Get() {
case distro.Synology:
authorized, err := authorizeSynology(r)
Expand Down
24 changes: 16 additions & 8 deletions cmd/tailscale/cli/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ Tailscale, as opposed to a CLI or a native app.
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
webf.StringVar(&webArgs.prefix, "prefix", "", "URL prefix added to requests (for cgi or reverse proxies)")
webf.BoolVar(&webArgs.readonly, "readonly", false, "run web UI in read-only mode")
return webf
})(),
Exec: runWeb,
}

var webArgs struct {
listen string
cgi bool
prefix string
listen string
cgi bool
prefix string
readonly bool
}

func tlsConfigFromEnvironment() *tls.Config {
Expand Down Expand Up @@ -94,20 +96,26 @@ func runWeb(ctx context.Context, args []string) error {
if prefs, err := localClient.GetPrefs(ctx); err == nil {
existingWebClient = prefs.RunWebClient
}
if !existingWebClient {
var startedManagementClient bool // we started the management client
if !existingWebClient && !webArgs.readonly {
// Also start full client in tailscaled.
log.Printf("starting tailscaled web client at %s:%d\n", selfIP.String(), web.ListenPort)
if err := setRunWebClient(ctx, true); err != nil {
return fmt.Errorf("starting web client in tailscaled: %w", err)
}
startedManagementClient = true
}

webServer, err := web.NewServer(web.ServerOpts{
opts := web.ServerOpts{
Mode: web.LoginServerMode,
CGIMode: webArgs.cgi,
PathPrefix: webArgs.prefix,
LocalClient: &localClient,
})
}
if webArgs.readonly {
opts.Mode = web.ReadOnlyServerMode
}
webServer, err := web.NewServer(opts)
if err != nil {
log.Printf("tailscale.web: %v", err)
return err
Expand All @@ -117,10 +125,10 @@ func runWeb(ctx context.Context, args []string) error {
case <-ctx.Done():
// Shutdown the server.
webServer.Shutdown()
if !webArgs.cgi && !existingWebClient {
if !webArgs.cgi && startedManagementClient {
log.Println("stopping tailscaled web client")
// When not in cgi mode, shut down the tailscaled
// web client on cli termination.
// web client on cli termination if we started it.
if err := setRunWebClient(context.Background(), false); err != nil {
log.Printf("stopping tailscaled web client: %v", err)
}
Expand Down

0 comments on commit 128c99d

Please sign in to comment.