github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/permission/rule.go (about)

     1  package permission
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/cozy/cozy-stack/pkg/consts"
    10  )
    11  
    12  const ruleSep = " "
    13  const valueSep = ","
    14  const partSep = ":"
    15  
    16  // RefSep is used to separate doctype and value for a referenced selector
    17  const RefSep = "/"
    18  
    19  var ErrImpossibleMerge = errors.New("cannot merge these rules")
    20  
    21  // Rule represent a single permissions rule, ie a Verb and a type
    22  type Rule struct {
    23  	// Type is the JSON-API type or couchdb Doctype
    24  	Type string `json:"type"`
    25  
    26  	// Title is a human readable (i18n key) header for this rule
    27  	Title string `json:"-"`
    28  
    29  	// Description is a human readable (i18n key) purpose of this rule
    30  	Description string `json:"description,omitempty"`
    31  
    32  	// Verbs is a subset of http methods.
    33  	Verbs VerbSet `json:"verbs,omitempty"`
    34  
    35  	// Selector is the field which must be one of Values.
    36  	Selector string   `json:"selector,omitempty"`
    37  	Values   []string `json:"values,omitempty"`
    38  }
    39  
    40  // MarshalScopeString transform a Rule into a string of the shape
    41  // io.cozy.files:GET:io.cozy.files.music-dir
    42  func (r Rule) MarshalScopeString() (string, error) {
    43  	out := r.Type
    44  	hasVerbs := len(r.Verbs) != 0
    45  	hasValues := len(r.Values) != 0
    46  	hasSelector := r.Selector != ""
    47  
    48  	if hasVerbs || hasValues || hasSelector {
    49  		out += partSep + r.Verbs.String()
    50  	}
    51  
    52  	if hasValues {
    53  		out += partSep + strings.Join(r.Values, valueSep)
    54  	}
    55  
    56  	if hasSelector {
    57  		out += partSep + r.Selector
    58  	}
    59  
    60  	return out, nil
    61  }
    62  
    63  // UnmarshalRuleString parse a scope formated rule
    64  func UnmarshalRuleString(in string) (Rule, error) {
    65  	var out Rule
    66  	parts := strings.Split(in, partSep)
    67  	switch len(parts) {
    68  	case 4:
    69  		out.Selector = parts[3]
    70  		fallthrough
    71  	case 3:
    72  		out.Values = strings.Split(parts[2], valueSep)
    73  		fallthrough
    74  	case 2:
    75  		out.Verbs = VerbSplit(parts[1])
    76  		fallthrough
    77  	case 1:
    78  		if CheckDoctypeName(parts[0], true) != nil {
    79  			return out, ErrBadScope
    80  		}
    81  		out.Type = parts[0]
    82  	default:
    83  		return out, ErrBadScope
    84  	}
    85  	return out, nil
    86  }
    87  
    88  // SomeValue returns true if any value statisfy the predicate
    89  func (r Rule) SomeValue(predicate func(v string) bool) bool {
    90  	for _, v := range r.Values {
    91  		if predicate(v) {
    92  			return true
    93  		}
    94  	}
    95  	return false
    96  }
    97  
    98  func contains(haystack []string, needle string) bool {
    99  	for _, v := range haystack {
   100  		if needle == v {
   101  			return true
   102  		}
   103  	}
   104  	return false
   105  }
   106  
   107  // ValuesMatch returns true if any value statisfy the predicate
   108  func (r Rule) ValuesMatch(o Fetcher) bool {
   109  	candidates := o.Fetch(r.Selector)
   110  	for _, v := range r.Values {
   111  		if contains(candidates, v) {
   112  			return true
   113  		}
   114  	}
   115  	return false
   116  }
   117  
   118  // ValuesContain returns true if all the values are in r.Values
   119  func (r Rule) ValuesContain(values ...string) bool {
   120  	for _, value := range values {
   121  		valueOK := false
   122  		for _, v := range r.Values {
   123  			if v == value {
   124  				valueOK = true
   125  			}
   126  		}
   127  		if !valueOK {
   128  			return false
   129  		}
   130  	}
   131  	return true
   132  }
   133  
   134  // ValuesChanged returns true if the value for the given selector has changed
   135  func (r Rule) ValuesChanged(old, current Fetcher) bool {
   136  	value := current.Fetch(r.Selector)
   137  	was := old.Fetch(r.Selector)
   138  	return !reflect.DeepEqual(value, was)
   139  }
   140  
   141  // TranslationKey returns a string that can be used as a key for translating a
   142  // description of this rule
   143  func (r Rule) TranslationKey() string {
   144  	switch r.Type {
   145  	case allDocTypes:
   146  		return "Permissions Maximal"
   147  	case consts.Settings:
   148  		if r.Verbs.ReadOnly() && len(r.Values) == 1 && r.Values[0] == consts.DiskUsageID {
   149  			return "Permissions disk usage"
   150  		}
   151  	case consts.Jobs, consts.Triggers:
   152  		if len(r.Values) == 1 && r.Selector == "worker" {
   153  			return "Permissions worker " + r.Values[0]
   154  		}
   155  	}
   156  	return "Permissions " + strings.TrimSuffix(r.Type, ".*")
   157  }
   158  
   159  // Merge merges the rule2 in rule1
   160  // Rule1 name & description are kept
   161  func (r Rule) Merge(r2 Rule) (*Rule, error) {
   162  	if r.Type != r2.Type {
   163  		return nil, fmt.Errorf("%w: type is different", ErrImpossibleMerge)
   164  	}
   165  
   166  	newRule := &r
   167  
   168  	// Verbs
   169  	for verb, content := range r2.Verbs {
   170  		if !newRule.Verbs.Contains(verb) {
   171  			newRule.Verbs[verb] = content
   172  		}
   173  	}
   174  
   175  	for _, value := range r2.Values {
   176  		if !newRule.ValuesContain(value) {
   177  			newRule.Values = append(newRule.Values, value)
   178  		}
   179  	}
   180  
   181  	return newRule, nil
   182  }