auth-proxy: verify group membership (#105)

* auth-proxy: verify group membership

* memberships: install memberships app and use it in few apps

* app-repo: render auth

* installer: always use external dependencies option in app configs

* installer: fix auth handling

* auth-proxy: configure membership-addr and groups flags in helm chart

* installer: fix indentation

* app-manager: fix how auth block is rendered

---------

Co-authored-by: Giorgi Lekveishvili <lekva@gl-mbp-m1-max.local>
diff --git a/charts/auth-proxy/templates/install.yaml b/charts/auth-proxy/templates/install.yaml
index 63310c6..bcc3a15 100644
--- a/charts/auth-proxy/templates/install.yaml
+++ b/charts/auth-proxy/templates/install.yaml
@@ -41,4 +41,6 @@
         - --port=8080
         - --whoami-addr={{ .Values.whoAmIAddr }}
         - --login-addr={{ .Values.loginAddr }}
+        - --membership-addr={{ .Values.membershipAddr }}
+        - --groups={{ .Values.groups }}
         - --upstream={{ .Values.upstream }}
diff --git a/charts/auth-proxy/values.yaml b/charts/auth-proxy/values.yaml
index 9f61b34..15788e6 100644
--- a/charts/auth-proxy/values.yaml
+++ b/charts/auth-proxy/values.yaml
@@ -5,4 +5,6 @@
 upstream: bar.svc.cluster.local
 whoAmIAddr: https://accounts.example.com/sessions/whoami
 loginAddr: https://accounts-ui.example.com/login
+membershipAddr: https://memberships.p.example.com/api/user
+groups: ""
 portName: http
diff --git a/core/auth/proxy/Makefile b/core/auth/proxy/Makefile
index 053ab05..4ec89b0 100644
--- a/core/auth/proxy/Makefile
+++ b/core/auth/proxy/Makefile
@@ -8,21 +8,21 @@
 	rm -f server server_*
 
 build: clean
-	go build -o server *.go
+	/usr/local/go/bin/go build -o server *.go
 
 build_arm64: export CGO_ENABLED=0
 build_arm64: export GO111MODULE=on
 build_arm64: export GOOS=linux
 build_arm64: export GOARCH=arm64
 build_arm64:
-	go build -o server_arm64 *.go
+	/usr/local/go/bin/go build -o server_arm64 *.go
 
 build_amd64: export CGO_ENABLED=0
 build_amd64: export GO111MODULE=on
 build_amd64: export GOOS=linux
 build_amd64: export GOARCH=amd64
 build_amd64:
-	go build -o server_amd64 *.go
+	/usr/local/go/bin/go build -o server_amd64 *.go
 
 push_arm64: clean build_arm64
 	$(podman) build --platform linux/arm64 --tag=$(repo_name)/auth-proxy:arm64 .
diff --git a/core/auth/proxy/go.mod b/core/auth/proxy/go.mod
new file mode 100644
index 0000000..856b8bf
--- /dev/null
+++ b/core/auth/proxy/go.mod
@@ -0,0 +1,5 @@
+module github.com/giolekva/pcloud/core/auth/proxy
+
+go 1.21.5
+
+require golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81
diff --git a/core/auth/proxy/go.sum b/core/auth/proxy/go.sum
new file mode 100644
index 0000000..76a41df
--- /dev/null
+++ b/core/auth/proxy/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc=
+golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
diff --git a/core/auth/proxy/main.go b/core/auth/proxy/main.go
index 8b3d837..d1e1b49 100644
--- a/core/auth/proxy/main.go
+++ b/core/auth/proxy/main.go
@@ -13,11 +13,15 @@
 	"net/http/cookiejar"
 	"net/url"
 	"strings"
+
+	"golang.org/x/exp/slices"
 )
 
 var port = flag.Int("port", 3000, "Port to listen on")
 var whoAmIAddr = flag.String("whoami-addr", "", "Kratos whoami endpoint address")
 var loginAddr = flag.String("login-addr", "", "Login page address")
