DodoApp: Support dev virtual machines

Change-Id: Ib7641adb5be477bdde7cd9a06df4b45aa65a1c01
diff --git a/core/auth/memberships/main.go b/core/auth/memberships/main.go
index ba5db7c..e72a163 100644
--- a/core/auth/memberships/main.go
+++ b/core/auth/memberships/main.go
@@ -691,6 +691,8 @@
 	go func() {
 		r := mux.NewRouter()
 		r.HandleFunc("/api/init", s.apiInitHandler)
+		// TODO(gio): change to /api/users/{username}
+		r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
 		r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
 		r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
 		r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
@@ -1153,7 +1155,7 @@
 		http.Error(w, "SSH key not present", http.StatusBadRequest)
 		return
 	}
-	if err := s.store.AddSSHKeyForUser(username, sshKey); err != nil {
+	if err := s.store.AddSSHKeyForUser(strings.ToLower(username), sshKey); err != nil {
 		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
 		http.Redirect(w, r, redirectURL, http.StatusFound)
 		return
@@ -1238,11 +1240,7 @@
 }
 
 func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
-	defer s.pingAllSyncAddresses()
-	selfAddress := r.FormValue("selfAddress")
-	if selfAddress != "" {
-		s.addSyncAddress(selfAddress)
-	}
+	s.addSyncAddress(r.FormValue("selfAddress"))
 	var users []User
 	var err error
 	groups := r.FormValue("groups")
@@ -1316,23 +1314,58 @@
 	}
 }
 
+type addUserKeyRequest struct {
+	User      string `json:"user"`
+	PublicKey string `json:"publicKey"`
+}
+
+func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
+	var req addUserKeyRequest
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, "Invalid request body", http.StatusBadRequest)
+		return
+	}
+	if req.User == "" {
+		http.Error(w, "Username cannot be empty", http.StatusBadRequest)
+		return
+	}
+	if req.PublicKey == "" {
+		http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
+		return
+	}
+	if err := s.store.AddSSHKeyForUser(strings.ToLower(req.User), req.PublicKey); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+// TODO(gio): enque sync event instead of directly reaching out to clients.
+// This will allow to deduplicate sync events and save resources.
 func (s *Server) pingAllSyncAddresses() {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	for address := range s.syncAddresses {
-		resp, err := http.Get(address)
-		if err != nil {
-			log.Printf("Failed to ping %s: %v", address, err)
-			continue
-		}
-		resp.Body.Close()
-		if resp.StatusCode != http.StatusOK {
-			log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
-		}
+		go func(address string) {
+			log.Printf("Pinging %s", address)
+			resp, err := http.Get(address)
+			if err != nil {
+				// TODO(gio): remove sync address after N number of failures.
+				log.Printf("Failed to ping %s: %v", address, err)
+				return
+			}
+			defer resp.Body.Close()
+			if resp.StatusCode != http.StatusOK {
+				log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
+			}
+		}(address)
 	}
 }
 
 func (s *Server) addSyncAddress(address string) {
+	if address == "" {
+		return
+	}
+	fmt.Printf("Adding sync address: %s\n", address)
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	s.syncAddresses[address] = struct{}{}