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