+var membershipAddr = flag.String("membership-addr", "", "Group membership API endpoint")
+var groups = flag.String("groups", "", "Comma separated list of groups. User must be part of at least one of them. If empty group membership will not be checked.")
 var upstream = flag.String("upstream", "", "Upstream service address")
 
 type user struct {
@@ -62,6 +66,25 @@
 		http.Redirect(w, r, addr, http.StatusSeeOther)
 		return
 	}
+	if *groups != "" {
+		hasPermission := false
+		tg, err := getTransitiveGroups(user.Identity.Traits.Username)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		for _, i := range strings.Split(*groups, ",") {
+			if slices.Contains(tg, strings.TrimSpace(i)) {
+				hasPermission = true
+				break
+			}
+		}
+		if !hasPermission {
+			http.Error(w, "not authorized", http.StatusUnauthorized)
+			return
+		}
+
+	}
 	rc := r.Clone(context.Background())
 	rc.Header.Add("X-User", user.Identity.Traits.Username)
 	ru, err := url.Parse(fmt.Sprintf("http://%s%s", *upstream, r.URL.RequestURI()))
@@ -148,8 +171,27 @@
 	return nil, fmt.Errorf("Unknown error: %s", tmp)
 }
 
+type MembershipInfo struct {
+	MemberOf []string `json:"memberOf"`
+}
+
+func getTransitiveGroups(user string) ([]string, error) {
+	resp, err := http.Get(fmt.Sprintf("%s/%s", *membershipAddr, user))
+	if err != nil {
+		return nil, err
+	}
+	var info MembershipInfo
+	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
+		return nil, err
+	}
+	return info.MemberOf, nil
+}
+
 func main() {
 	flag.Parse()
+	if *groups != "" && *membershipAddr == "" {
+		log.Fatal("membership-addr flag is required when groups are provided")
+	}
 	http.HandleFunc("/", handle)
 	fmt.Printf("Starting HTTP server on port: %d\n", *port)
 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
diff --git a/core/installer/app.go b/core/installer/app.go
index 8cfb2b5..938d08f 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -59,12 +59,6 @@
 	"values-tmpl/hydra-maester.cue",
 }
 
