github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/permission/permissions_test.go (about)

     1  package permission
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/labstack/echo/v4"
    10  	"github.com/stretchr/testify/assert"
    11  )
    12  
    13  func TestCheckDoctypeName(t *testing.T) {
    14  	assert.NoError(t, CheckDoctypeName("io.cozy.files", false))
    15  	assert.NoError(t, CheckDoctypeName("io.cozy.account_types", false))
    16  	assert.Error(t, CheckDoctypeName("IO.COZY.FILES", false))
    17  	assert.Error(t, CheckDoctypeName("io.cozy.account-types", false))
    18  	assert.Error(t, CheckDoctypeName(".io.cozy.files", false))
    19  	assert.Error(t, CheckDoctypeName("io.cozy.files.", false))
    20  	assert.Error(t, CheckDoctypeName("io.cozy.files.*", false))
    21  	assert.Error(t, CheckDoctypeName("io..cozy..files", false))
    22  	assert.Error(t, CheckDoctypeName("*", false))
    23  
    24  	assert.NoError(t, CheckDoctypeName("io.cozy.files", true))
    25  	assert.NoError(t, CheckDoctypeName("io.cozy.banks.*", true))
    26  	assert.NoError(t, CheckDoctypeName("io.cozy.files.*", true))
    27  	assert.Error(t, CheckDoctypeName("io.cozy.*", true))
    28  	assert.Error(t, CheckDoctypeName("com.bitwarden.*", true))
    29  	assert.Error(t, CheckDoctypeName("*", true))
    30  }
    31  
    32  func TestVerbToString(t *testing.T) {
    33  	vs := Verbs(GET, DELETE)
    34  	assert.Equal(t, "GET,DELETE", vs.String())
    35  
    36  	vs3 := ALL
    37  	assert.Equal(t, "ALL", vs3.String())
    38  
    39  	vs4 := VerbSplit("ALL")
    40  	assert.Equal(t, "ALL", vs4.String())
    41  }
    42  
    43  func TestRuleToJSON(t *testing.T) {
    44  	r := Rule{
    45  		Type:  "io.cozy.contacts",
    46  		Verbs: Verbs(GET, POST),
    47  	}
    48  
    49  	b, err := json.Marshal(r)
    50  	assert.NoError(t, err)
    51  	assert.Equal(t, `{"type":"io.cozy.contacts","verbs":["GET","POST"]}`, string(b))
    52  }
    53  
    54  func TestSetToJSON(t *testing.T) {
    55  	s := Set{
    56  		Rule{
    57  			Title:       "images",
    58  			Description: "Required for the background",
    59  			Type:        "io.cozy.files",
    60  			Verbs:       Verbs(GET),
    61  			Values:      []string{"io.cozy.files.music-dir"},
    62  		},
    63  		Rule{
    64  			Title:       "contacts",
    65  			Description: "Required for autocompletion on @name",
    66  			Type:        "io.cozy.contacts",
    67  			Verbs:       Verbs(GET),
    68  		},
    69  		Rule{
    70  			Title:       "mail",
    71  			Description: "Required to send a congratulations email to your friends",
    72  			Type:        "io.cozy.jobs",
    73  			Selector:    "worker",
    74  			Values:      []string{"sendmail"},
    75  		},
    76  	}
    77  
    78  	b, err := json.Marshal(s)
    79  	assert.NoError(t, err)
    80  	assertEqualJSON(t, b, `{
    81      "images": {
    82        "type": "io.cozy.files",
    83        "description": "Required for the background",
    84        "verbs": ["GET"],
    85        "values": ["io.cozy.files.music-dir"]
    86      },
    87      "contacts": {
    88        "type": "io.cozy.contacts",
    89        "description": "Required for autocompletion on @name",
    90        "verbs": ["GET"]
    91      },
    92      "mail": {
    93        "type": "io.cozy.jobs",
    94        "description": "Required to send a congratulations email to your friends",
    95        "selector": "worker",
    96        "values": ["sendmail"]
    97      }
    98    }`)
    99  }
   100  
   101  func TestJSON2Set(t *testing.T) {
   102  	jsonSet := []byte(`{
   103      "images": {
   104        "type": "io.cozy.files",
   105        "description": "Required for the background",
   106        "verbs": ["ALL"],
   107        "values": ["io.cozy.files.music-dir"]
   108      },
   109      "contacts": {
   110        "type": "io.cozy.contacts",
   111        "description": "Required for autocompletion on @name",
   112        "verbs": ["GET","PUT"]
   113      },
   114      "mail": {
   115        "type": "io.cozy.jobs",
   116        "description": "Required to send a congratulations email to your friends",
   117        "selector": "worker",
   118        "values": ["sendmail"]
   119      }
   120    }`)
   121  	var s Set
   122  	err := json.Unmarshal(jsonSet, &s)
   123  	assert.NoError(t, err)
   124  	assert.Len(t, s, 3)
   125  	assert.Equal(t, "images", s[0].Title)
   126  	assert.Equal(t, "contacts", s[1].Title)
   127  	assert.Equal(t, "mail", s[2].Title)
   128  }
   129  
   130  func TestHasSameRules(t *testing.T) {
   131  	s := Set{
   132  		Rule{
   133  			Title:       "images",
   134  			Description: "Required for the background",
   135  			Type:        "io.cozy.files",
   136  			Verbs:       Verbs(GET),
   137  			Values:      []string{"io.cozy.files.music-dir"},
   138  		},
   139  		Rule{
   140  			Title:       "contacts",
   141  			Description: "Required for autocompletion on @name",
   142  			Type:        "io.cozy.contacts",
   143  			Verbs:       Verbs(GET),
   144  		},
   145  		Rule{
   146  			Title:       "mail",
   147  			Description: "Required to send a congratulations email to your friends",
   148  			Type:        "io.cozy.jobs",
   149  			Selector:    "worker",
   150  			Values:      []string{"sendmail"},
   151  		},
   152  	}
   153  
   154  	b, err := json.Marshal(s)
   155  	assert.NoError(t, err)
   156  	var other Set
   157  	err = json.Unmarshal(b, &other)
   158  	assert.NoError(t, err)
   159  	assert.Len(t, other, 3)
   160  	assert.True(t, s.HasSameRules(other))
   161  }
   162  
   163  func TestBadJSONSet(t *testing.T) {
   164  	jsonSet := []byte(`{
   165      "contacts": {
   166        "type": "io.cozy.contacts",
   167        "description": "Required for autocompletion on @name",
   168        "verbs": ["BAD"]
   169      }
   170    }`)
   171  	var s Set
   172  	err := json.Unmarshal(jsonSet, &s)
   173  	assert.Error(t, err)
   174  	assert.Equal(t, ErrBadScope, err)
   175  }
   176  
   177  func TestJSONSetVerbParsing(t *testing.T) {
   178  	var s Set
   179  	jsonSet := []byte(`{
   180      "contacts": {
   181        "type": "io.cozy.contacts",
   182        "description": "Required for autocompletion on @name",
   183        "verbs": ["GET","PUT"]
   184      }
   185    }`)
   186  	err := json.Unmarshal(jsonSet, &s)
   187  	assert.NoError(t, err)
   188  	assert.Len(t, s, 1)
   189  	assert.EqualValues(t, VerbSet{"GET": struct{}{}, "PUT": struct{}{}}, s[0].Verbs)
   190  
   191  	jsonSet = []byte(`{
   192      "contacts": {
   193        "type": "io.cozy.contacts",
   194        "description": "Required for autocompletion on @name",
   195        "verbs": ["ALL", "GET"]
   196      }
   197    }`)
   198  	err = json.Unmarshal(jsonSet, &s)
   199  	assert.NoError(t, err)
   200  	assert.Len(t, s, 1)
   201  	assert.EqualValues(t, VerbSet{}, s[0].Verbs)
   202  }
   203  
   204  func TestSetToString(t *testing.T) {
   205  	s := Set{
   206  		Rule{
   207  			Title:       "contacts",
   208  			Description: "Required for autocompletion on @name",
   209  			Type:        "io.cozy.contacts",
   210  		},
   211  		Rule{
   212  			Title:       "images",
   213  			Description: "Required for the background",
   214  			Type:        "io.cozy.files",
   215  			Verbs:       Verbs(GET),
   216  			Values:      []string{"io.cozy.files.music-dir"},
   217  		},
   218  		Rule{
   219  			Title:    "sendmail",
   220  			Type:     "io.cozy.jobs",
   221  			Selector: "worker",
   222  			Values:   []string{"sendmail"},
   223  		},
   224  	}
   225  
   226  	out, err := s.MarshalScopeString()
   227  	assert.NoError(t, err)
   228  	assert.Equal(t, out, "io.cozy.contacts io.cozy.files:GET:io.cozy.files.music-dir io.cozy.jobs:ALL:sendmail:worker")
   229  }
   230  
   231  func TestStringToSet(t *testing.T) {
   232  	_, err := UnmarshalRuleString("")
   233  	assert.Error(t, err)
   234  
   235  	_, err = UnmarshalRuleString("*")
   236  	assert.Error(t, err)
   237  
   238  	_, err = UnmarshalRuleString("type:verb:selec:value:wtf")
   239  	assert.Error(t, err)
   240  
   241  	set, err := UnmarshalScopeString("io.cozy.contacts io.cozy.files:GET:io.cozy.files.music-dir")
   242  
   243  	assert.NoError(t, err)
   244  	assert.Len(t, set, 2)
   245  	assert.Equal(t, "io.cozy.contacts", set[0].Type)
   246  	assert.Equal(t, "io.cozy.files", set[1].Type)
   247  	assert.Len(t, set[1].Verbs, 1)
   248  	assert.Equal(t, Verbs(GET), set[1].Verbs)
   249  	assert.Len(t, set[1].Values, 1)
   250  	assert.Equal(t, "io.cozy.files.music-dir", set[1].Values[0])
   251  
   252  	rule, err := UnmarshalRuleString("io.cozy.events:GET:mygreatcalendar,othercalendar:calendar-id")
   253  	assert.NoError(t, err)
   254  	assert.Equal(t, "io.cozy.events", rule.Type)
   255  	assert.Equal(t, Verbs(GET), rule.Verbs)
   256  	assert.Len(t, rule.Values, 2)
   257  	assert.Equal(t, "mygreatcalendar", rule.Values[0])
   258  	assert.Equal(t, "othercalendar", rule.Values[1])
   259  	assert.Equal(t, "calendar-id", rule.Selector)
   260  }
   261  
   262  func TestAllowType(t *testing.T) {
   263  	s := Set{Rule{Type: "io.cozy.contacts"}}
   264  	assert.True(t, s.Allow(GET, &validable{doctype: "io.cozy.contacts"}))
   265  	assert.True(t, s.Allow(DELETE, &validable{doctype: "io.cozy.contacts"}))
   266  	assert.False(t, s.Allow(GET, &validable{doctype: "io.cozy.files"}))
   267  }
   268  
   269  func TestAllowWildcard(t *testing.T) {
   270  	s := Set{Rule{Type: "io.cozy.bank.*"}}
   271  	assert.True(t, s.Allow(GET, &validable{doctype: "io.cozy.bank"}))
   272  	assert.True(t, s.Allow(DELETE, &validable{doctype: "io.cozy.bank.accounts"}))
   273  	assert.True(t, s.Allow(DELETE, &validable{doctype: "io.cozy.bank.accounts.stats"}))
   274  	assert.True(t, s.Allow(DELETE, &validable{doctype: "io.cozy.bank.settings"}))
   275  	assert.False(t, s.Allow(GET, &validable{doctype: "io.cozy.files"}))
   276  	assert.False(t, s.Allow(GET, &validable{doctype: "io.cozy.files.bank"}))
   277  	assert.False(t, s.Allow(GET, &validable{doctype: "io.cozy.banks"}))
   278  	assert.False(t, s.Allow(GET, &validable{doctype: "io.cozy.bankrupts"}))
   279  }
   280  
   281  func TestAllowMaximal(t *testing.T) {
   282  	s := Set{Rule{Type: "*"}}
   283  	assert.True(t, s.Allow(GET, &validable{doctype: "io.cozy.files"}))
   284  	assert.True(t, s.Allow(DELETE, &validable{doctype: "io.cozy.files.versions"}))
   285  }
   286  
   287  func TestAllowVerbs(t *testing.T) {
   288  	s := Set{Rule{Type: "io.cozy.contacts", Verbs: Verbs(GET)}}
   289  	assert.True(t, s.Allow(GET, &validable{doctype: "io.cozy.contacts"}))
   290  	assert.False(t, s.Allow(DELETE, &validable{doctype: "io.cozy.contacts"}))
   291  	assert.False(t, s.Allow(GET, &validable{doctype: "io.cozy.files"}))
   292  }
   293  
   294  func TestAllowValues(t *testing.T) {
   295  	s := Set{Rule{
   296  		Type:   "io.cozy.contacts",
   297  		Values: []string{"id1"},
   298  	}}
   299  	assert.True(t, s.Allow(POST, &validable{doctype: "io.cozy.contacts", id: "id1"}))
   300  	assert.False(t, s.Allow(POST, &validable{doctype: "io.cozy.contacts", id: "id2"}))
   301  }
   302  
   303  func TestAllowValuesSelector(t *testing.T) {
   304  	s := Set{Rule{
   305  		Type:     "io.cozy.contacts",
   306  		Selector: "foo",
   307  		Values:   []string{"bar"},
   308  	}}
   309  	assert.True(t, s.Allow(GET, &validable{
   310  		doctype: "io.cozy.contacts",
   311  		values:  map[string]string{"foo": "bar"}}))
   312  
   313  	assert.False(t, s.Allow(GET, &validable{
   314  		doctype: "io.cozy.contacts",
   315  		values:  map[string]string{"foo": "baz"}}))
   316  }
   317  
   318  func TestAllowWholeType(t *testing.T) {
   319  	s := Set{Rule{Type: "io.cozy.contacts", Verbs: Verbs(GET)}}
   320  	assert.True(t, s.AllowWholeType(GET, "io.cozy.contacts"))
   321  
   322  	s2 := Set{Rule{Type: "io.cozy.contacts", Values: []string{"id1"}}}
   323  	assert.False(t, s2.AllowWholeType(GET, "io.cozy.contacts"))
   324  }
   325  
   326  func TestAllowID(t *testing.T) {
   327  	s := Set{Rule{Type: "io.cozy.contacts"}}
   328  	assert.True(t, s.AllowID(GET, "io.cozy.contacts", "id1"))
   329  
   330  	s2 := Set{Rule{Type: "io.cozy.contacts", Values: []string{"id1"}}}
   331  	assert.True(t, s2.AllowID(GET, "io.cozy.contacts", "id1"))
   332  
   333  	s3 := Set{Rule{Type: "io.cozy.contacts", Selector: "foo", Values: []string{"bar"}}}
   334  	assert.False(t, s3.AllowID(GET, "io.cozy.contacts", "id1"))
   335  }
   336  
   337  func TestAllowCustomType(t *testing.T) {
   338  	s := Set{Rule{Type: "io.cozy.files", Selector: "path", Values: []string{"/testp/"}}}
   339  
   340  	y := &validableFile{"/testp/test"}
   341  	n := &validableFile{"/not-testp/test"}
   342  
   343  	assert.True(t, s.Allow(GET, y))
   344  	assert.False(t, s.Allow(GET, n))
   345  }
   346  
   347  func TestSubset(t *testing.T) {
   348  	s := Set{Rule{Type: "io.cozy.events"}}
   349  
   350  	s2 := Set{Rule{Type: "io.cozy.events"}}
   351  	assert.True(t, s2.IsSubSetOf(s))
   352  
   353  	s3 := Set{Rule{Type: "io.cozy.events", Values: []string{"foo", "bar"}}}
   354  	assert.True(t, s3.IsSubSetOf(s))
   355  
   356  	s4 := Set{Rule{Type: "io.cozy.events", Values: []string{"foo"}}}
   357  	assert.True(t, s4.IsSubSetOf(s3))
   358  	assert.False(t, s3.IsSubSetOf(s4))
   359  
   360  	s5 := Set{Rule{Type: "io.cozy.events", Selector: "calendar", Values: []string{"foo", "bar"}}}
   361  	s6 := Set{Rule{Type: "io.cozy.events", Selector: "calendar", Values: []string{"foo"}}}
   362  	assert.True(t, s6.IsSubSetOf(s5))
   363  	assert.False(t, s5.IsSubSetOf(s6))
   364  }
   365  
   366  func TestShareSetPermissions(t *testing.T) {
   367  	setFiles := Set{Rule{Type: "io.cozy.files"}}
   368  	setFilesWildCard := Set{Rule{Type: "io.cozy.files.*"}}
   369  	setEvents := Set{Rule{Type: "io.cozy.events"}}
   370  
   371  	parent := &Permission{Type: TypeCLI, Permissions: setEvents}
   372  	err := checkSetPermissions(setFiles, parent)
   373  	assert.Error(t, err)
   374  
   375  	parent.Type = TypeWebapp
   376  	err = checkSetPermissions(setFiles, parent)
   377  	assert.Error(t, err)
   378  
   379  	parent.Permissions = setFiles
   380  	err = checkSetPermissions(setFiles, parent)
   381  	assert.NoError(t, err)
   382  
   383  	err = checkSetPermissions(setFilesWildCard, parent)
   384  	assert.Error(t, err)
   385  
   386  	parent.Permissions = setFilesWildCard
   387  	err = checkSetPermissions(setFilesWildCard, parent)
   388  	assert.NoError(t, err)
   389  }
   390  
   391  func TestCreateShareSetBlocklist(t *testing.T) {
   392  	s := Set{Rule{Type: "io.cozy.notifications"}}
   393  	subdoc := Permission{
   394  		Permissions: s,
   395  	}
   396  	parent := &Permission{Type: TypeWebapp, Permissions: s}
   397  	_, err := CreateShareSet(nil, parent, "", nil, nil, subdoc, nil)
   398  	assert.Error(t, err)
   399  	e, ok := err.(*echo.HTTPError)
   400  	assert.True(t, ok)
   401  	assert.Equal(t, "reserved doctype io.cozy.notifications unwritable", e.Message)
   402  
   403  	s = Set{Rule{Type: "*"}}
   404  	subdoc = Permission{
   405  		Permissions: s,
   406  	}
   407  	parent = &Permission{Type: TypeWebapp, Permissions: s}
   408  	_, err = CreateShareSet(nil, parent, "", nil, nil, subdoc, nil)
   409  	assert.Error(t, err)
   410  }
   411  
   412  func assertEqualJSON(t *testing.T, value []byte, expected string) {
   413  	expectedBytes := new(bytes.Buffer)
   414  	err := json.Compact(expectedBytes, []byte(expected))
   415  	assert.NoError(t, err)
   416  	assert.Equal(t, expectedBytes.String(), string(value))
   417  }
   418  
   419  type validable struct {
   420  	id      string
   421  	doctype string
   422  	values  map[string]string
   423  }
   424  
   425  func (t *validable) ID() string      { return t.id }
   426  func (t *validable) DocType() string { return t.doctype }
   427  func (t *validable) Fetch(field string) []string {
   428  	return []string{t.values[field]}
   429  }
   430  
   431  type validableFile struct {
   432  	path string
   433  }
   434  
   435  func (t *validableFile) ID() string      { return t.path }
   436  func (t *validableFile) DocType() string { return "io.cozy.files" }
   437  func (t *validableFile) Fetch(field string) []string {
   438  	if field != "path" {
   439  		return nil
   440  	}
   441  	var prefixes []string
   442  	parts := strings.Split(t.path, "/")
   443  	for i := 1; i < len(parts); i++ {
   444  		prefix := strings.Join(parts[:i], "/") + "/"
   445  		prefixes = append(prefixes, prefix)
   446  	}
   447  	return prefixes
   448  }