Skip to content

Commit dbca33c

Browse files
authored
Added echo middleware with tests (#16)
1 parent 28c1b9a commit dbca33c

File tree

5 files changed

+324
-17
lines changed

5 files changed

+324
-17
lines changed

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Open Policy Agent Middleware
22

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

@@ -200,4 +200,46 @@ func main() {
200200
})
201201
app.Listen(":8080")
202202
}
203+
```
204+
205+
## Usage with Echo
206+
```go
207+
package main
208+
209+
import (
210+
"github.com/Joffref/opa-middleware"
211+
"github.com/Joffref/opa-middleware/config"
212+
"github.com/labstack/echo/v4"
213+
)
214+
215+
func main() {
216+
e := echo.New()
217+
middleware, err := opamiddleware.NewEchoMiddleware(
218+
&config.Config{
219+
URL: "http://localhost:8181/",
220+
Query: "data.policy.allow",
221+
ExceptedResult: true,
222+
DeniedStatusCode: 403,
223+
DeniedMessage: "Forbidden",
224+
},
225+
func(c echo.Context) (map[string]interface{}, error) {
226+
return map[string]interface{}{
227+
"path": c.Request().URL.Path,
228+
"method": c.Request().Method,
229+
}, nil
230+
},
231+
)
232+
if err != nil {
233+
return
234+
}
235+
e.Use(middleware.Use())
236+
e.GET("/ping", func(c echo.Context) error {
237+
err := c.JSON(200, "pong")
238+
if err != nil {
239+
return err
240+
}
241+
return nil
242+
})
243+
e.Start(":8080")
244+
}
203245
```

echo_middleware.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package opamiddleware
2+
3+
import (
4+
"errors"
5+
"github.com/Joffref/opa-middleware/config"
6+
"github.com/Joffref/opa-middleware/internal"
7+
"github.com/labstack/echo/v4"
8+
"net/http"
9+
)
10+
11+
type EchoInputCreationMethod func(c echo.Context) (map[string]interface{}, error)
12+
13+
type EchoMiddleware struct {
14+
Config *config.Config
15+
InputCreationMethod EchoInputCreationMethod `json:"binding_method,omitempty"`
16+
}
17+
18+
func NewEchoMiddleware(cfg *config.Config, input EchoInputCreationMethod) (*EchoMiddleware, error) {
19+
err := cfg.Validate()
20+
if err != nil {
21+
return nil, err
22+
}
23+
if input == nil {
24+
if cfg.InputCreationMethod == nil {
25+
return nil, errors.New("[opa-middleware-echo] InputCreationMethod must be provided")
26+
}
27+
input = func(c echo.Context) (map[string]interface{}, error) {
28+
bind, err := cfg.InputCreationMethod(c.Request())
29+
if err != nil {
30+
return nil, err
31+
}
32+
return bind, nil
33+
}
34+
}
35+
return &EchoMiddleware{
36+
Config: cfg,
37+
InputCreationMethod: input,
38+
}, nil
39+
}
40+
41+
func (e *EchoMiddleware) Use() echo.MiddlewareFunc {
42+
return func(next echo.HandlerFunc) echo.HandlerFunc {
43+
return func(c echo.Context) error {
44+
if e.Config.Debug {
45+
e.Config.Logger.Printf("[opa-middleware-echo] Request received")
46+
}
47+
result, err := e.query(c)
48+
if err != nil {
49+
if e.Config.Debug {
50+
e.Config.Logger.Printf("[opa-middleware-echo] Error: %s", err.Error())
51+
}
52+
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
53+
}
54+
if e.Config.Debug {
55+
e.Config.Logger.Printf("[opa-middleware-echo] Result: %t", result)
56+
}
57+
if result != e.Config.ExceptedResult {
58+
return c.JSON(e.Config.DeniedStatusCode, map[string]interface{}{"error": e.Config.DeniedMessage})
59+
}
60+
return next(c)
61+
}
62+
}
63+
}
64+
65+
func (e *EchoMiddleware) query(c echo.Context) (bool, error) {
66+
bind, err := e.InputCreationMethod(c)
67+
if err != nil {
68+
return !e.Config.ExceptedResult, err
69+
}
70+
if e.Config.URL != "" {
71+
input := make(map[string]interface{})
72+
input["input"] = bind
73+
return internal.QueryURL(c.Request(), e.Config, input)
74+
}
75+
return internal.QueryPolicy(c.Request(), e.Config, bind)
76+
}

echo_middleware_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package opamiddleware
2+
3+
import (
4+
"github.com/Joffref/opa-middleware/config"
5+
"github.com/labstack/echo/v4"
6+
"net/http"
7+
"net/http/httptest"
8+
"net/url"
9+
"testing"
10+
)
11+
12+
var Test_Policy = `
13+
package policy
14+
15+
default allow = false
16+
17+
allow {
18+
input.path = "/api/v1/users"
19+
input.method = "GET"
20+
}`
21+
22+
func TestEchoMiddleware_Query(t *testing.T) {
23+
type fields struct {
24+
Config *config.Config
25+
InputCreationMethod EchoInputCreationMethod
26+
}
27+
type args struct {
28+
req *http.Request
29+
}
30+
tests := []struct {
31+
name string
32+
fields fields
33+
args args
34+
want bool
35+
wantErr bool
36+
}{
37+
{
38+
name: "Test EchoMiddleware_Query",
39+
fields: fields{
40+
Config: &config.Config{
41+
Policy: Test_Policy,
42+
Query: "data.policy.allow",
43+
ExceptedResult: true,
44+
DeniedStatusCode: 403,
45+
DeniedMessage: "Forbidden",
46+
},
47+
InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
48+
return map[string]interface{}{
49+
"path": c.Request().URL.Path,
50+
"method": c.Request().Method,
51+
}, nil
52+
},
53+
},
54+
args: args{
55+
req: &http.Request{
56+
URL: &url.URL{
57+
Path: "/api/v1/users",
58+
},
59+
Method: "GET",
60+
},
61+
},
62+
want: true,
63+
wantErr: false,
64+
},
65+
}
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
e := echo.New()
69+
h := &EchoMiddleware{
70+
Config: tt.fields.Config,
71+
InputCreationMethod: tt.fields.InputCreationMethod,
72+
}
73+
c := e.NewContext(tt.args.req, httptest.NewRecorder())
74+
got, err := h.query(c)
75+
if (err != nil) != tt.wantErr {
76+
t.Errorf("Query() error = %v, wantErr %v", err, tt.wantErr)
77+
return
78+
}
79+
if got != tt.want {
80+
t.Errorf("Query() got = %v, want %v", got, tt.want)
81+
}
82+
})
83+
}
84+
}
85+
86+
func TestEchoMiddleware_Use(t *testing.T) {
87+
type fields struct {
88+
Config *config.Config
89+
InputCreationMethod EchoInputCreationMethod
90+
}
91+
tests := []struct {
92+
name string
93+
fields fields
94+
}{
95+
{
96+
name: "Test EchoMiddleware_Use",
97+
fields: fields{
98+
Config: &config.Config{
99+
Policy: Test_Policy,
100+
Query: "data.policy.allow",
101+
ExceptedResult: true,
102+
DeniedStatusCode: 403,
103+
DeniedMessage: "Forbidden",
104+
},
105+
InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
106+
return map[string]interface{}{
107+
"path": c.Request().URL.Path,
108+
"method": c.Request().Method,
109+
}, nil
110+
},
111+
},
112+
},
113+
}
114+
for _, tt := range tests {
115+
t.Run(tt.name, func(t *testing.T) {
116+
h := &EchoMiddleware{
117+
Config: tt.fields.Config,
118+
InputCreationMethod: tt.fields.InputCreationMethod,
119+
}
120+
h.Use()
121+
})
122+
}
123+
}
124+
125+
func TestNewEchoMiddleware(t *testing.T) {
126+
type args struct {
127+
cfg *config.Config
128+
inputCreationMethod EchoInputCreationMethod
129+
}
130+
tests := []struct {
131+
name string
132+
args args
133+
want *EchoMiddleware
134+
wantErr bool
135+
}{
136+
{
137+
name: "Test NewEchoMiddleware",
138+
args: args{
139+
cfg: &config.Config{
140+
Policy: "policy",
141+
Query: "data.query",
142+
ExceptedResult: true,
143+
DeniedStatusCode: 403,
144+
DeniedMessage: "Forbidden",
145+
},
146+
inputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
147+
return map[string]interface{}{
148+
"path": c.Request().URL.Path,
149+
"method": c.Request().Method,
150+
}, nil
151+
},
152+
},
153+
want: &EchoMiddleware{
154+
Config: &config.Config{
155+
Policy: "policy",
156+
Query: "data.query",
157+
ExceptedResult: true,
158+
DeniedStatusCode: 403,
159+
DeniedMessage: "Forbidden",
160+
},
161+
InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) {
162+
return map[string]interface{}{
163+
"path": c.Request().URL.Path,
164+
"method": c.Request().Method,
165+
}, nil
166+
},
167+
},
168+
wantErr: false,
169+
},
170+
}
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
_, err := NewEchoMiddleware(tt.args.cfg, tt.args.inputCreationMethod)
174+
if (err != nil) != tt.wantErr {
175+
t.Errorf("NewEchoMiddleware() error = %v, wantErr %v", err, tt.wantErr)
176+
return
177+
}
178+
})
179+
}
180+
}

go.mod

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.18
55
require (
66
github.com/gin-gonic/gin v1.9.1
77
github.com/gofiber/fiber/v2 v2.49.2
8+
github.com/labstack/echo/v4 v4.11.4
89
github.com/open-policy-agent/opa v0.43.1
910
github.com/valyala/fasthttp v1.49.0
1011
)
@@ -27,9 +28,10 @@ require (
2728
github.com/json-iterator/go v1.1.12 // indirect
2829
github.com/klauspost/compress v1.16.7 // indirect
2930
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
31+
github.com/labstack/gommon v0.4.2 // indirect
3032
github.com/leodido/go-urn v1.2.4 // indirect
3133
github.com/mattn/go-colorable v0.1.13 // indirect
32-
github.com/mattn/go-isatty v0.0.19 // indirect
34+
github.com/mattn/go-isatty v0.0.20 // indirect
3335
github.com/mattn/go-runewidth v0.0.15 // indirect
3436
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3537
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -40,16 +42,17 @@ require (
4042
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
4143
github.com/ugorji/go/codec v1.2.11 // indirect
4244
github.com/valyala/bytebufferpool v1.0.0 // indirect
45+
github.com/valyala/fasttemplate v1.2.2 // indirect
4346
github.com/valyala/tcplisten v1.0.0 // indirect
4447
github.com/vektah/gqlparser/v2 v2.4.6 // indirect
4548
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
4649
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
4750
github.com/yashtewari/glob-intersection v0.1.0 // indirect
4851
golang.org/x/arch v0.3.0 // indirect
49-
golang.org/x/crypto v0.14.0 // indirect
50-
golang.org/x/net v0.17.0 // indirect
51-
golang.org/x/sys v0.13.0 // indirect
52-
golang.org/x/text v0.13.0 // indirect
52+
golang.org/x/crypto v0.17.0 // indirect
53+
golang.org/x/net v0.19.0 // indirect
54+
golang.org/x/sys v0.15.0 // indirect
55+
golang.org/x/text v0.14.0 // indirect
5356
google.golang.org/protobuf v1.30.0 // indirect
5457
gopkg.in/yaml.v2 v2.4.0 // indirect
5558
gopkg.in/yaml.v3 v3.0.1 // indirect

0 commit comments

Comments
 (0)