github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/rule.go (about) 1 package sharing 2 3 import ( 4 "strings" 5 6 "github.com/cozy/cozy-stack/model/permission" 7 "github.com/cozy/cozy-stack/model/vfs" 8 "github.com/cozy/cozy-stack/pkg/consts" 9 "github.com/cozy/cozy-stack/pkg/couchdb" 10 ) 11 12 const ( 13 // ActionRuleNone is used when an add/update/remove should not be 14 // replicated to the other cozys 15 ActionRuleNone = "none" 16 // ActionRulePush is used when an add/update/remove should be replicated 17 // only if it happened on the owner's cozy 18 ActionRulePush = "push" 19 // ActionRuleSync is used when an add/update/remove should be always replicated 20 ActionRuleSync = "sync" 21 // ActionRuleRevoke is used when a remove should revoke the sharing 22 ActionRuleRevoke = "revoke" 23 ) 24 25 // Rule describes how the sharing behave when a document matching the rule is 26 // added, updated or deleted. 27 type Rule struct { 28 Title string `json:"title"` 29 DocType string `json:"doctype"` 30 Mime string `json:"mime,omitempty"` 31 Selector string `json:"selector,omitempty"` 32 Values []string `json:"values"` 33 Local bool `json:"local,omitempty"` 34 Add string `json:"add"` 35 Update string `json:"update"` 36 Remove string `json:"remove"` 37 } 38 39 // FilesByID returns true if the rule is for the files by doctype and the 40 // selector is an id (not a referenced_by). With such a rule, the identifiers 41 // must be xored before being sent to another cozy instance. 42 func (r Rule) FilesByID() bool { 43 if r.DocType != consts.Files { 44 return false 45 } 46 return r.Selector == "" || r.Selector == "id" || r.Selector == "_id" 47 } 48 49 // ValidateRules returns an error if the rules are invalid (the doctype is 50 // missing for example) 51 func (s *Sharing) ValidateRules() error { 52 if len(s.Rules) == 0 { 53 return ErrNoRules 54 } 55 for i, rule := range s.Rules { 56 if rule.Title == "" || len(rule.Values) == 0 { 57 return ErrInvalidRule 58 } 59 if permission.CheckDoctypeName(rule.DocType, false) != nil { 60 return ErrInvalidRule 61 } 62 if rule.DocType == consts.Files { 63 for _, val := range rule.Values { 64 if val == consts.RootDirID || 65 val == consts.TrashDirID || 66 val == consts.SharedWithMeDirID { 67 return ErrInvalidRule 68 } 69 } 70 // XXX Currently, we only support one file/folder per rule for the id selector 71 if rule.Selector == "" || rule.Selector == "id" || rule.Selector == "_id" { 72 if len(rule.Values) > 1 { 73 return ErrInvalidRule 74 } 75 } 76 if rule.Selector == couchdb.SelectorReferencedBy { 77 // For a referenced_by rule, values should be "doctype/docid" 78 for _, val := range rule.Values { 79 parts := strings.SplitN(val, "/", 2) 80 if len(parts) != 2 { 81 return ErrInvalidRule 82 } 83 } 84 } 85 } else if permission.CheckWritable(rule.DocType) != nil { 86 return ErrInvalidRule 87 } 88 if rule.Add == "" { 89 s.Rules[i].Add = ActionRuleNone 90 rule.Add = s.Rules[i].Add 91 } 92 rule.Add = strings.ToLower(rule.Add) 93 if rule.Add != ActionRuleNone && 94 rule.Add != ActionRulePush && 95 rule.Add != ActionRuleSync { 96 return ErrInvalidRule 97 } 98 if rule.Update == "" { 99 s.Rules[i].Update = ActionRuleNone 100 rule.Update = s.Rules[i].Update 101 } 102 rule.Update = strings.ToLower(rule.Update) 103 if rule.Update != ActionRuleNone && 104 rule.Update != ActionRulePush && 105 rule.Update != ActionRuleSync { 106 return ErrInvalidRule 107 } 108 if rule.Remove == "" { 109 s.Rules[i].Remove = ActionRuleNone 110 rule.Remove = s.Rules[i].Remove 111 } 112 rule.Remove = strings.ToLower(rule.Remove) 113 if rule.Remove != ActionRuleNone && 114 rule.Remove != ActionRulePush && 115 rule.Remove != ActionRuleSync && 116 rule.Remove != ActionRuleRevoke { 117 return ErrInvalidRule 118 } 119 } 120 return nil 121 } 122 123 // Accept returns true if the document matches the rule criteria 124 func (r Rule) Accept(doctype string, doc map[string]interface{}) bool { 125 if r.Local || doctype != r.DocType { 126 return false 127 } 128 var obj interface{} = doc 129 if r.Selector == "" || r.Selector == "id" { 130 obj = doc["_id"] 131 } else if doctype == consts.Files && r.Selector == couchdb.SelectorReferencedBy { 132 if o, k := doc[couchdb.SelectorReferencedBy].([]map[string]interface{}); k { 133 refs := make([]string, len(o)) 134 for i, ref := range o { 135 refs[i] = ref["type"].(string) + "/" + ref["id"].(string) 136 } 137 obj = refs 138 } 139 } else { 140 keys := strings.Split(r.Selector, ".") 141 for _, key := range keys { 142 if o, k := obj.(map[string]interface{}); k { 143 obj = o[key] 144 } else { 145 obj = nil 146 break 147 } 148 } 149 } 150 if val, ok := obj.(string); ok { 151 for _, v := range r.Values { 152 if v == val { 153 return true 154 } 155 } 156 } 157 if val, ok := obj.([]string); ok { 158 for _, vv := range val { 159 for _, v := range r.Values { 160 if v == vv { 161 return true 162 } 163 } 164 } 165 } 166 return false 167 } 168 169 // TriggerArgs returns the string that can be used as an argument to create a 170 // trigger for this rule. The result can be an empty string if the rule doesn't 171 // need a trigger (a local or one-shot rule). 172 func (r Rule) TriggerArgs() string { 173 if r.Local { 174 return "" 175 } 176 verbs := make([]string, 1, 3) 177 // We always need the CREATED verb to have io.cozy.shared for all the 178 // shared documents, as the io.cozy.shared documents are needed to 179 // accept the updates and deletes later. 180 verbs[0] = "CREATED" 181 if r.Update == ActionRuleSync || r.Update == ActionRulePush { 182 verbs = append(verbs, "UPDATED") 183 } 184 if r.Remove == ActionRuleSync || r.Remove == ActionRulePush || r.Remove == ActionRuleRevoke { 185 verbs = append(verbs, "DELETED") 186 } 187 if len(verbs) == 0 { 188 return "" 189 } 190 args := r.DocType + ":" + strings.Join(verbs, ",") 191 if len(r.Values) > 0 { 192 args += ":" + strings.Join(r.Values, ",") 193 if r.Selector != "" && r.Selector != "id" { 194 args += ":" + r.Selector 195 } 196 } 197 return args 198 } 199 200 // FirstBitwardenOrganizationRule returns the first not-local rules for the 201 // com.bitwarden.organizations doctype. 202 func (s *Sharing) FirstBitwardenOrganizationRule() *Rule { 203 for i, rule := range s.Rules { 204 if !rule.Local && rule.DocType == consts.BitwardenOrganizations { 205 return &s.Rules[i] 206 } 207 } 208 return nil 209 } 210 211 // FirstFilesRule returns the first not-local rules for the files doctype. 212 func (s *Sharing) FirstFilesRule() *Rule { 213 for i, rule := range s.Rules { 214 if !rule.Local && rule.DocType == consts.Files { 215 return &s.Rules[i] 216 } 217 } 218 return nil 219 } 220 221 func (s *Sharing) findRuleForNewDirectory(dir *vfs.DirDoc) (*Rule, int) { 222 for i, rule := range s.Rules { 223 if rule.Local || rule.DocType != consts.Files { 224 continue 225 } 226 if rule.Selector != couchdb.SelectorReferencedBy { 227 return &s.Rules[i], i 228 } 229 if len(dir.ReferencedBy) == 0 { 230 continue 231 } 232 allFound := true 233 for _, ref := range dir.ReferencedBy { 234 if !rule.hasReferencedBy(ref) { 235 allFound = false 236 break 237 } 238 } 239 if allFound { 240 return &s.Rules[i], i 241 } 242 } 243 return nil, 0 244 } 245 246 func (s *Sharing) findRuleForNewFile(file *vfs.FileDoc) (*Rule, int) { 247 for i, rule := range s.Rules { 248 if rule.Local || rule.DocType != consts.Files { 249 continue 250 } 251 if rule.Selector != couchdb.SelectorReferencedBy { 252 return &s.Rules[i], i 253 } 254 if len(file.ReferencedBy) == 0 { 255 continue 256 } 257 allFound := true 258 for _, ref := range file.ReferencedBy { 259 if !rule.hasReferencedBy(ref) { 260 allFound = false 261 break 262 } 263 } 264 if allFound { 265 return &s.Rules[i], i 266 } 267 } 268 return nil, 0 269 } 270 271 func (s *Sharing) hasExplicitRuleForFile(file *vfs.FileDoc) bool { 272 for _, rule := range s.Rules { 273 if rule.Local || rule.DocType != consts.Files { 274 continue 275 } 276 if rule.Selector != "" { 277 continue 278 } 279 for _, id := range rule.Values { 280 if id == file.DocID { 281 return true 282 } 283 } 284 } 285 return false 286 } 287 288 // HasSync returns true if the rule has a sync behaviour 289 func (r *Rule) HasSync() bool { 290 return r.Add == ActionRuleSync || r.Update == ActionRuleSync || 291 r.Remove == ActionRuleSync 292 } 293 294 // HasPush returns true if the rule has a push behaviour 295 func (r *Rule) HasPush() bool { 296 return r.Add == ActionRulePush || r.Update == ActionRulePush || 297 r.Remove == ActionRulePush 298 } 299 300 // hasReferencedBy returns true if the rule matches a file that has this reference 301 func (r *Rule) hasReferencedBy(ref couchdb.DocReference) bool { 302 if r.Selector != couchdb.SelectorReferencedBy { 303 return false 304 } 305 v := ref.Type + "/" + ref.ID 306 for _, val := range r.Values { 307 if val == v { 308 return true 309 } 310 } 311 return false 312 }