Merge branch 'release-v1.0.0' into NM-96-v1

This commit is contained in:
Abhishek K 2025-08-28 20:13:54 +05:30 committed by GitHub
commit a670bdb18a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 201 additions and 70 deletions

View file

@ -38,6 +38,24 @@ func UpsertServerSettings(s models.ServerSettings) error {
s.BasicAuth = true
}
var userFilters []string
for _, userFilter := range s.UserFilters {
userFilter = strings.TrimSpace(userFilter)
if userFilter != "" {
userFilters = append(userFilters, userFilter)
}
}
s.UserFilters = userFilters
var groupFilters []string
for _, groupFilter := range s.GroupFilters {
groupFilter = strings.TrimSpace(groupFilter)
if groupFilter != "" {
groupFilters = append(groupFilters, groupFilter)
}
}
s.GroupFilters = groupFilters
data, err := json.Marshal(s)
if err != nil {
return err

View file

@ -3,6 +3,10 @@ package auth
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/gravitl/netmaker/database"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/logic"
@ -12,9 +16,6 @@ import (
"github.com/gravitl/netmaker/pro/idp/google"
"github.com/gravitl/netmaker/pro/idp/okta"
proLogic "github.com/gravitl/netmaker/pro/logic"
"strings"
"sync"
"time"
)
var (
@ -85,15 +86,23 @@ func SyncFromIDP() error {
}
if settings.AuthProvider != "" && idpClient != nil {
idpUsers, err = idpClient.GetUsers()
idpUsers, err = idpClient.GetUsers(settings.UserFilters)
if err != nil {
return err
}
idpGroups, err = idpClient.GetGroups()
idpGroups, err = idpClient.GetGroups(settings.GroupFilters)
if err != nil {
return err
}
if len(settings.GroupFilters) > 0 {
idpUsers = filterUsersByGroupMembership(idpUsers, idpGroups)
}
if len(settings.UserFilters) > 0 {
idpGroups = filterGroupsByMembers(idpGroups, idpUsers)
}
}
err = syncUsers(idpUsers)
@ -316,3 +325,64 @@ func syncGroups(idpGroups []idp.Group) error {
return nil
}
func filterUsersByGroupMembership(idpUsers []idp.User, idpGroups []idp.Group) []idp.User {
usersMap := make(map[string]int)
for i, user := range idpUsers {
usersMap[user.ID] = i
}
filteredUsersMap := make(map[string]int)
for _, group := range idpGroups {
for _, member := range group.Members {
if userIdx, ok := usersMap[member]; ok {
// user at index `userIdx` is a member of at least one of the
// groups in the `idpGroups` list, so we keep it.
filteredUsersMap[member] = userIdx
}
}
}
i := 0
filteredUsers := make([]idp.User, len(filteredUsersMap))
for _, userIdx := range filteredUsersMap {
filteredUsers[i] = idpUsers[userIdx]
i++
}
return filteredUsers
}
func filterGroupsByMembers(idpGroups []idp.Group, idpUsers []idp.User) []idp.Group {
usersMap := make(map[string]int)
for i, user := range idpUsers {
usersMap[user.ID] = i
}
filteredGroupsMap := make(map[int]bool)
for i, group := range idpGroups {
var members []string
for _, member := range group.Members {
if _, ok := usersMap[member]; ok {
members = append(members, member)
}
if len(members) > 0 {
// the group at index `i` has members from the `idpUsers` list,
// so we keep it.
filteredGroupsMap[i] = true
// filter out members that were not provided in the `idpUsers` list.
idpGroups[i].Members = members
}
}
}
i := 0
filteredGroups := make([]idp.Group, len(filteredGroupsMap))
for groupIdx := range filteredGroupsMap {
filteredGroups[i] = idpGroups[groupIdx]
i++
}
return filteredGroups
}

View file

@ -4,10 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/pro/idp"
"net/http"
"net/url"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/pro/idp"
)
type Client struct {
@ -26,89 +27,103 @@ func NewAzureEntraIDClient() *Client {
}
}
func (a *Client) GetUsers() ([]idp.User, error) {
func (a *Client) GetUsers(filters []string) ([]idp.User, error) {
accessToken, err := a.getAccessToken()
if err != nil {
return nil, err
}
client := &http.Client{}
req, err := http.NewRequest("GET", "https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,displayName,accountEnabled", nil)
if err != nil {
return nil, err
getUsersURL := "https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,displayName,accountEnabled"
if len(filters) > 0 {
getUsersURL += "&" + buildPrefixFilter("userPrincipalName", filters)
}
req.Header.Add("Authorization", "Bearer "+accessToken)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
var users getUsersResponse
err = json.NewDecoder(resp.Body).Decode(&users)
if err != nil {
return nil, err
}
retval := make([]idp.User, len(users.Value))
for i, user := range users.Value {
retval[i] = idp.User{
ID: user.Id,
Username: user.UserPrincipalName,
DisplayName: user.DisplayName,
AccountDisabled: !user.AccountEnabled,
var retval []idp.User
for getUsersURL != "" {
req, err := http.NewRequest("GET", getUsersURL, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+accessToken)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
var users getUsersResponse
err = json.NewDecoder(resp.Body).Decode(&users)
_ = resp.Body.Close()
if err != nil {
return nil, err
}
for _, user := range users.Value {
retval = append(retval, idp.User{
ID: user.Id,
Username: user.UserPrincipalName,
DisplayName: user.DisplayName,
AccountDisabled: !user.AccountEnabled,
})
}
getUsersURL = users.NextLink
}
return retval, nil
}
func (a *Client) GetGroups() ([]idp.Group, error) {
func (a *Client) GetGroups(filters []string) ([]idp.Group, error) {
accessToken, err := a.getAccessToken()
if err != nil {
return nil, err
}
client := &http.Client{}
req, err := http.NewRequest("GET", "https://graph.microsoft.com/v1.0/groups?$select=id,displayName&$expand=members($select=id)", nil)
if err != nil {
return nil, err
getGroupsURL := "https://graph.microsoft.com/v1.0/groups?$select=id,displayName&$expand=members($select=id)"
if len(filters) > 0 {
getGroupsURL += "&" + buildPrefixFilter("displayName", filters)
}
req.Header.Add("Authorization", "Bearer "+accessToken)
req.Header.Add("Accept", "application/json")
var retval []idp.Group
for getGroupsURL != "" {
req, err := http.NewRequest("GET", getGroupsURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
req.Header.Add("Authorization", "Bearer "+accessToken)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
var groups getGroupsResponse
err = json.NewDecoder(resp.Body).Decode(&groups)
_ = resp.Body.Close()
}()
var groups getGroupsResponse
err = json.NewDecoder(resp.Body).Decode(&groups)
if err != nil {
return nil, err
}
retval := make([]idp.Group, len(groups.Value))
for i, group := range groups.Value {
retvalMembers := make([]string, len(group.Members))
for j, member := range group.Members {
retvalMembers[j] = member.Id
if err != nil {
return nil, err
}
retval[i] = idp.Group{
ID: group.Id,
Name: group.DisplayName,
Members: retvalMembers,
for _, group := range groups.Value {
retvalMembers := make([]string, len(group.Members))
for j, member := range group.Members {
retvalMembers[j] = member.Id
}
retval = append(retval, idp.Group{
ID: group.Id,
Name: group.DisplayName,
Members: retvalMembers,
})
}
getGroupsURL = groups.NextLink
}
return retval, nil
@ -144,6 +159,18 @@ func (a *Client) getAccessToken() (string, error) {
return "", errors.New("failed to get access token")
}
func buildPrefixFilter(field string, prefixes []string) string {
if len(prefixes) == 0 {
return ""
}
if len(prefixes) == 1 {
return fmt.Sprintf("$filter=startswith(%s,'%s')", field, prefixes[0])
}
return buildPrefixFilter(field, prefixes[1:]) + fmt.Sprintf("%%20or%%20startswith(%s,'%s')", field, prefixes[0])
}
type getUsersResponse struct {
OdataContext string `json:"@odata.context"`
Value []struct {
@ -152,6 +179,7 @@ type getUsersResponse struct {
DisplayName string `json:"displayName"`
AccountEnabled bool `json:"accountEnabled"`
} `json:"value"`
NextLink string `json:"@odata.nextLink"`
}
type getGroupsResponse struct {
@ -164,4 +192,5 @@ type getGroupsResponse struct {
Id string `json:"id"`
} `json:"members"`
} `json:"value"`
NextLink string `json:"@odata.nextLink"`
}

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/pro/idp"
admindir "google.golang.org/api/admin/directory/v1"
@ -59,7 +60,7 @@ func NewGoogleWorkspaceClient() (*Client, error) {
}, nil
}
func (g *Client) GetUsers() ([]idp.User, error) {
func (g *Client) GetUsers(filters []string) ([]idp.User, error) {
var retval []idp.User
err := g.service.Users.List().
Customer("my_customer").
@ -81,7 +82,7 @@ func (g *Client) GetUsers() ([]idp.User, error) {
return retval, err
}
func (g *Client) GetGroups() ([]idp.Group, error) {
func (g *Client) GetGroups(filters []string) ([]idp.Group, error) {
var retval []idp.Group
err := g.service.Groups.List().
Customer("my_customer").

View file

@ -1,8 +1,8 @@
package idp
type Client interface {
GetUsers() ([]User, error)
GetGroups() ([]Group, error)
GetUsers(filters []string) ([]User, error)
GetGroups(filters []string) ([]Group, error)
}
type User struct {

View file

@ -3,6 +3,7 @@ package okta
import (
"context"
"fmt"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/pro/idp"
"github.com/okta/okta-sdk-golang/v5/okta"
@ -42,7 +43,7 @@ func (o *Client) Verify() error {
return err
}
func (o *Client) GetUsers() ([]idp.User, error) {
func (o *Client) GetUsers(filters []string) ([]idp.User, error) {
var retval []idp.User
var allUsersFetched bool
@ -81,7 +82,7 @@ func (o *Client) GetUsers() ([]idp.User, error) {
return retval, nil
}
func (o *Client) GetGroups() ([]idp.Group, error) {
func (o *Client) GetGroups(filters []string) ([]idp.Group, error) {
var retval []idp.Group
var allGroupsFetched bool

View file

@ -449,6 +449,15 @@ func ListUserPolicies(u models.User) []models.Acl {
func listPoliciesOfUser(user models.User, netID models.NetworkID) []models.Acl {
allAcls := logic.ListAcls()
userAcls := []models.Acl{}
if _, ok := user.UserGroups[globalNetworksAdminGroupID]; ok {
user.UserGroups[GetDefaultNetworkAdminGroupID(netID)] = struct{}{}
}
if _, ok := user.UserGroups[globalNetworksUserGroupID]; ok {
user.UserGroups[GetDefaultNetworkUserGroupID(netID)] = struct{}{}
}
if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole {
user.UserGroups[GetDefaultNetworkAdminGroupID(netID)] = struct{}{}
}
for _, acl := range allAcls {
if acl.NetworkID == netID && acl.RuleType == models.UserPolicy {
srcMap := logic.ConvAclTagToValueMap(acl.Src)

View file

@ -729,7 +729,10 @@ func GetUserRAGNodes(user models.User) (gws map[string]models.Node) {
continue
}
if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole {
gws[node.ID.String()] = node
if ok, _ := IsUserAllowedToCommunicate(user.UserName, node); ok {
gws[node.ID.String()] = node
continue
}
} else {
// check if user has network role assigned
if roles, ok := user.NetworkRoles[models.NetworkID(node.Network)]; ok && len(roles) > 0 {