DodoApp: Restrict users to one domain

Change-Id: I4d09d5ee61d0ec712fd9dfa848c0af0c8e550d68
diff --git a/charts/dodo-app/templates/install.yaml b/charts/dodo-app/templates/install.yaml
index f94202b..d49a6d1 100644
--- a/charts/dodo-app/templates/install.yaml
+++ b/charts/dodo-app/templates/install.yaml
@@ -121,13 +121,14 @@
         - --port={{ .Values.port }}
         - --api-port={{ .Values.apiPort }}
         - --self={{ .Values.self }}
+        - --repo-public-addr={{ .Values.repoPublicAddr }}
         - --namespace={{ .Values.namespace }} # TODO(gio): maybe use .Release.Namespace ?
         - --env-app-manager-addr={{ .Values.envAppManagerAddr }}
         - --env-config=/pcloud/env-config/config.json
-        - --app-admin-key={{ .Values.appAdminKey }}
         - --git-repo-public-key={{ .Values.gitRepoPublicKey }}
         - --db=/dodo-app/db/apps.db
         - --networks={{ .Values.allowedNetworks }}
+        - --external={{ .Values.external }}
         volumeMounts:
         - name: ssh-key
           readOnly: true
diff --git a/charts/dodo-app/values.yaml b/charts/dodo-app/values.yaml
index abb976a..2fba879 100644
--- a/charts/dodo-app/values.yaml
+++ b/charts/dodo-app/values.yaml
@@ -8,10 +8,11 @@
 repoAddr: 192.168.0.11
 sshPrivateKey: key
 self: ""
+repoPublicAddr: ""
 namespace: ""
 envAppManagerAddr: ""
 envConfig: ""
-appAdminKey: ""
 gitRepoPublicKey: ""
 persistentVolumeClaimName: ""
 allowedNetworks: ""