-const cueBaseConfigImports = `
-import (
-    "list"
-)
-`
-
 // TODO(gio): import
 const cueBaseConfig = `
 name: string | *""
@@ -73,6 +67,11 @@
 icon: string | *""
 namespace: string | *""
 
+#Auth: {
+  enabled: bool | *false // TODO(gio): enabled by default?
+  groups: string | *"" // TODO(gio): []string
+}
+
 #Network: {
 	name: string
 	ingressClass: string
@@ -142,8 +141,7 @@
 
 #Helm: {
 	name: string
-	dependsOn: [...#Helm] | *[]
-    dependsOnExternal: [...#ResourceReference] | *[]
+	dependsOn: [...#ResourceReference] | *[]
 	...
 }
 
@@ -159,8 +157,7 @@
 	_name: string
 	_chart: #Chart
 	_values: _
-	_dependencies: [...#Helm] | *[]
-	_externalDependencies: [...#ResourceReference] | *[]
+	_dependencies: [...#ResourceReference] | *[]
 
 	apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
 	kind: "HelmRelease"
@@ -170,12 +167,7 @@
 	}
 	spec: {
 		interval: "1m0s"
-		dependsOn: list.Concat([_externalDependencies, [
-			for d in _dependencies {
-				name: d.name
-				namespace: release.namespace
-			}
-    	]])
+		dependsOn: _dependencies
 		chart: {
 			spec: _chart
 		}
@@ -190,7 +182,6 @@
 			_chart: r.chart
 			_values: r.values
 			_dependencies: r.dependsOn
-            _externalDependencies: r.dependsOnExternal
 		}
 	}
 }
@@ -304,12 +295,12 @@
 		return Rendered{}, err
 	}
 	for i.Next() {
-		name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
-		contents, err := cueyaml.Encode(i.Value())
-		if err != nil {
+		if contents, err := cueyaml.Encode(i.Value()); err != nil {
 			return Rendered{}, err
+		} else {
+			name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
+			ret.Resources[name] = contents
 		}
-		ret.Resources[name] = contents
 	}
 	return ret, nil
 }
@@ -536,7 +527,7 @@
 
 func processCueConfig(contents string) (*cue.Value, error) {
 	ctx := cuecontext.New()
-	cfg := ctx.CompileString(cueBaseConfigImports + contents + cueBaseConfig)
+	cfg := ctx.CompileString(contents + cueBaseConfig)
 	if err := cfg.Err(); err != nil {
 		return nil, err
 	}
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index 5b90327..dbbeda9 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -397,6 +397,12 @@
 				return nil, err
 			}
 			ret[k] = n
+		case KindAuth:
+			r, err := deriveValues(v, AuthSchema, networks)
+			if err != nil {
+				return nil, err
+			}
+			ret[k] = r
 		case KindStruct:
 			r, err := deriveValues(v, def, networks)
 			if err != nil {
diff --git a/core/installer/schema.go b/core/installer/schema.go
index 249190b..a692ecf 100644
--- a/core/installer/schema.go
+++ b/core/installer/schema.go
@@ -16,6 +16,7 @@
 	KindString       = 1
 	KindStruct       = 2
 	KindNetwork      = 3
+	KindAuth         = 5
 	KindNumber       = 4
 )
 
@@ -24,6 +25,13 @@
 	Fields() map[string]Schema
 }
 
+var AuthSchema Schema = structSchema{
+	fields: map[string]Schema{
+		"enabled": basicSchema{KindBoolean},
+		"groups":  basicSchema{KindString},
+	},
+}
+
 const networkSchema = `
 #Network: {
     name: string
@@ -50,6 +58,30 @@
 	return false
 }
 
+const authSchema = `
+#Auth: {
+    enabled: bool | false
+    groups: string | *""
+}
+
+value: { %s }
+`
+
+func isAuth(v cue.Value) bool {
+	if v.Value().Kind() != cue.StructKind {
+		return false
+	}
+	s := fmt.Sprintf(authSchema, fmt.Sprintf("%#v", v))
+	c := cuecontext.New()
+	u := c.CompileString(s)
+	auth := u.LookupPath(cue.ParsePath("#Auth"))
+	vv := u.LookupPath(cue.ParsePath("value"))
+	if err := auth.Subsume(vv); err == nil {
+		return true
+	}
+	return false
+}
+
 type basicSchema struct {
 	kind Kind
 }
@@ -85,6 +117,8 @@
 	case cue.StructKind:
 		if isNetwork(v) {
 			return basicSchema{KindNetwork}, nil
+		} else if isAuth(v) {
+			return basicSchema{KindAuth}, nil
 		}
 		s := structSchema{make(map[string]Schema)}
 		f, err := v.Fields(cue.Schema())
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index 59bc986..c7e6f9e 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -35,6 +35,7 @@
 			SetupNetwork(env, startIP, st),
 			SetupCertificateIssuers(env, st),
 			SetupAuth(env, st),
+			SetupGroupMemberships(env, st),
 			SetupHeadscale(env, startIP, st),
 			SetupWelcome(env, st),
 			SetupAppStore(env, st),
@@ -229,6 +230,24 @@
 	)
 }
 
