Skip to content

Commit

Permalink
new /forward-auth endpoint (#309)
Browse files Browse the repository at this point in the history
support input.method and input.path extraction from the X-Forwarded-Uri/Method headers set by Traefik's forwardAuth middleware
  • Loading branch information
nelsonunbasicalgillo committed Mar 14, 2024
1 parent 8d67e85 commit 4cee476
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 10 deletions.
30 changes: 30 additions & 0 deletions internal/pkg/api/rest-handler.go
Expand Up @@ -103,6 +103,36 @@ func (proxy *restProxy) handleV1DataPost(w http.ResponseWriter, r *http.Request)
}
}

func (proxy *restProxy) handleV1DataForwardAuth(w http.ResponseWriter, r *http.Request) {
// Build input body from traefik's forward auth request
path := r.Header.Get(constants.HeaderXForwardedURI)
method := r.Header.Get(constants.HeaderXForwardedMethod)

inputBody := map[string]map[string]interface{}{
"input": {
"method": method,
"path": path,
}}

if r.Header.Get(constants.HeaderAuthorization) != "" {
inputBody["input"]["token"] = r.Header.Get(constants.HeaderAuthorization)
}

body, err := json.Marshal(inputBody)
if err != nil {
proxy.handleError(r.Context(), w, wrapErrorInLoggingContext(err))
return
}

endpointData := proxy.pathPrefix + constants.EndpointSuffixData
if trans, err := http.NewRequest("POST", endpointData, bytes.NewReader(body)); err == nil {
// Handle request like post
proxy.handleV1DataPost(w, trans)
} else {
logging.LogForComponent("restProxy").Fatal("Unable to map GET request to POST: ", err.Error())
}
}

// Migration from github.com/open-policy-agent/opa/server/server.go
func (proxy *restProxy) handleV1DataPut(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand Down
13 changes: 8 additions & 5 deletions internal/pkg/api/rest-proxy.go
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"time"

"github.com/unbasical/kelon/pkg/constants"
"github.com/unbasical/kelon/pkg/constants/logging"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -78,12 +79,14 @@ func (proxy *restProxy) Start() error {

ctx := context.Background()

endpointData := proxy.pathPrefix + "/data"
endpointPolicies := proxy.pathPrefix + "/policies"
endpointData := proxy.pathPrefix + constants.EndpointSuffixData
endpointForwardAuth := proxy.pathPrefix + constants.EndpointSuffixForwardAuth
endpointPolicies := proxy.pathPrefix + constants.EndpointSuffixPolicies

// Endpoints to validate queries
proxy.router.PathPrefix(endpointData).Handler(proxy.applyHandlerMiddlewareIfSet(ctx, proxy.handleV1DataGet, endpointData)).Methods("GET")
proxy.router.PathPrefix(endpointData).Handler(proxy.applyHandlerMiddlewareIfSet(ctx, proxy.handleV1DataPost, endpointData)).Methods("POST")
proxy.router.PathPrefix(endpointForwardAuth).Handler(proxy.applyHandlerMiddlewareIfSet(ctx, proxy.handleV1DataForwardAuth, endpointForwardAuth)).Methods("GET")

// Endpoints to update policies and data
proxy.router.PathPrefix(endpointData).Handler(proxy.applyHandlerMiddlewareIfSet(ctx, proxy.handleV1DataPut, endpointData)).Methods("PUT")
Expand All @@ -92,10 +95,10 @@ func (proxy *restProxy) Start() error {
proxy.router.PathPrefix(endpointPolicies).Handler(proxy.applyHandlerMiddlewareIfSet(ctx, proxy.handleV1PolicyPut, endpointPolicies)).Methods("PUT")
proxy.router.PathPrefix(endpointPolicies).Handler(proxy.applyHandlerMiddlewareIfSet(ctx, proxy.handleV1PolicyDelete, endpointPolicies)).Methods("DELETE")
if proxy.metricsHandler != nil {
logging.LogForComponent("restProxy").Infoln("Registered /metrics endpoint")
proxy.router.PathPrefix("/metrics").Handler(proxy.metricsHandler)
logging.LogForComponent("restProxy").Infof("Registered %s endpoint", constants.EndpointMetrics)
proxy.router.PathPrefix(constants.EndpointMetrics).Handler(proxy.metricsHandler)
}
proxy.router.PathPrefix("/health").Methods("GET").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
proxy.router.PathPrefix(constants.EndpointHealth).Methods("GET").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{\"status\": \"healthy\"}"))
})
Expand Down
11 changes: 11 additions & 0 deletions pkg/constants/request.go
Expand Up @@ -5,3 +5,14 @@ type ContextKey string
// ContextKeyRequestID is the ContextKey for RequestID
const ContextKeyRequestID = ContextKey("requestUID") // can be unexported
const ContextKeyRegoPackage = ContextKey("regoPackage")

