...

Source file src/cuelabs.dev/go/oci/ociregistry/ociauth/scope.go

Documentation: cuelabs.dev/go/oci/ociregistry/ociauth

     1  package ociauth
     2  
     3  import (
     4  	"math/bits"
     5  	"strings"
     6  
     7  	"cuelabs.dev/go/oci/ociregistry/internal/exp/slices"
     8  )
     9  
    10  // knownAction represents an action that we know about
    11  // and use a more efficient internal representation for.
    12  type knownAction byte
    13  
    14  const (
    15  	unknownAction knownAction = iota
    16  	// Note: ordered by lexical string representation.
    17  	pullAction
    18  	pushAction
    19  	numActions
    20  )
    21  
    22  const (
    23  	// Known resource types.
    24  	TypeRepository = "repository"
    25  	TypeRegistry   = "registry"
    26  
    27  	// Known action types.
    28  	ActionPull = "pull"
    29  	ActionPush = "push"
    30  )
    31  
    32  func (a knownAction) String() string {
    33  	switch a {
    34  	case pullAction:
    35  		return ActionPull
    36  	case pushAction:
    37  		return ActionPush
    38  	default:
    39  		return "unknown"
    40  	}
    41  }
    42  
    43  // CatalogScope defines the resource scope used to allow
    44  // listing all the items in a registry.
    45  var CatalogScope = ResourceScope{
    46  	ResourceType: TypeRegistry,
    47  	Resource:     "catalog",
    48  	Action:       "*",
    49  }
    50  
    51  // ResourceScope defines a component of an authorization scope
    52  // associated with a single resource and action only.
    53  // See [Scope] for a way of combining multiple ResourceScopes
    54  // into a single value.
    55  type ResourceScope struct {
    56  	// ResourceType holds the type of resource the scope refers to.
    57  	// Known values for this include TypeRegistry and TypeRepository.
    58  	// When a scope does not conform to the standard resourceType:resource:actions
    59  	// syntax, ResourceType will hold the entire scope.
    60  	ResourceType string
    61  
    62  	// Resource names the resource the scope pertains to.
    63  	// For resource type TypeRepository, this will be the name of the repository.
    64  	Resource string
    65  
    66  	// Action names an action that can be performed on the resource.
    67  	// This is usually ActionPush or ActionPull.
    68  	Action string
    69  }
    70  
    71  func (rs1 ResourceScope) Equal(rs2 ResourceScope) bool {
    72  	return rs1.Compare(rs2) == 0
    73  }
    74  
    75  // Compare returns -1, 0 or 1 depending on whether
    76  // rs1 compares less than, equal, or greater than, rs2.
    77  //
    78  // In most to least precedence, the fields are compared in the order
    79  // ResourceType, Resource, Action.
    80  func (rs1 ResourceScope) Compare(rs2 ResourceScope) int {
    81  	if c := strings.Compare(rs1.ResourceType, rs2.ResourceType); c != 0 {
    82  		return c
    83  	}
    84  	if c := strings.Compare(rs1.Resource, rs2.Resource); c != 0 {
    85  		return c
    86  	}
    87  	return strings.Compare(rs1.Action, rs2.Action)
    88  }
    89  
    90  func (rs ResourceScope) isKnown() bool {
    91  	switch rs.ResourceType {
    92  	case TypeRepository:
    93  		return parseKnownAction(rs.Action) != unknownAction
    94  	case TypeRegistry:
    95  		return rs == CatalogScope
    96  	}
    97  	return false
    98  }
    99  
   100  // Scope holds a set of [ResourceScope] values. The zero value
   101  // represents the empty set.
   102  type Scope struct {
   103  	// original holds the original string from which
   104  	// this Scope was parsed. This maintains the string
   105  	// representation unchanged as far as possible.
   106  	original string
   107  
   108  	// unlimited holds whether this scope is considered to include all
   109  	// other scopes.
   110  	unlimited bool
   111  
   112  	// repositories holds all the repositories that the scope
   113  	// refers to. An empty repository name implies a CatalogScope
   114  	// entry. The elements of this are maintained in sorted order.
   115  	repositories []string
   116  
   117  	// actions holds an element for each element in repositories
   118  	// defining the set of allowed actions for that repository
   119  	// as a bitmask of 1<<knownAction bytes.
   120  	// For CatalogScope, this is 1<<pullAction so that
   121  	// the bit count reflects the number of resource scopes.
   122  	actions []byte
   123  
   124  	// others holds actions that don't fit into
   125  	// the above categories. These may or may not be repository-scoped:
   126  	// we just store them here verbatim.
   127  	others []ResourceScope
   128  }
   129  
   130  // ParseScope parses a scope as defined in the [Docker distribution spec].
   131  //
   132  // For scopes that don't fit that syntax, it returns a Scope with
   133  // the ResourceType field set to the whole string.
   134  //
   135  // [Docker distribution spec]: https://distribution.github.io/distribution/spec/auth/scope/
   136  func ParseScope(s string) Scope {
   137  	fields := strings.Fields(s)
   138  	rscopes := make([]ResourceScope, 0, len(fields))
   139  	for _, f := range fields {
   140  		parts := strings.Split(f, ":")
   141  		if len(parts) != 3 {
   142  			rscopes = append(rscopes, ResourceScope{
   143  				ResourceType: f,
   144  			})
   145  			continue
   146  		}
   147  		for _, action := range strings.Split(parts[2], ",") {
   148  			rscopes = append(rscopes, ResourceScope{
   149  				ResourceType: parts[0],
   150  				Resource:     parts[1],
   151  				Action:       action,
   152  			})
   153  		}
   154  	}
   155  	scope := NewScope(rscopes...)
   156  	scope.original = s
   157  	return scope
   158  }
   159  
   160  // NewScope returns a Scope value that holds the set of everything in rss.
   161  func NewScope(rss ...ResourceScope) Scope {
   162  	// TODO it might well be worth special-casing the single element scope case.
   163  	slices.SortFunc(rss, ResourceScope.Compare)
   164  	rss = slices.Compact(rss)
   165  	var s Scope
   166  	for _, rs := range rss {
   167  		if !rs.isKnown() {
   168  			s.others = append(s.others, rs)
   169  			continue
   170  		}
   171  		if rs.ResourceType == TypeRegistry {
   172  			// CatalogScope
   173  			s.repositories = append(s.repositories, "")
   174  			s.actions = append(s.actions, 1<<pullAction)
   175  			continue
   176  		}
   177  		actionMask := byte(1 << parseKnownAction(rs.Action))
   178  		if i := len(s.repositories); i > 0 && s.repositories[i-1] == rs.Resource {
   179  			s.actions[i-1] |= actionMask
   180  		} else {
   181  			s.repositories = append(s.repositories, rs.Resource)
   182  			s.actions = append(s.actions, actionMask)
   183  		}
   184  	}
   185  	slices.SortFunc(s.others, ResourceScope.Compare)
   186  	s.others = slices.Compact(s.others)
   187  	return s
   188  }
   189  
   190  // Len returns the number of ResourceScopes in the scope set.
   191  // It panics if the scope is unlimited.
   192  func (s Scope) Len() int {
   193  	if s.IsUnlimited() {
   194  		panic("Len called on unlimited scope")
   195  	}
   196  	n := len(s.others)
   197  	for _, b := range s.actions {
   198  		n += bits.OnesCount8(b)
   199  	}
   200  	return n
   201  }
   202  
   203  // UnlimitedScope returns a scope that contains all other
   204  // scopes. This is not representable in the docker scope syntax,
   205  // but it's useful to represent the scope of tokens that can
   206  // be used for arbitrary access.
   207  func UnlimitedScope() Scope {
   208  	return Scope{
   209  		unlimited: true,
   210  	}
   211  }
   212  
   213  // IsUnlimited reports whether s is unlimited in scope.
   214  func (s Scope) IsUnlimited() bool {
   215  	return s.unlimited
   216  }
   217  
   218  // IsEmpty reports whether the scope holds the empty set.
   219  func (s Scope) IsEmpty() bool {
   220  	return len(s.repositories) == 0 &&
   221  		len(s.others) == 0 &&
   222  		!s.unlimited
   223  }
   224  
   225  // Iter returns an iterator over all the individual scopes that are
   226  // part of s. The items will be produced according to [Scope.Compare]
   227  // ordering.
   228  //
   229  // The unlimited scope does not yield any scopes.
   230  func (s Scope) Iter() func(yield func(ResourceScope) bool) {
   231  	return func(yield0 func(ResourceScope) bool) {
   232  		if s.unlimited {
   233  			return
   234  		}
   235  		others := s.others
   236  		yield := func(scope ResourceScope) bool {
   237  			// Yield any scopes from others that are ready to
   238  			// be produced, thus preserving ordering of all
   239  			// values in the iterator.
   240  			for len(others) > 0 && others[0].Compare(scope) < 0 {
   241  				if !yield0(others[0]) {
   242  					return false
   243  				}
   244  				others = others[1:]
   245  			}
   246  			return yield0(scope)
   247  		}
   248  		for i, repo := range s.repositories {
   249  			if repo == "" {
   250  				if !yield(CatalogScope) {
   251  					return
   252  				}
   253  				continue
   254  			}
   255  			acts := s.actions[i]
   256  			for k := knownAction(0); k < numActions; k++ {
   257  				if acts&(1<<k) == 0 {
   258  					continue
   259  				}
   260  				rscope := ResourceScope{
   261  					ResourceType: TypeRepository,
   262  					Resource:     repo,
   263  					Action:       k.String(),
   264  				}
   265  				if !yield(rscope) {
   266  					return
   267  				}
   268  			}
   269  		}
   270  		// Send any scopes in others that haven't already been sent.
   271  		for _, rscope := range others {
   272  			if !yield0(rscope) {
   273  				return
   274  			}
   275  		}
   276  	}
   277  }
   278  
   279  // Union returns a scope consisting of all the resource scopes from
   280  // both s1 and s2. If the result is the same as s1, its
   281  // string representation will also be the same as s1.
   282  func (s1 Scope) Union(s2 Scope) Scope {
   283  	if s1.IsUnlimited() || s2.IsUnlimited() {
   284  		return UnlimitedScope()
   285  	}
   286  	// Cheap test that we can return the original unchanged.
   287  	if s2.IsEmpty() || s1.Equal(s2) {
   288  		return s1
   289  	}
   290  	r := Scope{
   291  		repositories: make([]string, 0, len(s1.repositories)+len(s2.repositories)),
   292  		actions:      make([]byte, 0, len(s1.repositories)+len(s2.repositories)),
   293  		others:       make([]ResourceScope, 0, len(s1.others)+len(s2.others)),
   294  	}
   295  	i1, i2 := 0, 0
   296  	for i1 < len(s1.repositories) && i2 < len(s2.repositories) {
   297  		repo1, repo2 := s1.repositories[i1], s2.repositories[i2]
   298  
   299  		switch strings.Compare(repo1, repo2) {
   300  		case 0:
   301  			r.repositories = append(r.repositories, repo1)
   302  			r.actions = append(r.actions, s1.actions[i1]|s2.actions[i2])
   303  			i1++
   304  			i2++
   305  		case -1:
   306  			r.repositories = append(r.repositories, s1.repositories[i1])
   307  			r.actions = append(r.actions, s1.actions[i1])
   308  			i1++
   309  		case 1:
   310  			r.repositories = append(r.repositories, s2.repositories[i2])
   311  			r.actions = append(r.actions, s2.actions[i2])
   312  			i2++
   313  		default:
   314  			panic("unreachable")
   315  		}
   316  	}
   317  	switch {
   318  	case i1 < len(s1.repositories):
   319  		r.repositories = append(r.repositories, s1.repositories[i1:]...)
   320  		r.actions = append(r.actions, s1.actions[i1:]...)
   321  	case i2 < len(s2.repositories):
   322  		r.repositories = append(r.repositories, s2.repositories[i2:]...)
   323  		r.actions = append(r.actions, s2.actions[i2:]...)
   324  	}
   325  	i1, i2 = 0, 0
   326  	for i1 < len(s1.others) && i2 < len(s2.others) {
   327  		a1, a2 := s1.others[i1], s2.others[i2]
   328  		switch a1.Compare(a2) {
   329  		case 0:
   330  			r.others = append(r.others, a1)
   331  			i1++
   332  			i2++
   333  		case -1:
   334  			r.others = append(r.others, a1)
   335  			i1++
   336  		case 1:
   337  			r.others = append(r.others, a2)
   338  			i2++
   339  		}
   340  	}
   341  	switch {
   342  	case i1 < len(s1.others):
   343  		r.others = append(r.others, s1.others[i1:]...)
   344  	case i2 < len(s2.others):
   345  		r.others = append(r.others, s2.others[i2:]...)
   346  	}
   347  	if r.Equal(s1) {
   348  		// Maintain the string representation.
   349  		return s1
   350  	}
   351  	return r
   352  }
   353  
   354  func (s Scope) Holds(r ResourceScope) bool {
   355  	if s.IsUnlimited() {
   356  		return true
   357  	}
   358  	if r == CatalogScope {
   359  		_, ok := slices.BinarySearch(s.repositories, "")
   360  		return ok
   361  	}
   362  	if r.ResourceType == TypeRepository {
   363  		if action := parseKnownAction(r.Action); action != unknownAction {
   364  			// It's a known action on a repository.
   365  			i, ok := slices.BinarySearch(s.repositories, r.Resource)
   366  			if !ok {
   367  				return false
   368  			}
   369  			return s.actions[i]&(1<<action) != 0
   370  		}
   371  	}
   372  	// We're either searching for an unknown resource type or
   373  	// an unknown action on a repository. In any case,
   374  	// we'll find the result in s.other.
   375  	_, ok := slices.BinarySearchFunc(s.others, r, ResourceScope.Compare)
   376  	return ok
   377  }
   378  
   379  // Contains reports whether s1 is a (non-strict) superset of s2.
   380  func (s1 Scope) Contains(s2 Scope) bool {
   381  	if s1.IsUnlimited() {
   382  		return true
   383  	}
   384  	if s2.IsUnlimited() {
   385  		return false
   386  	}
   387  	i1 := 0
   388  outer1:
   389  	for i2, repo2 := range s2.repositories {
   390  		for i1 < len(s1.repositories) {
   391  			switch repo1 := s1.repositories[i1]; strings.Compare(repo1, repo2) {
   392  			case 1:
   393  				// repo2 definitely doesn't exist in s1.
   394  				return false
   395  			case 0:
   396  				if (s1.actions[i1] & s2.actions[i2]) != s2.actions[i2] {
   397  					// s2's actions for this repo aren't in s1.
   398  					return false
   399  				}
   400  				i1++
   401  				continue outer1
   402  			case -1:
   403  				i1++
   404  				// continue looking through s1 for repo2.
   405  			}
   406  		}
   407  		// We ran out of repositories in s1 to look for.
   408  		return false
   409  	}
   410  	i1 = 0
   411  outer2:
   412  	for _, sc2 := range s2.others {
   413  		for i1 < len(s1.others) {
   414  			sc1 := s1.others[i1]
   415  			switch sc1.Compare(sc2) {
   416  			case 1:
   417  				return false
   418  			case 0:
   419  				i1++
   420  				continue outer2
   421  			case -1:
   422  				i1++
   423  			}
   424  		}
   425  		return false
   426  	}
   427  	return true
   428  }
   429  
   430  func (s1 Scope) Equal(s2 Scope) bool {
   431  	return s1.IsUnlimited() == s2.IsUnlimited() &&
   432  		slices.Equal(s1.repositories, s2.repositories) &&
   433  		slices.Equal(s1.actions, s2.actions) &&
   434  		slices.Equal(s1.others, s2.others)
   435  }
   436  
   437  // Canonical returns s with the same contents
   438  // but with its string form made canonical (the
   439  // default is to mirror exactly the string that it was
   440  // created with).
   441  func (s Scope) Canonical() Scope {
   442  	s.original = ""
   443  	return s
   444  }
   445  
   446  // String returns the string representation of the scope, as suitable
   447  // for passing to the token refresh "scopes" attribute.
   448  func (s Scope) String() string {
   449  	if s.IsUnlimited() {
   450  		// There's no official representation of this, but
   451  		// we shouldn't be passing an unlimited scope
   452  		// as a scopes attribute anyway.
   453  		return "*"
   454  	}
   455  	if s.original != "" || s.IsEmpty() {
   456  		return s.original
   457  	}
   458  	var buf strings.Builder
   459  	var prev ResourceScope
   460  	// TODO use range when we can use range-over-func.
   461  	s.Iter()(func(s ResourceScope) bool {
   462  		prev0 := prev
   463  		prev = s
   464  		if s.ResourceType == TypeRepository && prev0.ResourceType == TypeRepository && s.Resource == prev0.Resource {
   465  			buf.WriteByte(',')
   466  			buf.WriteString(s.Action)
   467  			return true
   468  		}
   469  		if buf.Len() > 0 {
   470  			buf.WriteByte(' ')
   471  		}
   472  		buf.WriteString(s.ResourceType)
   473  		if s.Resource != "" || s.Action != "" {
   474  			buf.WriteByte(':')
   475  			buf.WriteString(s.Resource)
   476  			buf.WriteByte(':')
   477  			buf.WriteString(s.Action)
   478  		}
   479  		return true
   480  	})
   481  	return buf.String()
   482  }
   483  
   484  func parseKnownAction(s string) knownAction {
   485  	switch s {
   486  	case ActionPull:
   487  		return pullAction
   488  	case ActionPush:
   489  		return pushAction
   490  	default:
   491  		return unknownAction
   492  	}
   493  }
   494  

View as plain text