+external: false
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index 47af9a0..a9edbde 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -65,6 +65,7 @@
 			if auth.enabled {
 				"auth-proxy": {
 					chart: charts.authProxy
+					info: "Installing authentication proxy"
 					values: {
 						image: {
 							repository: images.authProxy.fullName
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index ea5e7fd..5fc6e3e 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -17,15 +17,16 @@
 )
 
 var dodoAppFlags struct {
+	external          bool
 	port              int
 	apiPort           int
 	sshKey            string
 	repoAddr          string
 	self              string
+	repoPublicAddr    string
 	namespace         string
 	envAppManagerAddr string
 	envConfig         string
-	appAdminKey       string
 	gitRepoPublicKey  string
 	db                string
 	networks          []string
@@ -36,6 +37,12 @@
 		Use:  "dodo-app",
 		RunE: dodoAppCmdRun,
 	}
+	cmd.Flags().BoolVar(
+		&dodoAppFlags.external,
+		"external",
+		false,
+		"",
+	)
 	cmd.Flags().IntVar(
 		&dodoAppFlags.port,
 		"port",
@@ -73,6 +80,12 @@
 		"",
 	)
 	cmd.Flags().StringVar(
+		&dodoAppFlags.repoPublicAddr,
+		"repo-public-addr",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
 		&dodoAppFlags.namespace,
 		"namespace",
 		"",
@@ -91,12 +104,6 @@
 		"",
 	)
 	cmd.Flags().StringVar(
-		&dodoAppFlags.appAdminKey,
-		"app-admin-key",
-		"",
-		"",
-	)
-	cmd.Flags().StringVar(
 		&dodoAppFlags.gitRepoPublicKey,
 		"git-repo-public-key",
 		"",
@@ -157,17 +164,34 @@
 	if err != nil {
 		return err
 	}
+	var nf welcome.NetworkFilter
+	if len(dodoAppFlags.networks) == 0 {
+		nf = welcome.NewNoNetworkFilter()
+	} else {
+		nf = welcome.NewAllowListFilter(dodoAppFlags.networks)
+	}
+	if dodoAppFlags.external {
+		nf = welcome.NewCombinedFilter(welcome.NewNetworkFilterByOwner(st), nf)
+	}
+	var ug welcome.UserGetter
+	if dodoAppFlags.external {
+		ug = welcome.NewExternalUserGetter()
+	} else {
+		ug = welcome.NewInternalUserGetter()
+	}
 	s, err := welcome.NewDodoAppServer(
 		st,
+		nf,
+		ug,
 		dodoAppFlags.port,
 		dodoAppFlags.apiPort,
 		dodoAppFlags.self,
+		dodoAppFlags.repoPublicAddr,
 		string(sshKey),
 		dodoAppFlags.gitRepoPublicKey,
 		softClient,
 		dodoAppFlags.namespace,
 		dodoAppFlags.envAppManagerAddr,
-		dodoAppFlags.networks,
 		nsc,
 		jc,
 		env,
@@ -175,10 +199,5 @@
 	if err != nil {
 		return err
 	}
-	if dodoAppFlags.appAdminKey != "" {
-		if _, err := s.CreateApp("app", dodoAppFlags.appAdminKey, "Private"); err != nil {
-			return err
-		}
-	}
 	return s.Start()
 }
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 4a5021e..a5cfa31 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -96,7 +96,7 @@
 }
 
 func (ss *realClient) UserExists(name string) (bool, error) {
-	log.Printf("Adding user %s", name)
+	log.Printf("Checking user exists %s", name)
 	out, err := ss.RunCommand("user", "list")
 	if err != nil {
 		return false, err
diff --git a/core/installer/values-tmpl/dodo-app.cue b/core/installer/values-tmpl/dodo-app.cue
index ac688c9..ee96044 100644
--- a/core/installer/values-tmpl/dodo-app.cue
+++ b/core/installer/values-tmpl/dodo-app.cue
@@ -8,8 +8,8 @@
 	network: #Network @name(Network)
 	subdomain: string @name(Subdomain)
 	sshPort: int @name(SSH Port) @role(port)
-	adminKey: string | *"" @name(Admin SSH Public Key)
 	allowedNetworks: string | *"" @name(Allowed Networks)
+	external: bool | *false @name(External)
 
 	// TODO(gio): auto generate
 	ssKeys: #SSHKey
@@ -23,6 +23,7 @@
 description: "Deploy app by pushing to Git repository"
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 48 48'><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M2.837 27.257c3.363 2.45 11.566 3.523 12.546 1.4s.424-10.94.424-10.94s-1.763 1.192-2.302.147s.44-2.433 2.319-2.858c-1.96.05-2.221-.571-2.205-.93s.67-1.878 3.527-1.241c-1.6-.751-1.943-2.956 2.352-1.568c-1.421-.735-.36-2.825 1.649-.62c-.261-1.323 1.584-1.46 2.694.907M10.648 34.633a19 19 0 0 0-4.246.719'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M15.144 43.402c3.625-2.482 7.685-6.32 7.293-13.406s-1.6-6.368-.523-7.577s6.924-.99 10.712 3.353c.032-2.874-2.504-5.508-2.504-5.508a33 33 0 0 1 5.53.163c2.852.49 2.394 2.514 3.58 2.035s.971-3.472-.39-5.377c-1.666-2.33-3.223-2.83-6.358-2.188s-4.474.458-5.54-.587s-2.026-3.538-4.605-2.515c-2.935 1.164-4.398 2.438-3.767 5.04s2.34 4.558 2.972 6.844'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M22.001 16.552c-.925-.043-1.894.055-1.709 1.328'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M20.662 16.763c1.72 2.695 3.405 3.643 9.46 3.501'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M32.14 14.966c-1.223.879-2.18 3.781-2.496 5.307M23.1 14.908c.48 1.209 1.23.728 1.315.283a1.552 1.552 0 0 0-1.543-1.883m-.408 17.472c5.328 2.71 11.631.229 16.269-2.123c-1.176 4.572-5.911 5.585-8.916 6.107'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M29.099 37.115c4.376-.294 8.024-1.578 7.833-5.296'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M20.27 38.702c6.771 3.834 12.505.798 13.786-2.615'/><circle cx='24' cy='24' r='21.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>"
 _domain: "\(input.subdomain).\(input.network.domain)"
+url: "https://\(_domain)"
 
 images: {
 	softserve: {
@@ -58,7 +59,14 @@
 
 ingress: {
 	"dodo-app": {
-		auth: enabled: false
+		auth: {
+			if input.external {
+				enabled: false
+			}
+			if !input.external {
+				enabled: true
+			}
+		}
 		network: input.network
 		subdomain: input.subdomain
 		service: {
@@ -113,13 +121,14 @@
 			repoAddr: "soft-serve.\(release.namespace).svc.cluster.local:22"
 			sshPrivateKey: base64.Encode(null, input.dAppKeys.private)
 			self: "api.\(release.namespace).svc.cluster.local"
+			repoPublicAddr: "ssh://\(_domain):\(input.sshPort)"
 			namespace: release.namespace
 			envAppManagerAddr: "http://appmanager.\(global.namespacePrefix)appmanager.svc.cluster.local"
 			envConfig: base64.Encode(null, json.Marshal(global))
-			appAdminKey: input.adminKey
 			gitRepoPublicKey: input.ssKeys.public
 			persistentVolumeClaimName: volumes.db.name
 			allowedNetworks: input.allowedNetworks
+			external: input.external
 		}
 	}
 }
@@ -173,10 +182,3 @@
 		}
 	}
 }
-
-help: [{
-	title: "How to use"
-	"contents": """
-	Clone: git clone ssh://\(_domain):\(input.sshPort)/app <div onClick='copyToClipboard(this, "git clone ssh://\(_domain):\(input.sshPort)/app")' style='display: inline-block; cursor: pointer;'> <svg width='26px' height='26px' viewBox='-0 -0 28.80 28.80' fill='#7f9f7f' xmlns='http://www.w3.org/2000/svg' style='outline: none;'> <g id='SVGRepo_bgCarrier' stroke-width='0'></g> <g id='SVGRepo_tracerCarrier' stroke-linecap='round' stroke-linejoin='round'></g> <g id='SVGRepo_iconCarrier'> <path fill-rule='evenodd' clip-rule='evenodd' d='M19.5 16.5L19.5 4.5L18.75 3.75H9L8.25 4.5L8.25 7.5L5.25 7.5L4.5 8.25V20.25L5.25 21H15L15.75 20.25V17.25H18.75L19.5 16.5ZM15.75 15.75L15.75 8.25L15 7.5L9.75 7.5V5.25L18 5.25V15.75H15.75ZM6 9L14.25 9L14.25 19.5L6 19.5L6 9Z' fill='#7f9f7f'></path> </g> </svg> </div>  Server public key: \(input.ssKeys.public)
-	"""
-}]
diff --git a/core/installer/welcome/dodo-app-tmpl/index.html b/core/installer/welcome/dodo-app-tmpl/index.html
index e9a9b11..af82173 100644
--- a/core/installer/welcome/dodo-app-tmpl/index.html
+++ b/core/installer/welcome/dodo-app-tmpl/index.html
@@ -5,6 +5,15 @@
 		<meta charset='utf-8'>
 	</head>
 	<body>
+		<form action="" method="POST">
+			<select name="network">
+				{{ range .Networks }}
+				<option value="{{ .Name }}">{{ .Name }} - {{ .Domain }}</option>
+				{{ end }}
+			</select>
+			<input type="text" name="admin-public-key" placeholder="Admin Public Key" />
+			<button type="submit" name="create-app">Create App</button>
+		</form>
 		{{ range .Apps }}
 		<a href="/{{ . }}">{{ . }}</a>
 		{{ end }}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 1c91cbc..1315266 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -49,15 +49,17 @@
 type DodoAppServer struct {
 	l                 sync.Locker
 	st                Store
+	nf                NetworkFilter
+	ug                UserGetter
 	port              int
 	apiPort           int
 	self              string
+	repoPublicAddr    string
 	sshKey            string
 	gitRepoPublicKey  string
 	client            soft.Client
 	namespace         string
 	envAppManagerAddr string
-	networks          []string
 	env               installer.EnvConfig
 	nsc               installer.NamespaceCreator
 	jc                installer.JobCreator
@@ -70,15 +72,17 @@
 // TODO(gio): Initialize appNs on startup
 func NewDodoAppServer(
 	st Store,
+	nf NetworkFilter,
+	ug UserGetter,
 	port int,
 	apiPort int,
 	self string,
+	repoPublicAddr string,
 	sshKey string,
 	gitRepoPublicKey string,
 	client soft.Client,
 	namespace string,
 	envAppManagerAddr string,
-	networks []string,
 	nsc installer.NamespaceCreator,
 	jc installer.JobCreator,
 	env installer.EnvConfig,
@@ -94,15 +98,17 @@
 	s := &DodoAppServer{
 		&sync.Mutex{},
 		st,
+		nf,
+		ug,
 		port,
 		apiPort,
 		self,
+		repoPublicAddr,
 		sshKey,
 		gitRepoPublicKey,
 		client,
 		namespace,
 		envAppManagerAddr,
-		networks,
 		env,
 		nsc,
 		jc,
@@ -137,6 +143,7 @@
 		r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
 		r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
 		r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
+		r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
 		e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
 	}()
 	go func() {
@@ -150,14 +157,48 @@
 	return <-e
 }
 
+type UserGetter interface {
+	Get(r *http.Request) string
+}
+
+type externalUserGetter struct {
+	sc *securecookie.SecureCookie
+}
+
+func NewExternalUserGetter() UserGetter {
+	return &externalUserGetter{}
+}
+
+func (ug *externalUserGetter) Get(r *http.Request) string {
+	cookie, err := r.Cookie(sessionCookie)
+	if err != nil {
+		return ""
+	}
+	var user string
+	if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
+		return ""
+	}
+	return user
+}
+
+type internalUserGetter struct{}
+
+func NewInternalUserGetter() UserGetter {
+	return internalUserGetter{}
+}
+
+func (ug internalUserGetter) Get(r *http.Request) string {
+	return r.Header.Get("X-User")
+}
+
 func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) {
 			next.ServeHTTP(w, r)
 			return
 		}
-		cookie, err := r.Cookie(sessionCookie)
-		if err != nil {
+		user := s.ug.Get(r)
+		if user == "" {
 			vars := mux.Vars(r)
 			appName, ok := vars["app-name"]
 			if !ok || appName == "" {
@@ -167,11 +208,6 @@
 			http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
 			return
 		}
-		var user string
-		if err := s.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
-			http.Error(w, "unauthorized", http.StatusUnauthorized)
-			return
-		}
 		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
 	})
 }
@@ -251,7 +287,8 @@
 }
 
 type statusData struct {
-	Apps []string
+	Apps     []string
+	Networks []installer.Network
 }
 
 func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
@@ -265,7 +302,12 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	data := statusData{apps}
+	networks, err := s.getNetworks(user.(string))
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	data := statusData{apps, networks}
 	if err := s.tmplts.index.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -279,6 +321,7 @@
 		http.Error(w, "missing app-name", http.StatusBadRequest)
 		return
 	}
