DodoApp: Sync deleted users and keys

Change-Id: Ib71fbdd142fd13fbaa4c24fb8971afe157a184b6
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 3e58279..a285ffd 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -3,7 +3,6 @@
 import (
 	"errors"
 	"fmt"
-	"golang.org/x/crypto/ssh"
 	"log"
 	"net"
 	"os"
@@ -12,6 +11,8 @@
 	"strings"
 	"time"
 
+	"golang.org/x/crypto/ssh"
+
 	"github.com/cenkalti/backoff/v4"
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-git/v5"
@@ -33,9 +34,12 @@
 	AddRepository(name string) error
 	UserExists(name string) (bool, error)
 	FindUser(pubKey string) (string, error)
+	GetAllUsers() ([]string, error)
 	AddUser(name, pubKey string) error
+	RemoveUser(user string) error
 	AddPublicKey(user string, pubKey string) error
 	RemovePublicKey(user string, pubKey string) error
+	GetUserPublicKeys(user string) ([]string, error)
 	MakeUserAdmin(name string) error
 	AddReadWriteCollaborator(repo, user string) error
 	AddReadOnlyCollaborator(repo, user string) error
@@ -96,6 +100,15 @@
 	return ss.signer
 }
 
+func (ss *realClient) GetAllUsers() ([]string, error) {
+	log.Printf("Getting all users")
+	out, err := ss.RunCommand("user", "list")
+	if err != nil {
+		return nil, err
+	}
+	return strings.Fields(out), nil
+}
+
 func (ss *realClient) UserExists(name string) (bool, error) {
 	log.Printf("Checking user exists %s", name)
 	out, err := ss.RunCommand("user", "list")
@@ -132,6 +145,12 @@
 	return ss.AddPublicKey(name, pubKey)
 }
 
+func (ss *realClient) RemoveUser(user string) error {
+	log.Printf("Removing user: %s\n", user)
+	_, err := ss.RunCommand("user", "delete", user)
+	return err
+}
+
 func (ss *realClient) MakeUserAdmin(name string) error {
 	log.Printf("Making user %s admin", name)
 	_, err := ss.RunCommand("user", "set-admin", name, "true")
@@ -150,6 +169,31 @@
 	return err
 }
 
+func (ss *realClient) GetUserPublicKeys(user string) ([]string, error) {
+	log.Printf("Getting public keys for user: %s\n", user)
+	out, err := ss.RunCommand("user", "info", user)
+	if err != nil {
+		return nil, err
+	}
+	return extractPublicKeys(out), nil
+}
+
+func extractPublicKeys(userInfo string) []string {
+	var keys []string
+	lines := strings.Split(userInfo, "\n")
+	gettingKeys := false
+	for _, line := range lines {
+		if strings.HasPrefix(line, "Public keys:") {
+			gettingKeys = true
+			continue
+		}
+		if gettingKeys {
+			keys = append(keys, strings.TrimSpace(line))
+		}
+	}
+	return keys
+}
+
 func (ss *realClient) RunCommand(args ...string) (string, error) {
 	cmd := strings.Join(args, " ")
 	log.Printf("Running command %s", cmd)
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
 }