Skip to content

Commit 96b24bc

Browse files
committed
feat: enhance PAT validation in middleware
Signed-off-by: BruceAko <chongzhi@hust.edu.cn>
1 parent cc966bc commit 96b24bc

File tree

1 file changed

+74
-1
lines changed

1 file changed

+74
-1
lines changed

manager/middlewares/personal_access_token.go

+74-1
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,23 @@
1717
package middlewares
1818

1919
import (
20+
"fmt"
2021
"net/http"
22+
"regexp"
2123
"strings"
24+
"time"
2225

2326
"github.com/gin-gonic/gin"
2427
"github.com/go-http-utils/headers"
2528
"gorm.io/gorm"
2629

30+
logger "d7y.io/dragonfly/v2/internal/dflog"
2731
"d7y.io/dragonfly/v2/manager/models"
32+
"d7y.io/dragonfly/v2/manager/types"
33+
)
34+
35+
var (
36+
oapiResourceRegexp = regexp.MustCompile(`^/oapi/v[0-9]+/([-_a-zA-Z0-9]*)[/.*]*`)
2837
)
2938

3039
func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
@@ -42,14 +51,78 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
4251

4352
// Check if the personal access token is valid.
4453
personalAccessToken := tokenFields[1]
45-
if err := gdb.WithContext(c).Where("token = ?", personalAccessToken).First(&models.PersonalAccessToken{}).Error; err != nil {
54+
var token models.PersonalAccessToken
55+
if err := gdb.WithContext(c).Where("token = ?", personalAccessToken).First(&token).Error; err != nil {
56+
logger.Errorf("Invalid personal access token attempt: %s, error: %v", c.Request.URL.Path, err)
4657
c.JSON(http.StatusUnauthorized, ErrorResponse{
4758
Message: http.StatusText(http.StatusUnauthorized),
4859
})
4960
c.Abort()
5061
return
5162
}
5263

64+
// Check if the token is active.
65+
if token.State != models.PersonalAccessTokenStateActive {
66+
logger.Errorf("Inactive token used: %s, token name: %s, user_id: %d", c.Request.URL.Path, token.Name, token.UserID)
67+
c.JSON(http.StatusForbidden, ErrorResponse{
68+
Message: "Token is inactive",
69+
})
70+
c.Abort()
71+
return
72+
}
73+
74+
// Check if the token has expired.
75+
if time.Now().After(token.ExpiredAt) {
76+
logger.Errorf("Expired token used: %s, token name: %s, user_id: %d, expired: %v",
77+
c.Request.URL.Path, token.Name, token.UserID, token.ExpiredAt)
78+
c.JSON(http.StatusForbidden, ErrorResponse{
79+
Message: "Token has expired",
80+
})
81+
c.Abort()
82+
return
83+
}
84+
85+
// Check if the token's scopes include the required resource type.
86+
hasScope := false
87+
resourceType := getAPIResourceType(c.Request.URL.Path)
88+
for _, scope := range token.Scopes {
89+
if scope == resourceType {
90+
hasScope = true
91+
break
92+
}
93+
}
94+
95+
if !hasScope {
96+
logger.Errorf("Insufficient scope token used: %s, token name: %s, user_id: %d, required: %s, available: %v",
97+
c.Request.URL.Path, token.Name, token.UserID, resourceType, token.Scopes)
98+
c.JSON(http.StatusForbidden, ErrorResponse{
99+
Message: fmt.Sprintf("Token doesn't have permission to access this resource. Required scope: %s", resourceType),
100+
})
101+
c.Abort()
102+
return
103+
}
104+
53105
c.Next()
54106
}
55107
}
108+
109+
// getAPIResourceType extracts the resource type from the path.
110+
// For example: /oapi/v1/jobs -> job, /oapi/v1/clusters -> cluster
111+
func getAPIResourceType(path string) string {
112+
matches := oapiResourceRegexp.FindStringSubmatch(path)
113+
if len(matches) != 2 {
114+
return ""
115+
}
116+
117+
resource := strings.ToLower(matches[1])
118+
switch resource {
119+
case "jobs":
120+
return types.PersonalAccessTokenScopeJob
121+
case "clusters":
122+
return types.PersonalAccessTokenScopeCluster
123+
case "preheats":
124+
return types.PersonalAccessTokenScopePreheat
125+
default:
126+
return resource
127+
}
128+
}

0 commit comments

Comments
 (0)