mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-11 14:28:33 +01:00
security: scoped filer tokens are not admin credentials (#12551)
* security: do not accept a path- or method-scoped filer token as admin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * security: test that scoped filer tokens fail the admin token check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2143761319
commit
8d96985131
3 files changed
+21
-2
No files matched your search
@@ -63,7 +63,8 @@ func CheckGrpcAdminToken(ctx context.Context, signingKey SigningKey) error {
|
||||
}
|
||||
|
||||
// IsFilerAdminToken reports whether token is an admin token signed by
|
||||
// signingKey. An STS session token signed with the same key does not count.
|
||||
// signingKey. An STS session token, or a filer token scoped to paths or
|
||||
// methods, signed with the same key does not count.
|
||||
func IsFilerAdminToken(signingKey SigningKey, token EncodedJwt) bool {
|
||||
if len(signingKey) == 0 || token == "" {
|
||||
return false
|
||||
@@ -73,7 +74,7 @@ func IsFilerAdminToken(signingKey SigningKey, token EncodedJwt) bool {
|
||||
return false
|
||||
}
|
||||
claims, ok := parsed.Claims.(*SeaweedFilerAdminClaims)
|
||||
return ok && claims.SessionId == ""
|
||||
return ok && claims.SessionId == "" && len(claims.AllowedPrefixes) == 0 && len(claims.AllowedMethods) == 0
|
||||
}
|
||||
|
||||
// GrpcAdminPeerCredentials returns outgoing metadata carrying a Bearer token
|
||||
|
||||
@@ -87,6 +87,17 @@ func TestCheckGrpcAdminToken(t *testing.T) {
|
||||
if signErr != nil {
|
||||
t.Fatalf("sign session token: %v", signErr)
|
||||
}
|
||||
scoped := func(claims SeaweedFilerClaims) string {
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour))
|
||||
signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(key))
|
||||
if err != nil {
|
||||
t.Fatalf("sign scoped token: %v", err)
|
||||
}
|
||||
return "Bearer " + signed
|
||||
}
|
||||
pathScoped := scoped(SeaweedFilerClaims{AllowedPrefixes: []string{"/tenants/t1"}})
|
||||
methodScoped := scoped(SeaweedFilerClaims{AllowedMethods: []string{"GET"}})
|
||||
unscoped := scoped(SeaweedFilerClaims{})
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
values []string
|
||||
@@ -98,6 +109,10 @@ func TestCheckGrpcAdminToken(t *testing.T) {
|
||||
"only expired": {[]string{"Bearer " + expired}, "invalid admin token"},
|
||||
"only stale": {[]string{stale}, "invalid admin token"},
|
||||
"same-key session": {[]string{"Bearer " + session}, "invalid admin token"},
|
||||
"path-scoped": {[]string{pathScoped}, "invalid admin token"},
|
||||
"method-scoped": {[]string{methodScoped}, "invalid admin token"},
|
||||
"scoped then fresh": {[]string{pathScoped, fresh}, ""},
|
||||
"unscoped filer": {[]string{unscoped}, ""},
|
||||
"basic then stale": {[]string{"Basic dXNlcjpwYXNz", stale}, "invalid admin token"},
|
||||
"no bearer value": {[]string{"Basic dXNlcjpwYXNz", "garbage"}, "authorization header must use Bearer scheme"},
|
||||
"lowercase scheme ok": {[]string{"bearer " + fresh[len("Bearer "):]}, ""},
|
||||
|
||||
@@ -52,6 +52,9 @@ type SeaweedFilerAdminClaims struct {
|
||||
// SessionId is present only on STS session tokens; a token carrying it is
|
||||
// an S3 session credential, not a filer admin credential.
|
||||
SessionId string `json:"sid,omitempty"`
|
||||
// A filer token scoped to paths or methods is not an admin credential.
|
||||
AllowedPrefixes []string `json:"allowed_prefixes,omitempty"`
|
||||
AllowedMethods []string `json:"allowed_methods,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user