+	fmt.Fprintf(w, "git clone %s/%s\n\n\n", s.repoPublicAddr, appName)
 	commits, err := s.st.GetCommitHistory(appName)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -313,7 +356,11 @@
 	}
 	// TODO(gio): Create commit record on app init as well
 	go func() {
-		networks, err := s.getNetworks()
+		owner, err := s.st.GetAppOwner(req.Repository.Name)
+		if err != nil {
+			return
+		}
+		networks, err := s.getNetworks(owner)
 		if err != nil {
 			return
 		}
@@ -357,9 +404,60 @@
 	s.workers[appName][req.Address] = struct{}{}
 }
 
+func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
+	u := r.Context().Value(userCtx)
+	if u == nil {
+		http.Error(w, "unauthorized", http.StatusUnauthorized)
+		return
+	}
+	user, ok := u.(string)
+	if !ok {
+		http.Error(w, "could not get user", http.StatusInternalServerError)
+		return
+	}
+	network := r.FormValue("network")
+	if network == "" {
+		http.Error(w, "missing network", http.StatusBadRequest)
+		return
+	}
+	adminPublicKey := r.FormValue("admin-public-key")
+	if network == "" {
+		http.Error(w, "missing admin public key", http.StatusBadRequest)
+		return
+	}
+	g := installer.NewFixedLengthRandomNameGenerator(3)
+	appName, err := g.Generate()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if ok, err := s.client.UserExists(user); err != nil {
+		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
+		}
+	}
+	if err := s.st.CreateUser(user, nil, adminPublicKey, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.st.CreateApp(appName, user); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.CreateApp(user, appName, adminPublicKey, network); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
+}
+
 type apiCreateAppReq struct {
 	AdminPublicKey string `json:"adminPublicKey"`
-	NetworkName    string `json:"networkName"`
+	Network        string `json:"network"`
 }
 
 type apiCreateAppResp struct {
@@ -379,11 +477,38 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	password, err := s.CreateApp(appName, req.AdminPublicKey, req.NetworkName)
+	user, err := s.client.FindUser(req.AdminPublicKey)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	if user != "" {
+		http.Error(w, "public key already registered", http.StatusBadRequest)
+		return
+	}
+	user = appName
+	if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	password := generatePassword()
+	hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.st.CreateUser(user, hashed, req.AdminPublicKey, req.Network); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.st.CreateApp(appName, user); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.CreateApp(user, appName, req.AdminPublicKey, req.Network); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	resp := apiCreateAppResp{
 		AppName:  appName,
 		Password: password,
@@ -394,81 +519,52 @@
 	}
 }
 
