Skip to content

Commit

Permalink
Merge pull request #14 from delta10/feat/multiple-backends-authz-server
Browse files Browse the repository at this point in the history
Add support for multiple backends and authorization service
  • Loading branch information
bartjkdp authored Sep 14, 2023
2 parents 69a8a9b + 6e2e18b commit fbc0979
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 132 deletions.
198 changes: 95 additions & 103 deletions cmd/filter-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,24 @@ package main
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"time"

"github.com/MicahParks/keyfunc"
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/mux"
"github.com/itchyny/gojq"
"github.com/ory/oathkeeper/helper"

"github.com/delta10/filter-proxy/internal/config"
"github.com/delta10/filter-proxy/internal/route"
"github.com/delta10/filter-proxy/internal/utils"
)

type ClaimsWithGroups struct {
Groups []string
jwt.StandardClaims
jwt.RegisteredClaims
Groups []string `json:"groups"`
}

func main() {
Expand All @@ -33,42 +30,50 @@ func main() {
}

router := mux.NewRouter()
for _, path := range config.Paths {
currentPath := path
router.HandleFunc(currentPath.Path, func(w http.ResponseWriter, r *http.Request) {
authorized := authorizeRequest(config.JwksUrl, currentPath.Authorization.Groups, w, r)
if !authorized {
for _, configuredPath := range config.Paths {
path := configuredPath
router.HandleFunc(path.Path, func(w http.ResponseWriter, r *http.Request) {
backend, ok := config.Backends[path.Backend.Slug]
if !ok {
writeError(w, http.StatusBadRequest, "could not find backend associated with this path: "+path.Backend.Slug)
return
}

routeRegexp, _ := route.NewRouteRegexp(currentPath.Backend.URL, route.RegexpTypePath, route.RouteRegexpOptions{})
utils.DelHopHeaders(r.Header)

requestUrl, err := routeRegexp.URL(mux.Vars(r))
if !authorizeRequestWithService(config, path, r) {
writeError(w, http.StatusUnauthorized, "unauthorized request")
return
}

routeRegexp, _ := route.NewRouteRegexp(path.Backend.Path, route.RegexpTypePath, route.RouteRegexpOptions{})

parsedRequestPath, err := routeRegexp.URL(mux.Vars(r))
if err != nil {
writeError(w, http.StatusBadRequest, "could not parse request URL")
return
}

backendURL, err := url.Parse(requestUrl)
backendBaseUrl, err := url.Parse(backend.BaseURL)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not parse backend URL")
return
}

fullBackendURL := backendBaseUrl.JoinPath(parsedRequestPath)

// Copy query parameters to backend
backendURL.RawQuery = r.URL.Query().Encode()

request := &http.Request{
Method: "GET",
URL: backendURL,
Header: map[string][]string{
"X-Api-Key": {os.Getenv("API_KEY")},
},
fullBackendURL.RawQuery = r.URL.Query().Encode()

request, err := http.NewRequest("GET", fullBackendURL.String(), nil)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not construct backend request")
return
}

tlsConfig := &tls.Config{}
if path.Backend.TLSCertificate != "" && path.Backend.TLSKey != "" {
cert, err := tls.LoadX509KeyPair(path.Backend.TLSCertificate, path.Backend.TLSKey)
if backend.Auth.TLS.Certificate != "" && backend.Auth.TLS.Key != "" {
cert, err := tls.LoadX509KeyPair(backend.Auth.TLS.Certificate, backend.Auth.TLS.Key)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not load TLS keypair for backend")
return
Expand All @@ -80,8 +85,18 @@ func main() {
}

transport := &http.Transport{TLSClientConfig: tlsConfig}

if backend.Auth.Basic.Username != "" && backend.Auth.Basic.Password != "" {
request.SetBasicAuth(backend.Auth.Basic.Username, backend.Auth.Basic.Password)
}

for headerKey, headerValue := range backend.Auth.Header {
parsedHeaderValue := utils.EnvSubst(headerValue)
request.Header.Set(headerKey, parsedHeaderValue)
}

client := &http.Client{
Timeout: 5 * time.Second,
Timeout: 25 * time.Second,
Transport: transport,
}

Expand All @@ -90,57 +105,46 @@ func main() {
writeError(w, http.StatusInternalServerError, "could not fetch backend response")
return
}
defer proxyResp.Body.Close()

if proxyResp.StatusCode != http.StatusOK {
copyHeader(w.Header(), proxyResp.Header)
w.WriteHeader(proxyResp.StatusCode)
io.Copy(w, proxyResp.Body)
return
}
defer proxyResp.Body.Close()

body, _ := io.ReadAll(proxyResp.Body)
if path.Filter != "" {
body, _ := io.ReadAll(proxyResp.Body)

var result map[string]interface{}
json.Unmarshal(body, &result)
var result map[string]interface{}
json.Unmarshal(body, &result)

if currentPath.Filter == "" {
response, err := json.MarshalIndent(result, "", " ")
query, err := gojq.Parse(path.Filter)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not marshall json")
writeError(w, http.StatusInternalServerError, "could not parse filter")
return
}

w.Header().Set("Content-Type", "application/json")
w.Write(response)
return
}

query, err := gojq.Parse(currentPath.Filter)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not parse filter")
return
}

iter := query.Run(result)
for {
v, ok := iter.Next()
if !ok {
break
}

if _, ok := v.(error); ok {
continue
}

response, err := json.MarshalIndent(v, "", " ")
if err != nil {
writeError(w, http.StatusInternalServerError, "could not marshal json")
return
iter := query.Run(result)
for {
v, ok := iter.Next()
if !ok {
break
}

if _, ok := v.(error); ok {
continue
}

response, err := json.MarshalIndent(v, "", " ")
if err != nil {
writeError(w, http.StatusInternalServerError, "could not marshal json")
return
}

w.Header().Set("Content-Type", "application/json")
w.Write(response)
}

w.Header().Set("Content-Type", "application/json")
w.Write(response)
} else {
utils.DelHopHeaders(proxyResp.Header)
utils.CopyHeader(w.Header(), proxyResp.Header)
w.WriteHeader(proxyResp.StatusCode)
io.Copy(w, proxyResp.Body)
}
})
}
Expand All @@ -156,48 +160,44 @@ func main() {
log.Fatal(s.ListenAndServe())
}

func authorizeRequest(jwksUrl string, authorizedGroups []string, w http.ResponseWriter, r *http.Request) bool {
// Create the JWKS from the resource at the given URL.
jwks, err := keyfunc.Get(jwksUrl, keyfunc.Options{})
if err != nil {
writeError(w, http.StatusUnauthorized, fmt.Sprintf("could not authorize request: %s", err.Error()))
func authorizeRequestWithService(config *config.Config, path config.Path, r *http.Request) bool {
if config.AuthorizationServiceURL == "" {
log.Print("returned unauthenticated as there is no authorization service URL configured.")
return false
}

tokenFromRequest := helper.DefaultBearerTokenFromRequest(r)
if tokenFromRequest == "" {
writeError(w, http.StatusUnauthorized, "could not fetch token from request")
authorizationServiceURL, err := url.Parse(config.AuthorizationServiceURL)
if err != nil {
log.Printf("could not parse authorization url: %s", err)
return false
}

parsedToken, err := jwt.ParseWithClaims(tokenFromRequest, &ClaimsWithGroups{}, jwks.Keyfunc)
if err != nil {
writeError(w, http.StatusUnauthorized, fmt.Sprintf("could not authorize request: %s", err.Error()))
return false
authorizationServiceURL.RawQuery = r.URL.RawQuery

authorizationHeaders := r.Header

authorizationHeaders.Set("X-Source-Slug", path.Backend.Slug)
authorizationHeaders.Set("X-Original-Uri", r.URL.RequestURI())

request := &http.Request{
Method: "GET",
URL: authorizationServiceURL,
Header: authorizationHeaders,
}

if _, ok := parsedToken.Method.(*jwt.SigningMethodRSA); !ok {
writeError(w, http.StatusUnauthorized, fmt.Sprintf("unexpected signing method: %v", parsedToken.Header["alg"]))
return false
client := &http.Client{
Timeout: 5 * time.Second,
}

if !parsedToken.Valid {
writeError(w, http.StatusUnauthorized, "parsed token is not valid")
resp, err := client.Do(request)
if err != nil {
log.Printf("could not fetch authorization response: %s", err)
return false
}

if userClaims, ok := parsedToken.Claims.(*ClaimsWithGroups); ok {
for _, authorizedGroup := range authorizedGroups {
for _, userGroup := range userClaims.Groups {
if authorizedGroup == userGroup {
return true
}
}
}
}
defer resp.Body.Close()

writeError(w, http.StatusForbidden, "user is not in required groups")
return false
return resp.StatusCode == http.StatusOK
}

func writeError(w http.ResponseWriter, statusCode int, message string) {
Expand All @@ -212,11 +212,3 @@ func writeError(w http.ResponseWriter, statusCode int, message string) {
w.Header().Set("Content-Type", "application/json")
w.Write(jsonResp)
}

func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
44 changes: 29 additions & 15 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
listenAddress: localhost:8080

jwksUrl: http://localhost:8081/realms/datalab/protocol/openid-connect/certs
listenAddress: localhost:8050
authorizationServiceUrl: http://localhost:8000/atlas/api/v1/authorize

paths:
- path: /api/ows
backend:
slug: geoserver
path: /ows
- path: /api/wmts
backend:
slug: geoserver
path: /gwc/service/wmts
- path: /api/brk/v1/kadastraalonroerendezaken/{kadastraalOnroerendeZaakIdentificatie:[0-9]+}
backend:
url: https://api.brk.kadaster.nl/esd-eto-apikey/bevragen/v1/kadastraalonroerendezaken/{kadastraalOnroerendeZaakIdentificatie:[0-9]+}
authorization:
groups:
- /BRK Bevragen
slug: brk
path: /kadastraalonroerendezaken/{kadastraalOnroerendeZaakIdentificatie:[0-9]+}
filter: |
{
aardCultuurBebouwd: .aardCultuurBebouwd,
Expand All @@ -26,7 +31,8 @@ paths:
}
- path: /api/brk/v1/kadastraalonroerendezaken/{kadastraalOnroerendeZaakIdentificatie:[0-9]+}/zakelijkgerechtigden
backend:
url: https://api.brk.kadaster.nl/esd-eto-apikey/bevragen/v1/kadastraalonroerendezaken/{kadastraalOnroerendeZaakIdentificatie:[0-9]+}/zakelijkgerechtigden
slug: brk
path: /kadastraalonroerendezaken/{kadastraalOnroerendeZaakIdentificatie:[0-9]+}/zakelijkgerechtigden
filter: |
{
"_embedded": {
Expand Down Expand Up @@ -64,12 +70,10 @@ paths:
]
}
}
authorization:
groups:
- /BRK Bevragen
- path: /api/brk/v1/publiekrechtelijkebeperkingen
backend:
url: https://api.brk.kadaster.nl/esd-eto-apikey/bevragen/v1/publiekrechtelijkebeperkingen
slug: brk
path: /publiekrechtelijkebeperkingen
filter: |
{
"_embedded": {
Expand All @@ -91,6 +95,16 @@ paths:
]
}
}
authorization:
groups:
- /BRK Bevragen
backends:
geoserver:
baseUrl: http://localhost:8051/geoserver
auth:
basic:
username: user
spassword: geoserver
brk:
baseUrl: https://api.brk.kadaster.nl/esd-eto-apikey/bevragen/v1
auth:
header:
X-Api-Key: ${API_KEY}
Loading

0 comments on commit fbc0979

Please sign in to comment.