tailcfg: reintroduce UserProfile.Groups
This change reintroduces UserProfile.Groups, a slice that contains
the ACL-defined and synced groups that a user is a member of.
The slice will only be non-nil for clients with the node attribute
see-groups, and will only contain groups that the client is allowed
to see as per the app payload of the see-groups node attribute.
For example:
```
"nodeAttrs": [
{
"target": ["tag:dev"],
"app": {
"tailscale.com/see-groups": [{"groups": ["group:dev"]}]
}
},
[...]
]
```
UserProfile.Groups will also be gated by a feature flag for the time
being.
Updates tailscale/corp#31529
Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
This commit is contained in:
committed by
Gesa Stupperich
parent
ac74dfa5cd
commit
6a19995f13
@@ -139,8 +139,8 @@ func (e *Extension) onChangeProfile(profile ipn.LoginProfileView, _ ipn.PrefsVie
|
|||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
uid := profile.UserProfile().ID
|
uid := profile.UserProfile().ID()
|
||||||
activeLogin := profile.UserProfile().LoginName
|
activeLogin := profile.UserProfile().LoginName()
|
||||||
|
|
||||||
if uid == 0 {
|
if uid == 0 {
|
||||||
e.setMgrLocked(nil)
|
e.setMgrLocked(nil)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func (src *LoginProfile) Clone() *LoginProfile {
|
|||||||
}
|
}
|
||||||
dst := new(LoginProfile)
|
dst := new(LoginProfile)
|
||||||
*dst = *src
|
*dst = *src
|
||||||
|
dst.UserProfile = *src.UserProfile.Clone()
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ func (v LoginProfileView) Key() StateKey { return v.ж.Key }
|
|||||||
|
|
||||||
// UserProfile is the server provided UserProfile for this profile.
|
// UserProfile is the server provided UserProfile for this profile.
|
||||||
// This is updated whenever the server provides a new UserProfile.
|
// This is updated whenever the server provides a new UserProfile.
|
||||||
func (v LoginProfileView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
|
func (v LoginProfileView) UserProfile() tailcfg.UserProfileView { return v.ж.UserProfile.View() }
|
||||||
|
|
||||||
// NodeID is the NodeID of the node that this profile is logged into.
|
// NodeID is the NodeID of the node that this profile is logged into.
|
||||||
// This should be stable across tagging and untagging nodes.
|
// This should be stable across tagging and untagging nodes.
|
||||||
|
|||||||
@@ -4689,7 +4689,7 @@ func (b *LocalBackend) setPrefsLocked(newp *ipn.Prefs) ipn.PrefsView {
|
|||||||
if !oldp.Persist().Valid() {
|
if !oldp.Persist().Valid() {
|
||||||
b.logf("active login: %s", newLoginName)
|
b.logf("active login: %s", newLoginName)
|
||||||
} else {
|
} else {
|
||||||
oldLoginName := oldp.Persist().UserProfile().LoginName
|
oldLoginName := oldp.Persist().UserProfile().LoginName()
|
||||||
if oldLoginName != newLoginName {
|
if oldLoginName != newLoginName {
|
||||||
b.logf("active login: %q (changed from %q)", newLoginName, oldLoginName)
|
b.logf("active login: %q (changed from %q)", newLoginName, oldLoginName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ func (pm *profileManager) matchingProfiles(uid ipn.WindowsUserID, f func(ipn.Log
|
|||||||
func (pm *profileManager) findMatchingProfiles(uid ipn.WindowsUserID, prefs ipn.PrefsView) []ipn.LoginProfileView {
|
func (pm *profileManager) findMatchingProfiles(uid ipn.WindowsUserID, prefs ipn.PrefsView) []ipn.LoginProfileView {
|
||||||
return pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
return pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||||
return p.ControlURL() == prefs.ControlURL() &&
|
return p.ControlURL() == prefs.ControlURL() &&
|
||||||
(p.UserProfile().ID == prefs.Persist().UserProfile().ID ||
|
(p.UserProfile().ID() == prefs.Persist().UserProfile().ID() ||
|
||||||
p.NodeID() == prefs.Persist().NodeID())
|
p.NodeID() == prefs.Persist().NodeID())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -337,7 +337,7 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
|
|||||||
// across user switches to disambiguate the same account but a different tailnet.
|
// across user switches to disambiguate the same account but a different tailnet.
|
||||||
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
|
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
|
||||||
cp := pm.currentProfile
|
cp := pm.currentProfile
|
||||||
if persist := prefsIn.Persist(); !persist.Valid() || persist.NodeID() == "" || persist.UserProfile().LoginName == "" {
|
if persist := prefsIn.Persist(); !persist.Valid() || persist.NodeID() == "" || persist.UserProfile().LoginName() == "" {
|
||||||
// We don't know anything about this profile, so ignore it for now.
|
// We don't know anything about this profile, so ignore it for now.
|
||||||
return pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefsIn.AsStruct().View())
|
return pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefsIn.AsStruct().View())
|
||||||
}
|
}
|
||||||
@@ -410,7 +410,7 @@ func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
|
|||||||
// and it hasn't been persisted yet. We'll generate both an ID and [ipn.StateKey]
|
// and it hasn't been persisted yet. We'll generate both an ID and [ipn.StateKey]
|
||||||
// once the information is available and needs to be persisted.
|
// once the information is available and needs to be persisted.
|
||||||
if lp.ID == "" {
|
if lp.ID == "" {
|
||||||
if persist := prefsIn.Persist(); persist.Valid() && persist.NodeID() != "" && persist.UserProfile().LoginName != "" {
|
if persist := prefsIn.Persist(); persist.Valid() && persist.NodeID() != "" && persist.UserProfile().LoginName() != "" {
|
||||||
// Generate an ID and [ipn.StateKey] now that we have the node info.
|
// Generate an ID and [ipn.StateKey] now that we have the node info.
|
||||||
lp.ID, lp.Key = newUnusedID(pm.knownProfiles)
|
lp.ID, lp.Key = newUnusedID(pm.knownProfiles)
|
||||||
}
|
}
|
||||||
@@ -425,7 +425,7 @@ func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
|
|||||||
|
|
||||||
var up tailcfg.UserProfile
|
var up tailcfg.UserProfile
|
||||||
if persist := prefsIn.Persist(); persist.Valid() {
|
if persist := prefsIn.Persist(); persist.Valid() {
|
||||||
up = persist.UserProfile()
|
up = *persist.UserProfile().AsStruct()
|
||||||
if up.DisplayName == "" {
|
if up.DisplayName == "" {
|
||||||
up.DisplayName = up.LoginName
|
up.DisplayName = up.LoginName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -606,7 +606,7 @@ func runTestStateMachine(t *testing.T, seamless bool) {
|
|||||||
cc.assertCalls()
|
cc.assertCalls()
|
||||||
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
|
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
|
||||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user1")
|
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName(), qt.Equals, "user1")
|
||||||
// nn[2] is a state notification after login
|
// nn[2] is a state notification after login
|
||||||
// Verify login finished but need machine auth using backend state
|
// Verify login finished but need machine auth using backend state
|
||||||
c.Assert(isFullyAuthenticated(b), qt.IsTrue)
|
c.Assert(isFullyAuthenticated(b), qt.IsTrue)
|
||||||
@@ -818,7 +818,7 @@ func runTestStateMachine(t *testing.T, seamless bool) {
|
|||||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||||
c.Assert(nn[1].Prefs.Persist(), qt.IsNotNil)
|
c.Assert(nn[1].Prefs.Persist(), qt.IsNotNil)
|
||||||
// Prefs after finishing the login, so LoginName updated.
|
// Prefs after finishing the login, so LoginName updated.
|
||||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user2")
|
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName(), qt.Equals, "user2")
|
||||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||||
// If a user initiates an interactive login, they also expect WantRunning to become true.
|
// If a user initiates an interactive login, they also expect WantRunning to become true.
|
||||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||||
@@ -964,7 +964,7 @@ func runTestStateMachine(t *testing.T, seamless bool) {
|
|||||||
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
|
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
|
||||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||||
// Prefs after finishing the login, so LoginName updated.
|
// Prefs after finishing the login, so LoginName updated.
|
||||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user3")
|
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName(), qt.Equals, "user3")
|
||||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||||
// nn[2] is state notification (Starting) - verify using backend state
|
// nn[2] is state notification (Starting) - verify using backend state
|
||||||
|
|||||||
+9
-1
@@ -282,6 +282,13 @@ type UserProfile struct {
|
|||||||
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
|
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
|
||||||
DisplayName string // "Alice Smith"
|
DisplayName string // "Alice Smith"
|
||||||
ProfilePicURL string `json:",omitzero"`
|
ProfilePicURL string `json:",omitzero"`
|
||||||
|
|
||||||
|
// Groups is a subset of SCIM groups (e.g. "engineering@example.com")
|
||||||
|
// or group names in the tailnet policy document (e.g. "group:eng")
|
||||||
|
// that contain this user and that the coordination server was
|
||||||
|
// configured to report to this node.
|
||||||
|
// The list is always sorted when loaded from storage.
|
||||||
|
Groups []string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UserProfile) Equal(p2 *UserProfile) bool {
|
func (p *UserProfile) Equal(p2 *UserProfile) bool {
|
||||||
@@ -294,7 +301,8 @@ func (p *UserProfile) Equal(p2 *UserProfile) bool {
|
|||||||
return p.ID == p2.ID &&
|
return p.ID == p2.ID &&
|
||||||
p.LoginName == p2.LoginName &&
|
p.LoginName == p2.LoginName &&
|
||||||
p.DisplayName == p2.DisplayName &&
|
p.DisplayName == p2.DisplayName &&
|
||||||
p.ProfilePicURL == p2.ProfilePicURL
|
p.ProfilePicURL == p2.ProfilePicURL &&
|
||||||
|
slices.Equal(p.Groups, p2.Groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawMessage is a raw encoded JSON value. It implements Marshaler and
|
// RawMessage is a raw encoded JSON value. It implements Marshaler and
|
||||||
|
|||||||
@@ -620,6 +620,7 @@ func (src *UserProfile) Clone() *UserProfile {
|
|||||||
}
|
}
|
||||||
dst := new(UserProfile)
|
dst := new(UserProfile)
|
||||||
*dst = *src
|
*dst = *src
|
||||||
|
dst.Groups = append(src.Groups[:0:0], src.Groups...)
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,6 +630,7 @@ var _UserProfileCloneNeedsRegeneration = UserProfile(struct {
|
|||||||
LoginName string
|
LoginName string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
ProfilePicURL string
|
ProfilePicURL string
|
||||||
|
Groups []string
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// Clone makes a deep copy of VIPService.
|
// Clone makes a deep copy of VIPService.
|
||||||
|
|||||||
+10
-2
@@ -2505,8 +2505,15 @@ func (v UserProfileView) ID() UserID { return v.ж.ID }
|
|||||||
func (v UserProfileView) LoginName() string { return v.ж.LoginName }
|
func (v UserProfileView) LoginName() string { return v.ж.LoginName }
|
||||||
|
|
||||||
// "Alice Smith"
|
// "Alice Smith"
|
||||||
func (v UserProfileView) DisplayName() string { return v.ж.DisplayName }
|
func (v UserProfileView) DisplayName() string { return v.ж.DisplayName }
|
||||||
func (v UserProfileView) ProfilePicURL() string { return v.ж.ProfilePicURL }
|
func (v UserProfileView) ProfilePicURL() string { return v.ж.ProfilePicURL }
|
||||||
|
|
||||||
|
// Groups is a subset of SCIM groups (e.g. "engineering@example.com")
|
||||||
|
// or group names in the tailnet policy document (e.g. "group:eng")
|
||||||
|
// that contain this user and that the coordination server was
|
||||||
|
// configured to report to this node.
|
||||||
|
// The list is always sorted when loaded from storage.
|
||||||
|
func (v UserProfileView) Groups() views.Slice[string] { return views.SliceOf(v.ж.Groups) }
|
||||||
func (v UserProfileView) Equal(v2 UserProfileView) bool { return v.ж.Equal(v2.ж) }
|
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.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
@@ -2515,6 +2522,7 @@ var _UserProfileViewNeedsRegeneration = UserProfile(struct {
|
|||||||
LoginName string
|
LoginName string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
ProfilePicURL string
|
ProfilePicURL string
|
||||||
|
Groups []string
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// View returns a read-only view of VIPService.
|
// View returns a read-only view of VIPService.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func (src *Persist) Clone() *Persist {
|
|||||||
}
|
}
|
||||||
dst := new(Persist)
|
dst := new(Persist)
|
||||||
*dst = *src
|
*dst = *src
|
||||||
|
dst.UserProfile = *src.UserProfile.Clone()
|
||||||
if src.AttestationKey != nil {
|
if src.AttestationKey != nil {
|
||||||
dst.AttestationKey = src.AttestationKey.Clone()
|
dst.AttestationKey = src.AttestationKey.Clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeK
|
|||||||
|
|
||||||
// needed to request key rotation
|
// needed to request key rotation
|
||||||
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
|
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
|
||||||
func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
|
func (v PersistView) UserProfile() tailcfg.UserProfileView { return v.ж.UserProfile.View() }
|
||||||
func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey }
|
func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey }
|
||||||
func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
|
func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
|
||||||
func (v PersistView) AttestationKey() tailcfg.StableNodeID { panic("unsupported") }
|
func (v PersistView) AttestationKey() tailcfg.StableNodeID { panic("unsupported") }
|
||||||
|
|||||||
Reference in New Issue
Block a user