-func (s *DodoAppServer) CreateApp(appName, adminPublicKey, networkName string) (string, error) {
+func (s *DodoAppServer) CreateApp(user, appName, adminPublicKey, network string) error {
 	s.l.Lock()
 	defer s.l.Unlock()
 	fmt.Printf("Creating app: %s\n", appName)
 	if ok, err := s.client.RepoExists(appName); err != nil {
-		return "", err
+		return err
 	} else if ok {
-		return "", nil
-	}
-	user, err := s.client.FindUser(adminPublicKey)
-	if err != nil {
-		return "", err
-	}
-	if user == "" {
-		user = appName
-		if err := s.client.AddUser(user, adminPublicKey); err != nil {
-			return "", err
-		}
-	}
-	password := generatePassword()
-	// TODO(gio): take admin password for initial application as input
-	if appName == "app" {
-		password = "app"
-	}
-	hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
-	if err != nil {
-		return "", err
-	}
-	if err := s.st.CreateUser(user, hashed); err != nil {
-		if !errors.Is(err, ErrorAlreadyExists) {
-			return "", err
-		} else {
-			password = ""
-		}
-	}
-	if err := s.st.CreateApp(appName, user); err != nil {
-		return "", err
+		return nil
 	}
 	if err := s.client.AddRepository(appName); err != nil {
-		return "", err
+		return err
 	}
 	appRepo, err := s.client.GetRepo(appName)
 	if err != nil {
-		return "", err
+		return err
 	}
-	if err := InitRepo(appRepo, networkName); err != nil {
-		return "", err
+	if err := InitRepo(appRepo, network); err != nil {
+		return err
 	}
 	apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
 	app, err := installer.FindEnvApp(apps, "dodo-app-instance")
 	if err != nil {
-		return "", err
+		return err
 	}
 	suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
 	suffix, err := suffixGen.Generate()
 	if err != nil {
-		return "", err
+		return err
 	}
 	namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
 	s.appNs[appName] = namespace
-	networks, err := s.getNetworks()
+	networks, err := s.getNetworks(user)
 	if err != nil {
-		return "", err
+		return err
 	}
 	if err := s.updateDodoApp(appName, namespace, networks); err != nil {
-		return "", err
+		return err
 	}
 	repo, err := s.client.GetRepo(ConfigRepoName)
 	if err != nil {
-		return "", err
+		return err
 	}
 	hf := installer.NewGitHelmFetcher()
 	m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
 	if err != nil {
-		return "", err
+		return err
 	}
 	if err := repo.Do(func(fs soft.RepoFS) (string, error) {
 		w, err := fs.Writer(namespacesFile)
@@ -498,41 +594,41 @@
 		}
 		return fmt.Sprintf("Installed app: %s", appName), nil
 	}); err != nil {
-		return "", err
+		return err
 	}
 	cfg, err := m.FindInstance(appName)
 	if err != nil {
-		return "", err
+		return err
 	}
 	fluxKeys, ok := cfg.Input["fluxKeys"]
 	if !ok {
-		return "", fmt.Errorf("Fluxcd keys not found")
+		return fmt.Errorf("Fluxcd keys not found")
 	}
 	fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
 	if !ok {
-		return "", fmt.Errorf("Fluxcd keys not found")
+		return fmt.Errorf("Fluxcd keys not found")
 	}
 	if ok, err := s.client.UserExists("fluxcd"); err != nil {
-		return "", err
+		return err
 	} else if ok {
 		if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
-			return "", err
+			return err
 		}
 	} else {
 		if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
-			return "", err
+			return err
 		}
 	}
 	if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
