123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- package auth
- import (
- "context"
- "fmt"
- "strings"
- "sync"
- "time"
- "github.com/gravitl/netmaker/database"
- "github.com/gravitl/netmaker/logger"
- "github.com/gravitl/netmaker/logic"
- "github.com/gravitl/netmaker/models"
- "github.com/gravitl/netmaker/pro/idp"
- "github.com/gravitl/netmaker/pro/idp/azure"
- "github.com/gravitl/netmaker/pro/idp/google"
- "github.com/gravitl/netmaker/pro/idp/okta"
- proLogic "github.com/gravitl/netmaker/pro/logic"
- )
- var (
- cancelSyncHook context.CancelFunc
- hookStopWg sync.WaitGroup
- )
- func ResetIDPSyncHook() {
- if cancelSyncHook != nil {
- cancelSyncHook()
- hookStopWg.Wait()
- cancelSyncHook = nil
- }
- if logic.IsSyncEnabled() {
- ctx, cancel := context.WithCancel(context.Background())
- cancelSyncHook = cancel
- hookStopWg.Add(1)
- go runIDPSyncHook(ctx)
- }
- }
- func runIDPSyncHook(ctx context.Context) {
- defer hookStopWg.Done()
- ticker := time.NewTicker(logic.GetIDPSyncInterval())
- defer ticker.Stop()
- for {
- select {
- case <-ctx.Done():
- logger.Log(0, "idp sync hook stopped")
- return
- case <-ticker.C:
- if err := SyncFromIDP(); err != nil {
- logger.Log(0, "failed to sync from idp: ", err.Error())
- } else {
- logger.Log(0, "sync from idp complete")
- }
- }
- }
- }
- func SyncFromIDP() error {
- settings := logic.GetServerSettings()
- var idpClient idp.Client
- var idpUsers []idp.User
- var idpGroups []idp.Group
- var err error
- switch settings.AuthProvider {
- case "google":
- idpClient, err = google.NewGoogleWorkspaceClient()
- if err != nil {
- return err
- }
- case "azure-ad":
- idpClient = azure.NewAzureEntraIDClient()
- case "okta":
- idpClient, err = okta.NewOktaClientFromSettings()
- if err != nil {
- return err
- }
- default:
- if settings.AuthProvider != "" {
- return fmt.Errorf("invalid auth provider: %s", settings.AuthProvider)
- }
- }
- if settings.AuthProvider != "" && idpClient != nil {
- idpUsers, err = idpClient.GetUsers(settings.UserFilters)
- if err != nil {
- return err
- }
- 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)
- if err != nil {
- return err
- }
- return syncGroups(idpGroups)
- }
- func syncUsers(idpUsers []idp.User) error {
- dbUsers, err := logic.GetUsersDB()
- if err != nil && !database.IsEmptyRecord(err) {
- return err
- }
- password, err := logic.FetchPassValue("")
- if err != nil {
- return err
- }
- idpUsersMap := make(map[string]struct{})
- for _, user := range idpUsers {
- idpUsersMap[user.Username] = struct{}{}
- }
- dbUsersMap := make(map[string]models.User)
- for _, user := range dbUsers {
- dbUsersMap[user.UserName] = user
- }
- filters := logic.GetServerSettings().UserFilters
- for _, user := range idpUsers {
- if user.AccountArchived {
- // delete the user if it has been archived.
- _ = logic.DeleteUser(user.Username)
- continue
- }
- var found bool
- for _, filter := range filters {
- if strings.HasPrefix(user.Username, filter) {
- found = true
- break
- }
- }
- // if there are filters but none of them match, then skip this user.
- if len(filters) > 0 && !found {
- continue
- }
- dbUser, ok := dbUsersMap[user.Username]
- if !ok {
- // create the user only if it doesn't exist.
- err = logic.CreateUser(&models.User{
- UserName: user.Username,
- ExternalIdentityProviderID: user.ID,
- DisplayName: user.DisplayName,
- AccountDisabled: user.AccountDisabled,
- Password: password,
- AuthType: models.OAuth,
- PlatformRoleID: models.ServiceUser,
- })
- if err != nil {
- return err
- }
- // It's possible that a user can attempt to log in to Netmaker
- // after the IDP is configured but before the users are synced.
- // Since the user doesn't exist, a pending user will be
- // created. Now, since the user is created, the pending user
- // can be deleted.
- _ = logic.DeletePendingUser(user.Username)
- } else if dbUser.AuthType == models.OAuth {
- if dbUser.AccountDisabled != user.AccountDisabled ||
- dbUser.DisplayName != user.DisplayName ||
- dbUser.ExternalIdentityProviderID != user.ID {
- dbUser.AccountDisabled = user.AccountDisabled
- dbUser.DisplayName = user.DisplayName
- dbUser.ExternalIdentityProviderID = user.ID
- err = logic.UpsertUser(dbUser)
- if err != nil {
- return err
- }
- }
- } else {
- logger.Log(0, "user with username "+user.Username+" already exists, skipping creation")
- continue
- }
- }
- for _, user := range dbUsersMap {
- if user.ExternalIdentityProviderID == "" {
- continue
- }
- if _, ok := idpUsersMap[user.UserName]; !ok {
- // delete the user if it has been deleted on idp.
- err = logic.DeleteUser(user.UserName)
- if err != nil {
- return err
- }
- }
- }
- return nil
- }
- func syncGroups(idpGroups []idp.Group) error {
- dbGroups, err := proLogic.ListUserGroups()
- if err != nil && !database.IsEmptyRecord(err) {
- return err
- }
- dbUsers, err := logic.GetUsersDB()
- if err != nil && !database.IsEmptyRecord(err) {
- return err
- }
- idpGroupsMap := make(map[string]struct{})
- for _, group := range idpGroups {
- idpGroupsMap[group.ID] = struct{}{}
- }
- dbGroupsMap := make(map[string]models.UserGroup)
- for _, group := range dbGroups {
- if group.ExternalIdentityProviderID != "" {
- dbGroupsMap[group.ExternalIdentityProviderID] = group
- }
- }
- dbUsersMap := make(map[string]models.User)
- for _, user := range dbUsers {
- if user.ExternalIdentityProviderID != "" {
- dbUsersMap[user.ExternalIdentityProviderID] = user
- }
- }
- modifiedUsers := make(map[string]struct{})
- filters := logic.GetServerSettings().GroupFilters
- for _, group := range idpGroups {
- var found bool
- for _, filter := range filters {
- if strings.HasPrefix(group.Name, filter) {
- found = true
- break
- }
- }
- // if there are filters but none of them match, then skip this group.
- if len(filters) > 0 && !found {
- continue
- }
- dbGroup, ok := dbGroupsMap[group.ID]
- if !ok {
- dbGroup.ExternalIdentityProviderID = group.ID
- dbGroup.Name = group.Name
- dbGroup.Default = false
- dbGroup.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
- err := proLogic.CreateUserGroup(&dbGroup)
- if err != nil {
- return err
- }
- } else {
- dbGroup.Name = group.Name
- err = proLogic.UpdateUserGroup(dbGroup)
- if err != nil {
- return err
- }
- }
- groupMembersMap := make(map[string]struct{})
- for _, member := range group.Members {
- groupMembersMap[member] = struct{}{}
- }
- for _, user := range dbUsers {
- // use dbGroup.Name because the group name may have been changed on idp.
- _, inNetmakerGroup := user.UserGroups[dbGroup.ID]
- _, inIDPGroup := groupMembersMap[user.ExternalIdentityProviderID]
- if inNetmakerGroup && !inIDPGroup {
- // use dbGroup.Name because the group name may have been changed on idp.
- delete(dbUsersMap[user.ExternalIdentityProviderID].UserGroups, dbGroup.ID)
- modifiedUsers[user.ExternalIdentityProviderID] = struct{}{}
- }
- if !inNetmakerGroup && inIDPGroup {
- // use dbGroup.Name because the group name may have been changed on idp.
- dbUsersMap[user.ExternalIdentityProviderID].UserGroups[dbGroup.ID] = struct{}{}
- modifiedUsers[user.ExternalIdentityProviderID] = struct{}{}
- }
- }
- }
- for userID := range modifiedUsers {
- err = logic.UpsertUser(dbUsersMap[userID])
- if err != nil {
- return err
- }
- }
- for _, group := range dbGroups {
- if group.ExternalIdentityProviderID != "" {
- if _, ok := idpGroupsMap[group.ExternalIdentityProviderID]; !ok {
- // delete the group if it has been deleted on idp.
- err = proLogic.DeleteUserGroup(group.ID)
- if err != nil {
- return err
- }
- }
- }
- }
- 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
- }
|