mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-16 10:38:39 +00:00
cmd/tailscale/cli,ipn/ipnlocal: restrict logout when AlwaysOn mode is enabled
In this PR, we start passing a LocalAPI actor to (*LocalBackend).Logout to make it subject to the same access check as disconnects made via tailscale down or the GUI. We then update the CLI to allow `tailscale logout` to accept a reason, similar to `tailscale down`. Updates tailscale/corp#26249 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
5b0074729d
commit
1fe82d6ef5
@ -5,12 +5,18 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/client/tailscale/apitype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logoutArgs struct {
|
||||||
|
reason string
|
||||||
|
}
|
||||||
|
|
||||||
var logoutCmd = &ffcli.Command{
|
var logoutCmd = &ffcli.Command{
|
||||||
Name: "logout",
|
Name: "logout",
|
||||||
ShortUsage: "tailscale logout",
|
ShortUsage: "tailscale logout",
|
||||||
@ -22,11 +28,17 @@ the current node key, forcing a future use of it to cause
|
|||||||
a reauthentication.
|
a reauthentication.
|
||||||
`),
|
`),
|
||||||
Exec: runLogout,
|
Exec: runLogout,
|
||||||
|
FlagSet: (func() *flag.FlagSet {
|
||||||
|
fs := newFlagSet("logout")
|
||||||
|
fs.StringVar(&logoutArgs.reason, "reason", "", "reason for the logout, if required by a policy")
|
||||||
|
return fs
|
||||||
|
})(),
|
||||||
}
|
}
|
||||||
|
|
||||||
func runLogout(ctx context.Context, args []string) error {
|
func runLogout(ctx context.Context, args []string) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||||
}
|
}
|
||||||
|
ctx = apitype.RequestReasonKey.WithValue(ctx, logoutArgs.reason)
|
||||||
return localClient.Logout(ctx)
|
return localClient.Logout(ctx)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnauth"
|
||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
"tailscale.com/ipn/ipnserver"
|
"tailscale.com/ipn/ipnserver"
|
||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
@ -336,7 +337,7 @@ func (i *jsIPN) logout() {
|
|||||||
go func() {
|
go func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
i.lb.Logout(ctx)
|
i.lb.Logout(ctx, ipnauth.Self)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1077,7 +1077,7 @@ func (b *LocalBackend) Shutdown() {
|
|||||||
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
err := b.Logout(ctx) // best effort
|
err := b.Logout(ctx, ipnauth.Self) // best effort
|
||||||
td := time.Since(t0).Round(time.Millisecond)
|
td := time.Since(t0).Round(time.Millisecond)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.logf("failed to log out ephemeral node on shutdown after %v: %v", td, err)
|
b.logf("failed to log out ephemeral node on shutdown after %v: %v", td, err)
|
||||||
@ -5884,7 +5884,7 @@ func (b *LocalBackend) ShouldHandleViaIP(ip netip.Addr) bool {
|
|||||||
|
|
||||||
// Logout logs out the current profile, if any, and waits for the logout to
|
// Logout logs out the current profile, if any, and waits for the logout to
|
||||||
// complete.
|
// complete.
|
||||||
func (b *LocalBackend) Logout(ctx context.Context) error {
|
func (b *LocalBackend) Logout(ctx context.Context, actor ipnauth.Actor) error {
|
||||||
unlock := b.lockAndGetUnlock()
|
unlock := b.lockAndGetUnlock()
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
@ -5898,11 +5898,8 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
|||||||
// delete it later.
|
// delete it later.
|
||||||
profile := b.pm.CurrentProfile()
|
profile := b.pm.CurrentProfile()
|
||||||
|
|
||||||
// TODO(nickkhyl): change [LocalBackend.Logout] to accept an [ipnauth.Actor].
|
|
||||||
// This will allow enforcing Always On mode when a user tries to log out
|
|
||||||
// while logged in and connected. See tailscale/corp#26249.
|
|
||||||
_, err := b.editPrefsLockedOnEntry(
|
_, err := b.editPrefsLockedOnEntry(
|
||||||
ipnauth.TODO,
|
actor,
|
||||||
&ipn.MaskedPrefs{
|
&ipn.MaskedPrefs{
|
||||||
WantRunningSet: true,
|
WantRunningSet: true,
|
||||||
LoggedOutSet: true,
|
LoggedOutSet: true,
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnauth"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/net/dns"
|
"tailscale.com/net/dns"
|
||||||
@ -607,7 +608,7 @@ func TestStateMachine(t *testing.T) {
|
|||||||
store.awaitWrite()
|
store.awaitWrite()
|
||||||
t.Logf("\n\nLogout")
|
t.Logf("\n\nLogout")
|
||||||
notifies.expect(5)
|
notifies.expect(5)
|
||||||
b.Logout(context.Background())
|
b.Logout(context.Background(), ipnauth.Self)
|
||||||
{
|
{
|
||||||
nn := notifies.drain(5)
|
nn := notifies.drain(5)
|
||||||
previousCC.assertCalls("pause", "Logout", "unpause", "Shutdown")
|
previousCC.assertCalls("pause", "Logout", "unpause", "Shutdown")
|
||||||
@ -637,7 +638,7 @@ func TestStateMachine(t *testing.T) {
|
|||||||
// A second logout should be a no-op as we are in the NeedsLogin state.
|
// A second logout should be a no-op as we are in the NeedsLogin state.
|
||||||
t.Logf("\n\nLogout2")
|
t.Logf("\n\nLogout2")
|
||||||
notifies.expect(0)
|
notifies.expect(0)
|
||||||
b.Logout(context.Background())
|
b.Logout(context.Background(), ipnauth.Self)
|
||||||
{
|
{
|
||||||
notifies.drain(0)
|
notifies.drain(0)
|
||||||
cc.assertCalls()
|
cc.assertCalls()
|
||||||
@ -650,7 +651,7 @@ func TestStateMachine(t *testing.T) {
|
|||||||
// AuthCantContinue state.
|
// AuthCantContinue state.
|
||||||
t.Logf("\n\nLogout3")
|
t.Logf("\n\nLogout3")
|
||||||
notifies.expect(3)
|
notifies.expect(3)
|
||||||
b.Logout(context.Background())
|
b.Logout(context.Background(), ipnauth.Self)
|
||||||
{
|
{
|
||||||
notifies.drain(0)
|
notifies.drain(0)
|
||||||
cc.assertCalls()
|
cc.assertCalls()
|
||||||
|
@ -1460,7 +1460,7 @@ func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "want POST", http.StatusBadRequest)
|
http.Error(w, "want POST", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := h.b.Logout(r.Context())
|
err := h.b.Logout(r.Context(), h.Actor)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
|
Loading…
x
Reference in New Issue
Block a user