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:
Bethuel Mmbaga 2024-06-13 09:55:09 +03:00 committed by GitHub
parent 85b8f36ec1
commit f68d5e965f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 68 additions and 30 deletions

View File

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

View File

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