github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/remote/access/access.go (about)

     1  package access
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  
     8  	golog "github.com/ipfs/go-log"
     9  	"github.com/qri-io/qri/dsref"
    10  	"github.com/qri-io/qri/profile"
    11  )
    12  
    13  // special tokens in access grammer
    14  const (
    15  	matchAll     = "*"
    16  	matchSubject = "_subject"
    17  )
    18  
    19  var (
    20  	// ErrAccessDenied is returned by policy enforce
    21  	ErrAccessDenied = fmt.Errorf("access denied")
    22  	log             = golog.Logger("access")
    23  	// DefaultAccessControlPolicyFilename is the file name for the policy
    24  	// expected file is format yaml
    25  	DefaultAccessControlPolicyFilename = "access_control_policy.yaml"
    26  )
    27  
    28  // Effect is the set of outcomes a rule can have
    29  type Effect string
    30  
    31  const (
    32  	// EffectAllow describes a rule that adds permissions
    33  	EffectAllow = Effect("allow")
    34  	// EffectDeny describes a rule that removes permissions
    35  	EffectDeny = Effect("deny")
    36  )
    37  
    38  // Policy is a set of rules
    39  type Policy []Rule
    40  
    41  // Rule is a permissions statement. It determines who (subject) can/can't
    42  // (effect) do something (actions) to things (resources)
    43  type Rule struct {
    44  	Title     string    // human-legible title for the rule, informative only
    45  	Subject   string    // User this rule is about
    46  	Resources Resources // Thing being accessed. eg: a dataset,
    47  	Actions   Actions   // Thing user can do
    48  	Effect    Effect    // "allow" or "deny"
    49  }
    50  
    51  type rule Rule
    52  
    53  // UnmarshalJSON unmarshals the slice of bytes into a Rule
    54  func (r *Rule) UnmarshalJSON(d []byte) error {
    55  	_rule := rule{}
    56  	if err := json.Unmarshal(d, &_rule); err != nil {
    57  		return err
    58  	}
    59  
    60  	rule := Rule(_rule)
    61  	if err := rule.Validate(); err != nil {
    62  		return err
    63  	}
    64  
    65  	*r = rule
    66  	return nil
    67  }
    68  
    69  // Validate returns a descriptive error if the rule is not well-formed
    70  func (r *Rule) Validate() error {
    71  	if r.Subject == "" {
    72  		return fmt.Errorf("rule.Subject is required")
    73  	}
    74  	if r.Effect != EffectAllow && r.Effect != EffectDeny {
    75  		return fmt.Errorf(`rule.Effect must be one of ("allow"|"deny")`)
    76  	}
    77  	if len(r.Resources) == 0 {
    78  		return fmt.Errorf("rule.Resources field is required")
    79  	}
    80  	if len(r.Actions) == 0 {
    81  		return fmt.Errorf("rule.Actions field is required")
    82  	}
    83  	return nil
    84  }
    85  
    86  // Enforce evaluates a request against the policy, returning either nil or
    87  // ErrAccessDenied
    88  func (pol Policy) Enforce(subject *profile.Profile, resource, action string) error {
    89  	log.Debugf("policy.Enforce username=%q resource=%q action=%q", subject.Peername, resource, action)
    90  	rsc, err := ParseResource(resource)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	act, err := ParseAction(action)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	for _, rule := range pol {
   101  		log.Debugf("rule=%q effect=%q subject=%t resources=%t actions=%t", rule.Title, rule.Effect,
   102  			(rule.Subject == subject.ID.Encode() || rule.Subject == matchAll),
   103  			rule.Resources.Contains(rsc, subject.Peername),
   104  			rule.Actions.Contains(act),
   105  		)
   106  
   107  		if rule.Effect == EffectAllow &&
   108  			(rule.Subject == subject.ID.Encode() || rule.Subject == matchAll) &&
   109  			rule.Resources.Contains(rsc, subject.Peername) &&
   110  			rule.Actions.Contains(act) {
   111  			log.Debugf("matched rule title=%q", rule.Title)
   112  			return nil
   113  		}
   114  	}
   115  	return ErrAccessDenied
   116  }
   117  
   118  // Resources is a collection of resoureces
   119  type Resources []Resource
   120  
   121  // Contains iterates all Resources in the slice, returns true for the first
   122  // resource that contains the given resource
   123  func (rs Resources) Contains(b Resource, subjectUsername string) bool {
   124  	for _, r := range rs {
   125  		if r.Contains(b, subjectUsername) {
   126  			return true
   127  		}
   128  	}
   129  	return false
   130  }
   131  
   132  // Resource is a stateful thing in qri
   133  type Resource []string
   134  
   135  // MustParseResource wraps ParseResource, panics on error. Useful for tests
   136  func MustParseResource(str string) Resource {
   137  	rsc, err := ParseResource(str)
   138  	if err != nil {
   139  		panic(err)
   140  	}
   141  	return rsc
   142  }
   143  
   144  // ParseResource constructs a resource from a string
   145  func ParseResource(str string) (Resource, error) {
   146  	if str == "" {
   147  		return nil, fmt.Errorf("resource string cannot be empty")
   148  	}
   149  
   150  	rsc := strings.Split(str, ":")
   151  
   152  	foundStar := false
   153  	for _, name := range rsc {
   154  		if name == "*" {
   155  			if foundStar {
   156  				return nil, fmt.Errorf(`invalid resource string %q. '*' character cannot occur twice`, str)
   157  			}
   158  			foundStar = true
   159  		} else if foundStar {
   160  			return nil, fmt.Errorf(`invalid resource string %q. '*' must come last`, str)
   161  		}
   162  	}
   163  
   164  	return rsc, nil
   165  }
   166  
   167  // MarshalJSON marshals the resource into a string separated by ":"
   168  func (r Resource) MarshalJSON() ([]byte, error) {
   169  	return []byte(strings.Join(r, ":")), nil
   170  }
   171  
   172  // UnmarshalJSON unmarshals a slice of bytes into a Resource
   173  func (r *Resource) UnmarshalJSON(data []byte) error {
   174  	var str string
   175  	if err := json.Unmarshal(data, &str); err != nil {
   176  		return err
   177  	}
   178  
   179  	rsc, err := ParseResource(str)
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	*r = rsc
   185  	return nil
   186  }
   187  
   188  // Contains determins if the subject is referenced in the resource
   189  // returns true if the rule's resource contains the `matchAll` symbol
   190  // and returns true if the rule's resource contains the `matchSubject`
   191  // and the subjectUsername is in the given resource (allows us to create rules
   192  // that say, "only allow subjects to do this action, if the resource matches
   193  // the subject's name"
   194  func (r Resource) Contains(b Resource, subjectUsername string) bool {
   195  	if len(r) > len(b) {
   196  		return false
   197  	}
   198  
   199  	for i, aName := range r {
   200  		if aName == matchAll {
   201  			return true
   202  		}
   203  		if aName == matchSubject && b[i] == subjectUsername {
   204  			continue
   205  		}
   206  		if b[i] != aName {
   207  			return false
   208  		}
   209  	}
   210  
   211  	return len(r) == len(b)
   212  }
   213  
   214  // ResourceStrFromRef takes a dsref.Ref and returns a string that can be parsed
   215  // as a resource
   216  func ResourceStrFromRef(ref dsref.Ref) string {
   217  	return strings.Join([]string{"dataset", ref.Username, ref.Name}, ":")
   218  }
   219  
   220  // Actions is a slice of Action
   221  type Actions []Action
   222  
   223  // Contains determines if the given action is contained by the Actions
   224  func (as Actions) Contains(b Action) bool {
   225  	for _, a := range as {
   226  		if a.Contains(b) {
   227  			return true
   228  		}
   229  	}
   230  	return false
   231  }
   232  
   233  // Action is a description of the action the Subject is attempting to take on
   234  // the Resource
   235  type Action []string
   236  
   237  // MustParseAction parses a string into an Action. It panics if the string
   238  // cannot be parsed correctly
   239  func MustParseAction(str string) Action {
   240  	rsc, err := ParseAction(str)
   241  	if err != nil {
   242  		panic(err)
   243  	}
   244  	return rsc
   245  }
   246  
   247  // ParseAction parses a string into an Action
   248  func ParseAction(str string) (Action, error) {
   249  	if str == "" {
   250  		return nil, fmt.Errorf("action string cannot be empty")
   251  	}
   252  
   253  	rsc := strings.Split(str, ":")
   254  
   255  	foundStar := false
   256  	for _, name := range rsc {
   257  		if name == matchAll {
   258  			if foundStar {
   259  				return nil, fmt.Errorf(`invalid action string %q. '*' character cannot occur twice`, str)
   260  			}
   261  			foundStar = true
   262  		} else if foundStar {
   263  			return nil, fmt.Errorf(`invalid action string %q. '*' must come last`, str)
   264  		}
   265  	}
   266  
   267  	return rsc, nil
   268  }
   269  
   270  // MarshalJSON marshals the Action into a string separated by ":"
   271  func (a Action) MarshalJSON() ([]byte, error) {
   272  	return []byte(strings.Join(a, ":")), nil
   273  }
   274  
   275  // UnmarshalJSON unmarshals the given slice of bytes into an Action
   276  func (a *Action) UnmarshalJSON(data []byte) error {
   277  	var str string
   278  	if err := json.Unmarshal(data, &str); err != nil {
   279  		return err
   280  	}
   281  
   282  	act, err := ParseAction(str)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	*a = act
   288  	return nil
   289  }
   290  
   291  // Contains determines if the given action is described in the rule's Action
   292  // it returns true if the action matches using the glob `*` pattern
   293  func (a Action) Contains(b Action) bool {
   294  	if len(a) > len(b) {
   295  		return false
   296  	}
   297  
   298  	for i, aName := range a {
   299  		if aName == matchAll {
   300  			return true
   301  		}
   302  		if b[i] != aName {
   303  			return false
   304  		}
   305  	}
   306  
   307  	return len(a) == len(b)
   308  }