+func SetupGroupMemberships(env Env, st *state) Task {
+	t := newLeafTask("Setup", func() error {
+		app, err := st.appsRepo.Find("memberships")
+		if err != nil {
+			return err
+		}
+		if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{}); err != nil {
+			return err
+		}
+		return nil
+	})
+	return newSequentialParentTask(
+		"Group Membership",
+		&t,
+		waitForAddr(fmt.Sprintf("https://memberships.p.%s", env.Domain)),
+	)
+}
+
 func SetupHeadscale(env Env, startIP net.IP, st *state) Task {
 	t := newLeafTask("Setup", func() error {
 		app, err := st.appsRepo.Find("headscale")
diff --git a/core/installer/values-tmpl/cert-manager.cue b/core/installer/values-tmpl/cert-manager.cue
index 7f4f55a..fdede37 100644
--- a/core/installer/values-tmpl/cert-manager.cue
+++ b/core/installer/values-tmpl/cert-manager.cue
@@ -55,7 +55,7 @@
 helm: {
 	"cert-manager": {
 		chart: charts.certManager
-		dependsOnExternal: [{
+		dependsOn: [{
 			name: "ingress-public"
 			namespace: _ingressPublic
 		}]
@@ -87,7 +87,10 @@
 	}
 	"cert-manager-webhook-pcloud": {
 		chart: charts.dnsChallengeSolver
-		dependsOn: [helm["cert-manager"]]
+		dependsOn: [{
+			name: "cert-manager"
+			namespace: release.namespace
+		}]
 		values: {
 			fullnameOverride: "\(global.pcloudEnvName)-cert-manager-webhook-pcloud"
 			certManager: {
diff --git a/core/installer/values-tmpl/certificate-issuer-private.cue b/core/installer/values-tmpl/certificate-issuer-private.cue
index ca4c7c2..fc490a3 100644
--- a/core/installer/values-tmpl/certificate-issuer-private.cue
+++ b/core/installer/values-tmpl/certificate-issuer-private.cue
@@ -24,7 +24,7 @@
 helm: {
 	"certificate-issuer-private": {
 		chart: charts["certificate-issuer-private"]
-		dependsOnExternal: [{
+		dependsOn: [{
 			name: "ingress-nginx"
 			namespace: "\(global.namespacePrefix)ingress-private"
 		}]
diff --git a/core/installer/values-tmpl/certificate-issuer-public.cue b/core/installer/values-tmpl/certificate-issuer-public.cue
index 33be2a9..58a4bfd 100644
--- a/core/installer/values-tmpl/certificate-issuer-public.cue
+++ b/core/installer/values-tmpl/certificate-issuer-public.cue
@@ -19,7 +19,7 @@
 helm: {
 	"certificate-issuer-public": {
 		chart: charts["certificate-issuer-public"]
-		dependsOnExternal: [{
+		dependsOn: [{
 			name: "ingress-nginx"
 			namespace: "\(global.namespacePrefix)ingress-private"
 		}]
diff --git a/core/installer/values-tmpl/core-auth.cue b/core/installer/values-tmpl/core-auth.cue
index 192f806..0e9f26f 100644
--- a/core/installer/values-tmpl/core-auth.cue
+++ b/core/installer/values-tmpl/core-auth.cue
@@ -128,10 +128,12 @@
 	}
 	auth: {
 		chart: charts.auth
-		dependsOn: [postgres]
-		dependsOnExternal: [{
+		dependsOn: [{
 			name: "ingress-nginx"
 			namespace: "\(global.namespacePrefix)ingress-private"
+		}, {
+			name: "postgres"
+			namespace: release.namespace
 		}]
 		values: {
 			kratos: {
diff --git a/core/installer/values-tmpl/headscale.cue b/core/installer/values-tmpl/headscale.cue
index 1db5eb8..fee75ab 100644
--- a/core/installer/values-tmpl/headscale.cue
+++ b/core/installer/values-tmpl/headscale.cue
@@ -47,7 +47,7 @@
 	"oauth2-client": {
 		chart: charts.oauth2Client
 		// TODO(gio): remove once hydra maester is installed as part of dodo itself
-		dependsOnExternal: [{
+		dependsOn: [{
 			name: "auth"
 			namespace: "\(global.namespacePrefix)core-auth"
 		}]
@@ -63,7 +63,7 @@
 	}
 	headscale: {
 		chart: charts.headscale
-		dependsOnExternal: [{
+		dependsOn: [{
 			name: "auth"
 			namespace: "\(global.namespacePrefix)core-auth"
 		}]
diff --git a/core/installer/values-tmpl/matrix.cue b/core/installer/values-tmpl/matrix.cue
index 9daf2eb..1f32318 100644
--- a/core/installer/values-tmpl/matrix.cue
+++ b/core/installer/values-tmpl/matrix.cue
@@ -69,9 +69,10 @@
 		}
 	}
 	matrix: {
-		dependsOn: [
-			postgres
-	    ]
+		dependsOn: [{
+			name: "postgres"
+			namespace: release.namespace
+		}]
 		chart: charts.matrix
 		values: {
 			domain: global.domain
diff --git a/core/installer/values-tmpl/memberships.cue b/core/installer/values-tmpl/memberships.cue
index 4abd6d6..6790cfd 100644
--- a/core/installer/values-tmpl/memberships.cue
+++ b/core/installer/values-tmpl/memberships.cue
@@ -1,14 +1,10 @@
-input: {
-    network: #Network
-    subdomain: string
-    requireAuth: bool
-}
+input: {}
 
-_domain: "\(input.subdomain).\(input.network.domain)"
+_domain: "memberships.\(global.privateDomain)"
 
 name: "memberships"
-namespace: "app-memberships"
-readme: "Memberships application will be installed on \(input.network.name) network and be accessible at https://\(_domain)"
+namespace: "core-auth-memberships"
+readme: "Memberships application will be installed on Private network and be accessible at https://\(_domain)"
 description: "The application is a membership management system designed to facilitate the organization and administration of groups and memberships. Can be configured to be reachable only from private network or publicly."
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 24 24'><path fill='currentColor' d='M15.43 15.48c-1.1-.49-2.26-.73-3.43-.73c-1.18 0-2.33.25-3.43.73c-.23.1-.4.29-.49.52h7.85a.978.978 0 0 0-.5-.52m-2.49-6.69C12.86 8.33 12.47 8 12 8s-.86.33-.94.79l-.2 1.21h2.28z' opacity='0.3'/><path fill='currentColor' d='M10.27 12h3.46a1.5 1.5 0 0 0 1.48-1.75l-.3-1.79a2.951 2.951 0 0 0-5.82.01l-.3 1.79c-.15.91.55 1.74 1.48 1.74m.79-3.21c.08-.46.47-.79.94-.79s.86.33.94.79l.2 1.21h-2.28zm-9.4 2.32c-.13.26-.18.57-.1.88c.16.69.76 1.03 1.53 1h1.95c.83 0 1.51-.58 1.51-1.29c0-.14-.03-.27-.07-.4c-.01-.03-.01-.05.01-.08c.09-.16.14-.34.14-.53c0-.31-.14-.6-.36-.82c-.03-.03-.03-.06-.02-.1c.07-.2.07-.43.01-.65a1.12 1.12 0 0 0-.99-.74a.09.09 0 0 1-.07-.03C5.03 8.14 4.72 8 4.37 8c-.3 0-.57.1-.75.26c-.03.03-.06.03-.09.02a1.24 1.24 0 0 0-1.7 1.03c0 .02-.01.04-.03.06c-.29.26-.46.65-.41 1.05c.03.22.12.43.25.6c.03.02.03.06.02.09m14.58 2.54c-1.17-.52-2.61-.9-4.24-.9c-1.63 0-3.07.39-4.24.9A2.988 2.988 0 0 0 6 16.39V18h12v-1.61c0-1.18-.68-2.26-1.76-2.74M8.07 16a.96.96 0 0 1 .49-.52c1.1-.49 2.26-.73 3.43-.73c1.18 0 2.33.25 3.43.73c.23.1.4.29.49.52zm-6.85-1.42A2.01 2.01 0 0 0 0 16.43V18h4.5v-1.61c0-.83.23-1.61.63-2.29c-.37-.06-.74-.1-1.13-.1c-.99 0-1.93.21-2.78.58m21.56 0A6.95 6.95 0 0 0 20 14c-.39 0-.76.04-1.13.1c.4.68.63 1.46.63 2.29V18H24v-1.57c0-.81-.48-1.53-1.22-1.85M22 11v-.5c0-1.1-.9-2-2-2h-2c-.42 0-.65.48-.39.81l.7.63c-.19.31-.31.67-.31 1.06c0 1.1.9 2 2 2s2-.9 2-2'/></svg>"
 
@@ -59,7 +55,7 @@
 _httpPortName: "http"
 
 helm: {
-    "memberships": {
+    memberships: {
         chart: charts.memberships
         values: {
             storage: {
diff --git a/core/installer/values-tmpl/pihole.cue b/core/installer/values-tmpl/pihole.cue
index 35d4c51..ff04fe2 100644
--- a/core/installer/values-tmpl/pihole.cue
+++ b/core/installer/values-tmpl/pihole.cue
@@ -1,7 +1,7 @@
 input: {
 	network: #Network
 	subdomain: string
-	requireAuth: bool
+	auth: #Auth
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -108,7 +108,7 @@
 			}
 		}
 	}
-	if input.requireAuth {
+	if input.auth.enabled {
 		"auth-proxy": {
 			chart: charts.authProxy
 			values: {
@@ -120,6 +120,8 @@
 				upstream: "\(_piholeServiceName).\(release.namespace).svc.cluster.local"
 				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
 				loginAddr: "https://accounts-ui.\(global.domain)/login"
+				membershipAddr: "http://memberships.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
+				groups: input.auth.groups
 				portName: _httpPortName
 			}
 		}
@@ -131,11 +133,11 @@
 			ingressClassName: input.network.ingressClass
 			certificateIssuer: input.network.certificateIssuer
 			service: {
-				if input.requireAuth {
+				if input.auth.enabled {
 					name: _authProxyServiceName
 					port: name: _httpPortName
 				}
-				if !input.requireAuth {
+				if !input.auth.enabled {
 					name: _piholeServiceName
 					port: number: _serviceWebPort
 				}
diff --git a/core/installer/values-tmpl/rpuppy.cue b/core/installer/values-tmpl/rpuppy.cue
index ff316a0..4955f81 100644
--- a/core/installer/values-tmpl/rpuppy.cue
+++ b/core/installer/values-tmpl/rpuppy.cue
@@ -1,7 +1,7 @@
 input: {
 	network: #Network
 	subdomain: string
-	requireAuth: bool
+	auth: #Auth
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -70,7 +70,7 @@
 			portName: _httpPortName
 		}
 	}
-	if input.requireAuth {
+	if input.auth.enabled {
 		"auth-proxy": {
 			chart: charts.authProxy
 			values: {
@@ -82,6 +82,8 @@
 				upstream: "\(_rpuppyServiceName).\(release.namespace).svc.cluster.local"
 				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
 				loginAddr: "https://accounts-ui.\(global.domain)/login"
+				membershipAddr: "http://memberships.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
+				groups: input.auth.groups
 				portName: _httpPortName
 			}
 		}
@@ -93,10 +95,10 @@
 			ingressClassName: input.network.ingressClass
 			certificateIssuer: input.network.certificateIssuer
 			service: {
-				if input.requireAuth {
+				if input.auth.enabled {
 					name: _authProxyServiceName
 				}
-				if !input.requireAuth {
+				if !input.auth.enabled {
 					name: _rpuppyServiceName
 				}
 				port: name: _httpPortName
diff --git a/core/installer/values-tmpl/url-shortener.cue b/core/installer/values-tmpl/url-shortener.cue
index 7d854e8..42a3ce8 100644
--- a/core/installer/values-tmpl/url-shortener.cue
+++ b/core/installer/values-tmpl/url-shortener.cue
@@ -1,7 +1,7 @@
 input: {
     network: #Network
     subdomain: string
-	requireAuth: bool
+	auth: #Auth
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -73,7 +73,7 @@
             portName: _httpPortName
         }
     }
-	if input.requireAuth {
+	if input.auth.enabled {
 		"auth-proxy": {
 			chart: charts.authProxy
 			values: {
@@ -85,6 +85,8 @@
 				upstream: "\(_urlShortenerServiceName).\(release.namespace).svc.cluster.local"
 				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
 				loginAddr: "https://accounts-ui.\(global.domain)/login"
+				membershipAddr: "http://memberships.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
+				groups: input.auth.groups
 				portName: _httpPortName
 			}
 		}
@@ -96,10 +98,10 @@
 			ingressClassName: input.network.ingressClass
 			certificateIssuer: input.network.certificateIssuer
 			service: {
-				if input.requireAuth {
+				if input.auth.enabled {
 					name: _authProxyServiceName
 				}
-				if !input.requireAuth {
+				if !input.auth.enabled {
 					name: _urlShortenerServiceName
 				}
 				port: name: _httpPortName
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index aebbd39..088a4c5 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -7,25 +7,42 @@
       <label for="{{ $name }}">
         <span>{{ $name }}</span>
       </label>
-	  <input type="checkbox" role="swtich" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.checked)" {{ if $readonly }}disabled{{ end }} {{ if index $data $name }}checked{{ end }}/>
+	  <input type="checkbox" role="swtich" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.checked)" {{ if $readonly }}disabled{{ end }} {{ if index $data $name }}checked{{ end }} />
     {{ else if eq $schema.Kind 1 }}
       <label for="{{ $name }}">
         <span>{{ $name }}</span>
       </label>
-	  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}"/>
+	  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
     {{ else if eq $schema.Kind 4 }}
       <label for="{{ $name }}">
         <span>{{ $name }}</span>
       </label>
-	  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}"/>
+	  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
 	{{ else if eq $schema.Kind 3 }}
-	  <select oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} >
+      <label for="{{ $name }}">
+        <span>{{ $name }}</span>
+      </label>
+	  <select name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} >
 		{{ if not $readonly }}<option disabled selected value> -- select an option -- </option>{{ end }}
 		{{ range $networks }}
 		  <option {{if eq .Name (index $data $name) }}selected{{ end }}>{{ .Name }}</option>
 		{{ end }}
 	  </select>
-	{{ end }}
+	{{ else if eq $schema.Kind 5 }}
+      <label for="authEnabled">
+        <span>Require authentication</span>
+      </label>
+	  {{ $auth := index $data $name }}
+	  {{ $authEnabled := false }}
+	  {{ $authGroups := "" }}
+	  {{ if and $auth (index $auth "enabled") }}{{ $authEnabled = true }}{{ end }}
+	  {{ if and $auth (index $auth "groups") }}{{ $authGroups = index $auth "groups" }}{{ end }}
+      <input type="checkbox" role="swtich" name="authEnabled" oninput="valueChanged('{{- $name -}}.enabled', this.checked)" {{ if $readonly }}disabled{{ end }} {{ if $authEnabled  }}checked{{ end }} />
+      <label for="authGroups">
+        <span>Authentication Groups</span>
+      </label>
+      <input type="text" name="authGroups" oninput="valueChanged('{{- $name -}}.groups', this.value)" {{ if $readonly }}disabled{{ end }} value="{{ $authGroups }}" />
+    {{ end }}
   {{ end }}
 {{ end }}
 
@@ -105,8 +122,18 @@
  let readme = "";
  let config = {{ if $instance }}JSON.parse({{ toJson $instance.Config }}){{ else }}{}{{ end }};
 
+ function setValue(name, value, config) {
+  let items = name.split(".")
+  for (let i = 0; i < items.length - 1; i++) {
+    if (!(items[i] in config)) {
+      config[items[i]] = {}
+    }
+    config = config[items[i]];
+  }
+  config[items[items.length - 1]] = value;
+}
  function valueChanged(name, value) {
-     config[name] = value;
+	 setValue(name, value, config);
      renderReadme();
  }