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()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
uid := profile.UserProfile().ID
|
||||
activeLogin := profile.UserProfile().LoginName
|
||||
uid := profile.UserProfile().ID()
|
||||
activeLogin := profile.UserProfile().LoginName()
|
||||
|
||||
if uid == 0 {
|
||||
e.setMgrLocked(nil)
|
||||
|
||||
@@ -24,6 +24,7 @@ func (src *LoginProfile) Clone() *LoginProfile {
|
||||
}
|
||||
dst := new(LoginProfile)
|
||||
*dst = *src
|
||||
dst.UserProfile = *src.UserProfile.Clone()
|
||||
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.
|
||||
// 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.
|
||||
// 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() {
|
||||
b.logf("active login: %s", newLoginName)
|
||||
} else {
|
||||
oldLoginName := oldp.Persist().UserProfile().LoginName
|
||||
oldLoginName := oldp.Persist().UserProfile().LoginName()
|
||||
if oldLoginName != newLoginName {
|
||||
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 {
|
||||
return pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||
return p.ControlURL() == prefs.ControlURL() &&
|
||||
(p.UserProfile().ID == prefs.Persist().UserProfile().ID ||
|
||||
(p.UserProfile().ID() == prefs.Persist().UserProfile().ID() ||
|
||||
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.
|
||||
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
|
||||
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.
|
||||
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]
|
||||
// once the information is available and needs to be persisted.
|
||||
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.
|
||||
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
|
||||
if persist := prefsIn.Persist(); persist.Valid() {
|
||||
up = persist.UserProfile()
|
||||
up = *persist.UserProfile().AsStruct()
|
||||
if up.DisplayName == "" {
|
||||
up.DisplayName = up.LoginName
|
||||
}
|
||||
|
||||
@@ -606,7 +606,7 @@ func runTestStateMachine(t *testing.T, seamless bool) {
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].LoginFinished, 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
|
||||
// Verify login finished but need machine auth using backend state
|
||||
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.Persist(), qt.IsNotNil)
|
||||
// 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)
|
||||
// If a user initiates an interactive login, they also expect WantRunning to become true.
|
||||
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[1].Prefs, qt.IsNotNil)
|
||||
// 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.WantRunning(), qt.IsTrue)
|
||||
// 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)
|
||||
DisplayName string // "Alice Smith"
|
||||
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 {
|
||||
@@ -294,7 +301,8 @@ func (p *UserProfile) Equal(p2 *UserProfile) bool {
|
||||
return p.ID == p2.ID &&
|
||||
p.LoginName == p2.LoginName &&
|
||||
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
|
||||
|
||||
@@ -620,6 +620,7 @@ func (src *UserProfile) Clone() *UserProfile {
|
||||
}
|
||||
dst := new(UserProfile)
|
||||
*dst = *src
|
||||
dst.Groups = append(src.Groups[:0:0], src.Groups...)
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -629,6 +630,7 @@ var _UserProfileCloneNeedsRegeneration = UserProfile(struct {
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Groups []string
|
||||
}{})
|
||||
|
||||
// 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 }
|
||||
|
||||
// "Alice Smith"
|
||||
func (v UserProfileView) DisplayName() string { return v.ж.DisplayName }
|
||||
func (v UserProfileView) ProfilePicURL() string { return v.ж.ProfilePicURL }
|
||||
func (v UserProfileView) DisplayName() string { return v.ж.DisplayName }
|
||||
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.ж) }
|
||||
|
||||
// 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
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Groups []string
|
||||
}{})
|
||||
|
||||
// View returns a read-only view of VIPService.
|
||||
|
||||
@@ -19,6 +19,7 @@ func (src *Persist) Clone() *Persist {
|
||||
}
|
||||
dst := new(Persist)
|
||||
*dst = *src
|
||||
dst.UserProfile = *src.UserProfile.Clone()
|
||||
if src.AttestationKey != nil {
|
||||
dst.AttestationKey = src.AttestationKey.Clone()
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeK
|
||||
|
||||
// needed to request key rotation
|
||||
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) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
|
||||
func (v PersistView) AttestationKey() tailcfg.StableNodeID { panic("unsupported") }
|
||||
|
||||
Reference in New Issue
Block a user