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 11 months ago committed by Sonia Appasamy
parent 8af503b0c5
commit bcc9b44cb1

@ -73,7 +73,7 @@ function WebClient({
<Route path="/ssh">
<SSHView
readonly={!auth.canManageNode}
runningSSH={data.RunningSSHServer}
node={data}
nodeUpdaters={nodeUpdaters}
/>
</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 React from "react"
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 { NodeData } from "src/hooks/node-data"
import { useLocation } from "wouter"
import ACLTag from "../acl-tag"
export default function DeviceDetailsView({
readonly,
@ -63,7 +64,7 @@ export default function DeviceDetailsView({
<td className="flex gap-1 flex-wrap">
{node.IsTagged
? node.Tags.map((t) => <ACLTag key={t} tag={t} />)
: node.Profile.DisplayName}
: node.Profile?.DisplayName}
</td>
</tr>
<tr>
@ -119,19 +120,16 @@ export default function DeviceDetailsView({
</tbody>
</table>
</div>
<p className="text-neutral-500 text-sm leading-tight text-center">
Want even more details? Visit{" "}
<a
// TODO: pipe control serve url from backend
href="https://login.tailscale.com/admin"
target="_blank"
rel="noreferrer"
className="text-indigo-700 text-sm"
<Control.AdminContainer
className="text-neutral-500 text-sm leading-tight text-center"
node={node}
>
Want even more details? Visit{" "}
<Control.AdminLink node={node} path={`/machines/${node.IP}`}>
this devices page
</a>{" "}
</Control.AdminLink>{" "}
in the admin console.
</p>
</Control.AdminContainer>
</div>
</>
)

@ -2,16 +2,17 @@
// SPDX-License-Identifier: BSD-3-Clause
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"
export default function SSHView({
readonly,
runningSSH,
node,
nodeUpdaters,
}: {
readonly: boolean
runningSSH: boolean
node: NodeData
nodeUpdaters: NodeUpdaters
}) {
return (
@ -31,9 +32,12 @@ export default function SSHView({
</p>
<div className="-mx-5 px-4 py-3 bg-white rounded-lg border border-gray-200 flex gap-2.5 mb-3">
<Toggle
checked={runningSSH}
checked={node.RunningSSHServer}
onChange={() =>
nodeUpdaters.patchPrefs({ RunSSHSet: true, RunSSH: !runningSSH })
nodeUpdaters.patchPrefs({
RunSSHSet: true,
RunSSH: !node.RunningSSHServer,
})
}
disabled={readonly}
/>
@ -41,18 +45,16 @@ export default function SSHView({
Run Tailscale SSH server
</div>
</div>
<p className="text-neutral-500 text-sm leading-tight">
Remember to make sure that the{" "}
<a
href="https://login.tailscale.com/admin/acls/"
className="text-indigo-700"
target="_blank"
rel="noreferrer"
<Control.AdminContainer
className="text-neutral-500 text-sm leading-tight"
node={node}
>
Remember to make sure that the{" "}
<Control.AdminLink node={node} path="/acls">
tailnet policy file
</a>{" "}
</Control.AdminLink>{" "}
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 Clock } from "src/assets/icons/clock.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 Button from "src/ui/button"
import Input from "src/ui/input"
@ -122,18 +123,16 @@ export default function SubnetRouterView({
</div>
))}
</div>
<div className="mt-3 w-full text-center text-neutral-500 text-sm leading-tight">
To approve routes, in the admin console go to{" "}
<a
href={`https://login.tailscale.com/admin/machines/${node.IP}`}
className="text-indigo-700"
target="_blank"
rel="noreferrer"
<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{" "}
<Control.AdminLink node={node} path={`/machines/${node.IP}`}>
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">

@ -19,7 +19,6 @@ export type NodeData = {
UsingExitNode?: ExitNode
AdvertisingExitNode: boolean
AdvertisedRoutes?: SubnetRoute[]
LicensesURL: string
TUNMode: boolean
IsSynology: boolean
DSMVersion: number
@ -33,6 +32,8 @@ export type NodeData = {
IsTagged: boolean
Tags: string[]
RunningSSHServer: boolean
ControlAdminURL: string
LicensesURL: string
}
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,6 +561,7 @@ type nodeData struct {
ClientVersion *tailcfg.ClientVersion
ControlAdminURL string
LicensesURL string
}
@ -596,8 +597,10 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
RunningSSHServer: prefs.RunSSH,
URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
ControlAdminURL: prefs.AdminPageURL(),
LicensesURL: licenses.LicensesURL(),
}
cv, err := s.lc.CheckUpdate(r.Context())
if err != nil {
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
url = "https://login.tailscale.com"
}
return url + "/admin/machines"
return url + "/admin"
}
// AdvertisesExitNode reports whether p is advertising both the v4 and

Loading…
Cancel
Save