diff --git a/ipn/ipnauth/access.go b/ipn/ipnauth/access.go new file mode 100644 index 000000000..4d0aeb850 --- /dev/null +++ b/ipn/ipnauth/access.go @@ -0,0 +1,8 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package ipnauth + +// ProfileAccess is a bitmask representing the requested, required, or granted +// access rights to an [ipn.LoginProfile]. +type ProfileAccess uint32 diff --git a/ipn/ipnauth/actor.go b/ipn/ipnauth/actor.go index 040d9b522..2c713e441 100644 --- a/ipn/ipnauth/actor.go +++ b/ipn/ipnauth/actor.go @@ -27,6 +27,10 @@ type Actor interface { // a connected LocalAPI client. Otherwise, it returns a zero value and false. ClientID() (_ ClientID, ok bool) + // CheckProfileAccess checks whether the actor has the requested access rights + // to the specified Tailscale profile. It returns an error if the access is denied. + CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess) error + // IsLocalSystem reports whether the actor is the Windows' Local System account. // // Deprecated: this method exists for compatibility with the current (as of 2024-08-27) diff --git a/ipn/ipnauth/test_actor.go b/ipn/ipnauth/test_actor.go index d38aa2196..0d4a0e37d 100644 --- a/ipn/ipnauth/test_actor.go +++ b/ipn/ipnauth/test_actor.go @@ -4,6 +4,8 @@ package ipnauth import ( + "errors" + "tailscale.com/ipn" ) @@ -17,7 +19,6 @@ type TestActor struct { CID ClientID // non-zero if the actor represents a connected LocalAPI client LocalSystem bool // whether the actor represents the special Local System account on Windows LocalAdmin bool // whether the actor has local admin access - } // UserID implements [Actor]. @@ -29,6 +30,11 @@ func (a *TestActor) Username() (string, error) { return a.Name, a.NameErr } // ClientID implements [Actor]. func (a *TestActor) ClientID() (_ ClientID, ok bool) { return a.CID, a.CID != NoClientID } +// CheckProfileAccess implements [Actor]. +func (a *TestActor) CheckProfileAccess(profile ipn.LoginProfileView, _ ProfileAccess) error { + return errors.New("profile access denied") +} + // IsLocalSystem implements [Actor]. func (a *TestActor) IsLocalSystem() bool { return a.LocalSystem } diff --git a/ipn/ipnserver/actor.go b/ipn/ipnserver/actor.go index 2df8986c3..8f743a3eb 100644 --- a/ipn/ipnserver/actor.go +++ b/ipn/ipnserver/actor.go @@ -58,6 +58,14 @@ func newActor(logf logger.Logf, c net.Conn) (*actor, error) { return &actor{logf: logf, ci: ci, clientID: clientID, isLocalSystem: connIsLocalSystem(ci)}, nil } +// CheckProfileAccess implements [ipnauth.Actor]. +func (a *actor) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ipnauth.ProfileAccess) error { + if profile.LocalUserID() != a.UserID() { + return errors.New("the target profile does not belong to the user") + } + return errors.New("the requested operation is not allowed") +} + // IsLocalSystem implements [ipnauth.Actor]. func (a *actor) IsLocalSystem() bool { return a.isLocalSystem