DodoApp: Implement user synchronization API

Change-Id: Id38c96f379832d2d5034e215de2e51d28a25634c
diff --git a/charts/dodo-app/templates/install.yaml b/charts/dodo-app/templates/install.yaml
index d49a6d1..58c920c 100644
--- a/charts/dodo-app/templates/install.yaml
+++ b/charts/dodo-app/templates/install.yaml
@@ -129,6 +129,7 @@
         - --db=/dodo-app/db/apps.db
         - --networks={{ .Values.allowedNetworks }}
         - --external={{ .Values.external }}
+        - --fetch-users-addr={{ .Values.fetchUsersAddr }}
         volumeMounts:
         - name: ssh-key
           readOnly: true
diff --git a/charts/dodo-app/values.yaml b/charts/dodo-app/values.yaml
index 2fba879..b1d57c4 100644
--- a/charts/dodo-app/values.yaml
+++ b/charts/dodo-app/values.yaml
@@ -16,3 +16,4 @@
 persistentVolumeClaimName: ""
 allowedNetworks: ""
 external: false
+fetchUsersAddr: ""
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index f6d9c43..ef62f5c 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -30,6 +30,7 @@
 	gitRepoPublicKey  string
 	db                string
 	networks          []string
+	fetchUsersAddr    string
 }
 
 func dodoAppCmd() *cobra.Command {
@@ -80,6 +81,12 @@
 		"",
 	)
 	cmd.Flags().StringVar(
+		&dodoAppFlags.fetchUsersAddr,
+		"fetch-users-addr",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
 		&dodoAppFlags.repoPublicAddr,
 		"repo-public-addr",
 		"",
@@ -195,7 +202,8 @@
 		nsc,
 		jc,
 		env,
-		!dodoAppFlags.external,
+		dodoAppFlags.external,
+		dodoAppFlags.fetchUsersAddr,
 	)
 	if err != nil {
 		return err
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index a5cfa31..3e58279 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -28,6 +28,7 @@
 	GetPublicKeys() ([]string, error)
 	RepoExists(name string) (bool, error)
 	GetRepo(name string) (RepoIO, error)
+	GetAllRepos() ([]string, error)
 	GetRepoAddress(name string) string
 	AddRepository(name string) error
 	UserExists(name string) (bool, error)
@@ -222,6 +223,15 @@
 	return NewRepoIO(r, ss.signer)
 }
 
+func (ss *realClient) GetAllRepos() ([]string, error) {
+	log.Printf("Getting all repos")
+	out, err := ss.RunCommand("repo", "list")
+	if err != nil {
+		return nil, err
+	}
+	return strings.Fields(out), nil
+}
+
 type RepositoryAddress struct {
 	Addr string
 	Name string
diff --git a/core/installer/values-tmpl/dodo-app.cue b/core/installer/values-tmpl/dodo-app.cue
index 7e2a1a7..cfd0933 100644
--- a/core/installer/values-tmpl/dodo-app.cue
+++ b/core/installer/values-tmpl/dodo-app.cue
@@ -129,6 +129,7 @@
 			persistentVolumeClaimName: volumes.db.name
 			allowedNetworks: strings.Join([for n in input.allowedNetworks { n.name }], ",")
 			external: input.external
+			fetchUsersAddr: "http://memberships-api.\(global.namespacePrefix)core-auth-memberships.svc.cluster.local/api/users"
 		}
 	}
 }
diff --git a/core/installer/welcome/dodo-app-tmpl/base.html b/core/installer/welcome/dodo-app-tmpl/base.html
index 85b480e..9cb4e07 100644
--- a/core/installer/welcome/dodo-app-tmpl/base.html
+++ b/core/installer/welcome/dodo-app-tmpl/base.html
@@ -5,7 +5,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ block "title" . }}{{ end }}</title>
     <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
-    <link rel="stylesheet" href="/static/dodo_app.css?v=0.0.3">
+    <link rel="stylesheet" href="/static/dodo_app.css?v=0.0.4">
 </head>
 <body class="container">
     {{- block "content" . }}
diff --git a/core/installer/welcome/dodo-app-tmpl/index.html b/core/installer/welcome/dodo-app-tmpl/index.html
index ff53b2a..9874d12 100644
--- a/core/installer/welcome/dodo-app-tmpl/index.html
+++ b/core/installer/welcome/dodo-app-tmpl/index.html
@@ -15,7 +15,6 @@
 			<option value="{{ . }}">{{ . }}</option>
 			{{- end -}}
 		</select>
-		<input type="text" name="admin-public-key" placeholder="Admin Public Key" />
 		<button type="submit" name="create-app">Create App</button>
 	</fieldset>
 </form>
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 87b003a..488047c 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -15,6 +15,7 @@
 	"slices"
 	"strings"
 	"sync"
+	"time"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
@@ -93,7 +94,8 @@
 	appConfigs        map[string]appConfig
 	tmplts            dodoAppTmplts
 	appTmpls          AppTmplStore
