mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-26 10:40:58 +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">
|
<Route path="/ssh">
|
||||||
<SSHView
|
<SSHView
|
||||||
readonly={!auth.canManageNode}
|
readonly={!auth.canManageNode}
|
||||||
runningSSH={data.RunningSSHServer}
|
node={data}
|
||||||
nodeUpdaters={nodeUpdaters}
|
nodeUpdaters={nodeUpdaters}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</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 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
|
||||||
Want even more details? Visit{" "}
|
className="text-neutral-500 text-sm leading-tight text-center"
|
||||||
<a
|
node={node}
|
||||||
// TODO: pipe control serve url from backend
|
|
||||||
href="https://login.tailscale.com/admin"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-indigo-700 text-sm"
|
|
||||||
>
|
>
|
||||||
|
Want even more details? Visit{" "}
|
||||||
|
<Control.AdminLink node={node} path={`/machines/${node.IP}`}>
|
||||||
this device’s page
|
this device’s 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
|
||||||
Remember to make sure that the{" "}
|
className="text-neutral-500 text-sm leading-tight"
|
||||||
<a
|
node={node}
|
||||||
href="https://login.tailscale.com/admin/acls/"
|
|
||||||
className="text-indigo-700"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
>
|
||||||
|
Remember to make sure that the{" "}
|
||||||
|
<Control.AdminLink node={node} path="/acls">
|
||||||
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
|
||||||
To approve routes, in the admin console go to{" "}
|
className="mt-3 w-full text-center text-neutral-500 text-sm leading-tight"
|
||||||
<a
|
node={node}
|
||||||
href={`https://login.tailscale.com/admin/machines/${node.IP}`}
|
|
||||||
className="text-indigo-700"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
>
|
||||||
|
To approve routes, in the admin console go to{" "}
|
||||||
|
<Control.AdminLink node={node} path={`/machines/${node.IP}`}>
|
||||||
the machine’s route settings
|
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">
|
<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,6 +561,7 @@ type nodeData struct {
|
|||||||
|
|
||||||
ClientVersion *tailcfg.ClientVersion
|
ClientVersion *tailcfg.ClientVersion
|
||||||
|
|
||||||
|
ControlAdminURL string
|
||||||
LicensesURL string
|
LicensesURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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…
x
Reference in New Issue
Block a user