Skip to content

Commit 92a1265

Browse files
committed
feat(manager): an empty permissions list grants all permissions
Signed-off-by: Gaius <gaius.qi@gmail.com>
1 parent 5004b43 commit 92a1265

File tree

6 files changed

+69
-32
lines changed

6 files changed

+69
-32
lines changed

manager/job/preheat.go

-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727
"io"
2828
"net/http"
2929
"net/url"
30-
"regexp"
3130
"time"
3231

3332
machineryv1tasks "github.com/RichardKnop/machinery/v1/tasks"
@@ -73,9 +72,6 @@ var defaultHTTPTransport = &http.Transport{
7372
IdleConnTimeout: 120 * time.Second,
7473
}
7574

76-
// accessURLPattern is the pattern of access url.
77-
var accessURLPattern, _ = regexp.Compile("^(.*)://(.*)/v2/(.*)/manifests/(.*)")
78-
7975
// Preheat is an interface for preheat job.
8076
type Preheat interface {
8177
// CreatePreheat creates a preheat job.

manager/job/types.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ package job
33
import (
44
"errors"
55
"fmt"
6+
"regexp"
67
)
78

9+
// accessURLRegexp is the regular expression for parsing access url.
10+
var accessURLRegexp, _ = regexp.Compile("^(.*)://(.*)/v2/(.*)/manifests/(.*)")
11+
812
// preheatImage is image information for preheat.
913
type preheatImage struct {
1014
protocol string
@@ -23,7 +27,7 @@ func (p *preheatImage) blobsURL(digest string) string {
2327

2428
// parseManifestURL parses manifest url.
2529
func parseManifestURL(url string) (*preheatImage, error) {
26-
r := accessURLPattern.FindStringSubmatch(url)
30+
r := accessURLRegexp.FindStringSubmatch(url)
2731
if len(r) != 5 {
2832
return nil, errors.New("parse access url failed")
2933
}

manager/middlewares/personal_access_token.go

+50-23
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import (
3333
)
3434

3535
var (
36+
// oapiResourceRegexp is a regular expression to extract the resource type from the path.
37+
// Example: /oapi/v1/jobs/1 -> jobs.
3638
oapiResourceRegexp = regexp.MustCompile(`^/oapi/v[0-9]+/([-_a-zA-Z]*)[/.*]*`)
3739
)
3840

@@ -45,6 +47,7 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
4547
c.JSON(http.StatusUnauthorized, ErrorResponse{
4648
Message: http.StatusText(http.StatusUnauthorized),
4749
})
50+
4851
c.Abort()
4952
return
5053
}
@@ -53,51 +56,56 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
5356
personalAccessToken := tokenFields[1]
5457
var token models.PersonalAccessToken
5558
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)
5760
c.JSON(http.StatusUnauthorized, ErrorResponse{
5861
Message: http.StatusText(http.StatusUnauthorized),
5962
})
63+
6064
c.Abort()
6165
return
6266
}
6367

6468
// Check if the token is active.
6569
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)
6771
c.JSON(http.StatusForbidden, ErrorResponse{
6872
Message: "Token is inactive",
6973
})
74+
7075
c.Abort()
7176
return
7277
}
7378

7479
// Check if the token has expired.
7580
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",
7782
c.Request.URL.Path, token.Name, token.UserID, token.ExpiredAt)
7883
c.JSON(http.StatusForbidden, ErrorResponse{
7984
Message: "Token has expired",
8085
})
86+
8187
c.Abort()
8288
return
8389
}
8490

8591
// 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
93101
}
94102

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)
98105
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),
100107
})
108+
101109
c.Abort()
102110
return
103111
}
@@ -106,23 +114,42 @@ func PersonalAccessToken(gdb *gorm.DB) gin.HandlerFunc {
106114
}
107115
}
108116

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) {
112141
matches := oapiResourceRegexp.FindStringSubmatch(path)
113142
if len(matches) != 2 {
114-
return ""
143+
return "", fmt.Errorf("failed to extract resource type from path: %s", path)
115144
}
116145

117146
resource := strings.ToLower(matches[1])
118147
switch resource {
119148
case "jobs":
120-
return types.PersonalAccessTokenScopeJob
149+
return types.PersonalAccessTokenScopeJob, nil
121150
case "clusters":
122-
return types.PersonalAccessTokenScopeCluster
123-
case "preheats":
124-
return types.PersonalAccessTokenScopePreheat
151+
return types.PersonalAccessTokenScopeCluster, nil
125152
default:
126-
return resource
153+
return "", fmt.Errorf("unsupported resource type: %s", resource)
127154
}
128155
}

manager/service/personal_access_token.go

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import (
2727
)
2828

2929
func (s *service) CreatePersonalAccessToken(ctx context.Context, json types.CreatePersonalAccessTokenRequest) (*models.PersonalAccessToken, error) {
30+
if len(json.Scopes) == 0 {
31+
json.Scopes = types.DefaultPersonalAccessTokenScopes
32+
}
33+
3034
personalAccessToken := models.PersonalAccessToken{
3135
Name: json.Name,
3236
BIO: json.BIO,
@@ -58,6 +62,10 @@ func (s *service) DestroyPersonalAccessToken(ctx context.Context, id uint) error
5862
}
5963

6064
func (s *service) UpdatePersonalAccessToken(ctx context.Context, id uint, json types.UpdatePersonalAccessTokenRequest) (*models.PersonalAccessToken, error) {
65+
if len(json.Scopes) == 0 {
66+
json.Scopes = types.DefaultPersonalAccessTokenScopes
67+
}
68+
6169
personalAccessToken := models.PersonalAccessToken{}
6270
if err := s.db.WithContext(ctx).Preload("User").First(&personalAccessToken, id).Updates(models.PersonalAccessToken{
6371
BIO: json.BIO,

manager/types/persional_access_token.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@ package types
1919
import "time"
2020

2121
const (
22-
// PersonalAccessTokenScopePreheat represents the personal access token whose scope is preheat.
23-
PersonalAccessTokenScopePreheat = "preheat"
24-
2522
// PersonalAccessTokenScopeJob represents the personal access token whose scope is job.
2623
PersonalAccessTokenScopeJob = "job"
2724

2825
// PersonalAccessTokenScopeCluster represents the personal access token whose scope is cluster.
2926
PersonalAccessTokenScopeCluster = "cluster"
3027
)
3128

29+
var (
30+
// DefaultPersonalAccessTokenScopes represents the default scopes of personal access token.
31+
DefaultPersonalAccessTokenScopes = []string{PersonalAccessTokenScopeJob, PersonalAccessTokenScopeCluster}
32+
)
33+
3234
type CreatePersonalAccessTokenRequest struct {
3335
Name string `json:"name" binding:"required"`
3436
BIO string `json:"bio" binding:"omitempty"`

0 commit comments

Comments
 (0)