diff --git a/weed/security/jwt.go b/weed/security/jwt.go index d859e9ea8..abea0198d 100644 --- a/weed/security/jwt.go +++ b/weed/security/jwt.go @@ -24,6 +24,8 @@ type SeaweedFileIdClaims struct { // Right now, it only contains the standard claims; but this might be extended later // for more fine-grained permissions. type SeaweedFilerClaims struct { + AllowedPrefixes []string `json:"allowed_prefixes,omitempty"` + AllowedMethods []string `json:"allowed_methods,omitempty"` jwt.RegisteredClaims } @@ -56,7 +58,7 @@ func GenJwtForFilerServer(signingKey SigningKey, expiresAfterSec int) EncodedJwt } claims := SeaweedFilerClaims{ - jwt.RegisteredClaims{}, + RegisteredClaims: jwt.RegisteredClaims{}, } if expiresAfterSec > 0 { claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(expiresAfterSec))) diff --git a/weed/server/filer_jwt_test.go b/weed/server/filer_jwt_test.go new file mode 100644 index 000000000..565289b8b --- /dev/null +++ b/weed/server/filer_jwt_test.go @@ -0,0 +1,116 @@ +package weed_server + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/seaweedfs/seaweedfs/weed/security" +) + +func TestFilerServer_maybeCheckJwtAuthorization_Scoped(t *testing.T) { + signingKey := "secret" + filerGuard := security.NewGuard(nil, signingKey, 0, signingKey, 0) + fs := &FilerServer{ + filerGuard: filerGuard, + } + + // Helper to generate token + genToken := func(allowedPrefixes []string, allowedMethods []string) string { + claims := security.SeaweedFilerClaims{ + AllowedPrefixes: allowedPrefixes, + AllowedMethods: allowedMethods, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + str, _ := token.SignedString([]byte(signingKey)) + return str + } + + tests := []struct { + name string + token string + method string + path string + isWrite bool + expectAuthorized bool + }{ + { + name: "no restrictions", + token: genToken(nil, nil), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: true, + }, + { + name: "allowed prefix match", + token: genToken([]string{"/data"}, nil), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: true, + }, + { + name: "allowed prefix mismatch", + token: genToken([]string{"/private"}, nil), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: false, + }, + { + name: "allowed method match", + token: genToken(nil, []string{"GET"}), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: true, + }, + { + name: "allowed method mismatch", + token: genToken(nil, []string{"POST"}), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: false, + }, + { + name: "both match", + token: genToken([]string{"/data"}, []string{"GET"}), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: true, + }, + { + name: "prefix match, method mismatch", + token: genToken([]string{"/data"}, []string{"POST"}), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: false, + }, + { + name: "multiple prefixes match", + token: genToken([]string{"/other", "/data"}, nil), + method: "GET", + path: "/data/test", + isWrite: false, + expectAuthorized: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(tt.method, tt.path, nil) + req.Header.Set("Authorization", "Bearer "+tt.token) + if authorized := fs.maybeCheckJwtAuthorization(req, tt.isWrite); authorized != tt.expectAuthorized { + t.Errorf("expected authorized=%v, got %v", tt.expectAuthorized, authorized) + } + }) + } +} diff --git a/weed/server/filer_server_handlers.go b/weed/server/filer_server_handlers.go index 57d675740..d7214ccaa 100644 --- a/weed/server/filer_server_handlers.go +++ b/weed/server/filer_server_handlers.go @@ -242,9 +242,38 @@ func (fs *FilerServer) maybeCheckJwtAuthorization(r *http.Request, isWrite bool) if !token.Valid { glog.V(1).Infof("jwt invalid from %s: %v", r.RemoteAddr, tokenStr) return false - } else { - return true } + + if claims, ok := token.Claims.(*security.SeaweedFilerClaims); ok { + if len(claims.AllowedPrefixes) > 0 { + hasPrefix := false + for _, prefix := range claims.AllowedPrefixes { + if strings.HasPrefix(r.URL.Path, prefix) { + hasPrefix = true + break + } + } + if !hasPrefix { + glog.V(1).Infof("jwt path not allowed from %s: %v", r.RemoteAddr, r.URL.Path) + return false + } + } + if len(claims.AllowedMethods) > 0 { + hasMethod := false + for _, method := range claims.AllowedMethods { + if method == r.Method { + hasMethod = true + break + } + } + if !hasMethod { + glog.V(1).Infof("jwt method not allowed from %s: %v", r.RemoteAddr, r.Method) + return false + } + } + } + + return true } func (fs *FilerServer) filerHealthzHandler(w http.ResponseWriter, r *http.Request) {