-		return "", err
+		return err
 	}
 	if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
-		return "", err
+		return err
 	}
 	if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
-		return "", err
+		return err
 	}
-	return password, nil
+	return nil
 }
 
 type apiAddAdminKeyReq struct {
@@ -630,7 +726,7 @@
 }
 `
 
-func InitRepo(repo soft.RepoIO, networkName string) error {
+func InitRepo(repo soft.RepoIO, network string) error {
 	return repo.Do(func(fs soft.RepoFS) (string, error) {
 		{
 			w, err := fs.Writer("go.mod")
@@ -654,7 +750,7 @@
 				return "", err
 			}
 			defer w.Close()
-			fmt.Fprintf(w, appCue, networkName)
+			fmt.Fprintf(w, appCue, network)
 		}
 		return "go web app template", nil
 	})
@@ -664,7 +760,7 @@
 	return "foo"
 }
 
-func (s *DodoAppServer) getNetworks() ([]installer.Network, error) {
+func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
 	addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
 	resp, err := http.Get(addr)
 	if err != nil {
@@ -674,14 +770,88 @@
 	if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
 		return nil, err
 	}
-	if len(s.networks) == 0 {
-		return networks, nil
+	return s.nf.Filter(user, networks)
+}
+
+func pickNetwork(networks []installer.Network, network string) []installer.Network {
+	for _, n := range networks {
+		if n.Name == network {
+			return []installer.Network{n}
+		}
+	}
+	return []installer.Network{}
+}
+
+type NetworkFilter interface {
+	Filter(user string, networks []installer.Network) ([]installer.Network, error)
+}
+
+type noNetworkFilter struct{}
+
+func NewNoNetworkFilter() NetworkFilter {
+	return noNetworkFilter{}
+}
+
+func (f noNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
+	return networks, nil
+}
+
+type filterByOwner struct {
+	st Store
+}
+
+func NewNetworkFilterByOwner(st Store) NetworkFilter {
+	return &filterByOwner{st}
+}
+
+func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
+	network, err := f.st.GetUserNetwork(user)
+	if err != nil {
+		return nil, err
 	}
 	ret := []installer.Network{}
 	for _, n := range networks {
-		if slices.Contains(s.networks, n.Name) {
+		if n.Name == network {
 			ret = append(ret, n)
 		}
 	}
 	return ret, nil
 }
+
+type allowListFilter struct {
+	allowed []string
+}
+
+func NewAllowListFilter(allowed []string) NetworkFilter {
+	return &allowListFilter{allowed}
+}
+
+func (f *allowListFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
+	ret := []installer.Network{}
+	for _, n := range networks {
+		if slices.Contains(f.allowed, n.Name) {
+			ret = append(ret, n)
+		}
+	}
+	return ret, nil
+}
+
+type combinedNetworkFilter struct {
+	filters []NetworkFilter
+}
+
+func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
+	return &combinedNetworkFilter{filters}
+}
+
+func (f *combinedNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
+	ret := networks
+	var err error
+	for _, f := range f.filters {
+		ret, err = f.Filter(app, ret)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return ret, nil
+}
diff --git a/core/installer/welcome/store.go b/core/installer/welcome/store.go
index 857045c..e93c03e 100644
--- a/core/installer/welcome/store.go
+++ b/core/installer/welcome/store.go
@@ -10,7 +10,7 @@
 )
 
 const (
-	errorUniqueConstraintViolation = 2067
+	errorConstraintPrimaryKeyViolation = 1555
 )
 
 var (
@@ -23,8 +23,10 @@
 }
 
 type Store interface {
-	CreateUser(username string, password []byte) error
+	// TODO(gio): Remove publicKey once auto user sync is implemented
+	CreateUser(username string, password []byte, publicKey, network string) error
 	GetUserPassword(username string) ([]byte, error)
+	GetUserNetwork(username string) (string, error)
 	GetApps() ([]string, error)
 	GetUserApps(username string) ([]string, error)
 	CreateApp(name, username string) error
@@ -50,7 +52,9 @@
 	_, err := s.db.Exec(`
 		CREATE TABLE IF NOT EXISTS users (
 			username TEXT PRIMARY KEY,
-            password BLOB
+            password BLOB,
+            public_key TEXT,
+            network TEXT
 		);
 		CREATE TABLE IF NOT EXISTS apps (
 			name TEXT PRIMARY KEY,
@@ -66,12 +70,12 @@
 
 }
 
