mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
feat(jwt-exp): exp claim at the access entry level (#3761)
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
@@ -16,9 +17,18 @@ var bearerTokenMatch = regexp.MustCompile("(?i)bearer (.*)")
|
||||
// ResourceAccess is a single entry in the private 'access' claim specified by the distribution token authentication
|
||||
// specification.
|
||||
type ResourceAccess struct {
|
||||
// Standard claims defined in the Distribution spec:
|
||||
// https://distribution.github.io/distribution/spec/auth/jwt/
|
||||
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Actions []string `json:"actions"`
|
||||
|
||||
// Zot extensions
|
||||
|
||||
// ExpiresAt is an optional expiration time for this specific resource access entry.
|
||||
// If not set, the overall token expiration time (the standard 'exp' claim) applies.
|
||||
ExpiresAt *jwt.NumericDate `json:"exp,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceAction struct {
|
||||
@@ -123,6 +133,10 @@ func (a *BearerAuthorizer) Authorize(header string, requested *ResourceAction) e
|
||||
continue
|
||||
}
|
||||
|
||||
if allowed.ExpiresAt != nil && allowed.ExpiresAt.Time.Before(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// requested action is allowed, so don't return an error
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -111,6 +111,155 @@ func TestBearerAuthorizer(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Access entry with per-entry ExpiresAt", func() {
|
||||
now := time.Now()
|
||||
|
||||
Convey("Authorized when ExpiresAt is in the future", func() {
|
||||
access := []api.ResourceAccess{
|
||||
{
|
||||
Name: "authorized-repository",
|
||||
Type: "repository",
|
||||
Actions: []string{"pull"},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
|
||||
claims := api.ClaimsWithAccess{
|
||||
Access: access,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Issuer: "Zot",
|
||||
Audience: []string{"Zot Registry"},
|
||||
},
|
||||
}
|
||||
|
||||
token, err := jwt.NewWithClaims(signingMethod, claims).SignedString(privKey)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
requested := &api.ResourceAction{
|
||||
Type: "repository",
|
||||
Name: "authorized-repository",
|
||||
Action: "pull",
|
||||
}
|
||||
|
||||
err = authorizer.Authorize("Bearer "+token, requested)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Denied when ExpiresAt is in the past", func() {
|
||||
access := []api.ResourceAccess{
|
||||
{
|
||||
Name: "authorized-repository",
|
||||
Type: "repository",
|
||||
Actions: []string{"pull"},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(-time.Hour)),
|
||||
},
|
||||
}
|
||||
|
||||
claims := api.ClaimsWithAccess{
|
||||
Access: access,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Issuer: "Zot",
|
||||
Audience: []string{"Zot Registry"},
|
||||
},
|
||||
}
|
||||
|
||||
token, err := jwt.NewWithClaims(signingMethod, claims).SignedString(privKey)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
requested := &api.ResourceAction{
|
||||
Type: "repository",
|
||||
Name: "authorized-repository",
|
||||
Action: "pull",
|
||||
}
|
||||
|
||||
err = authorizer.Authorize("Bearer "+token, requested)
|
||||
So(err, ShouldHaveSameTypeAs, &api.AuthChallengeError{})
|
||||
So(err, ShouldBeError, zerr.ErrInsufficientScope)
|
||||
})
|
||||
|
||||
Convey("Only the expired entry is skipped, other entries still work", func() {
|
||||
access := []api.ResourceAccess{
|
||||
{
|
||||
Name: "authorized-repository",
|
||||
Type: "repository",
|
||||
Actions: []string{"pull"},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(-time.Hour)),
|
||||
},
|
||||
{
|
||||
Name: "authorized-repository",
|
||||
Type: "repository",
|
||||
Actions: []string{"pull", "push"},
|
||||
},
|
||||
}
|
||||
|
||||
claims := api.ClaimsWithAccess{
|
||||
Access: access,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Issuer: "Zot",
|
||||
Audience: []string{"Zot Registry"},
|
||||
},
|
||||
}
|
||||
|
||||
token, err := jwt.NewWithClaims(signingMethod, claims).SignedString(privKey)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
requested := &api.ResourceAction{
|
||||
Type: "repository",
|
||||
Name: "authorized-repository",
|
||||
Action: "pull",
|
||||
}
|
||||
|
||||
err = authorizer.Authorize("Bearer "+token, requested)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("All entries expired results in insufficient scope", func() {
|
||||
access := []api.ResourceAccess{
|
||||
{
|
||||
Name: "authorized-repository",
|
||||
Type: "repository",
|
||||
Actions: []string{"pull"},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(-time.Hour)),
|
||||
},
|
||||
{
|
||||
Name: "authorized-repository",
|
||||
Type: "repository",
|
||||
Actions: []string{"pull"},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(-2 * time.Hour)),
|
||||
},
|
||||
}
|
||||
|
||||
claims := api.ClaimsWithAccess{
|
||||
Access: access,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Issuer: "Zot",
|
||||
Audience: []string{"Zot Registry"},
|
||||
},
|
||||
}
|
||||
|
||||
token, err := jwt.NewWithClaims(signingMethod, claims).SignedString(privKey)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
requested := &api.ResourceAction{
|
||||
Type: "repository",
|
||||
Name: "authorized-repository",
|
||||
Action: "pull",
|
||||
}
|
||||
|
||||
err = authorizer.Authorize("Bearer "+token, requested)
|
||||
So(err, ShouldHaveSameTypeAs, &api.AuthChallengeError{})
|
||||
So(err, ShouldBeError, zerr.ErrInsufficientScope)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Invalid token", func() {
|
||||
authHeader := "invalid"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user