mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
client/web: hide admin panel links for non-tailscale control servers
Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
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>
|
||||
|
57
client/web/src/components/control-components.tsx
Normal file
57
client/web/src/components/control-components.tsx
Normal file
@ -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">
|
||||
<Control.AdminContainer
|
||||
className="text-neutral-500 text-sm leading-tight text-center"
|
||||
node={node}
|
||||
>
|
||||
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.AdminLink node={node} path={`/machines/${node.IP}`}>
|
||||
this device’s 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">
|
||||
<Control.AdminContainer
|
||||
className="text-neutral-500 text-sm leading-tight"
|
||||
node={node}
|
||||
>
|
||||
Remember to make sure that the{" "}
|
||||
<a
|
||||
href="https://login.tailscale.com/admin/acls/"
|
||||
className="text-indigo-700"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<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">
|
||||
<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{" "}
|
||||
<a
|
||||
href={`https://login.tailscale.com/admin/machines/${node.IP}`}
|
||||
className="text-indigo-700"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Control.AdminLink node={node} path={`/machines/${node.IP}`}>
|
||||
the machine’s 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,7 +561,8 @@ type nodeData struct {
|
||||
|
||||
ClientVersion *tailcfg.ClientVersion
|
||||
|
||||
LicensesURL string
|
||||
ControlAdminURL string
|
||||
LicensesURL string
|
||||
}
|
||||
|
||||
type subnetRoute struct {
|
||||
@ -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…
x
Reference in New Issue
Block a user