-func (s *storeImpl) CreateUser(username string, password []byte) error {
-	query := `INSERT INTO users (username, password) VALUES (?, ?)`
-	_, err := s.db.Exec(query, username, password)
+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)
 	if err != nil {
 		sqliteErr, ok := err.(*sqlite3.Error)
-		if ok && sqliteErr.ExtendedCode() == errorUniqueConstraintViolation {
+		if ok && sqliteErr.ExtendedCode() == errorConstraintPrimaryKeyViolation {
 			return ErrorAlreadyExists
 		}
 	}
@@ -91,6 +95,22 @@
 	return ret, nil
 }
 
+func (s *storeImpl) GetUserNetwork(username string) (string, error) {
+	query := `SELECT network FROM users WHERE username = ?`
+	row := s.db.QueryRow(query, username)
+	if err := row.Err(); err != nil {
+		return "", err
+	}
+	var ret string
+	if err := row.Scan(&ret); err != nil {
+		if errors.Is(sql.ErrNoRows, err) {
+			return "", nil
+		}
+		return "", err
+	}
+	return ret, nil
+}
+
 func (s *storeImpl) CreateApp(name, username string) error {
 	query := `INSERT INTO apps (name, username) VALUES (?, ?)`
 	_, err := s.db.Exec(query, name, username)