mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-18 20:59:07 +02:00
[client] Profile ownership console user tofu (#7529)
* Add consoleuser and stamp default profile on known username in migration * Refactor consoleuser to verify Id, fix seats on linux and default stamp * Add default profile claim * Add disable auto-claim of default profile and always fail close * Add disable auto-claim flag to migration * Adding timeout to console user on Linux and close library load on darwin * Fixed failed close test * Close both Dlopen for darwin * Replace RegisterFunc with purego.Dlsym to avoid possible panic * Fix freebsd tty enumeration * Fix active profile migration logic and add test * Log defaultClaimDisabled error once * Guard against panicking console user lookup. * Fix merge conflict * Fix broken tests
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package ipcauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
loginDest = "org.freedesktop.login1"
|
||||
loginPath = dbus.ObjectPath("/org/freedesktop/login1")
|
||||
loginInterface = "org.freedesktop.login1.Manager"
|
||||
listSeats = loginInterface + ".ListSeats"
|
||||
|
||||
seatInterface = "org.freedesktop.login1.Seat"
|
||||
seatActiveSession = seatInterface + ".ActiveSession"
|
||||
|
||||
sessionInterface = "org.freedesktop.login1.Session"
|
||||
sessionActive = sessionInterface + ".Active"
|
||||
sessionClass = sessionInterface + ".Class"
|
||||
sessionRemote = sessionInterface + ".Remote"
|
||||
sessionUser = sessionInterface + ".User"
|
||||
|
||||
// propertiesGet is the standard property reader. godbus offers no
|
||||
// context-aware GetProperty, and GetProperty is only this call underneath,
|
||||
// so a bounded read has to make it directly.
|
||||
propertiesGet = "org.freedesktop.DBus.Properties.Get"
|
||||
|
||||
// nullObjectPath is what logind puts in an object path field that refers to
|
||||
// nothing, a seat with no session in the foreground being the one that
|
||||
// matters here.
|
||||
nullObjectPath = dbus.ObjectPath("/")
|
||||
|
||||
// consoleLookupTimeout bounds the whole seat walk, not each call in it, so
|
||||
// a machine with several seats cannot multiply what an unanswering bus
|
||||
// costs. A healthy lookup is well under a millisecond.
|
||||
consoleLookupTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
// isConsoleUser reports whether id holds the foreground session of one of this
|
||||
// machine's seats.
|
||||
//
|
||||
// No seats means no console, which is the honest answer for a headless machine
|
||||
// and the one that keeps its profiles to the privileged caller.
|
||||
func isConsoleUser(id Identity) bool {
|
||||
// A SID belongs to a Windows principal and has no uid to compare. Nothing
|
||||
// on Linux produces one, and comparing anyway would match uid 0.
|
||||
if id.IsWindows() {
|
||||
return false
|
||||
}
|
||||
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), consoleLookupTimeout)
|
||||
defer cancel()
|
||||
|
||||
// ListSeats returns a(so): seat id and object path.
|
||||
var seats []struct {
|
||||
ID string
|
||||
Path dbus.ObjectPath
|
||||
}
|
||||
if err := conn.Object(loginDest, loginPath).CallWithContext(ctx, listSeats, 0).Store(&seats); err != nil {
|
||||
log.Debugf("cannot list seats, treating the caller as not at the console: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, seat := range seats {
|
||||
uid, ok := consoleSessionUID(ctx, conn, seat.Path)
|
||||
if ok && uid == id.UID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// consoleSessionUID returns who holds a seat's foreground session, and false
|
||||
// unless that session is a person logged in locally.
|
||||
func consoleSessionUID(ctx context.Context, conn *dbus.Conn, seatPath dbus.ObjectPath) (uint32, bool) {
|
||||
// ActiveSession is (so): session id and object path. It names nothing on a
|
||||
// seat whose VT has been switched away from, and on one whose display
|
||||
// manager has not started a session yet.
|
||||
prop, err := property(ctx, conn.Object(loginDest, seatPath), seatActiveSession)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
var active struct {
|
||||
ID string
|
||||
Path dbus.ObjectPath
|
||||
}
|
||||
if err := prop.Store(&active); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
if active.ID == "" || active.Path == "" || active.Path == nullObjectPath {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
session := conn.Object(loginDest, active.Path)
|
||||
|
||||
// Only "user" sessions count: a greeter or a lock screen is the display
|
||||
// manager sitting at the seat, not somebody to hand a profile to.
|
||||
if class, ok := stringProperty(ctx, session, sessionClass); !ok || class != "user" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// A remote session can hold a seat, and unlike the session's class and
|
||||
// type, which pam_systemd takes from the environment of whoever opened the
|
||||
// session, remoteness is set by the thing that accepted the connection. So
|
||||
// it is worth asking even once the seat is established.
|
||||
if remote, ok := boolProperty(ctx, session, sessionRemote); !ok || remote {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Implied by the seat having named this session, and kept as a cross-check
|
||||
// against a foreground that moved between the two calls.
|
||||
if isActive, ok := boolProperty(ctx, session, sessionActive); !ok || !isActive {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// User is (uo): uid and the user object's path. Read from the session
|
||||
// object rather than carried over from a listing, so the uid returned and
|
||||
// the checks above are known to describe the same session.
|
||||
prop, err = property(ctx, session, sessionUser)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
var owner struct {
|
||||
UID uint32
|
||||
Path dbus.ObjectPath
|
||||
}
|
||||
if err := prop.Store(&owner); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return owner.UID, true
|
||||
}
|
||||
|
||||
// property reads one property under ctx, name being the interface.member form
|
||||
// godbus takes. It is what GetProperty does, with a deadline the caller owns.
|
||||
func property(ctx context.Context, obj dbus.BusObject, name string) (dbus.Variant, error) {
|
||||
idx := strings.LastIndex(name, ".")
|
||||
if idx < 0 || idx+1 == len(name) {
|
||||
return dbus.Variant{}, fmt.Errorf("invalid property name %q", name)
|
||||
}
|
||||
var v dbus.Variant
|
||||
err := obj.CallWithContext(ctx, propertiesGet, 0, name[:idx], name[idx+1:]).Store(&v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func stringProperty(ctx context.Context, obj dbus.BusObject, name string) (string, bool) {
|
||||
prop, err := property(ctx, obj, name)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
s, ok := prop.Value().(string)
|
||||
return s, ok
|
||||
}
|
||||
|
||||
func boolProperty(ctx context.Context, obj dbus.BusObject, name string) (bool, bool) {
|
||||
prop, err := property(ctx, obj, name)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
b, ok := prop.Value().(bool)
|
||||
return b, ok
|
||||
}
|
||||
Reference in New Issue
Block a user