2023-03-31 12:44:22 +02:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt"
|
|
|
|
|
|
|
|
"github.com/netbirdio/netbird/management/server"
|
2024-02-22 12:27:08 +01:00
|
|
|
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
2023-12-11 16:59:15 +01:00
|
|
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
2023-03-31 12:44:22 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-09-01 18:09:59 +02:00
|
|
|
audience = "audience"
|
|
|
|
userIDClaim = "userIDClaim"
|
|
|
|
accountID = "accountID"
|
|
|
|
domain = "domain"
|
|
|
|
userID = "userID"
|
|
|
|
tokenID = "tokenID"
|
2023-10-17 17:19:47 +02:00
|
|
|
PAT = "nbp_PAT"
|
2023-09-01 18:09:59 +02:00
|
|
|
JWT = "JWT"
|
|
|
|
wrongToken = "wrongToken"
|
2023-03-31 12:44:22 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var testAccount = &server.Account{
|
|
|
|
Id: accountID,
|
|
|
|
Domain: domain,
|
|
|
|
Users: map[string]*server.User{
|
|
|
|
userID: {
|
|
|
|
Id: userID,
|
|
|
|
PATs: map[string]*server.PersonalAccessToken{
|
|
|
|
tokenID: {
|
|
|
|
ID: tokenID,
|
|
|
|
Name: "My first token",
|
|
|
|
HashedToken: "someHash",
|
2023-04-03 15:09:35 +02:00
|
|
|
ExpirationDate: time.Now().UTC().AddDate(0, 0, 7),
|
2023-03-31 12:44:22 +02:00
|
|
|
CreatedBy: userID,
|
2023-04-03 15:09:35 +02:00
|
|
|
CreatedAt: time.Now().UTC(),
|
|
|
|
LastUsed: time.Now().UTC(),
|
2023-03-31 12:44:22 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
func mockGetAccountFromPAT(token string) (*server.Account, *server.User, *server.PersonalAccessToken, error) {
|
|
|
|
if token == PAT {
|
|
|
|
return testAccount, testAccount.Users[userID], testAccount.Users[userID].PATs[tokenID], nil
|
|
|
|
}
|
|
|
|
return nil, nil, nil, fmt.Errorf("PAT invalid")
|
|
|
|
}
|
|
|
|
|
|
|
|
func mockValidateAndParseToken(token string) (*jwt.Token, error) {
|
|
|
|
if token == JWT {
|
2023-12-11 16:59:15 +01:00
|
|
|
return &jwt.Token{
|
|
|
|
Claims: jwt.MapClaims{
|
|
|
|
userIDClaim: userID,
|
|
|
|
audience + jwtclaims.AccountIDSuffix: accountID,
|
|
|
|
},
|
|
|
|
Valid: true,
|
|
|
|
}, nil
|
2023-03-31 12:44:22 +02:00
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("JWT invalid")
|
|
|
|
}
|
|
|
|
|
|
|
|
func mockMarkPATUsed(token string) error {
|
|
|
|
if token == tokenID {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return fmt.Errorf("Should never get reached")
|
|
|
|
}
|
|
|
|
|
2023-12-13 11:18:35 +01:00
|
|
|
func mockCheckUserAccessByJWTGroups(claims jwtclaims.AuthorizationClaims) error {
|
2023-12-11 16:59:15 +01:00
|
|
|
if testAccount.Id != claims.AccountId {
|
2023-12-13 11:18:35 +01:00
|
|
|
return fmt.Errorf("account with id %s does not exist", claims.AccountId)
|
2023-12-11 16:59:15 +01:00
|
|
|
}
|
|
|
|
|
2023-12-13 11:18:35 +01:00
|
|
|
if _, ok := testAccount.Users[claims.UserId]; !ok {
|
|
|
|
return fmt.Errorf("user with id %s does not exist", claims.UserId)
|
2023-12-11 16:59:15 +01:00
|
|
|
}
|
|
|
|
|
2023-12-13 11:18:35 +01:00
|
|
|
return nil
|
2023-12-11 16:59:15 +01:00
|
|
|
}
|
|
|
|
|
2023-03-31 12:45:10 +02:00
|
|
|
func TestAuthMiddleware_Handler(t *testing.T) {
|
2023-03-31 12:44:22 +02:00
|
|
|
tt := []struct {
|
|
|
|
name string
|
2024-02-22 12:27:08 +01:00
|
|
|
path string
|
2023-03-31 12:44:22 +02:00
|
|
|
authHeader string
|
|
|
|
expectedStatusCode int
|
2024-02-22 12:27:08 +01:00
|
|
|
shouldBypassAuth bool
|
2023-03-31 12:44:22 +02:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "Valid PAT Token",
|
2024-02-22 12:27:08 +01:00
|
|
|
path: "/test",
|
2023-03-31 12:44:22 +02:00
|
|
|
authHeader: "Token " + PAT,
|
|
|
|
expectedStatusCode: 200,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Invalid PAT Token",
|
2024-02-22 12:27:08 +01:00
|
|
|
path: "/test",
|
2023-03-31 12:44:22 +02:00
|
|
|
authHeader: "Token " + wrongToken,
|
|
|
|
expectedStatusCode: 401,
|
|
|
|
},
|
2023-10-17 17:19:47 +02:00
|
|
|
{
|
|
|
|
name: "Fallback to PAT Token",
|
2024-02-22 12:27:08 +01:00
|
|
|
path: "/test",
|
2023-10-17 17:19:47 +02:00
|
|
|
authHeader: "Bearer " + PAT,
|
|
|
|
expectedStatusCode: 200,
|
|
|
|
},
|
2023-03-31 12:44:22 +02:00
|
|
|
{
|
|
|
|
name: "Valid JWT Token",
|
2024-02-22 12:27:08 +01:00
|
|
|
path: "/test",
|
2023-03-31 12:44:22 +02:00
|
|
|
authHeader: "Bearer " + JWT,
|
|
|
|
expectedStatusCode: 200,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Invalid JWT Token",
|
2024-02-22 12:27:08 +01:00
|
|
|
path: "/test",
|
2023-03-31 12:44:22 +02:00
|
|
|
authHeader: "Bearer " + wrongToken,
|
|
|
|
expectedStatusCode: 401,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Basic Auth",
|
2024-02-22 12:27:08 +01:00
|
|
|
path: "/test",
|
2023-03-31 12:44:22 +02:00
|
|
|
authHeader: "Basic " + PAT,
|
|
|
|
expectedStatusCode: 401,
|
|
|
|
},
|
2024-02-22 12:27:08 +01:00
|
|
|
{
|
|
|
|
name: "Webhook Path Bypass",
|
|
|
|
path: "/webhook",
|
|
|
|
authHeader: "",
|
|
|
|
expectedStatusCode: 200,
|
|
|
|
shouldBypassAuth: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Webhook Path Bypass with Subpath",
|
|
|
|
path: "/webhook/test",
|
|
|
|
authHeader: "",
|
|
|
|
expectedStatusCode: 200,
|
|
|
|
shouldBypassAuth: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Different Webhook Path",
|
|
|
|
path: "/webhooktest",
|
|
|
|
authHeader: "",
|
|
|
|
expectedStatusCode: 401,
|
|
|
|
shouldBypassAuth: false,
|
|
|
|
},
|
2023-03-31 12:44:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// do nothing
|
|
|
|
})
|
|
|
|
|
2023-12-11 16:59:15 +01:00
|
|
|
claimsExtractor := jwtclaims.NewClaimsExtractor(
|
|
|
|
jwtclaims.WithAudience(audience),
|
|
|
|
jwtclaims.WithUserIDClaim(userIDClaim),
|
|
|
|
)
|
|
|
|
|
|
|
|
authMiddleware := NewAuthMiddleware(
|
|
|
|
mockGetAccountFromPAT,
|
|
|
|
mockValidateAndParseToken,
|
|
|
|
mockMarkPATUsed,
|
2023-12-13 11:18:35 +01:00
|
|
|
mockCheckUserAccessByJWTGroups,
|
2023-12-11 16:59:15 +01:00
|
|
|
claimsExtractor,
|
|
|
|
audience,
|
|
|
|
userIDClaim,
|
|
|
|
)
|
2023-03-31 12:44:22 +02:00
|
|
|
|
|
|
|
handlerToTest := authMiddleware.Handler(nextHandler)
|
|
|
|
|
|
|
|
for _, tc := range tt {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
2024-02-22 12:27:08 +01:00
|
|
|
if tc.shouldBypassAuth {
|
|
|
|
bypass.AddBypassPath(tc.path)
|
|
|
|
}
|
|
|
|
|
|
|
|
req := httptest.NewRequest("GET", "http://testing"+tc.path, nil)
|
2023-03-31 12:44:22 +02:00
|
|
|
req.Header.Set("Authorization", tc.authHeader)
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
|
|
|
|
handlerToTest.ServeHTTP(rec, req)
|
|
|
|
|
2023-09-04 17:03:44 +02:00
|
|
|
result := rec.Result()
|
|
|
|
defer result.Body.Close()
|
|
|
|
if result.StatusCode != tc.expectedStatusCode {
|
|
|
|
t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, result.StatusCode)
|
2023-03-31 12:44:22 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|