17
17
package middlewares
18
18
19
19
import (
20
+ "fmt"
20
21
"net/http"
22
+ "regexp"
21
23
"strings"
24
+ "time"
22
25
23
26
"github.com/gin-gonic/gin"
24
27
"github.com/go-http-utils/headers"
25
28
"gorm.io/gorm"
26
29
30
+ logger "d7y.io/dragonfly/v2/internal/dflog"
27
31
"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]*)[/.*]*` )
28
37
)
29
38
30
39
func PersonalAccessToken (gdb * gorm.DB ) gin.HandlerFunc {
@@ -42,14 +51,78 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
42
51
43
52
// Check if the personal access token is valid.
44
53
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 )
46
57
c .JSON (http .StatusUnauthorized , ErrorResponse {
47
58
Message : http .StatusText (http .StatusUnauthorized ),
48
59
})
49
60
c .Abort ()
50
61
return
51
62
}
52
63
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
+
53
105
c .Next ()
54
106
}
55
107
}
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