-	allowNetworkReuse bool
+	external          bool
+	fetchUsersAddr    string
 }
 
 type appConfig struct {
@@ -118,7 +120,8 @@
 	nsc installer.NamespaceCreator,
 	jc installer.JobCreator,
 	env installer.EnvConfig,
-	allowNetworkReuse bool,
+	external bool,
+	fetchUsersAddr string,
 ) (*DodoAppServer, error) {
 	tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
 	if err != nil {
@@ -153,7 +156,8 @@
 		map[string]appConfig{},
 		tmplts,
 		appTmpls,
-		allowNetworkReuse,
+		external,
+		fetchUsersAddr,
 	}
 	config, err := client.GetRepo(ConfigRepoName)
 	if err != nil {
@@ -192,8 +196,23 @@
 		r.HandleFunc("/update", s.handleAPIUpdate)
 		r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
 		r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
+		if !s.external {
+			r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
+		}
 		e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
 	}()
+	if !s.external {
+		go func() {
+			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) {
+				s.syncUsers()
+			}
+		}()
+	}
 	return <-e
 }
 
@@ -533,11 +552,6 @@
 		http.Error(w, "missing type", http.StatusBadRequest)
 		return
 	}
-	adminPublicKey := r.FormValue("admin-public-key")
-	if adminPublicKey == "" {
-		http.Error(w, "missing admin public key", http.StatusBadRequest)
-		return
-	}
 	g := installer.NewFixedLengthRandomNameGenerator(3)
 	appName, err := g.Generate()
 	if err != nil {
@@ -548,12 +562,10 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	} else if !ok {
-		if err := s.client.AddUser(user, adminPublicKey); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
+		http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
+		return
 	}
-	if err := s.st.CreateUser(user, nil, adminPublicKey, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
+	if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -613,7 +625,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if err := s.st.CreateUser(user, hashed, req.AdminPublicKey, req.Network); err != nil {
+	if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -636,7 +648,7 @@
 }
 
 func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
-	if s.allowNetworkReuse {
+	if !s.external {
 		return true
 	}
 	for _, cfg := range s.appConfigs {
@@ -1020,3 +1032,71 @@
 	}
 	return ret, nil
 }
+
+type user struct {
+	Username      string   `json:"username"`
+	Email         string   `json:"email"`
+	SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
+}
+
+func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
+	go s.syncUsers()
+}
+
+func (s *DodoAppServer) syncUsers() {
+	if s.external {
+		panic("MUST NOT REACH!")
+	}
+	resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
+	if err != nil {
+		return
+	}
+	users := []user{}
+	if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
+		fmt.Println(err)
+		return
+	}
+	for _, u := range users {
+		if len(u.SSHPublicKeys) == 0 {
+			continue
+		}
+		if ok, err := s.client.UserExists(u.Username); 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
+				}
+			}
+		}
+	}
+	repos, err := s.client.GetAllRepos()
+	if err != nil {
+		return
+	}
+	for _, r := range repos {
+		if r == ConfigRepoName {
+			continue
+		}
+		for _, u := range users {
+			if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
+				fmt.Println(err)
+				return
+			}
+		}
+	}
+}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index 1c18470..414fac8 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -118,6 +118,10 @@
 	return mockRepoIO{soft.NewBillyRepoFS(f.envFS), "foo.bar", f.t, &l}, nil
 }
 
+func (f fakeSoftServeClient) GetAllRepos() ([]string, error) {
+	return []string{}, nil
+}
+
 func (f fakeSoftServeClient) GetRepoAddress(name string) string {
 	return ""
 }
diff --git a/core/installer/welcome/static/dodo_app.css b/core/installer/welcome/static/dodo_app.css
index 7381a9b..320f940 100644
--- a/core/installer/welcome/static/dodo_app.css
+++ b/core/installer/welcome/static/dodo_app.css
@@ -78,6 +78,6 @@
 
 @media (min-width: 768px) {
 	fieldset.grid {
-		grid-template-columns: 1fr 1fr 1fr 1fr 200px;
+		grid-template-columns: 1fr 1fr 1fr 200px;
 	}
 };
diff --git a/core/installer/welcome/store.go b/core/installer/welcome/store.go
index e93c03e..8ea0860 100644
--- a/core/installer/welcome/store.go
+++ b/core/installer/welcome/store.go
@@ -23,8 +23,7 @@
 }
 
 type Store interface {
-	// TODO(gio): Remove publicKey once auto user sync is implemented
-	CreateUser(username string, password []byte, publicKey, network string) error
+	CreateUser(username string, password []byte, network string) error
 	GetUserPassword(username string) ([]byte, error)
 	GetUserNetwork(username string) (string, error)
 	GetApps() ([]string, error)
@@ -53,7 +52,6 @@
 		CREATE TABLE IF NOT EXISTS users (
 			username TEXT PRIMARY KEY,
             password BLOB,
-            public_key TEXT,
             network TEXT
 		);
 		CREATE TABLE IF NOT EXISTS apps (
@@ -70,9 +68,9 @@
 
 }
 
-func (s *storeImpl) CreateUser(username string, password []byte, publicKey, network string) error {
-	query := `INSERT INTO users (username, password, public_key, network) VALUES (?, ?, ?, ?)`
-	_, err := s.db.Exec(query, username, password, publicKey, network)
+func (s *storeImpl) CreateUser(username string, password []byte, network string) error {
+	query := `INSERT INTO users (username, password, network) VALUES (?, ?, ?)`
+	_, err := s.db.Exec(query, username, password, network)
 	if err != nil {
 		sqliteErr, ok := err.(*sqlite3.Error)
 		if ok && sqliteErr.ExtendedCode() == errorConstraintPrimaryKeyViolation {