Explorar el Código

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 <[email protected]>
Gesa Stupperich hace 3 semanas
padre
commit
6a19995f13

+ 2 - 2
feature/taildrop/ext.go

@@ -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)

+ 1 - 0
ipn/ipn_clone.go

@@ -24,6 +24,7 @@ func (src *LoginProfile) Clone() *LoginProfile {
 	}
 	dst := new(LoginProfile)
 	*dst = *src
+	dst.UserProfile = *src.UserProfile.Clone()
 	return dst
 }
 

+ 1 - 1
ipn/ipn_view.go

@@ -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.

+ 1 - 1
ipn/ipnlocal/local.go

@@ -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)
 				}

+ 4 - 4
ipn/ipnlocal/profiles.go

@@ -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
 		}

+ 3 - 3
ipn/ipnlocal/state_test.go

@@ -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
tailcfg/tailcfg.go

@@ -282,6 +282,13 @@ type UserProfile struct {
 	LoginName     string // "[email protected]"; 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. "[email protected]")
+	// 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

+ 2 - 0
tailcfg/tailcfg_clone.go

@@ -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
tailcfg/tailcfg_view.go

@@ -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. "[email protected]")
+// 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.

+ 1 - 0
types/persist/persist_clone.go

@@ -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()
 	}

+ 1 - 1
types/persist/persist_view.go

@@ -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") }