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  }