From f1cc8ab3f97edfe8f039c976a14aa6dd2007769d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 23 Jul 2023 14:48:03 -0700 Subject: [PATCH] tailcfg: add UserProfile.Groups Updates tailscale/corp#13375 Signed-off-by: Brad Fitzpatrick --- tailcfg/tailcfg.go | 26 ++++++++++++-- tailcfg/tailcfg_clone.go | 33 ++++++++++++++++- tailcfg/tailcfg_view.go | 65 +++++++++++++++++++++++++++++++++- types/persist/persist.go | 2 +- types/persist/persist_clone.go | 1 + types/persist/persist_view.go | 14 ++++---- 6 files changed, 129 insertions(+), 12 deletions(-) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 3e5c75301..ca7b157e0 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -3,7 +3,7 @@ package tailcfg -//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location --clonefunc +//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile --clonefunc import ( "bytes" @@ -102,7 +102,8 @@ type CapabilityVersion int // - 63: 2023-06-08: Client understands SSHAction.AllowRemotePortForwarding. // - 64: 2023-07-11: Client understands s/CapabilityTailnetLockAlpha/CapabilityTailnetLock // - 65: 2023-07-12: Client understands DERPMap.HomeParams + incremental DERPMap updates with params -const CurrentCapabilityVersion CapabilityVersion = 65 +// - 66: 2023-07-23: UserProfile.Groups added (available via WhoIs) +const CurrentCapabilityVersion CapabilityVersion = 66 type StableID string @@ -175,6 +176,27 @@ type UserProfile struct { // Roles exists for legacy reasons, to keep old macOS clients // happy. It JSON marshals as []. Roles emptyStructJSONSlice + + // Groups contains group identifiers for any group that this user is + // a part of and that the coordination server is configured to tell + // your node about. (Thus, it may be empty or incomplete.) + // There's no semantic difference between a nil and an empty list. + // The list is always sorted. + Groups []string `json:",omitempty"` +} + +func (p *UserProfile) Equal(p2 *UserProfile) bool { + if p == nil && p2 == nil { + return true + } + if p == nil || p2 == nil { + return false + } + return p.ID == p2.ID && + p.LoginName == p2.LoginName && + p.DisplayName == p2.DisplayName && + p.ProfilePicURL == p2.ProfilePicURL && + (len(p.Groups) == 0 && len(p2.Groups) == 0 || reflect.DeepEqual(p.Groups, p2.Groups)) } type emptyStructJSONSlice struct{} diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 8ab367733..a2eee5bbd 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -506,9 +506,31 @@ var _LocationCloneNeedsRegeneration = Location(struct { Priority int }{}) +// Clone makes a deep copy of UserProfile. +// The result aliases no memory with the original. +func (src *UserProfile) Clone() *UserProfile { + if src == nil { + return nil + } + dst := new(UserProfile) + *dst = *src + dst.Groups = append(src.Groups[:0:0], src.Groups...) + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _UserProfileCloneNeedsRegeneration = UserProfile(struct { + ID UserID + LoginName string + DisplayName string + ProfilePicURL string + Roles emptyStructJSONSlice + Groups []string +}{}) + // Clone duplicates src into dst and reports whether it succeeded. // To succeed, must be of types <*T, *T> or <*T, **T>, -// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location. +// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile. func Clone(dst, src any) bool { switch src := src.(type) { case *User: @@ -655,6 +677,15 @@ func Clone(dst, src any) bool { *dst = src.Clone() return true } + case *UserProfile: + switch dst := dst.(type) { + case *UserProfile: + *dst = *src.Clone() + return true + case **UserProfile: + *dst = src.Clone() + return true + } } return false } diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 3236ece3a..60a86c8f6 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -20,7 +20,7 @@ import ( "tailscale.com/types/views" ) -//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location +//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile // View returns a readonly view of User. func (p *User) View() UserView { @@ -1201,3 +1201,66 @@ var _LocationViewNeedsRegeneration = Location(struct { CityCode string Priority int }{}) + +// View returns a readonly view of UserProfile. +func (p *UserProfile) View() UserProfileView { + return UserProfileView{ж: p} +} + +// UserProfileView provides a read-only view over UserProfile. +// +// Its methods should only be called if `Valid()` returns true. +type UserProfileView struct { + // ж is the underlying mutable value, named with a hard-to-type + // character that looks pointy like a pointer. + // It is named distinctively to make you think of how dangerous it is to escape + // to callers. You must not let callers be able to mutate it. + ж *UserProfile +} + +// Valid reports whether underlying value is non-nil. +func (v UserProfileView) Valid() bool { return v.ж != nil } + +// AsStruct returns a clone of the underlying value which aliases no memory with +// the original. +func (v UserProfileView) AsStruct() *UserProfile { + if v.ж == nil { + return nil + } + return v.ж.Clone() +} + +func (v UserProfileView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +func (v *UserProfileView) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + var x UserProfile + if err := json.Unmarshal(b, &x); err != nil { + return err + } + v.ж = &x + return nil +} + +func (v UserProfileView) ID() UserID { return v.ж.ID } +func (v UserProfileView) LoginName() string { return v.ж.LoginName } +func (v UserProfileView) DisplayName() string { return v.ж.DisplayName } +func (v UserProfileView) ProfilePicURL() string { return v.ж.ProfilePicURL } +func (v UserProfileView) Roles() emptyStructJSONSlice { return v.ж.Roles } +func (v UserProfileView) Groups() views.Slice[string] { return views.SliceOf(v.ж.Groups) } +func (v UserProfileView) Equal(v2 UserProfileView) bool { return v.ж.Equal(v2.ж) } + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _UserProfileViewNeedsRegeneration = UserProfile(struct { + ID UserID + LoginName string + DisplayName string + ProfilePicURL string + Roles emptyStructJSONSlice + Groups []string +}{}) diff --git a/types/persist/persist.go b/types/persist/persist.go index ce1f4c99e..4ed4c6070 100644 --- a/types/persist/persist.go +++ b/types/persist/persist.go @@ -81,7 +81,7 @@ func (p *Persist) Equals(p2 *Persist) bool { p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) && p.Provider == p2.Provider && p.LoginName == p2.LoginName && - p.UserProfile == p2.UserProfile && + p.UserProfile.Equal(&p2.UserProfile) && p.NetworkLockKey.Equal(p2.NetworkLockKey) && p.NodeID == p2.NodeID && reflect.DeepEqual(nilIfEmpty(p.DisallowedTKAStateIDs), nilIfEmpty(p2.DisallowedTKAStateIDs)) diff --git a/types/persist/persist_clone.go b/types/persist/persist_clone.go index 4bce7a03b..9e3385112 100644 --- a/types/persist/persist_clone.go +++ b/types/persist/persist_clone.go @@ -19,6 +19,7 @@ func (src *Persist) Clone() *Persist { } dst := new(Persist) *dst = *src + dst.UserProfile = *src.UserProfile.Clone() dst.DisallowedTKAStateIDs = append(src.DisallowedTKAStateIDs[:0:0], src.DisallowedTKAStateIDs...) return dst } diff --git a/types/persist/persist_view.go b/types/persist/persist_view.go index 81a61b472..c05b7fd1b 100644 --- a/types/persist/persist_view.go +++ b/types/persist/persist_view.go @@ -65,13 +65,13 @@ func (v *PersistView) UnmarshalJSON(b []byte) error { func (v PersistView) LegacyFrontendPrivateMachineKey() key.MachinePrivate { return v.ж.LegacyFrontendPrivateMachineKey } -func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey } -func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey } -func (v PersistView) Provider() string { return v.ж.Provider } -func (v PersistView) LoginName() string { return v.ж.LoginName } -func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile } -func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey } -func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID } +func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey } +func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey } +func (v PersistView) Provider() string { return v.ж.Provider } +func (v PersistView) LoginName() string { return v.ж.LoginName } +func (v PersistView) UserProfile() tailcfg.UserProfileView { return v.ж.UserProfile.View() } +func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey } +func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID } func (v PersistView) DisallowedTKAStateIDs() views.Slice[string] { return views.SliceOf(v.ж.DisallowedTKAStateIDs) }