client/web: hide admin panel links for non-tailscale control servers

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
pull/10404/head^2
Sonia Appasamy 1 year ago committed by Sonia Appasamy
parent 8af503b0c5
commit bcc9b44cb1

@ -73,7 +73,7 @@ function WebClient({
<Route path="/ssh"> <Route path="/ssh">
<SSHView <SSHView
readonly={!auth.canManageNode} readonly={!auth.canManageNode}
runningSSH={data.RunningSSHServer} node={data}
nodeUpdaters={nodeUpdaters} nodeUpdaters={nodeUpdaters}
/> />
</Route> </Route>

@ -0,0 +1,57 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
import { NodeData } from "src/hooks/node-data"
/**
* AdminContainer renders its contents only if the node's control
* server has an admin panel.
*
* TODO(sonia,will): Similarly, this could also hide the contents
* if the viewing user is a non-admin.
*/
export function AdminContainer({
node,
children,
className,
}: {
node: NodeData
children: React.ReactNode
className?: string
}) {
if (!node.ControlAdminURL.includes("tailscale.com")) {
// Admin panel only exists on Tailscale control servers.
return null
}
return <div className={className}>{children}</div>
}
/**
* AdminLink renders its contents wrapped in a link to the node's control
* server admin panel.
*
* AdminLink is meant for use only inside of a AdminContainer component,
* to avoid rendering a link when the node's control server does not have
* an admin panel.
*/
export function AdminLink({
node,
children,
path,
}: {
node: NodeData
children: React.ReactNode
path: string // admin path, e.g. "/settings/webhooks"
}) {
return (
<a
href={`${node.ControlAdminURL}${path}`}
className="link"
target="_blank"
rel="noreferrer"
>
{children}
</a>
)
}

@ -4,10 +4,11 @@
import cx from "classnames" import cx from "classnames"
import React from "react" import React from "react"
import { apiFetch } from "src/api" import { apiFetch } from "src/api"
import ACLTag from "src/components/acl-tag"
import * as Control from "src/components/control-components"
import { UpdateAvailableNotification } from "src/components/update-available" import { UpdateAvailableNotification } from "src/components/update-available"
import { NodeData } from "src/hooks/node-data" import { NodeData } from "src/hooks/node-data"
import { useLocation } from "wouter" import { useLocation } from "wouter"
import ACLTag from "../acl-tag"
export default function DeviceDetailsView({ export default function DeviceDetailsView({
readonly, readonly,
@ -63,7 +64,7 @@ export default function DeviceDetailsView({
<td className="flex gap-1 flex-wrap"> <td className="flex gap-1 flex-wrap">
{node.IsTagged {node.IsTagged
? node.Tags.map((t) => <ACLTag key={t} tag={t} />) ? node.Tags.map((t) => <ACLTag key={t} tag={t} />)
: node.Profile.DisplayName} : node.Profile?.DisplayName}
</td> </td>
</tr> </tr>
<tr> <tr>
@ -119,19 +120,16 @@ export default function DeviceDetailsView({
</tbody> </tbody>
</table> </table>
</div> </div>
<p className="text-neutral-500 text-sm leading-tight text-center"> <Control.AdminContainer
className="text-neutral-500 text-sm leading-tight text-center"
node={node}
>
Want even more details? Visit{" "} Want even more details? Visit{" "}
<a <Control.AdminLink node={node} path={`/machines/${node.IP}`}>
// TODO: pipe control serve url from backend
href="https://login.tailscale.com/admin"
target="_blank"
rel="noreferrer"
className="text-indigo-700 text-sm"
>
this devices page this devices page
</a>{" "} </Control.AdminLink>{" "}
in the admin console. in the admin console.
</p> </Control.AdminContainer>
</div> </div>
</> </>
) )

@ -2,16 +2,17 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React from "react" import React from "react"
import { NodeUpdaters } from "src/hooks/node-data" import * as Control from "src/components/control-components"
import { NodeData, NodeUpdaters } from "src/hooks/node-data"
import Toggle from "src/ui/toggle" import Toggle from "src/ui/toggle"
export default function SSHView({ export default function SSHView({
readonly, readonly,
runningSSH, node,
nodeUpdaters, nodeUpdaters,
}: { }: {
readonly: boolean readonly: boolean
runningSSH: boolean node: NodeData
nodeUpdaters: NodeUpdaters nodeUpdaters: NodeUpdaters
}) { }) {
return ( return (
@ -31,9 +32,12 @@ export default function SSHView({
</p> </p>
<div className="-mx-5 px-4 py-3 bg-white rounded-lg border border-gray-200 flex gap-2.5 mb-3"> <div className="-mx-5 px-4 py-3 bg-white rounded-lg border border-gray-200 flex gap-2.5 mb-3">
<Toggle <Toggle
checked={runningSSH} checked={node.RunningSSHServer}
onChange={() => onChange={() =>
nodeUpdaters.patchPrefs({ RunSSHSet: true, RunSSH: !runningSSH }) nodeUpdaters.patchPrefs({
RunSSHSet: true,
RunSSH: !node.RunningSSHServer,
})
} }
disabled={readonly} disabled={readonly}
/> />
@ -41,18 +45,16 @@ export default function SSHView({
Run Tailscale SSH server Run Tailscale SSH server
</div> </div>
</div> </div>
<p className="text-neutral-500 text-sm leading-tight"> <Control.AdminContainer
className="text-neutral-500 text-sm leading-tight"
node={node}
>
Remember to make sure that the{" "} Remember to make sure that the{" "}
<a <Control.AdminLink node={node} path="/acls">
href="https://login.tailscale.com/admin/acls/"
className="text-indigo-700"
target="_blank"
rel="noreferrer"
>
tailnet policy file tailnet policy file
</a>{" "} </Control.AdminLink>{" "}
allows other devices to SSH into this device. allows other devices to SSH into this device.
</p> </Control.AdminContainer>
</> </>
) )
} }

@ -5,6 +5,7 @@ import React, { useMemo, useState } from "react"
import { ReactComponent as CheckCircle } from "src/assets/icons/check-circle.svg" import { ReactComponent as CheckCircle } from "src/assets/icons/check-circle.svg"
import { ReactComponent as Clock } from "src/assets/icons/clock.svg" import { ReactComponent as Clock } from "src/assets/icons/clock.svg"
import { ReactComponent as Plus } from "src/assets/icons/plus.svg" import { ReactComponent as Plus } from "src/assets/icons/plus.svg"
import * as Control from "src/components/control-components"
import { NodeData, NodeUpdaters } from "src/hooks/node-data" import { NodeData, NodeUpdaters } from "src/hooks/node-data"
import Button from "src/ui/button" import Button from "src/ui/button"
import Input from "src/ui/input" import Input from "src/ui/input"
@ -122,18 +123,16 @@ export default function SubnetRouterView({
</div> </div>
))} ))}
</div> </div>
<div className="mt-3 w-full text-center text-neutral-500 text-sm leading-tight"> <Control.AdminContainer
className="mt-3 w-full text-center text-neutral-500 text-sm leading-tight"
node={node}
>
To approve routes, in the admin console go to{" "} To approve routes, in the admin console go to{" "}
<a <Control.AdminLink node={node} path={`/machines/${node.IP}`}>
href={`https://login.tailscale.com/admin/machines/${node.IP}`}
className="text-indigo-700"
target="_blank"
rel="noreferrer"
>
the machines route settings the machines route settings
</a> </Control.AdminLink>
. .
</div> </Control.AdminContainer>
</> </>
) : ( ) : (
<div className="px-5 py-4 bg-stone-50 rounded-lg border border-gray-200 text-center text-neutral-500"> <div className="px-5 py-4 bg-stone-50 rounded-lg border border-gray-200 text-center text-neutral-500">

@ -19,7 +19,6 @@ export type NodeData = {
UsingExitNode?: ExitNode UsingExitNode?: ExitNode
AdvertisingExitNode: boolean AdvertisingExitNode: boolean
AdvertisedRoutes?: SubnetRoute[] AdvertisedRoutes?: SubnetRoute[]
LicensesURL: string
TUNMode: boolean TUNMode: boolean
IsSynology: boolean IsSynology: boolean
DSMVersion: number DSMVersion: number
@ -33,6 +32,8 @@ export type NodeData = {
IsTagged: boolean IsTagged: boolean
Tags: string[] Tags: string[]
RunningSSHServer: boolean RunningSSHServer: boolean
ControlAdminURL: string
LicensesURL: string
} }
type NodeState = type NodeState =
@ -204,5 +205,10 @@ export default function useNodeData() {
] ]
) )
return { data, refreshData, nodeUpdaters, isPosting } return {
data: { ...data, ControlAdminURL: "somehting.com" },
refreshData,
nodeUpdaters,
isPosting,
}
} }

@ -561,7 +561,8 @@ type nodeData struct {
ClientVersion *tailcfg.ClientVersion ClientVersion *tailcfg.ClientVersion
LicensesURL string ControlAdminURL string
LicensesURL string
} }
type subnetRoute struct { type subnetRoute struct {
@ -596,8 +597,10 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"), UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
RunningSSHServer: prefs.RunSSH, RunningSSHServer: prefs.RunSSH,
URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"), URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
ControlAdminURL: prefs.AdminPageURL(),
LicensesURL: licenses.LicensesURL(), LicensesURL: licenses.LicensesURL(),
} }
cv, err := s.lc.CheckUpdate(r.Context()) cv, err := s.lc.CheckUpdate(r.Context())
if err != nil { if err != nil {
s.logf("could not check for updates: %v", err) s.logf("could not check for updates: %v", err)

@ -570,7 +570,7 @@ func (p *Prefs) AdminPageURL() string {
// TODO(crawshaw): In future release, make this https://console.tailscale.com // TODO(crawshaw): In future release, make this https://console.tailscale.com
url = "https://login.tailscale.com" url = "https://login.tailscale.com"
} }
return url + "/admin/machines" return url + "/admin"
} }
// AdvertisesExitNode reports whether p is advertising both the v4 and // AdvertisesExitNode reports whether p is advertising both the v4 and

Loading…
Cancel
Save