mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
Add account usage logic (#1567)
--------- Co-authored-by: Yury Gargay <yury.gargay@gmail.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -15,6 +17,8 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
const apiPrefix = "/api"
|
||||
|
||||
// AuthCfg contains parameters for authentication middleware
|
||||
type AuthCfg struct {
|
||||
Issuer string
|
||||
@@ -35,7 +39,7 @@ type emptyObject struct {
|
||||
}
|
||||
|
||||
// APIHandler creates the Management service HTTP API handler registering all the available endpoints.
|
||||
func APIHandler(accountManager s.AccountManager, LocationManager *geolocation.Geolocation, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg AuthCfg) (http.Handler, error) {
|
||||
func APIHandler(ctx context.Context, accountManager s.AccountManager, LocationManager *geolocation.Geolocation, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg AuthCfg) (http.Handler, error) {
|
||||
claimsExtractor := jwtclaims.NewClaimsExtractor(
|
||||
jwtclaims.WithAudience(authCfg.Audience),
|
||||
jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
|
||||
@@ -61,7 +65,8 @@ func APIHandler(accountManager s.AccountManager, LocationManager *geolocation.Ge
|
||||
rootRouter := mux.NewRouter()
|
||||
metricsMiddleware := appMetrics.HTTPMiddleware()
|
||||
|
||||
router := rootRouter.PathPrefix("/api").Subrouter()
|
||||
prefix := apiPrefix
|
||||
router := rootRouter.PathPrefix(prefix).Subrouter()
|
||||
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler, acMiddleware.Handler)
|
||||
|
||||
api := apiHandler{
|
||||
@@ -71,7 +76,10 @@ func APIHandler(accountManager s.AccountManager, LocationManager *geolocation.Ge
|
||||
AuthCfg: authCfg,
|
||||
}
|
||||
|
||||
integrations.RegisterHandlers(api.Router, accountManager, claimsExtractor)
|
||||
if _, err := integrations.RegisterHandlers(ctx, prefix, api.Router, accountManager, claimsExtractor); err != nil {
|
||||
return nil, fmt.Errorf("register integrations endpoints: %w", err)
|
||||
}
|
||||
|
||||
api.addAccountsEndpoint()
|
||||
api.addPeersEndpoint()
|
||||
api.addUsersEndpoint()
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
||||
"github.com/netbirdio/netbird/management/server/http/util"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
|
||||
@@ -36,9 +37,13 @@ func NewAccessControl(audience, userIDClaim string, getUser GetUser) *AccessCont
|
||||
var tokenPathRegexp = regexp.MustCompile(`^.*/api/users/.*/tokens.*$`)
|
||||
|
||||
// Handler method of the middleware which forbids all modify requests for non admin users
|
||||
// It also adds
|
||||
func (a *AccessControl) Handler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if bypass.ShouldBypass(r.URL.Path, h, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
claims := a.claimsExtract.FromRequestContext(r)
|
||||
|
||||
user, err := a.getUser(claims)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
||||
"github.com/netbirdio/netbird/management/server/http/util"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
@@ -66,6 +67,11 @@ func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParse
|
||||
// Handler method of the middleware which authenticates a user either by JWT claims or by PAT
|
||||
func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if bypass.ShouldBypass(r.URL.Path, h, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
auth := strings.Split(r.Header.Get("Authorization"), " ")
|
||||
authType := strings.ToLower(auth[0])
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/golang-jwt/jwt"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
)
|
||||
|
||||
@@ -88,39 +89,68 @@ func mockCheckUserAccessByJWTGroups(claims jwtclaims.AuthorizationClaims) error
|
||||
func TestAuthMiddleware_Handler(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
path string
|
||||
authHeader string
|
||||
expectedStatusCode int
|
||||
shouldBypassAuth bool
|
||||
}{
|
||||
{
|
||||
name: "Valid PAT Token",
|
||||
path: "/test",
|
||||
authHeader: "Token " + PAT,
|
||||
expectedStatusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Invalid PAT Token",
|
||||
path: "/test",
|
||||
authHeader: "Token " + wrongToken,
|
||||
expectedStatusCode: 401,
|
||||
},
|
||||
{
|
||||
name: "Fallback to PAT Token",
|
||||
path: "/test",
|
||||
authHeader: "Bearer " + PAT,
|
||||
expectedStatusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Valid JWT Token",
|
||||
path: "/test",
|
||||
authHeader: "Bearer " + JWT,
|
||||
expectedStatusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Invalid JWT Token",
|
||||
path: "/test",
|
||||
authHeader: "Bearer " + wrongToken,
|
||||
expectedStatusCode: 401,
|
||||
},
|
||||
{
|
||||
name: "Basic Auth",
|
||||
path: "/test",
|
||||
authHeader: "Basic " + PAT,
|
||||
expectedStatusCode: 401,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -146,7 +176,11 @@ func TestAuthMiddleware_Handler(t *testing.T) {
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://testing", nil)
|
||||
if tc.shouldBypassAuth {
|
||||
bypass.AddBypassPath(tc.path)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://testing"+tc.path, nil)
|
||||
req.Header.Set("Authorization", tc.authHeader)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -159,5 +193,4 @@ func TestAuthMiddleware_Handler(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
39
management/server/http/middleware/bypass/bypass.go
Normal file
39
management/server/http/middleware/bypass/bypass.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package bypass
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var byPassMutex sync.RWMutex
|
||||
|
||||
// bypassPaths is a set of paths that should bypass middleware.
|
||||
var bypassPaths = make(map[string]struct{})
|
||||
|
||||
// AddBypassPath adds an exact path to the list of paths that bypass middleware.
|
||||
func AddBypassPath(path string) {
|
||||
byPassMutex.Lock()
|
||||
defer byPassMutex.Unlock()
|
||||
bypassPaths[path] = struct{}{}
|
||||
}
|
||||
|
||||
// RemovePath removes a path from the list of paths that bypass middleware.
|
||||
func RemovePath(path string) {
|
||||
byPassMutex.Lock()
|
||||
defer byPassMutex.Unlock()
|
||||
delete(bypassPaths, path)
|
||||
}
|
||||
|
||||
// ShouldBypass checks if the request path is one of the auth bypass paths and returns true if the middleware should be bypassed.
|
||||
// This can be used to bypass authz/authn middlewares for certain paths, such as webhooks that implement their own authentication.
|
||||
func ShouldBypass(requestPath string, h http.Handler, w http.ResponseWriter, r *http.Request) bool {
|
||||
byPassMutex.RLock()
|
||||
defer byPassMutex.RUnlock()
|
||||
|
||||
if _, ok := bypassPaths[requestPath]; ok {
|
||||
h.ServeHTTP(w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
103
management/server/http/middleware/bypass/bypass_test.go
Normal file
103
management/server/http/middleware/bypass/bypass_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package bypass_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
||||
)
|
||||
|
||||
func TestAuthBypass(t *testing.T) {
|
||||
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToAdd string
|
||||
pathToRemove string
|
||||
testPath string
|
||||
expectBypass bool
|
||||
expectHTTPCode int
|
||||
}{
|
||||
{
|
||||
name: "Path added to bypass",
|
||||
pathToAdd: "/bypass",
|
||||
testPath: "/bypass",
|
||||
expectBypass: true,
|
||||
expectHTTPCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Path not added to bypass",
|
||||
testPath: "/no-bypass",
|
||||
expectBypass: false,
|
||||
expectHTTPCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Path removed from bypass",
|
||||
pathToAdd: "/remove-bypass",
|
||||
pathToRemove: "/remove-bypass",
|
||||
testPath: "/remove-bypass",
|
||||
expectBypass: false,
|
||||
expectHTTPCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Exact path matches bypass",
|
||||
pathToAdd: "/webhook",
|
||||
testPath: "/webhook",
|
||||
expectBypass: true,
|
||||
expectHTTPCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Subpath does not match bypass",
|
||||
pathToAdd: "/webhook",
|
||||
testPath: "/webhook/extra",
|
||||
expectBypass: false,
|
||||
expectHTTPCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Similar path does not match bypass",
|
||||
pathToAdd: "/webhook",
|
||||
testPath: "/webhooking",
|
||||
expectBypass: false,
|
||||
expectHTTPCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Prefix path does not match bypass",
|
||||
pathToAdd: "/webhook",
|
||||
testPath: "/web",
|
||||
expectBypass: false,
|
||||
expectHTTPCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.pathToAdd != "" {
|
||||
bypass.AddBypassPath(tc.pathToAdd)
|
||||
defer bypass.RemovePath(tc.pathToAdd)
|
||||
}
|
||||
|
||||
if tc.pathToRemove != "" {
|
||||
bypass.RemovePath(tc.pathToRemove)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("GET", tc.testPath, nil)
|
||||
require.NoError(t, err, "Creating request should not fail")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
bypassed := bypass.ShouldBypass(tc.testPath, dummyHandler, recorder, request)
|
||||
|
||||
assert.Equal(t, tc.expectBypass, bypassed, "Bypass check did not match expectation")
|
||||
|
||||
if tc.expectBypass {
|
||||
assert.Equal(t, tc.expectHTTPCode, recorder.Code, "HTTP status code did not match expectation for bypassed path")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user