diff --git a/contrib/token-server/main.go b/contrib/token-server/main.go index 1cebc0e16..e9d6d64fa 100644 --- a/contrib/token-server/main.go +++ b/contrib/token-server/main.go @@ -18,6 +18,10 @@ import ( "github.com/gorilla/mux" ) +var ( + enforceRepoClass bool +) + func main() { var ( issuer = &TokenIssuer{} @@ -44,6 +48,8 @@ func main() { flag.StringVar(&cert, "tlscert", "", "Certificate file for TLS") flag.StringVar(&certKey, "tlskey", "", "Certificate key for TLS") + flag.BoolVar(&enforceRepoClass, "enforce-class", false, "Enforce policy for single repository class") + flag.Parse() if debug { @@ -157,6 +163,8 @@ type tokenResponse struct { ExpiresIn int `json:"expires_in,omitempty"` } +var repositoryClassCache = map[string]string{} + func filterAccessList(ctx context.Context, scope string, requestedAccessList []auth.Access) []auth.Access { if !strings.HasSuffix(scope, "/") { scope = scope + "/" @@ -168,6 +176,16 @@ func filterAccessList(ctx context.Context, scope string, requestedAccessList []a context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name) continue } + if enforceRepoClass { + if class, ok := repositoryClassCache[access.Name]; ok { + if class != access.Class { + context.GetLogger(ctx).Debugf("Different repository class: %q, previously %q", access.Class, class) + continue + } + } else if strings.EqualFold(access.Action, "push") { + repositoryClassCache[access.Name] = access.Class + } + } } else if access.Type == "registry" { if access.Name != "catalog" { context.GetLogger(ctx).Debugf("Unknown registry resource: %s", access.Name) diff --git a/contrib/token-server/token.go b/contrib/token-server/token.go index e69fb9c19..b0c2abff7 100644 --- a/contrib/token-server/token.go +++ b/contrib/token-server/token.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "regexp" "strings" "time" @@ -32,12 +33,18 @@ func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Acc resourceType, resourceName, actions := parts[0], parts[1], parts[2] + resourceType, resourceClass := splitResourceClass(resourceType) + if resourceType == "" { + continue + } + // Actions should be a comma-separated list of actions. for _, action := range strings.Split(actions, ",") { requestedAccess := auth.Access{ Resource: auth.Resource{ - Type: resourceType, - Name: resourceName, + Type: resourceType, + Class: resourceClass, + Name: resourceName, }, Action: action, } @@ -55,6 +62,19 @@ func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Acc return requestedAccessList } +var typeRegexp = regexp.MustCompile(`^([a-z0-9]+)(\([a-z0-9]+\))?$`) + +func splitResourceClass(t string) (string, string) { + matches := typeRegexp.FindStringSubmatch(t) + if len(matches) < 2 { + return "", "" + } + if len(matches) == 2 || len(matches[2]) < 2 { + return matches[1], "" + } + return matches[1], matches[2][1 : len(matches[2])-1] +} + // ResolveScopeList converts a scope list from a token request's // `scope` parameter into a list of standard access objects. func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access { @@ -62,12 +82,19 @@ func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access { return ResolveScopeSpecifiers(ctx, scopes) } +func scopeString(a auth.Access) string { + if a.Class != "" { + return fmt.Sprintf("%s(%s):%s:%s", a.Type, a.Class, a.Name, a.Action) + } + return fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action) +} + // ToScopeList converts a list of access to a // scope list string func ToScopeList(access []auth.Access) string { var s []string for _, a := range access { - s = append(s, fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action)) + s = append(s, scopeString(a)) } return strings.Join(s, ",") } @@ -102,6 +129,7 @@ func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAcc accessEntries = append(accessEntries, &token.ResourceActions{ Type: resource.Type, + Class: resource.Class, Name: resource.Name, Actions: actions, })