Skip to content

Commit 2b43cb5

Browse files
committed
feat(gate): Add Gate.PermissionMiddleware
1 parent 71e2f63 commit 2b43cb5

File tree

3 files changed

+107
-35
lines changed

3 files changed

+107
-35
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,27 @@ have the `backup` permission:
177177
router.Handle("/backup", gate.ProtectWithPermissions(&testHandler{}, []string{"read", "backup"}))
178178
```
179179

180+
If you're using an HTTP library that supports middlewares like [mux](https://github.com/gorilla/mux), you can protect
181+
an entire group of handlers instead using `gate.Protect` or `gate.PermissionMiddleware()`:
182+
```go
183+
router := mux.NewRouter()
184+
185+
userRouter := router.PathPrefix("/").Subrouter()
186+
userRouter.Use(gate.Protect)
187+
userRouter.HandleFunc("/api/v1/users/me", getUserProfile).Methods("GET")
188+
userRouter.HandleFunc("/api/v1/users/me/friends", getUserFriends).Methods("GET")
189+
userRouter.HandleFunc("/api/v1/users/me/email", updateUserEmail).Methods("PATCH")
190+
191+
adminRouter := router.PathPrefix("/").Subrouter()
192+
adminRouter.Use(gate.PermissionMiddleware("admin"))
193+
adminRouter.HandleFunc("/api/v1/users/{id}/ban", banUserByID).Methods("POST")
194+
adminRouter.HandleFunc("/api/v1/users/{id}/delete", deleteUserByID).Methods("DELETE")
195+
```
196+
180197

181198
## Rate limiting
182199
To add a rate limit of 100 requests per second:
183-
```
200+
```go
184201
gate := g8.New().WithRateLimit(100)
185202
```
186203

