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 }