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/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))