const HeaderXForwardedMethod = "X-Forwarded-Method"
const HeaderXForwardedURI = "X-Forwarded-URI"
const HeaderAuthorization = "Authorization"

const EndpointSuffixData = "/data"
const EndpointSuffixForwardAuth = "/forward-auth"
const EndpointSuffixPolicies = "/policies"

const EndpointHealth = "/health"
const EndpointMetrics = "/metrics"
21 changes: 20 additions & 1 deletion test/e2e/e2e_test.go
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"syscall"
"testing"

Expand Down Expand Up @@ -100,7 +101,19 @@ func (env *E2ETestEnvironment) runTests() {
url := fmt.Sprintf(request.URL, "localhost", strconv.Itoa(int(env.kelonPort)))

//nolint:gosec,gocritic
resp, httpErr := http.Post(url, "application/json", bytes.NewBufferString(request.Body))
req, reqErr := http.NewRequest(strings.ToUpper(request.Method), url, bytes.NewBufferString(request.Body))
if reqErr != nil {
env.t.Errorf("%s: %s - %s", request.Name, url, reqErr.Error())
env.t.FailNow()
}

req.Header.Set("Content-Type", "application/json")

for k, v := range request.Headers {
req.Header.Set(k, v)
}

resp, httpErr := http.DefaultClient.Do(req)
if httpErr != nil {
env.t.Errorf("%s: %s - %s", request.Name, url, httpErr.Error())
env.t.FailNow()
Expand Down Expand Up @@ -173,6 +186,12 @@ func parseTestData(t *testing.T, path string) []Request {
t.Errorf("error parsing file %s: %s", path, err.Error())
t.FailNow()
}

// set default values for each request
for r := range data {
data[r].Defaults()
}

return data
}

Expand Down
16 changes: 12 additions & 4 deletions test/e2e/parseObjects.go
@@ -1,8 +1,16 @@
package e2e

type Request struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Body string `yaml:"body"`
StatusCode int `yaml:"statusCode"`
Name string `yaml:"name"`
Method string `yaml:"method"`
URL string `yaml:"url"`
Body string `yaml:"body"`
StatusCode int `yaml:"statusCode"`
Headers map[string]string `yaml:"header"`
}

func (r *Request) Defaults() {
if r.Method == "" {
r.Method = "POST"
}
}
9 changes: 9 additions & 0 deletions test/e2e/test_config/requests.yml
Expand Up @@ -157,3 +157,12 @@
url: "http://%s:%s/v1/data"
body: '{ "input": { "method": "GET", "path": "/api/pure/apps/2", "user": "Nobody", "password": "pw_nobody" } }'
statusCode: 403


- name: "ForwardAuth: Pure - First App visible for everyone"
method: "GET"
url: "http://%s:%s/v1/forward-auth"
header:
X-Forwarded-Method: "GET"
X-Forwarded-URI: "/api/pure/apps/1"
statusCode: 200

0 comments on commit 4cee476

Please sign in to comment.