Skip to content

Commit

Permalink
Added echo middleware with tests (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
nerdyslacker authored Mar 14, 2024
1 parent 28c1b9a commit dbca33c
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 17 deletions.
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Open Policy Agent Middleware

This middleware integrates Open Policy Agent (OPA) to your http/gin/fiber app.
This middleware integrates Open Policy Agent (OPA) to your http/gin/fiber/echo app.
You can use it to enforce policies on endpoints.
You can use OPA as local policy engine, or as a remote policy engine.

Expand Down Expand Up @@ -200,4 +200,46 @@ func main() {
})
app.Listen(":8080")
}
```

## Usage with Echo
```go
package main

import (
"github.com/Joffref/opa-middleware"
"github.com/Joffref/opa-middleware/config"
"github.com/labstack/echo/v4"
)

func main() {
e := echo.New()
middleware, err := opamiddleware.NewEchoMiddleware(
&config.Config{
URL: "http://localhost:8181/",
Query: "data.policy.allow",
ExceptedResult: true,
DeniedStatusCode: 403,
DeniedMessage: "Forbidden",
},
func(c echo.Context) (map[string]interface{}, error) {
return map[string]interface{}{
"path": c.Request().URL.Path,
"method": c.Request().Method,
}, nil
},
)
if err != nil {
return
}
e.Use(middleware.Use())
e.GET("/ping", func(c echo.Context) error {
err := c.JSON(200, "pong")
if err != nil {
return err
}
return nil
})
e.Start(":8080")
}
```
76 changes: 76 additions & 0 deletions echo_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package opamiddleware

import (
"errors"
"github.com/Joffref/opa-middleware/config"
"github.com/Joffref/opa-middleware/internal"
"github.com/labstack/echo/v4"
"net/http"
)

type EchoInputCreationMethod func(c echo.Context) (map[string]interface{}, error)

type EchoMiddleware struct {
Config *config.Config
InputCreationMethod EchoInputCreationMethod `json:"binding_method,omitempty"`
}

func NewEchoMiddleware(cfg *config.Config, input EchoInputCreationMethod) (*EchoMiddleware, error) {
err := cfg.Validate()
if err != nil {
return nil, err
}
if input == nil {
if cfg.InputCreationMethod == nil {
return nil, errors.New("[opa-middleware-echo] InputCreationMethod must be provided")
}
input = func(c echo.Context) (map[string]interface{}, error) {
bind, err := cfg.InputCreationMethod(c.Request())
if err != nil {
return nil, err
}
return bind, nil
}
}
return &EchoMiddleware{
Config: cfg,
InputCreationMethod: input,
}, nil
}

func (e *EchoMiddleware) Use() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if e.Config.Debug {
e.Config.Logger.Printf("[opa-middleware-echo] Request received")
}
result, err := e.query(c)
if err != nil {
if e.Config.Debug {
e.Config.Logger.Printf("[opa-middleware-echo] Error: %s", err.Error())
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
}
if e.Config.Debug {
e.Config.Logger.Printf("[opa-middleware-echo] Result: %t", result)
}
if result != e.Config.ExceptedResult {
return c.JSON(e.Config.DeniedStatusCode, map[string]interface{}{"error": e.Config.DeniedMessage})
}
return next(c)
}
}
}

func (e *EchoMiddleware) query(c echo.Context) (bool, error) {
bind, err := e.InputCreationMethod(c)
if err != nil {
return !e.Config.ExceptedResult, err
}
if e.Config.URL != "" {
input := make(map[string]interface{})
input["input"] = bind
return internal.QueryURL(c.Request(), e.Config, input)
}
return internal.QueryPolicy(c.Request(), e.Config, bind)
}
180 changes: 180 additions & 0 deletions echo_middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package opamiddleware

import (
"github.com/Joffref/opa-middleware/config"
"github.com/labstack/echo/v4"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)

var Test_Policy = `
package policy
default allow = false
allow {
input.path = "/api/v1/users"
input.method = "GET"
}`

func TestEchoMiddleware_Query(t *testing.T) {
type fields struct {
Config *config.Config
InputCreationMethod EchoInputCreationMethod
}
type args struct {
req *http.Request
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
}{
{
name: "Test EchoMiddleware_Query",
fields: fields{
Config: &config.Config{
Policy: Test_Policy,
Query: "data.policy.allow",
ExceptedResult: true,
DeniedStatusCode: 403,
DeniedMessage: "Forbidden",
},
InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
return map[string]interface{}{
"path": c.Request().URL.Path,
"method": c.Request().Method,
}, nil
},
},
args: args{
req: &http.Request{
URL: &url.URL{
Path: "/api/v1/users",
},
Method: "GET",
},
},
want: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := echo.New()
h := &EchoMiddleware{
Config: tt.fields.Config,
InputCreationMethod: tt.fields.InputCreationMethod,
}
c := e.NewContext(tt.args.req, httptest.NewRecorder())
got, err := h.query(c)
if (err != nil) != tt.wantErr {
t.Errorf("Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Query() got = %v, want %v", got, tt.want)
}
})
}
}

func TestEchoMiddleware_Use(t *testing.T) {
type fields struct {
Config *config.Config
InputCreationMethod EchoInputCreationMethod
}
tests := []struct {
name string
fields fields
}{
{
name: "Test EchoMiddleware_Use",
fields: fields{
Config: &config.Config{
Policy: Test_Policy,
Query: "data.policy.allow",
ExceptedResult: true,
DeniedStatusCode: 403,
DeniedMessage: "Forbidden",
},
InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
return map[string]interface{}{
"path": c.Request().URL.Path,
"method": c.Request().Method,
}, nil
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &EchoMiddleware{
Config: tt.fields.Config,
InputCreationMethod: tt.fields.InputCreationMethod,
}
h.Use()
})
}
}

func TestNewEchoMiddleware(t *testing.T) {
type args struct {
cfg *config.Config
inputCreationMethod EchoInputCreationMethod
}
tests := []struct {
name string
args args
want *EchoMiddleware
wantErr bool
}{
{
name: "Test NewEchoMiddleware",
args: args{
cfg: &config.Config{
Policy: "policy",
Query: "data.query",
ExceptedResult: true,
DeniedStatusCode: 403,
DeniedMessage: "Forbidden",
},
inputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
return map[string]interface{}{
"path": c.Request().URL.Path,
"method": c.Request().Method,
}, nil
},
},
want: &EchoMiddleware{
Config: &config.Config{
Policy: "policy",
Query: "data.query",
ExceptedResult: true,
DeniedStatusCode: 403,
DeniedMessage: "Forbidden",
},
InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
return map[string]interface{}{
"path": c.Request().URL.Path,
"method": c.Request().Method,
}, nil
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewEchoMiddleware(tt.args.cfg, tt.args.inputCreationMethod)
if (err != nil) != tt.wantErr {
t.Errorf("NewEchoMiddleware() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
13 changes: 8 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.18
require (
github.com/gin-gonic/gin v1.9.1
github.com/gofiber/fiber/v2 v2.49.2
github.com/labstack/echo/v4 v4.11.4
github.com/open-policy-agent/opa v0.43.1
github.com/valyala/fasthttp v1.49.0
)
Expand All @@ -27,9 +28,10 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand All @@ -40,16 +42,17 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/vektah/gqlparser/v2 v2.4.6 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.1.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
Loading

0 comments on commit dbca33c

Please sign in to comment.