Skip to content

Commit

Permalink
Merge pull request #27 from delta10/feat/improved-authorization-inter…
Browse files Browse the repository at this point in the history
…face

Improved authorization interface
  • Loading branch information
bartjkdp authored Dec 10, 2023
2 parents f1f55c5 + 610fde0 commit d0f3585
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 28 deletions.
108 changes: 85 additions & 23 deletions cmd/filter-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,8 @@ type ClaimsWithGroups struct {
}

type AuthorizationResponse struct {
User struct {
Id int64
Username string
Name string
}
Result bool `json:"result"`
ResponseFilter string `json:"response_filter"`
}

func main() {
Expand All @@ -53,12 +50,17 @@ func main() {

utils.DelHopHeaders(r.Header)

authorizationStatusCode, _ := authorizeRequestWithService(config, path, r)
authorizationStatusCode, authorizationResponse := authorizeRequestWithService(config, backend, path, r)
if authorizationStatusCode != http.StatusOK {
writeError(w, authorizationStatusCode, "unauthorized request")
return
}

if !authorizationResponse.Result {
writeError(w, http.StatusUnauthorized, "result field is not true")
return
}

allowedMethods := path.AllowedMethods
if len(allowedMethods) == 0 {
allowedMethods = []string{"GET"}
Expand Down Expand Up @@ -183,13 +185,20 @@ func main() {

defer proxyResp.Body.Close()

if path.ResponseRewrite != "" && proxyResp.StatusCode == http.StatusOK {
if proxyResp.StatusCode == http.StatusOK && (path.ResponseRewrite != "" || authorizationResponse.ResponseFilter != "") {
body, _ := io.ReadAll(proxyResp.Body)

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

query, err := gojq.Parse(path.ResponseRewrite)
var responseRewrite = ""
if authorizationResponse.ResponseFilter != "" {
responseRewrite = authorizationResponse.ResponseFilter
} else {
responseRewrite = path.ResponseRewrite
}

query, err := gojq.Parse(responseRewrite)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not parse filter")
return
Expand Down Expand Up @@ -239,37 +248,86 @@ func main() {
}
}

func authorizeRequestWithService(config *config.Config, path config.Path, r *http.Request) (int, *AuthorizationResponse) {
func authorizeRequestWithService(config *config.Config, backend config.Backend, path config.Path, r *http.Request) (int, *AuthorizationResponse) {
if path.AllowAlways {
return http.StatusOK, nil
}

if config.AuthorizationServiceURL == "" {
log.Print("returned unauthenticated as there is no authorization service URL configured.")
log.Print("returned unauthenticated as there is no authorization service URL configured")
return http.StatusInternalServerError, nil
}

authorizationServiceURL, err := url.Parse(config.AuthorizationServiceURL)
if err != nil {
log.Printf("could not parse authorization url: %s", err)
if utils.QueryParamsContainMultipleKeys(r.URL.Query()) {
log.Print("rejected request as query parameters contain multiple keys")
return http.StatusBadRequest, nil
}

authorizationBody := map[string]interface{}{
"source": path.Backend.Slug,
"user_agent": r.Header.Get("User-Agent"),
"ip": utils.ReadUserIP(r),
}

if backend.Type == "OWS" {
queryParams := utils.QueryParamsToLower(r.URL.Query())
authorizationBody["service"] = queryParams.Get("service")
authorizationBody["request"] = queryParams.Get("request")

if authorizationBody["service"] == "WMS" {
authorizationBody["resource"] = queryParams.Get("layers") + queryParams.Get("layer")
authorizationBody["params"] = map[string]interface{}{
"service": queryParams.Get("service"),
"request": queryParams.Get("request"),
"cql_filter": queryParams.Get("cql_filter"),
}
} else if authorizationBody["service"] == "WFS" {
authorizationBody["resource"] = queryParams.Get("typename") + queryParams.Get("typenames")
authorizationBody["params"] = map[string]interface{}{
"service": queryParams.Get("service"),
"request": queryParams.Get("request"),
"cql_filter": queryParams.Get("cql_filter"),
}
} else {
log.Printf("unauthorized service type: %s", authorizationBody["service"])
return http.StatusUnauthorized, nil
}
} else if backend.Type == "WMTS" {
queryParams := utils.QueryParamsToLower(r.URL.Query())
authorizationBody["resource"] = queryParams.Get("layer")
authorizationBody["params"] = map[string]interface{}{
"service": queryParams.Get("service"),
"request": queryParams.Get("request"),
}
} else if backend.Type == "REST" {
authorizationBody["resource"] = path.Backend.Path
} else if backend.Type != "" {
log.Printf("unsupported backend type configured: %s")
return http.StatusInternalServerError, nil
}

authorizationServiceURL.RawQuery = r.URL.RawQuery
marshalledAuthorizationBody, err := json.Marshal(authorizationBody)
if err != nil {
log.Print("could not marshall authorization body")
return http.StatusInternalServerError, nil
}

authorizationHeaders := r.Header
request, err := http.NewRequest("GET", config.AuthorizationServiceURL, bytes.NewReader(marshalledAuthorizationBody))
if err != nil {
log.Print("could not construct authorization request")
return http.StatusInternalServerError, nil
}

authorizationHeaders.Set("X-Source-Slug", path.Backend.Slug)
authorizationHeaders.Set("X-Original-Uri", r.URL.RequestURI())
if r.Header.Get("Cookie") != "" {
request.Header.Set("Cookie", r.Header.Get("Cookie"))
}

request := &http.Request{
Method: "GET",
URL: authorizationServiceURL,
Header: authorizationHeaders,
if r.Header.Get("Authorization") != "" {
request.Header.Set("Authorization", r.Header.Get("Authorization"))
}

client := &http.Client{
Timeout: 5 * time.Second,
Timeout: 10 * time.Second,
}

resp, err := client.Do(request)
Expand All @@ -280,7 +338,7 @@ func authorizeRequestWithService(config *config.Config, path config.Path, r *htt

defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("could not read authorization response: %s", err)
return http.StatusInternalServerError, nil
Expand All @@ -293,6 +351,10 @@ func authorizeRequestWithService(config *config.Config, path config.Path, r *htt
return http.StatusInternalServerError, nil
}

if resp.StatusCode != http.StatusOK {
log.Printf("received an authorization error: %v, %s", resp.StatusCode, body)
}

return resp.StatusCode, &responseData
}

Expand Down
10 changes: 5 additions & 5 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,22 @@ paths:

backends:
geoserver:
baseUrl: http://localhost:8051/geoserver
auth:
basic:
username: admin
password: geoserver
type: OWS
baseUrl: https://dpt.purmerend.nl/geoserver
haal-centraal-brp:
type: REST
baseUrl: https://proefomgeving.haalcentraal.nl/haalcentraal/api/brp
auth:
header:
X-API-KEY: ${BRP_API_KEY}
haal-centraal-brk:
type: REST
baseUrl: https://api.brk.kadaster.nl/esd-eto-apikey/bevragen/v1
auth:
header:
X-Api-Key: ${BRK_API_KEY}
kvk:
type: REST
baseUrl: https://api.kvk.nl/test/api/v1
auth:
tls:
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

type Backend struct {
Type string `yaml:"type"`
BaseURL string `yaml:"baseUrl"`

Auth struct {
Expand Down
15 changes: 15 additions & 0 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ func QueryParamsToLower(queryParams url.Values) url.Values {
return lowercaseParams
}

func QueryParamsContainMultipleKeys(queryParams url.Values) bool {
params := map[string]bool{}

for key := range queryParams {
lowercaseKey := strings.ToLower(key)
if params[lowercaseKey] {
return true
}

params[lowercaseKey] = true
}

return false
}

func GenerateBasicAuthHeader(username, password string) string {
auth := username + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
Expand Down

0 comments on commit d0f3585

Please sign in to comment.