package ociauth import ( "math/bits" "strings" "cuelabs.dev/go/oci/ociregistry/internal/exp/slices" ) // knownAction represents an action that we know about // and use a more efficient internal representation for. type knownAction byte const ( unknownAction knownAction = iota // Note: ordered by lexical string representation. pullAction pushAction numActions ) const ( // Known resource types. TypeRepository = "repository" TypeRegistry = "registry" // Known action types. ActionPull = "pull" ActionPush = "push" ) func (a knownAction) String() string { switch a { case pullAction: return ActionPull case pushAction: return ActionPush default: return "unknown" } } // CatalogScope defines the resource scope used to allow // listing all the items in a registry. var CatalogScope = ResourceScope{ ResourceType: TypeRegistry, Resource: "catalog", Action: "*", } // ResourceScope defines a component of an authorization scope // associated with a single resource and action only. // See [Scope] for a way of combining multiple ResourceScopes // into a single value. type ResourceScope struct { // ResourceType holds the type of resource the scope refers to. // Known values for this include TypeRegistry and TypeRepository. // When a scope does not conform to the standard resourceType:resource:actions // syntax, ResourceType will hold the entire scope. ResourceType string // Resource names the resource the scope pertains to. // For resource type TypeRepository, this will be the name of the repository. Resource string // Action names an action that can be performed on the resource. // This is usually ActionPush or ActionPull. Action string } func (rs1 ResourceScope) Equal(rs2 ResourceScope) bool { return rs1.Compare(rs2) == 0 } // Compare returns -1, 0 or 1 depending on whether // rs1 compares less than, equal, or greater than, rs2. // // In most to least precedence, the fields are compared in the order // ResourceType, Resource, Action. func (rs1 ResourceScope) Compare(rs2 ResourceScope) int { if c := strings.Compare(rs1.ResourceType, rs2.ResourceType); c != 0 { return c } if c := strings.Compare(rs1.Resource, rs2.Resource); c != 0 { return c } return strings.Compare(rs1.Action, rs2.Action) } func (rs ResourceScope) isKnown() bool { switch rs.ResourceType { case TypeRepository: return parseKnownAction(rs.Action) != unknownAction case TypeRegistry: return rs == CatalogScope } return false } // Scope holds a set of [ResourceScope] values. The zero value // represents the empty set. type Scope struct { // original holds the original string from which // this Scope was parsed. This maintains the string // representation unchanged as far as possible. original string // unlimited holds whether this scope is considered to include all // other scopes. unlimited bool // repositories holds all the repositories that the scope // refers to. An empty repository name implies a CatalogScope // entry. The elements of this are maintained in sorted order. repositories []string // actions holds an element for each element in repositories // defining the set of allowed actions for that repository // as a bitmask of 1< 0 && s.repositories[i-1] == rs.Resource { s.actions[i-1] |= actionMask } else { s.repositories = append(s.repositories, rs.Resource) s.actions = append(s.actions, actionMask) } } slices.SortFunc(s.others, ResourceScope.Compare) s.others = slices.Compact(s.others) return s } // Len returns the number of ResourceScopes in the scope set. // It panics if the scope is unlimited. func (s Scope) Len() int { if s.IsUnlimited() { panic("Len called on unlimited scope") } n := len(s.others) for _, b := range s.actions { n += bits.OnesCount8(b) } return n } // UnlimitedScope returns a scope that contains all other // scopes. This is not representable in the docker scope syntax, // but it's useful to represent the scope of tokens that can // be used for arbitrary access. func UnlimitedScope() Scope { return Scope{ unlimited: true, } } // IsUnlimited reports whether s is unlimited in scope. func (s Scope) IsUnlimited() bool { return s.unlimited } // IsEmpty reports whether the scope holds the empty set. func (s Scope) IsEmpty() bool { return len(s.repositories) == 0 && len(s.others) == 0 && !s.unlimited } // Iter returns an iterator over all the individual scopes that are // part of s. The items will be produced according to [Scope.Compare] // ordering. // // The unlimited scope does not yield any scopes. func (s Scope) Iter() func(yield func(ResourceScope) bool) { return func(yield0 func(ResourceScope) bool) { if s.unlimited { return } others := s.others yield := func(scope ResourceScope) bool { // Yield any scopes from others that are ready to // be produced, thus preserving ordering of all // values in the iterator. for len(others) > 0 && others[0].Compare(scope) < 0 { if !yield0(others[0]) { return false } others = others[1:] } return yield0(scope) } for i, repo := range s.repositories { if repo == "" { if !yield(CatalogScope) { return } continue } acts := s.actions[i] for k := knownAction(0); k < numActions; k++ { if acts&(1< 0 { buf.WriteByte(' ') } buf.WriteString(s.ResourceType) if s.Resource != "" || s.Action != "" { buf.WriteByte(':') buf.WriteString(s.Resource) buf.WriteByte(':') buf.WriteString(s.Action) } return true }) return buf.String() } func parseKnownAction(s string) knownAction { switch s { case ActionPull: return pullAction case ActionPush: return pushAction default: return unknownAction } }