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 }