DodoApp: Sync deleted users and keys

Change-Id: Ib71fbdd142fd13fbaa4c24fb8971afe157a184b6
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 488047c..6ccbb6a 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -7,7 +7,6 @@
 	"encoding/json"
 	"errors"
 	"fmt"
-	"golang.org/x/crypto/bcrypt"
 	"html/template"
 	"io"
 	"io/fs"
@@ -17,6 +16,9 @@
 	"sync"
 	"time"
 
+	"golang.org/x/crypto/bcrypt"
+	"golang.org/x/exp/rand"
+
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
 
@@ -203,12 +205,18 @@
 	}()
 	if !s.external {
 		go func() {
+			rand.Seed(uint64(time.Now().UnixNano()))
 			s.syncUsers()
 			// TODO(dtabidze): every sync delay should be randomized to avoid all client
 			// applications hitting memberships service at the same time.
 			// For every next sync new delay should be randomly generated from scratch.
 			// We can choose random delay from 1 to 2 minutes.
-			for range time.Tick(1 * time.Minute) {
+			// for range time.Tick(1 * time.Minute) {
+			// 	s.syncUsers()
+			// }
+			for {
+				delay := time.Duration(rand.Intn(60)+60) * time.Second
+				time.Sleep(delay)
 				s.syncUsers()
 			}
 		}()
@@ -1056,30 +1064,67 @@
 		fmt.Println(err)
 		return
 	}
+	validUsernames := make(map[string]user)
+	for _, u := range users {
+		validUsernames[u.Username] = u
+	}
+	allClientUsers, err := s.client.GetAllUsers()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	keyToUser := make(map[string]string)
+	for _, clientUser := range allClientUsers {
+		userData, ok := validUsernames[clientUser]
+		if !ok {
+			if err := s.client.RemoveUser(clientUser); err != nil {
+				fmt.Println(err)
+				return
+			}
+		} else {
+			existingKeys, err := s.client.GetUserPublicKeys(clientUser)
+			if err != nil {
+				fmt.Println(err)
+				return
+			}
+			for _, existingKey := range existingKeys {
+				cleanKey := CleanKey(existingKey)
+				keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
+					return cleanKey == CleanKey(key)
+				})
+				if !keyOk {
+					if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
+						fmt.Println(err)
+					}
+				} else {
+					keyToUser[cleanKey] = clientUser
+				}
+			}
+		}
+	}
 	for _, u := range users {
 		if len(u.SSHPublicKeys) == 0 {
 			continue
 		}
-		if ok, err := s.client.UserExists(u.Username); err != nil {
+		ok, err := s.client.UserExists(u.Username)
+		if err != nil {
 			fmt.Println(err)
 			return
-		} else if !ok {
-			for i, k := range u.SSHPublicKeys {
-				if i == 0 {
-					if err := s.client.AddUser(u.Username, k); err != nil {
-						fmt.Println(err)
-						return
-					}
-				} else {
-					if err := s.client.AddPublicKey(u.Username, k); err != nil {
-						fmt.Println(err)
-						// TODO(dtabidze): If current public key is already registered
-						// with Git server, this method call will return an error.
-						// We need to differentiate such errors, and only add key which
-						// are missing.
-						continue // return
-					}
-					// TODO(dtabidze): Implement RemovePublicKey
+		}
+		if !ok {
+			if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
+				fmt.Println(err)
+				return
+			}
+		} else {
+			for _, key := range u.SSHPublicKeys {
+				cleanKey := CleanKey(key)
+				if user, ok := keyToUser[cleanKey]; ok && u.Username == user {
+					panic("MUST NOT REACH!")
+				}
+				if err := s.client.AddPublicKey(u.Username, key); err != nil {
+					fmt.Println(err)
+					return
 				}
 			}
 		}
@@ -1100,3 +1145,11 @@
 		}
 	}
 }
+
+func CleanKey(key string) string {
+	fields := strings.Fields(key)
+	if len(fields) < 2 {
+		return key
+	}
+	return fields[0] + " " + fields[1]
+}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index 1ab6b39..040607d 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -3,7 +3,6 @@
 import (
 	"bytes"
 	"encoding/json"
-	"golang.org/x/crypto/ssh"
 	"io"
 	"io/fs"
 	"log"
@@ -14,11 +13,11 @@
 	"testing"
 	"time"
 
+	"golang.org/x/crypto/ssh"
+
 	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-billy/v5/util"
-	// "github.com/go-git/go-git/v5"
-	// "github.com/go-git/go-git/v5/storage/memory"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
@@ -137,14 +136,26 @@
 	return "", nil
 }
 
+func (f fakeSoftServeClient) GetAllUsers() ([]string, error) {
+	return []string{}, nil
+}
+
 func (f fakeSoftServeClient) AddUser(name, pubKey string) error {
 	return nil
 }
 
+func (f fakeSoftServeClient) RemoveUser(name string) error {
+	return nil
+}
+
 func (f fakeSoftServeClient) AddPublicKey(user string, pubKey string) error {
 	return nil
 }
 
+func (f fakeSoftServeClient) GetUserPublicKeys(username string) ([]string, error) {
+	return []string{}, nil
+}
+
 func (f fakeSoftServeClient) RemovePublicKey(user string, pubKey string) error {
 	return nil
 }