Skip to content

Commit 8b47985

Browse files
authored
feat!: Allow passing client data through g8.data context key (#36)
* feat!: Allow passing client data through g8.data context key This adds the Data field to the Client struct * docs: Fix comment for AuthorizationService.Authorize * chore: Rename tests to match method refactors
1 parent cc6ae9d commit 8b47985

10 files changed

+139
-60
lines changed

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# g8
22

33
![test](https://github.com/TwiN/g8/workflows/test/badge.svg?branch=master)
4-
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/g8)](https://goreportcard.com/report/github.com/TwiN/g8/v2)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/g8)](https://goreportcard.com/report/github.com/TwiN/g8/v3)
55
[![codecov](https://codecov.io/gh/TwiN/g8/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/g8)
66
[![Go version](https://img.shields.io/github/go-mod/go-version/TwiN/g8.svg)](https://github.com/TwiN/g8)
7-
[![Go Reference](https://pkg.go.dev/badge/github.com/TwiN/g8.svg)](https://pkg.go.dev/github.com/TwiN/g8/v2)
7+
[![Go Reference](https://pkg.go.dev/badge/github.com/TwiN/g8.svg)](https://pkg.go.dev/github.com/TwiN/g8/v3)
88
[![Follow TwiN](https://img.shields.io/github/followers/TwiN?label=Follow&style=social)](https://github.com/TwiN)
99

1010
g8, pronounced gate, is a simple Go library for protecting HTTP handlers.
@@ -14,7 +14,7 @@ Tired of constantly re-implementing a security layer for each application? Me to
1414

1515
## Installation
1616
```console
17-
go get -u github.com/TwiN/g8/v2
17+
go get -u github.com/TwiN/g8/v3
1818
```
1919

2020

@@ -284,7 +284,7 @@ gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenE
284284
package main
285285

286286
import (
287-
g8 "github.com/TwiN/g8/v2"
287+
g8 "github.com/TwiN/g8/v3"
288288
)
289289

290290
type customCache struct {
@@ -309,7 +309,7 @@ func main() {
309309
// has the user's token as well as the permissions granted to said user
310310
user := database.GetUserByToken(token)
311311
if user != nil {
312-
return g8.NewClient(user.Token).WithPermissions(user.Permissions)
312+
return g8.NewClient(user.Token).WithPermissions(user.Permissions).WithData(user.Data)
313313
}
314314
return nil
315315
}

authorization.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -104,24 +104,27 @@ func (authorizationService *AuthorizationService) WithClientProvider(provider *C
104104
return authorizationService
105105
}
106106

107-
// IsAuthorized checks whether a client with a given token exists and has the permissions required.
107+
// Authorize checks whether a client with a given token exists and has the permissions required.
108108
//
109109
// If permissionsRequired is nil or empty and a client with the given token exists, said client will have access to all
110110
// handlers that are not protected by a given permission.
111-
func (authorizationService *AuthorizationService) IsAuthorized(token string, permissionsRequired []string) bool {
111+
//
112+
// Returns the client is authorized (or nil if no client was authorized), as well as whether the token is authorized
113+
func (authorizationService *AuthorizationService) Authorize(token string, permissionsRequired []string) (client *Client, authorized bool) {
112114
if len(token) == 0 {
113-
return false
115+
return nil, false
114116
}
115117
authorizationService.mutex.RLock()
116-
client, _ := authorizationService.clients[token]
118+
client, _ = authorizationService.clients[token]
117119
authorizationService.mutex.RUnlock()
118120
// If there's no clients with the given token directly stored in the AuthorizationService, fall back to the
119121
// client provider, if there's one configured.
120122
if client == nil && authorizationService.clientProvider != nil {
121123
client = authorizationService.clientProvider.GetClientByToken(token)
122124
}
123-
if client != nil {
124-
return client.HasPermissions(permissionsRequired)
125+
if client != nil && client.HasPermissions(permissionsRequired) {
126+
// If the client has the required permissions, return true and the client
127+
return client, true
125128
}
126-
return false
129+
return nil, false
127130
}

authorization_test.go

+29-29
Original file line numberDiff line numberDiff line change
@@ -2,107 +2,107 @@ package g8
22

33
import "testing"
44

5-
func TestAuthorizationService_IsAuthorized(t *testing.T) {
5+
func TestAuthorizationService_Authorize(t *testing.T) {
66
authorizationService := NewAuthorizationService().WithToken("token")
7-
if !authorizationService.IsAuthorized("token", nil) {
7+
if _, authorized := authorizationService.Authorize("token", nil); !authorized {
88
t.Error("should've returned true")
99
}
10-
if authorizationService.IsAuthorized("bad-token", nil) {
10+
if _, authorized := authorizationService.Authorize("bad-token", nil); authorized {
1111
t.Error("should've returned false")
1212
}
13-
if authorizationService.IsAuthorized("token", []string{"admin"}) {
13+
if _, authorized := authorizationService.Authorize("token", []string{"admin"}); authorized {
1414
t.Error("should've returned false")
1515
}
16-
if authorizationService.IsAuthorized("", nil) {
16+
if _, authorized := authorizationService.Authorize("", nil); authorized {
1717
t.Error("should've returned false")
1818
}
1919
}
2020

21-
func TestAuthorizationService_IsAuthorizedWithPermissions(t *testing.T) {
21+
func TestAuthorizationService_AuthorizeWithPermissions(t *testing.T) {
2222
authorizationService := NewAuthorizationService().WithClient(NewClient("token").WithPermissions([]string{"a", "b"}))
23-
if !authorizationService.IsAuthorized("token", nil) {
23+
if _, authorized := authorizationService.Authorize("token", nil); !authorized {
2424
t.Error("should've returned true")
2525
}
26-
if !authorizationService.IsAuthorized("token", []string{"a"}) {
26+
if _, authorized := authorizationService.Authorize("token", []string{"a"}); !authorized {
2727
t.Error("should've returned true")
2828
}
29-
if !authorizationService.IsAuthorized("token", []string{"b"}) {
29+
if _, authorized := authorizationService.Authorize("token", []string{"b"}); !authorized {
3030
t.Error("should've returned true")
3131
}
32-
if !authorizationService.IsAuthorized("token", []string{"a", "b"}) {
32+
if _, authorized := authorizationService.Authorize("token", []string{"a", "b"}); !authorized {
3333
t.Error("should've returned true")
3434
}
35-
if authorizationService.IsAuthorized("token", []string{"c"}) {
35+
if _, authorized := authorizationService.Authorize("token", []string{"c"}); authorized {
3636
t.Error("should've returned false")
3737
}
38-
if authorizationService.IsAuthorized("token", []string{"a", "c"}) {
38+
if _, authorized := authorizationService.Authorize("token", []string{"a", "c"}); authorized {
3939
t.Error("should've returned false")
4040
}
41-
if authorizationService.IsAuthorized("bad-token", nil) {
41+
if _, authorized := authorizationService.Authorize("bad-token", nil); authorized {
4242
t.Error("should've returned false")
4343
}
44-
if authorizationService.IsAuthorized("bad-token", []string{"a"}) {
44+
if _, authorized := authorizationService.Authorize("bad-token", []string{"a"}); authorized {
4545
t.Error("should've returned false")
4646
}
47-
if authorizationService.IsAuthorized("", []string{"a"}) {
47+
if _, authorized := authorizationService.Authorize("", []string{"a"}); authorized {
4848
t.Error("should've returned false")
4949
}
5050
}
5151

5252
func TestAuthorizationService_WithToken(t *testing.T) {
5353
authorizationService := NewAuthorizationService().WithToken("token")
54-
if !authorizationService.IsAuthorized("token", nil) {
54+
if _, authorized := authorizationService.Authorize("token", nil); !authorized {
5555
t.Error("should've returned true")
5656
}
57-
if authorizationService.IsAuthorized("bad-token", nil) {
57+
if _, authorized := authorizationService.Authorize("bad-token", nil); authorized {
5858
t.Error("should've returned false")
5959
}
60-
if authorizationService.IsAuthorized("token", []string{"admin"}) {
60+
if _, authorized := authorizationService.Authorize("token", []string{"admin"}); authorized {
6161
t.Error("should've returned false")
6262
}
6363
}
6464

6565
func TestAuthorizationService_WithTokens(t *testing.T) {
6666
authorizationService := NewAuthorizationService().WithTokens([]string{"1", "2"})
67-
if !authorizationService.IsAuthorized("1", nil) {
67+
if _, authorized := authorizationService.Authorize("1", nil); !authorized {
6868
t.Error("should've returned true")
6969
}
70-
if !authorizationService.IsAuthorized("2", nil) {
70+
if _, authorized := authorizationService.Authorize("2", nil); !authorized {
7171
t.Error("should've returned true")
7272
}
73-
if authorizationService.IsAuthorized("3", nil) {
73+
if _, authorized := authorizationService.Authorize("3", nil); authorized {
7474
t.Error("should've returned false")
7575
}
7676
}
7777

7878
func TestAuthorizationService_WithClient(t *testing.T) {
7979
authorizationService := NewAuthorizationService().WithClient(NewClient("token").WithPermissions([]string{"a", "b"}))
80-
if !authorizationService.IsAuthorized("token", []string{"a", "b"}) {
80+
if _, authorized := authorizationService.Authorize("token", []string{"a", "b"}); !authorized {
8181
t.Error("should've returned true")
8282
}
83-
if !authorizationService.IsAuthorized("token", []string{"a"}) {
83+
if _, authorized := authorizationService.Authorize("token", []string{"a"}); !authorized {
8484
t.Error("should've returned true")
8585
}
86-
if !authorizationService.IsAuthorized("token", []string{"b"}) {
86+
if _, authorized := authorizationService.Authorize("token", []string{"b"}); !authorized {
8787
t.Error("should've returned true")
8888
}
89-
if authorizationService.IsAuthorized("token", []string{"c"}) {
89+
if _, authorized := authorizationService.Authorize("token", []string{"c"}); authorized {
9090
t.Error("should've returned false")
9191
}
9292
}
9393

9494
func TestAuthorizationService_WithClients(t *testing.T) {
9595
authorizationService := NewAuthorizationService().WithClients([]*Client{NewClient("1").WithPermission("a"), NewClient("2").WithPermission("b")})
96-
if !authorizationService.IsAuthorized("1", []string{"a"}) {
96+
if _, authorized := authorizationService.Authorize("1", []string{"a"}); !authorized {
9797
t.Error("should've returned true")
9898
}
99-
if !authorizationService.IsAuthorized("2", []string{"b"}) {
99+
if _, authorized := authorizationService.Authorize("2", []string{"b"}); !authorized {
100100
t.Error("should've returned true")
101101
}
102-
if authorizationService.IsAuthorized("1", []string{"b"}) {
102+
if _, authorized := authorizationService.Authorize("1", []string{"b"}); authorized {
103103
t.Error("should've returned false")
104104
}
105-
if authorizationService.IsAuthorized("2", []string{"a"}) {
105+
if _, authorized := authorizationService.Authorize("2", []string{"a"}); authorized {
106106
t.Error("should've returned false")
107107
}
108108
}

client.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ type Client struct {
1010
// If you only wish to use Gate.Protect and Gate.ProtectFunc, you do not have to worry about this,
1111
// since they're only used by Gate.ProtectWithPermissions and Gate.ProtectFuncWithPermissions
1212
Permissions []string
13+
14+
// Data is a field that can be used to store any data you want to associate with the client.
15+
Data any
1316
}
1417

1518
// NewClient creates a Client with a given token
@@ -25,6 +28,18 @@ func NewClientWithPermissions(token string, permissions []string) *Client {
2528
return NewClient(token).WithPermissions(permissions)
2629
}
2730

31+
// NewClientWithData creates a Client with some data
32+
// Equivalent to using NewClient and WithData
33+
func NewClientWithData(token string, data any) *Client {
34+
return NewClient(token).WithData(data)
35+
}
36+
37+
// NewClientWithPermissionsAndData creates a Client with a slice of permissions and some data
38+
// Equivalent to using NewClient, WithPermissions and WithData
39+
func NewClientWithPermissionsAndData(token string, permissions []string, data any) *Client {
40+
return NewClient(token).WithPermissions(permissions).WithData(data)
41+
}
42+
2843
// WithPermissions adds a slice of permissions to a client
2944
func (client *Client) WithPermissions(permissions []string) *Client {
3045
client.Permissions = append(client.Permissions, permissions...)
@@ -37,8 +52,14 @@ func (client *Client) WithPermission(permission string) *Client {
3752
return client
3853
}
3954

55+
// WithData attaches data to a client
56+
func (client *Client) WithData(data any) *Client {
57+
client.Data = data
58+
return client
59+
}
60+
4061
// HasPermission checks whether a client has a given permission
41-
func (client Client) HasPermission(permissionRequired string) bool {
62+
func (client *Client) HasPermission(permissionRequired string) bool {
4263
for _, permission := range client.Permissions {
4364
if permissionRequired == permission {
4465
return true
@@ -48,7 +69,7 @@ func (client Client) HasPermission(permissionRequired string) bool {
4869
}
4970

5071
// HasPermissions checks whether a client has the all permissions passed
51-
func (client Client) HasPermissions(permissionsRequired []string) bool {
72+
func (client *Client) HasPermissions(permissionsRequired []string) bool {
5273
for _, permissionRequired := range permissionsRequired {
5374
if !client.HasPermission(permissionRequired) {
5475
return false

client_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,41 @@ func TestClient_HasPermissions(t *testing.T) {
3636
t.Errorf("client has permissions %s, therefore HasPermissions([a, b, c]) should've been false", client.Permissions)
3737
}
3838
}
39+
40+
func TestClient_WithData(t *testing.T) {
41+
client := NewClient("token")
42+
if client.Data != nil {
43+
t.Error("expected client data to be nil")
44+
}
45+
client.WithData(5)
46+
if client.Data != 5 {
47+
t.Errorf("expected client data to be 5, got %d", client.Data)
48+
}
49+
client.WithData(map[string]string{"key": "value"})
50+
if data, ok := client.Data.(map[string]string); !ok || data["key"] != "value" {
51+
t.Errorf("expected client data to be map[string]string{key: value}, got %v", client.Data)
52+
}
53+
}
54+
55+
func TestNewClientWithData(t *testing.T) {
56+
client := NewClientWithData("token", 5)
57+
if client.Data != 5 {
58+
t.Errorf("expected client data to be 5, got %d", client.Data)
59+
}
60+
}
61+
62+
func TestNewClientWithPermissionsAndData(t *testing.T) {
63+
client := NewClientWithPermissionsAndData("token", []string{"a", "b"}, 5)
64+
if client.Data != 5 {
65+
t.Errorf("expected client data to be 5, got %d", client.Data)
66+
}
67+
if !client.HasPermission("a") {
68+
t.Errorf("client has permissions %s, therefore HasPermission(a) should've been true", client.Permissions)
69+
}
70+
if !client.HasPermission("b") {
71+
t.Errorf("client has permissions %s, therefore HasPermission(b) should've been true", client.Permissions)
72+
}
73+
if client.HasPermission("c") {
74+
t.Errorf("client has permissions %s, therefore HasPermission(c) should've been false", client.Permissions)
75+
}
76+
}

clientprovider_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
var (
1212
getClientByTokenFunc = func(token string) *Client {
1313
if token == "valid-token" {
14-
return &Client{Token: token}
14+
return NewClient("valid-token").WithData("client-data")
1515
}
1616
return nil
1717
}
@@ -21,6 +21,8 @@ func TestClientProvider_GetClientByToken(t *testing.T) {
2121
provider := NewClientProvider(getClientByTokenFunc)
2222
if client := provider.GetClientByToken("valid-token"); client == nil {
2323
t.Error("should've returned a client")
24+
} else if client.Data != "client-data" {
25+
t.Error("expected client data to be 'client-data', got", client.Data)
2426
}
2527
if client := provider.GetClientByToken("invalid-token"); client != nil {
2628
t.Error("should've returned nil")

gate.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ const (
1616
// DefaultTooManyRequestsResponseBody is the default response body returned if a request exceeded the allowed rate limit
1717
DefaultTooManyRequestsResponseBody = "too many requests"
1818

19-
// TokenContextKey is the key used to store the token in the context.
19+
// TokenContextKey is the key used to store the client's token in the context.
2020
TokenContextKey = "g8.token"
21+
22+
// DataContextKey is the key used to store the client's data in the context.
23+
DataContextKey = "g8.data"
2124
)
2225

2326
// Gate is lock to the front door of your API, letting only those you allow through.
@@ -187,12 +190,16 @@ func (gate *Gate) ProtectFuncWithPermissions(handlerFunc http.HandlerFunc, permi
187190
}
188191
if gate.authorizationService != nil {
189192
token := gate.ExtractTokenFromRequest(request)
190-
if !gate.authorizationService.IsAuthorized(token, permissions) {
193+
if client, authorized := gate.authorizationService.Authorize(token, permissions); !authorized {
191194
writer.WriteHeader(http.StatusUnauthorized)
192195
_, _ = writer.Write(gate.unauthorizedResponseBody)
193196
return
197+
} else {
198+
request = request.WithContext(context.WithValue(request.Context(), TokenContextKey, token))
199+
if client != nil && client.Data != nil {
200+
request = request.WithContext(context.WithValue(request.Context(), DataContextKey, client.Data))
201+
}
194202
}
195-
request = request.WithContext(context.WithValue(request.Context(), TokenContextKey, token))
196203
}
197204
handlerFunc(writer, request)
198205
}

gate_bench_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func BenchmarkGate_ProtectWithClientProviderConcurrently(b *testing.B) {
147147
gate := New().WithAuthorizationService(NewAuthorizationService().WithClientProvider(mockClientProvider))
148148

149149
request, _ := http.NewRequest("GET", "/handle", http.NoBody)
150-
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", TestProviderToken))
150+
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", TestProviderClientToken))
151151

152152
firstBadRequest, _ := http.NewRequest("GET", "/handle", http.NoBody)
153153
firstBadRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", "bad-token-1"))

0 commit comments

Comments
 (0)