mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
client/web: add new readonly mode
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 #10979 Signed-off-by: Will Norris <will@tailscale.com>
This commit is contained in:
parent
9f0eaa4464
commit
128c99d4ae
@ -162,7 +162,19 @@ function LoginPopoverContent({
|
|||||||
</div>
|
</div>
|
||||||
{!auth.canManageNode && (
|
{!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 →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : !auth.viewerIdentity ? (
|
||||||
// User is not connected over Tailscale.
|
// User is not connected over Tailscale.
|
||||||
// These states are only possible on the login client.
|
// These states are only possible on the login client.
|
||||||
<>
|
<>
|
||||||
|
@ -12,7 +12,7 @@ export enum AuthType {
|
|||||||
export type AuthResponse = {
|
export type AuthResponse = {
|
||||||
authNeeded?: AuthType
|
authNeeded?: AuthType
|
||||||
canManageNode: boolean
|
canManageNode: boolean
|
||||||
serverMode: "login" | "manage"
|
serverMode: "login" | "readonly" | "manage"
|
||||||
viewerIdentity?: {
|
viewerIdentity?: {
|
||||||
loginName: string
|
loginName: string
|
||||||
nodeName: string
|
nodeName: string
|
||||||
|
@ -95,6 +95,14 @@ type Server struct {
|
|||||||
// In this mode, API calls are authenticated via platform auth.
|
// In this mode, API calls are authenticated via platform auth.
|
||||||
LoginServerMode ServerMode = "login"
|
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
|
// ManageServerMode serves a management client for editing tailscale
|
||||||
// settings of a node.
|
// settings of a node.
|
||||||
//
|
//
|
||||||
@ -154,7 +162,7 @@ type ServerOpts struct {
|
|||||||
// and not the lifespan of the web server.
|
// and not the lifespan of the web server.
|
||||||
func NewServer(opts ServerOpts) (s *Server, err error) {
|
func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case LoginServerMode, ManageServerMode:
|
case LoginServerMode, ReadOnlyServerMode, ManageServerMode:
|
||||||
// valid types
|
// valid types
|
||||||
case "":
|
case "":
|
||||||
return nil, fmt.Errorf("must specify a Mode")
|
return nil, fmt.Errorf("must specify a Mode")
|
||||||
@ -207,10 +215,14 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
|||||||
// The client is secured by limiting the interface it listens on,
|
// The client is secured by limiting the interface it listens on,
|
||||||
// or by authenticating requests before they reach the web client.
|
// or by authenticating requests before they reach the web client.
|
||||||
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
||||||
if s.mode == LoginServerMode {
|
switch s.mode {
|
||||||
|
case LoginServerMode:
|
||||||
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
|
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
|
||||||
metric = "web_login_client_initialization"
|
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))
|
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
|
||||||
metric = "web_client_initialization"
|
metric = "web_client_initialization"
|
||||||
}
|
}
|
||||||
@ -483,7 +495,7 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// First verify platform auth.
|
// First verify platform auth.
|
||||||
// If platform auth is needed, this should happen first.
|
// If platform auth is needed, this should happen first.
|
||||||
if s.mode == LoginServerMode {
|
if s.mode == LoginServerMode || s.mode == ReadOnlyServerMode {
|
||||||
switch distro.Get() {
|
switch distro.Get() {
|
||||||
case distro.Synology:
|
case distro.Synology:
|
||||||
authorized, err := authorizeSynology(r)
|
authorized, err := authorizeSynology(r)
|
||||||
|
@ -42,15 +42,17 @@
|
|||||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
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.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.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
|
return webf
|
||||||
})(),
|
})(),
|
||||||
Exec: runWeb,
|
Exec: runWeb,
|
||||||
}
|
}
|
||||||
|
|
||||||
var webArgs struct {
|
var webArgs struct {
|
||||||
listen string
|
listen string
|
||||||
cgi bool
|
cgi bool
|
||||||
prefix string
|
prefix string
|
||||||
|
readonly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func tlsConfigFromEnvironment() *tls.Config {
|
func tlsConfigFromEnvironment() *tls.Config {
|
||||||
@ -94,20 +96,26 @@ func runWeb(ctx context.Context, args []string) error {
|
|||||||
if prefs, err := localClient.GetPrefs(ctx); err == nil {
|
if prefs, err := localClient.GetPrefs(ctx); err == nil {
|
||||||
existingWebClient = prefs.RunWebClient
|
existingWebClient = prefs.RunWebClient
|
||||||
}
|
}
|
||||||
if !existingWebClient {
|
var startedManagementClient bool // we started the management client
|
||||||
|
if !existingWebClient && !webArgs.readonly {
|
||||||
// Also start full client in tailscaled.
|
// Also start full client in tailscaled.
|
||||||
log.Printf("starting tailscaled web client at %s:%d\n", selfIP.String(), web.ListenPort)
|
log.Printf("starting tailscaled web client at %s:%d\n", selfIP.String(), web.ListenPort)
|
||||||
if err := setRunWebClient(ctx, true); err != nil {
|
if err := setRunWebClient(ctx, true); err != nil {
|
||||||
return fmt.Errorf("starting web client in tailscaled: %w", err)
|
return fmt.Errorf("starting web client in tailscaled: %w", err)
|
||||||
}
|
}
|
||||||
|
startedManagementClient = true
|
||||||
}
|
}
|
||||||
|
|
||||||
webServer, err := web.NewServer(web.ServerOpts{
|
opts := web.ServerOpts{
|
||||||
Mode: web.LoginServerMode,
|
Mode: web.LoginServerMode,
|
||||||
CGIMode: webArgs.cgi,
|
CGIMode: webArgs.cgi,
|
||||||
PathPrefix: webArgs.prefix,
|
PathPrefix: webArgs.prefix,
|
||||||
LocalClient: &localClient,
|
LocalClient: &localClient,
|
||||||
})
|
}
|
||||||
|
if webArgs.readonly {
|
||||||
|
opts.Mode = web.ReadOnlyServerMode
|
||||||
|
}
|
||||||
|
webServer, err := web.NewServer(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("tailscale.web: %v", err)
|
log.Printf("tailscale.web: %v", err)
|
||||||
return err
|
return err
|
||||||
@ -117,10 +125,10 @@ func runWeb(ctx context.Context, args []string) error {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// Shutdown the server.
|
// Shutdown the server.
|
||||||
webServer.Shutdown()
|
webServer.Shutdown()
|
||||||
if !webArgs.cgi && !existingWebClient {
|
if !webArgs.cgi && startedManagementClient {
|
||||||
log.Println("stopping tailscaled web client")
|
log.Println("stopping tailscaled web client")
|
||||||
// When not in cgi mode, shut down the tailscaled
|
// 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 {
|
if err := setRunWebClient(context.Background(), false); err != nil {
|
||||||
log.Printf("stopping tailscaled web client: %v", err)
|
log.Printf("stopping tailscaled web client: %v", err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user