@@ -33,6 +33,8 @@ import (
33
33
)
34
34
35
35
var (
36
+ // oapiResourceRegexp is a regular expression to extract the resource type from the path.
37
+ // Example: /oapi/v1/jobs/1 -> jobs.
36
38
oapiResourceRegexp = regexp .MustCompile (`^/oapi/v[0-9]+/([-_a-zA-Z]*)[/.*]*` )
37
39
)
38
40
@@ -45,6 +47,7 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
45
47
c .JSON (http .StatusUnauthorized , ErrorResponse {
46
48
Message : http .StatusText (http .StatusUnauthorized ),
47
49
})
50
+
48
51
c .Abort ()
49
52
return
50
53
}
@@ -53,51 +56,56 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
53
56
personalAccessToken := tokenFields [1 ]
54
57
var token models.PersonalAccessToken
55
58
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 )
59
+ logger .Errorf ("invalid personal access token attempt: %s, error: %v" , c .Request .URL .Path , err )
57
60
c .JSON (http .StatusUnauthorized , ErrorResponse {
58
61
Message : http .StatusText (http .StatusUnauthorized ),
59
62
})
63
+
60
64
c .Abort ()
61
65
return
62
66
}
63
67
64
68
// Check if the token is active.
65
69
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 )
70
+ logger .Errorf ("inactive token used: %s, token name: %s, user_id: %d" , c .Request .URL .Path , token .Name , token .UserID )
67
71
c .JSON (http .StatusForbidden , ErrorResponse {
68
72
Message : "Token is inactive" ,
69
73
})
74
+
70
75
c .Abort ()
71
76
return
72
77
}
73
78
74
79
// Check if the token has expired.
75
80
if time .Now ().After (token .ExpiredAt ) {
76
- logger .Errorf ("Expired token used: %s, token name: %s, user_id: %d, expired: %v" ,
81
+ logger .Errorf ("expired token used: %s, token name: %s, user_id: %d, expired: %v" ,
77
82
c .Request .URL .Path , token .Name , token .UserID , token .ExpiredAt )
78
83
c .JSON (http .StatusForbidden , ErrorResponse {
79
84
Message : "Token has expired" ,
80
85
})
86
+
81
87
c .Abort ()
82
88
return
83
89
}
84
90
85
91
// 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
- }
92
+ requiredPermission , err := requiredPermission (c .Request .URL .Path )
93
+ if err != nil {
94
+ logger .Errorf ("failed to extract resource type from path: %s, error: %v" , c .Request .URL .Path , err )
95
+ c .JSON (http .StatusForbidden , ErrorResponse {
96
+ Message : fmt .Sprintf ("Failed to extract resource type from path: %s" , c .Request .URL .Path ),
97
+ })
98
+
99
+ c .Abort ()
100
+ return
93
101
}
94
102
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 )
103
+ if ! hasPermission (token .Scopes , requiredPermission ) {
104
+ logger .Errorf ("insufficient scope token used %s. Required permission: %s" , token .Name , requiredPermission )
98
105
c .JSON (http .StatusForbidden , ErrorResponse {
99
- Message : fmt .Sprintf ("Token doesn't have permission to access this resource. Required scope : %s" , resourceType ),
106
+ Message : fmt .Sprintf ("Token doesn't have permission to access this resource. Required permission : %s" , requiredPermission ),
100
107
})
108
+
101
109
c .Abort ()
102
110
return
103
111
}
@@ -106,23 +114,42 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
106
114
}
107
115
}
108
116
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 {
117
+ // hasPermission checks if the required permission exists in the provided permissions list.
118
+ // For backward compatibility, an empty permissions list grants all permissions.
119
+ // This allows existing systems that don't have explicit permissions set to continue
120
+ // working without interruption.
121
+ //
122
+ // Returns true if:
123
+ // 1. The permissions list is empty (backward compatibility mode)
124
+ // 2. The requiredPermission is found in the permissions list
125
+ func hasPermission (permissions []string , requiredPermission string ) bool {
126
+ if len (permissions ) == 0 {
127
+ return true
128
+ }
129
+
130
+ for _ , permission := range permissions {
131
+ if permission == requiredPermission {
132
+ return true
133
+ }
134
+ }
135
+
136
+ return false
137
+ }
138
+
139
+ // requiredPermission extracts the resource type from the path and returns the required permission.
140
+ func requiredPermission (path string ) (string , error ) {
112
141
matches := oapiResourceRegexp .FindStringSubmatch (path )
113
142
if len (matches ) != 2 {
114
- return ""
143
+ return "" , fmt . Errorf ( "failed to extract resource type from path: %s" , path )
115
144
}
116
145
117
146
resource := strings .ToLower (matches [1 ])
118
147
switch resource {
119
148
case "jobs" :
120
- return types .PersonalAccessTokenScopeJob
149
+ return types .PersonalAccessTokenScopeJob , nil
121
150
case "clusters" :
122
- return types .PersonalAccessTokenScopeCluster
123
- case "preheats" :
124
- return types .PersonalAccessTokenScopePreheat
151
+ return types .PersonalAccessTokenScopeCluster , nil
125
152
default :
126
- return resource
153
+ return "" , fmt . Errorf ( "unsupported resource type: %s" , resource )
127
154
}
128
155
}
0 commit comments