gate.go

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,16 @@ func (gate *Gate) WithCustomUnauthorizedResponseBody(unauthorizedResponseBody []
6666
// If a custom token extractor is not specified, the token will be extracted from the Authorization header.
6767
//
6868
// For instance, if you're using a session cookie, you can extract the token from the cookie like so:
69-
// authorizationService := g8.NewAuthorizationService()
70-
// customTokenExtractorFunc := func(request *http.Request) string {
71-
// sessionCookie, err := request.Cookie("session")
72-
// if err != nil {
73-
// return ""
74-
// }
75-
// return sessionCookie.Value
76-
// }
77-
// gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
69+
//
70+
// authorizationService := g8.NewAuthorizationService()
71+
// customTokenExtractorFunc := func(request *http.Request) string {
72+
// sessionCookie, err := request.Cookie("session")
73+
// if err != nil {
74+
// return ""
75+
// }
76+
// return sessionCookie.Value
77+
// }
78+
// gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
7879
//
7980
// You would normally use this with a client provider that matches whatever need you have.
8081
// For example, if you're using a session cookie, your client provider would retrieve the user from the session ID
@@ -90,8 +91,8 @@ func (gate *Gate) WithCustomTokenExtractor(customTokenExtractorFunc func(request
9091
// WithRateLimit adds rate limiting to the Gate
9192
//
9293
// If you just want to use a gate for rate limiting purposes:
93-
// gate := g8.New().WithRateLimit(50)
9494
//
95+
// gate := g8.New().WithRateLimit(50)
9596
func (gate *Gate) WithRateLimit(maximumRequestsPerSecond int) *Gate {
9697
gate.rateLimiter = NewRateLimiter(maximumRequestsPerSecond)
9798
return gate
@@ -102,12 +103,13 @@ func (gate *Gate) WithRateLimit(maximumRequestsPerSecond int) *Gate {
102103
// or lack thereof.
103104
//
104105
// Example:
105-
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
106-
// router := http.NewServeMux()
107-
// // Without protection
108-
// router.Handle("/handle", yourHandler)
109-
// // With protection
110-
// router.Handle("/handle", gate.Protect(yourHandler))
106+
//
107+
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
108+
// router := http.NewServeMux()
109+
// // Without protection
110+
// router.Handle("/handle", yourHandler)
111+
// // With protection
112+
// router.Handle("/handle", gate.Protect(yourHandler))
111113
//
112114
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
113115
func (gate *Gate) Protect(handler http.Handler) http.Handler {
@@ -118,12 +120,13 @@ func (gate *Gate) Protect(handler http.Handler) http.Handler {
118120
// as well as a slice of permissions that must be met.
119121
//
120122
// Example:
121-
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
122-
// router := http.NewServeMux()
123-
// // Without protection
124-
// router.Handle("/handle", yourHandler)
125-
// // With protection
126-
// router.Handle("/handle", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
123+
//
124+
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("ADMIN")))
125+
// router := http.NewServeMux()
126+
// // Without protection
127+
// router.Handle("/handle", yourHandler)
128+
// // With protection
129+
// router.Handle("/handle", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
127130
//
128131
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
129132
func (gate *Gate) ProtectWithPermissions(handler http.Handler, permissions []string) http.Handler {
@@ -147,12 +150,13 @@ func (gate *Gate) ProtectWithPermission(handler http.Handler, permission string)
147150
// permissions or lack thereof.
148151
//
149152
// Example:
150-
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
151-
// router := http.NewServeMux()
152-
// // Without protection
153-
// router.HandleFunc("/handle", yourHandlerFunc)
154-
// // With protection
155-
// router.HandleFunc("/handle", gate.ProtectFunc(yourHandlerFunc))
153+
//
154+
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
155+
// router := http.NewServeMux()
156+
// // Without protection
157+
// router.HandleFunc("/handle", yourHandlerFunc)
158+
// // With protection
159+
// router.HandleFunc("/handle", gate.ProtectFunc(yourHandlerFunc))
156160
//
157161
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
158162
func (gate *Gate) ProtectFunc(handlerFunc http.HandlerFunc) http.HandlerFunc {
@@ -163,12 +167,13 @@ func (gate *Gate) ProtectFunc(handlerFunc http.HandlerFunc) http.HandlerFunc {
163167
// token as well as a slice of permissions that must be met.
164168
//
165169
// Example:
166-
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
167-
// router := http.NewServeMux()
168-
// // Without protection
169-
// router.HandleFunc("/handle", yourHandlerFunc)
170-
// // With protection
171-
// router.HandleFunc("/handle", gate.ProtectFuncWithPermissions(yourHandlerFunc, []string{"admin"}))
170+
//
171+
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
172+
// router := http.NewServeMux()
173+
// // Without protection
174+
// router.HandleFunc("/handle", yourHandlerFunc)
175+
// // With protection
176+
// router.HandleFunc("/handle", gate.ProtectFuncWithPermissions(yourHandlerFunc, []string{"admin"}))
172177
//
173178
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
174179
func (gate *Gate) ProtectFuncWithPermissions(handlerFunc http.HandlerFunc, permissions []string) http.HandlerFunc {
@@ -215,3 +220,19 @@ func (gate *Gate) ExtractTokenFromRequest(request *http.Request) string {
215220
}
216221
return strings.TrimPrefix(request.Header.Get(AuthorizationHeader), "Bearer ")
217222
}
223+
224+
// PermissionMiddleware is a middleware that behaves like ProtectWithPermission, but it is meant to be used
225+
// as a middleware for libraries that support such a feature.
226+
//
227+
// For instance, if you are using github.com/gorilla/mux, you can use PermissionMiddleware like so:
228+
//
229+
// router := mux.NewRouter()
230+
// router.Use(gate.PermissionMiddleware("admin"))
231+
// router.Handle("/admin/handle", adminHandler)
232+
//
233+
// If you do not want to protect a router with a specific permission, you can use Gate.Protect instead.
234+
func (gate *Gate) PermissionMiddleware(permissions ...string) func(http.Handler) http.Handler {
235+
return func(next http.Handler) http.Handler {
236+
return gate.ProtectWithPermissions(next, permissions)
237+
}
238+
}

gate_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,40 @@ func TestGate_ProtectWithPermissionWhenClientHasInsufficientPermissions(t *testi
311311
}
312312
}
313313

314+
func TestGate_PermissionMiddlewareWhenClientHasSufficientPermissions(t *testing.T) {
315+
gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClient("token").WithPermission("admin")))
316+
request, _ := http.NewRequest("GET", "/handle", http.NoBody)
317+
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token"))
318+
responseRecorder := httptest.NewRecorder()
319+
320+
router := http.NewServeMux()
321+
router.Handle("/handle", gate.PermissionMiddleware("admin")(&testHandler{}))
322+
router.ServeHTTP(responseRecorder, request)
323+
324+
// Since the client registered directly in the AuthorizationService has the permission "admin" and the testHandler
325+
// is protected by the permission "admin", the request should be authorized
326+
if responseRecorder.Code != http.StatusOK {
327+
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code)
328+
}
329+
}
330+
331+
func TestGate_PermissionMiddlewareWhenClientHasInsufficientPermissions(t *testing.T) {
332+
gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClientWithPermissions("token", []string{"mod"})))
333+
request, _ := http.NewRequest("GET", "/handle", http.NoBody)
334+
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token"))
335+
responseRecorder := httptest.NewRecorder()
336+
337+
router := http.NewServeMux()
338+
router.Handle("/handle", gate.PermissionMiddleware("admin")(&testHandler{}))
339+
router.ServeHTTP(responseRecorder, request)
340+
341+
// Since the client registered directly in the AuthorizationService has the permission "mod" and the
342+
// testHandler is protected by the permission "admin", the request should be not be authorized
343+
if responseRecorder.Code != http.StatusUnauthorized {
344+
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code)
345+
}
346+
}
347+
314348
func TestGate_ProtectFuncWithInvalidToken(t *testing.T) {
315349
gate := New().WithAuthorizationService(NewAuthorizationService().WithToken("good-token"))
316350
request, _ := http.NewRequest("GET", "/handle", http.NoBody)

0 commit comments

Comments
 (0)