tailscale/ipn/ipnauth/testutils.go

163 lines
5.2 KiB
Go
Raw Normal View History

ipn, ipn/ipnauth: implement API surface for LocalBackend access checking We have a lot of access checks spread around the ipnserver, ipnlocal, localapi, and ipnauth packages, with a significant number of platform-specific checks that are used exclusively on either Windows or Unix-like platforms. Additionally, with the exception of a few Windows-specific checks, most of these checks are per-device rather than per-profile, which is not always correct even on single-user/single-session environments, but even more problematic on multi-user/multi-session environments such as Windows. We initially attempted to map all possible operations onto the permitRead/permitWrite access flags. However, these flags are not utilized on Windows and prove insufficient on Unix machines. Specifically, on Windows, the first user to connect is granted full access, while subsequent logged-in users have no access to the LocalAPI at all. This restriction applies regardless of the environment, local user roles (e.g., whether a Windows user is a local admin), or whether they are the active user on a shared Windows client device. Conversely, on Unix, we introduced the permitCert flag to enable granting non-root web servers (such as www-data, caddy, nginx, etc.) access to certificates. We also added additional access check to distinguish local admins (root on Unix-like platforms, elevated admins on Windows) from users with permitWrite access, and used it as a fix for the serve path LPE. A more fine-grained access control system could better suit our current and future needs, especially in improving the UX across various scenarios on corporate and personal Windows devices. This adds an API surface in ipnauth that will be used in LocalBackend to check access to individual Tailscale profiles as well as any device-wide information and operations. Updates tailscale/corp#18342 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-04-08 13:10:18 -05:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
import (
"errors"
"runtime"
"tailscale.com/ipn"
"tailscale.com/types/ptr"
)
// TestIdentity is an identity with a predefined UID, Name and access rights.
// It should only be used for testing purposes, and allows external packages
// to test against a specific set of access rights.
type TestIdentity struct {
UID string // UID is an OS-specific user id of the test user.
Name string // Name is the login name of the test user.
DeviceAccess DeviceAccess // DeviceAccess is the test user's access rights on the device.
ProfileAccess ProfileAccess // ProfileAccess is the test user's access rights to Tailscale profiles.
AccessOthersProfiles bool // AccessOthersProfiles indicates whether the test user can access all profiles, regardless of their ownership.
}
var (
// TestAdmin is a test identity that has unrestricted access to the device
// and all Tailscale profiles on it. It should only be used for testing purposes.
TestAdmin = &TestIdentity{
Name: "admin",
DeviceAccess: UnrestrictedDeviceAccess,
ProfileAccess: UnrestrictedProfileAccess,
AccessOthersProfiles: true,
}
)
// NewTestIdentityWithGOOS returns a new test identity for the given GOOS,
// with the specified user name and the isAdmin flag indicating
// whether the user has administrative access on the local machine.
//
// When goos is windows, it returns an identity representing an elevated admin
// or a regular user account on a non-managed non-server environment. Callers
// that require fine-grained control over user's privileges or environment
// should use NewWindowsIdentity instead.
func NewTestIdentityWithGOOS(goos, name string, isAdmin bool) Identity {
if goos == "windows" {
token := &testToken{
SID: ipn.WindowsUserID(name),
Name: name,
Admin: isAdmin,
Elevated: isAdmin,
}
return newWindowsIdentity(token, WindowsEnvironment{})
}
identity := &unixIdentity{goos: goos}
identity.forceForTest.username = ptr.To(name)
identity.forceForTest.isAdmin = ptr.To(isAdmin)
if isAdmin {
identity.forceForTest.uid = ptr.To("0")
} else {
identity.forceForTest.uid = ptr.To("1000")
}
return identity
}
// NewTestIdentity is like NewTestIdentityWithGOOS, but returns a test identity
// for the current platform.
func NewTestIdentity(name string, isAdmin bool) Identity {
return NewTestIdentityWithGOOS(runtime.GOOS, name, isAdmin)
}
// UserID returns t.ID.
func (t *TestIdentity) UserID() ipn.WindowsUserID {
return ipn.WindowsUserID(t.UID)
}
// Username returns t.Name.
func (t *TestIdentity) Username() (string, error) {
return t.Name, nil
}
// CheckAccess reports whether the requested access is allowed or denied
// based on t.DeviceAccess.
func (t *TestIdentity) CheckAccess(requested DeviceAccess) AccessCheckResult {
if requested&t.DeviceAccess == requested {
return AllowAccess()
}
return DenyAccess(errors.New("access denied"))
}
// CheckProfileAccess reports whether the requested profile access is allowed or denied
// based on t.ProfileAccess.
func (t *TestIdentity) CheckProfileAccess(profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult {
if !t.AccessOthersProfiles && profile.LocalUserID() != t.UserID() && profile.LocalUserID() != "" {
return DenyAccess(errors.New("the requested profile is owned by another user"))
}
if t.ProfileAccess&requested == requested {
return AllowAccess()
}
return DenyAccess(errors.New("access denied"))
}
// testToken implements WindowsToken and should only be used for testing purposes.
type testToken struct {
SID ipn.WindowsUserID
Name string
Admin, Elevated bool
LocalSystem bool
}
// UID returns t's Security Identifier (SID).
func (t *testToken) UID() (ipn.WindowsUserID, error) {
return t.SID, nil
}
// Username returns t's username.
func (t *testToken) Username() (string, error) {
return t.Name, nil
}
// IsAdministrator reports whether t represents an admin's,
// but not necessarily elevated, security context.
func (t *testToken) IsAdministrator() (bool, error) {
return t.Admin, nil
}
// IsElevated reports whether t represents an elevated security context,
// such as of LocalSystem or "Run as administrator".
func (t *testToken) IsElevated() bool {
return t.Elevated || t.IsLocalSystem()
}
// IsLocalSystem reports whether t represents a LocalSystem's security context.
func (t *testToken) IsLocalSystem() bool {
return t.LocalSystem
}
// UserDir is not implemented.
func (t *testToken) UserDir(folderID string) (string, error) {
return "", errors.New("Not implemented")
}
// Close is a no-op.
func (t *testToken) Close() error {
return nil
}
// EqualUIDs reports whether two WindowsTokens have the same UIDs.
func (t *testToken) EqualUIDs(other WindowsToken) bool {
if t != nil && other == nil || t == nil && other != nil {
return false
}
ot, ok := other.(*testToken)
if !ok {
return false
}
return t == ot || t.SID == ot.SID
}
// IsUID reports whether t has the specified UID.
func (t *testToken) IsUID(uid ipn.WindowsUserID) bool {
return t.SID == uid
}