Commit e1fe97f

mo khan <mo@mokhan.ca>
2025-03-05 20:02:36
feat: connect the reverse proxy to a casbin policy enforcement and separate hostnames
1 parent 06a4e07
cmd/gtwy/main.go
@@ -7,23 +7,37 @@ import (
 	"net/http/httputil"
 	"strings"
 	"time"
+
+	"github.com/casbin/casbin/v2"
+	"github.com/xlgmokha/x/pkg/env"
+	"github.com/xlgmokha/x/pkg/x"
 )
 
-func NewProxy(from, to string) http.Handler {
+func NewRouter(routes map[string]string) http.Handler {
+	authz := x.Must(casbin.NewEnforcer("model.conf", "policy.csv"))
+
 	return &httputil.ReverseProxy{
 		Director: func(r *http.Request) {
-			log.Printf("%v (from: %v to: %v)\n", r.URL, from, to)
-			r.URL.Scheme = "http"
-			r.Host = to
-			r.URL.Host = to
-			r.URL.Path = strings.TrimPrefix(r.URL.Path, strings.TrimSuffix(from, "/*"))
-			r.URL.RawPath = strings.TrimPrefix(r.URL.RawPath, strings.TrimSuffix(from, "/*"))
+			segments := strings.SplitN(r.Host, ":", 2)
+			host := segments[0]
+			destinationHost := routes[host]
+
+			log.Printf("%v (from: %v to: %v)\n", r.URL, host, destinationHost)
+
+			subject := "71cbc18e-bd41-4229-9ad2-749546a2a4a7" // TODO:: unpack sub claim in JWT
+			if x.Must(authz.Enforce(subject, host, r.Method, r.URL.Path)) {
+				r.URL.Scheme = "http" // TODO:: use TLS
+				r.Host = destinationHost
+				r.URL.Host = destinationHost
+			} else {
+				log.Println("UNAUTHORIZED") // TODO:: Return forbidden, unauthorized or not found status code
+			}
 		},
 		Transport:     http.DefaultTransport,
 		FlushInterval: -1,
 		ErrorLog:      nil,
 		ModifyResponse: func(r *http.Response) error {
-			r.Header.Add("Via", fmt.Sprintf("%v gateway", r.Proto))
+			r.Header.Add("Via", fmt.Sprintf("%v gtwy", r.Proto))
 			return nil
 		},
 		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
@@ -34,11 +48,16 @@ func NewProxy(from, to string) http.Handler {
 
 func main() {
 	mux := http.NewServeMux()
-	mux.Handle("/idp/", NewProxy("/idp", "localhost:8282"))
-	mux.Handle("/sp/", NewProxy("/sp", "localhost:8283"))
+	routes := map[string]string{
+		"idp.example.com": "localhost:8282",
+		"ui.example.com":  "localhost:8283",
+		"api.example.com": "localhost:8284",
+	}
+	mux.Handle("/", NewRouter(routes))
 
+	bindAddress := env.Fetch("BIND_ADDR", ":8080")
 	log.Fatal((&http.Server{
-		Addr:              ":8080",
+		Addr:              bindAddress,
 		Handler:           mux,
 		ReadHeaderTimeout: 10 * time.Second,
 		ReadTimeout:       30 * time.Second,
go.mod
@@ -2,4 +2,13 @@ module gitlab.com/mokhax/spike
 
 go 1.24.0
 
-require github.com/magefile/mage v1.15.0
+require (
+	github.com/casbin/casbin/v2 v2.103.0
+	github.com/magefile/mage v1.15.0
+	github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8
+)
+
+require (
+	github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
+	github.com/casbin/govaluate v1.3.0 // indirect
+)
go.sum
@@ -1,2 +1,26 @@
+github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
+github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic=
+github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
+github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
+github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
 github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8 h1:Hmyf8pgNUs3l8TW0YdUarBVAU+hWX87efBukspg4nWc=
+github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8/go.mod h1:C9MUZ3A7PTPbrLNTvu2lKhpM0dFpPHt5yH8YGuYzmKQ=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
magefile.go
@@ -16,35 +16,55 @@ import (
 var Default = Run
 
 // Run the Identity Provider
-func RunIdp() error {
-	return sh.RunV("ruby", "./bin/idp")
+func Idp() error {
+	env := map[string]string{
+		"SCHEME": "http",
+		"PORT":   "8282",
+		"HOST":   "idp.example.com:8080",
+	}
+	return sh.RunWithV(env, "ruby", "./bin/idp")
 }
 
-// Run the Service Provider
-func RunSp() error {
-	return sh.RunV("ruby", "./bin/sp")
+// Run the UI (a.k.a Service Provider)
+func UI() error {
+	env := map[string]string{
+		"SCHEME":   "http",
+		"PORT":     "8283",
+		"HOST":     "ui.example.com:8080",
+		"IDP_HOST": "idp.example.com:8080",
+	}
+	return sh.RunWithV(env, "ruby", "./bin/ui")
 }
 
 // Run the API Gateway
-func RunGateway() error {
-	return sh.RunV("go", "run", "./cmd/gtwy/main.go")
+func Gateway() error {
+	env := map[string]string{
+		"BIND_ADDR": ":8080",
+	}
+	return sh.RunWithV(env, "go", "run", "./cmd/gtwy/main.go")
 }
 
 // Run the REST API
-func RunApi() error {
-	return sh.RunV("ruby", "./bin/rest-api")
+func Api() error {
+	env := map[string]string{
+		"SCHEME": "http",
+		"PORT":   "8284",
+		"HOST":   "localhost:8284",
+	}
+	return sh.RunWithV(env, "ruby", "./bin/api")
 }
 
 // Open a web browser to the login page
 func Browser() error {
+	url := "http://localhost:8080/ui/sessions/new"
 	if runtime.GOOS == "linux" {
-		return sh.RunV("xdg-open", "http://localhost:8080/sp/sessions/new")
+		return sh.RunV("xdg-open", url)
 	} else {
-		return sh.RunV("open", "http://localhost:8080/sp/sessions/new")
+		return sh.RunV("open", url)
 	}
 }
 
 // Run All the servers
 func Run(ctx context.Context) {
-	mg.CtxDeps(ctx, RunIdp, RunSp, RunApi, RunGateway)
+	mg.CtxDeps(ctx, Idp, UI, Api, Gateway)
 }
model.conf
@@ -0,0 +1,17 @@
+[request_definition]
+r = subject, domain, action, object
+
+[policy_definition]
+p = subject, domain, action, object
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m =\
+  (\
+    (p.subject == "*" || r.subject == p.subject || regexMatch(r.subject, p.subject))\
+    && (p.domain == "*" || r.domain == p.domain)\
+    && (p.action == "*" || regexMatch(r.action, p.action))\
+    && keyMatch(r.object, p.object)\
+  )
policy.csv
@@ -0,0 +1,8 @@
+p, "\A[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", api.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /*
+p, *, *, (GET)|(HEAD), /health
+p, *, *, GET, /.well-known/*
+p, *, idp.example.com, (GET)|(POST), /oauth/*
+p, *, idp.example.com, (GET)|(POST), /saml/*
+p, *, ui.example.com, (GET)|(POST), /oauth/*
+p, *, ui.example.com, (GET)|(POST), /saml/*
+p, 71cbc18e-bd41-4229-9ad2-749546a2a4a7, *, *, /*