mirror of
https://github.com/netbirdio/netbird.git
synced 2025-02-02 19:39:17 +01:00
Optimize JWT Group Sync (#2108)
* Optimize JWT group sync to avoid unnecessary account sync * Ignore adding matching API and JWT groups during Sync * add tests * refactor
This commit is contained in:
parent
85b8f36ec1
commit
f68d5e965f
@ -11,16 +11,13 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/eko/gocache/v3/cache"
|
"github.com/eko/gocache/v3/cache"
|
||||||
cacheStore "github.com/eko/gocache/v3/store"
|
cacheStore "github.com/eko/gocache/v3/store"
|
||||||
gocache "github.com/patrickmn/go-cache"
|
|
||||||
"github.com/rs/xid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/base62"
|
"github.com/netbirdio/netbird/base62"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server/account"
|
"github.com/netbirdio/netbird/management/server/account"
|
||||||
@ -35,6 +32,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/posture"
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
"github.com/netbirdio/netbird/management/server/status"
|
"github.com/netbirdio/netbird/management/server/status"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
|
gocache "github.com/patrickmn/go-cache"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -758,8 +759,13 @@ func (a *Account) GetPeer(peerID string) *nbpeer.Peer {
|
|||||||
return a.Peers[peerID]
|
return a.Peers[peerID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetJWTGroups to account and to user autoassigned groups
|
// SetJWTGroups updates the user's auto groups by synchronizing JWT groups.
|
||||||
|
// Returns true if there are changes in the JWT group membership.
|
||||||
func (a *Account) SetJWTGroups(userID string, groupsNames []string) bool {
|
func (a *Account) SetJWTGroups(userID string, groupsNames []string) bool {
|
||||||
|
if len(groupsNames) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
user, ok := a.Users[userID]
|
user, ok := a.Users[userID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
@ -770,23 +776,19 @@ func (a *Account) SetJWTGroups(userID string, groupsNames []string) bool {
|
|||||||
existedGroupsByName[group.Name] = group
|
existedGroupsByName[group.Name] = group
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove JWT groups from the autogroups, to sync them again
|
newAutoGroups, jwtGroupsMap := separateGroups(user.AutoGroups, a.Groups)
|
||||||
removed := 0
|
groupsToAdd := difference(groupsNames, maps.Keys(jwtGroupsMap))
|
||||||
jwtAutoGroups := make(map[string]struct{})
|
groupsToRemove := difference(maps.Keys(jwtGroupsMap), groupsNames)
|
||||||
for i, id := range user.AutoGroups {
|
|
||||||
if group, ok := a.Groups[id]; ok && group.Issued == nbgroup.GroupIssuedJWT {
|
// If no groups are added or removed, we should not sync account
|
||||||
jwtAutoGroups[group.Name] = struct{}{}
|
if len(groupsToAdd) == 0 && len(groupsToRemove) == 0 {
|
||||||
user.AutoGroups = append(user.AutoGroups[:i-removed], user.AutoGroups[i-removed+1:]...)
|
return false
|
||||||
removed++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create JWT groups if they doesn't exist
|
|
||||||
// and all of them to the autogroups
|
|
||||||
var modified bool
|
var modified bool
|
||||||
for _, name := range groupsNames {
|
for _, name := range groupsToAdd {
|
||||||
group, ok := existedGroupsByName[name]
|
group, exists := existedGroupsByName[name]
|
||||||
if !ok {
|
if !exists {
|
||||||
group = &nbgroup.Group{
|
group = &nbgroup.Group{
|
||||||
ID: xid.New().String(),
|
ID: xid.New().String(),
|
||||||
Name: name,
|
Name: name,
|
||||||
@ -794,20 +796,20 @@ func (a *Account) SetJWTGroups(userID string, groupsNames []string) bool {
|
|||||||
}
|
}
|
||||||
a.Groups[group.ID] = group
|
a.Groups[group.ID] = group
|
||||||
}
|
}
|
||||||
// only JWT groups will be synced
|
|
||||||
if group.Issued == nbgroup.GroupIssuedJWT {
|
if group.Issued == nbgroup.GroupIssuedJWT {
|
||||||
user.AutoGroups = append(user.AutoGroups, group.ID)
|
newAutoGroups = append(newAutoGroups, group.ID)
|
||||||
if _, ok := jwtAutoGroups[name]; !ok {
|
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
delete(jwtAutoGroups, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if not empty it means we removed some groups
|
for name, id := range jwtGroupsMap {
|
||||||
if len(jwtAutoGroups) > 0 {
|
if !slices.Contains(groupsToRemove, name) {
|
||||||
|
newAutoGroups = append(newAutoGroups, id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
|
user.AutoGroups = newAutoGroups
|
||||||
|
|
||||||
return modified
|
return modified
|
||||||
}
|
}
|
||||||
@ -1714,6 +1716,7 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat
|
|||||||
if err := am.Store.SaveAccount(account); err != nil {
|
if err := am.Store.SaveAccount(account); err != nil {
|
||||||
log.Errorf("failed to save account: %v", err)
|
log.Errorf("failed to save account: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
log.Tracef("user %s: JWT group membership changed, updating account peers", claims.UserId)
|
||||||
am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
unlock()
|
unlock()
|
||||||
alreadyUnlocked = true
|
alreadyUnlocked = true
|
||||||
@ -2064,3 +2067,22 @@ func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// separateGroups separates user's auto groups into non-JWT and JWT groups.
|
||||||
|
// Returns the list of standard auto groups and a map of JWT auto groups,
|
||||||
|
// where the keys are the group names and the values are the group IDs.
|
||||||
|
func separateGroups(autoGroups []string, allGroups map[string]*nbgroup.Group) ([]string, map[string]string) {
|
||||||
|
newAutoGroups := make([]string, 0)
|
||||||
|
jwtAutoGroups := make(map[string]string) // map of group name to group ID
|
||||||
|
|
||||||
|
for _, id := range autoGroups {
|
||||||
|
if group, ok := allGroups[id]; ok {
|
||||||
|
if group.Issued == nbgroup.GroupIssuedJWT {
|
||||||
|
jwtAutoGroups[group.Name] = id
|
||||||
|
} else {
|
||||||
|
newAutoGroups = append(newAutoGroups, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newAutoGroups, jwtAutoGroups
|
||||||
|
}
|
||||||
|
@ -2175,17 +2175,33 @@ func TestAccount_SetJWTGroups(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("api group already exists", func(t *testing.T) {
|
t.Run("empty jwt groups", func(t *testing.T) {
|
||||||
updated := account.SetJWTGroups("user1", []string{"group1"})
|
updated := account.SetJWTGroups("user1", []string{})
|
||||||
assert.False(t, updated, "account should not be updated")
|
assert.False(t, updated, "account should not be updated")
|
||||||
assert.Empty(t, account.Users["user1"].AutoGroups, "auto groups must be empty")
|
assert.Empty(t, account.Users["user1"].AutoGroups, "auto groups must be empty")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("jwt match existing api group", func(t *testing.T) {
|
||||||
|
updated := account.SetJWTGroups("user1", []string{"group1"})
|
||||||
|
assert.False(t, updated, "account should not be updated")
|
||||||
|
assert.Equal(t, 0, len(account.Users["user1"].AutoGroups))
|
||||||
|
assert.Equal(t, account.Groups["group1"].Issued, group.GroupIssuedAPI, "group should be api issued")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jwt match existing api group in user auto groups", func(t *testing.T) {
|
||||||
|
account.Users["user1"].AutoGroups = []string{"group1"}
|
||||||
|
|
||||||
|
updated := account.SetJWTGroups("user1", []string{"group1"})
|
||||||
|
assert.False(t, updated, "account should not be updated")
|
||||||
|
assert.Equal(t, 1, len(account.Users["user1"].AutoGroups))
|
||||||
|
assert.Equal(t, account.Groups["group1"].Issued, group.GroupIssuedAPI, "group should be api issued")
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("add jwt group", func(t *testing.T) {
|
t.Run("add jwt group", func(t *testing.T) {
|
||||||
updated := account.SetJWTGroups("user1", []string{"group1", "group2"})
|
updated := account.SetJWTGroups("user1", []string{"group1", "group2"})
|
||||||
assert.True(t, updated, "account should be updated")
|
assert.True(t, updated, "account should be updated")
|
||||||
assert.Len(t, account.Groups, 2, "new group should be added")
|
assert.Len(t, account.Groups, 2, "new group should be added")
|
||||||
assert.Len(t, account.Users["user1"].AutoGroups, 1, "new group should be added")
|
assert.Len(t, account.Users["user1"].AutoGroups, 2, "new group should be added")
|
||||||
assert.Contains(t, account.Groups, account.Users["user1"].AutoGroups[0], "groups must contain group2 from user groups")
|
assert.Contains(t, account.Groups, account.Users["user1"].AutoGroups[0], "groups must contain group2 from user groups")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user