github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/permissions/permissions_test.go (about) 1 package permissions 2 3 import ( 4 "fmt" 5 "testing" 6 "time" 7 8 "github.com/cozy/cozy-stack/model/app" 9 "github.com/cozy/cozy-stack/model/instance" 10 "github.com/cozy/cozy-stack/model/oauth" 11 "github.com/cozy/cozy-stack/model/permission" 12 "github.com/cozy/cozy-stack/pkg/config/config" 13 "github.com/cozy/cozy-stack/pkg/consts" 14 "github.com/cozy/cozy-stack/pkg/couchdb" 15 "github.com/cozy/cozy-stack/tests/testutils" 16 "github.com/cozy/cozy-stack/web/errors" 17 "github.com/cozy/cozy-stack/web/middlewares" 18 "github.com/gavv/httpexpect/v2" 19 "github.com/golang-jwt/jwt/v5" 20 "github.com/labstack/echo/v4" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/require" 23 ) 24 25 func TestPermissions(t *testing.T) { 26 if testing.Short() { 27 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 28 } 29 30 config.UseTestFile(t) 31 testutils.NeedCouchdb(t) 32 setup := testutils.NewSetup(t, t.Name()) 33 34 testInstance := setup.GetTestInstance() 35 scopes := "io.cozy.contacts io.cozy.files:GET io.cozy.events" 36 clientVal, token := setup.GetTestClient(scopes) 37 clientID := clientVal.ClientID 38 39 ts := setup.GetTestServer("/permissions", Routes) 40 ts.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler 41 t.Cleanup(ts.Close) 42 43 t.Run("CreateShareSetByMobileRevokeByLinkedApp", func(t *testing.T) { 44 e := testutils.CreateTestClient(t, ts.URL) 45 46 // Create OAuthLinkedClient 47 oauthLinkedClient := &oauth.Client{ 48 ClientName: "test-linked-shareset", 49 RedirectURIs: []string{"https://foobar"}, 50 SoftwareID: "registry://drive", 51 } 52 oauthLinkedClient.Create(testInstance) 53 54 // Install the app 55 installer, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), &app.InstallerOptions{ 56 Operation: app.Install, 57 Type: consts.WebappType, 58 SourceURL: "registry://drive", 59 Slug: "drive", 60 Registries: testInstance.Registries(), 61 }) 62 assert.NoError(t, err) 63 _, err = installer.RunSync() 64 assert.NoError(t, err) 65 66 // Generate a token for the client 67 tok, err := testInstance.MakeJWT(consts.AccessTokenAudience, 68 oauthLinkedClient.ClientID, "@io.cozy.apps/drive", "", time.Now()) 69 assert.NoError(t, err) 70 71 // Request to create a permission 72 obj := e.POST("/permissions"). 73 WithQuery("codes", "email"). 74 WithHost(testInstance.Domain). 75 WithHeader("Authorization", "Bearer "+tok). 76 WithHeader("Content-Type", "application/json"). 77 WithBytes([]byte(fmt.Sprintf(`{ 78 "data": { 79 "id": "%s", 80 "type": "io.cozy.permissions", 81 "attributes": { 82 "permissions": { 83 "files": { 84 "type": "io.cozy.files", 85 "verbs": ["GET"] 86 } 87 } 88 } 89 } 90 }`, oauthLinkedClient.ClientID))). 91 Expect().Status(200). 92 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 93 Object() 94 95 // Assert the permission received does not have the clientID as source_id 96 obj.Path("$.data.attributes.source_id").String().NotEqual(oauthLinkedClient.ClientID) 97 permID := obj.Path("$.data.id").String().NotEmpty().Raw() 98 99 // Create a webapp token 100 webAppToken, err := testInstance.MakeJWT(consts.AppAudience, "drive", "", "", time.Now()) 101 assert.NoError(t, err) 102 103 // Login to webapp and try to delete the shared link 104 e.DELETE("/permissions/"+permID). 105 WithHost(testInstance.Domain). 106 WithHeader("Authorization", "Bearer "+webAppToken). 107 Expect().Status(204) 108 109 // Cleaning 110 oauthLinkedClient, err = oauth.FindClientBySoftwareID(testInstance, "registry://drive") 111 assert.NoError(t, err) 112 oauthLinkedClient.Delete(testInstance) 113 114 uninstaller, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), 115 &app.InstallerOptions{ 116 Operation: app.Delete, 117 Type: consts.WebappType, 118 Slug: "drive", 119 SourceURL: "registry://drive", 120 Registries: testInstance.Registries(), 121 }, 122 ) 123 assert.NoError(t, err) 124 125 _, err = uninstaller.RunSync() 126 assert.NoError(t, err) 127 }) 128 129 t.Run("CreateShareSetByLinkedAppRevokeByMobile", func(t *testing.T) { 130 e := testutils.CreateTestClient(t, ts.URL) 131 132 // Create a webapp token 133 webAppToken, err := testInstance.MakeJWT(consts.AppAudience, "drive", "", "", time.Now()) 134 assert.NoError(t, err) 135 136 // Install the app 137 installer, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), &app.InstallerOptions{ 138 Operation: app.Install, 139 Type: consts.WebappType, 140 SourceURL: "registry://drive", 141 Slug: "drive", 142 Registries: testInstance.Registries(), 143 }) 144 assert.NoError(t, err) 145 _, err = installer.RunSync() 146 assert.NoError(t, err) 147 148 // Request to create a permission 149 obj := e.POST("/permissions"). 150 WithQuery("codes", "email"). 151 WithHost(testInstance.Domain). 152 WithHeader("Authorization", "Bearer "+webAppToken). 153 WithHeader("Content-Type", "application/json"). 154 WithBytes([]byte(`{ 155 "data": { 156 "id": "io.cozy.apps/drive", 157 "type": "io.cozy.permissions", 158 "attributes": { 159 "permissions": { 160 "files": { 161 "type": "io.cozy.files", 162 "verbs": ["GET"] 163 } 164 } 165 } 166 } 167 }`)). 168 Expect().Status(200). 169 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 170 Object() 171 172 permSourceID := obj.Path("$.data.attributes.source_id").String().NotEmpty().Raw() 173 permID := obj.Path("$.data.id").String().NotEmpty().Raw() 174 175 // Create OAuthLinkedClient 176 oauthLinkedClient := &oauth.Client{ 177 ClientName: "test-linked-shareset2", 178 RedirectURIs: []string{"https://foobar"}, 179 SoftwareID: "registry://drive", 180 } 181 oauthLinkedClient.Create(testInstance) 182 183 // Generate a token for the client 184 tok, err := testInstance.MakeJWT(consts.AccessTokenAudience, 185 oauthLinkedClient.ClientID, "@io.cozy.apps/drive", "", time.Now()) 186 assert.NoError(t, err) 187 188 // Assert the permission received does not have the clientID as source_id 189 assert.NotEqual(t, permSourceID, oauthLinkedClient.ClientID) 190 191 // Login to webapp and try to delete the shared link 192 e.DELETE("/permissions/"+permID). 193 WithHost(testInstance.Domain). 194 WithHeader("Authorization", "Bearer "+tok). 195 Expect().Status(204) 196 197 // Cleaning 198 oauthLinkedClient, err = oauth.FindClientBySoftwareID(testInstance, "registry://drive") 199 assert.NoError(t, err) 200 oauthLinkedClient.Delete(testInstance) 201 202 uninstaller, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), 203 &app.InstallerOptions{ 204 Operation: app.Delete, 205 Type: consts.WebappType, 206 Slug: "drive", 207 SourceURL: "registry://drive", 208 Registries: testInstance.Registries(), 209 }, 210 ) 211 assert.NoError(t, err) 212 213 _, err = uninstaller.RunSync() 214 assert.NoError(t, err) 215 }) 216 217 t.Run("GetPermissions", func(t *testing.T) { 218 e := testutils.CreateTestClient(t, ts.URL) 219 220 obj := e.GET("/permissions/self"). 221 WithHeader("Authorization", "Bearer "+token). 222 Expect().Status(200). 223 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 224 Object() 225 226 perms := obj.Path("$.data.attributes.permissions").Object() 227 228 for key, r := range perms.Iter() { 229 switch key { 230 case "rule1": 231 r.Object().ValueEqual("type", "io.cozy.files") 232 r.Object().ValueEqual("verbs", []interface{}{"GET"}) 233 case "rule0": 234 r.Object().ValueEqual("type", "io.cozy.contacts") 235 default: 236 r.Object().ValueEqual("type", "io.cozy.events") 237 } 238 } 239 }) 240 241 t.Run("GetPermissionsForRevokedClient", func(t *testing.T) { 242 e := testutils.CreateTestClient(t, ts.URL) 243 244 tok, err := testInstance.MakeJWT(consts.AccessTokenAudience, 245 "revoked-client", 246 "io.cozy.contacts io.cozy.files:GET", 247 "", time.Now()) 248 assert.NoError(t, err) 249 250 res := e.GET("/permissions/self"). 251 WithHeader("Authorization", "Bearer "+tok). 252 Expect().Status(400) 253 254 res.Text().Equal(`Invalid JWT token`) 255 res.Header("WWW-Authenticate").Equal(`Bearer error="invalid_token"`) 256 }) 257 258 t.Run("GetPermissionsForExpiredToken", func(t *testing.T) { 259 e := testutils.CreateTestClient(t, ts.URL) 260 261 pastTimestamp := time.Now().Add(-30 * 24 * time.Hour) // in seconds 262 263 tok, err := testInstance.MakeJWT(consts.AccessTokenAudience, 264 clientID, "io.cozy.contacts io.cozy.files:GET", "", pastTimestamp) 265 assert.NoError(t, err) 266 267 res := e.GET("/permissions/self"). 268 WithHeader("Authorization", "Bearer "+tok). 269 Expect().Status(400) 270 271 res.Text().Equal("Expired token") 272 res.Header("WWW-Authenticate").Equal(`Bearer error="invalid_token" error_description="The access token expired"`) 273 }) 274 275 t.Run("BadPermissionsBearer", func(t *testing.T) { 276 e := testutils.CreateTestClient(t, ts.URL) 277 278 e.GET("/permissions/self"). 279 WithHeader("Authorization", "Bearer barbage"). 280 Expect().Status(400) 281 }) 282 283 t.Run("CreateSubPermission", func(t *testing.T) { 284 e := testutils.CreateTestClient(t, ts.URL) 285 286 _, codes, err := createTestSubPermissions(e, token, "alice,bob") 287 require.NoError(t, err) 288 289 aCode := codes.Value("alice").String().NotEmpty().Raw() 290 bCode := codes.Value("bob").String().NotEmpty().Raw() 291 292 assert.NotEqual(t, aCode, token) 293 assert.NotEqual(t, bCode, token) 294 assert.NotEqual(t, aCode, bCode) 295 296 obj := e.GET("/permissions/self"). 297 WithHeader("Authorization", "Bearer "+aCode). 298 Expect().Status(200). 299 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 300 Object() 301 302 perms := obj.Path("$.data.attributes.permissions").Object() 303 perms.Keys().Length().Equal(2) 304 perms.Path("$.whatever.type").String().Equal("io.cozy.files") 305 }) 306 307 t.Run("CreateSubSubFail", func(t *testing.T) { 308 e := testutils.CreateTestClient(t, ts.URL) 309 310 _, codes, err := createTestSubPermissions(e, token, "eve") 311 require.NoError(t, err) 312 313 eveCode := codes.Value("eve").String().NotEmpty().Raw() 314 315 e.POST("/permissions"). 316 WithQuery("codes", codes). 317 WithHeader("Authorization", "Bearer "+eveCode). 318 WithHeader("Content-Type", "application/json"). 319 WithBytes([]byte(`{ 320 "data": { 321 "type": "io.cozy.permissions", 322 "attributes": { 323 "permissions": { 324 "whatever": { 325 "type": "io.cozy.files", 326 "verbs": ["GET"], 327 "values": ["io.cozy.music"] 328 }, 329 "otherrule": { 330 "type": "io.cozy.files", 331 "verbs": ["GET"], 332 "values": ["some-other-dir"] 333 } 334 } 335 } 336 } 337 }`)). 338 Expect().Status(403) 339 }) 340 341 t.Run("PatchNoopFail", func(t *testing.T) { 342 e := testutils.CreateTestClient(t, ts.URL) 343 344 id, _, err := createTestSubPermissions(e, token, "pierre") 345 require.NoError(t, err) 346 347 e.PATCH("/permissions/"+id). 348 WithHeader("Authorization", "Bearer "+token). 349 WithHeader("Content-Type", "application/json"). 350 WithBytes([]byte(`{ 351 "data": { 352 "id": "a340d5e0-d647-11e6-b66c-5fc9ce1e17c6", 353 "type": "io.cozy.permissions", 354 "attributes": { } 355 } 356 } 357 }`)). 358 Expect().Status(400) 359 }) 360 361 t.Run("BadPatchAddRuleForbidden", func(t *testing.T) { 362 e := testutils.CreateTestClient(t, ts.URL) 363 364 id, _, err := createTestSubPermissions(e, token, "jacque") 365 require.NoError(t, err) 366 367 e.PATCH("/permissions/"+id). 368 WithHeader("Authorization", "Bearer "+token). 369 WithHeader("Content-Type", "application/json"). 370 WithBytes([]byte(`{ 371 "data": { 372 "attributes": { 373 "permissions": { 374 "otherperm": { 375 "type":"io.cozy.token.cant.do.this" 376 } 377 } 378 } 379 } 380 }`)). 381 Expect().Status(403) 382 }) 383 384 t.Run("PatchAddRule", func(t *testing.T) { 385 e := testutils.CreateTestClient(t, ts.URL) 386 387 id, _, err := createTestSubPermissions(e, token, "paul") 388 require.NoError(t, err) 389 390 obj := e.PATCH("/permissions/"+id). 391 WithHeader("Authorization", "Bearer "+token). 392 WithHeader("Content-Type", "application/json"). 393 WithBytes([]byte(`{ 394 "data": { 395 "attributes": { 396 "permissions": { 397 "otherperm": { 398 "type":"io.cozy.contacts" 399 } 400 } 401 } 402 } 403 }`)). 404 Expect().Status(200). 405 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 406 Object() 407 408 perms := obj.Path("$.data.attributes.permissions").Object() 409 perms.Keys().Length().Equal(3) 410 perms.Path("$.whatever.type").String().Equal("io.cozy.files") 411 perms.Path("$.otherperm.type").String().Equal("io.cozy.contacts") 412 }) 413 414 t.Run("PatchRemoveRule", func(t *testing.T) { 415 e := testutils.CreateTestClient(t, ts.URL) 416 417 id, _, err := createTestSubPermissions(e, token, "paul") 418 require.NoError(t, err) 419 420 obj := e.PATCH("/permissions/"+id). 421 WithHeader("Authorization", "Bearer "+token). 422 WithHeader("Content-Type", "application/json"). 423 WithBytes([]byte(`{ 424 "data": { 425 "attributes": { 426 "permissions": { 427 "otherrule": { } 428 } 429 } 430 } 431 }`)). 432 Expect().Status(200). 433 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 434 Object() 435 436 perms := obj.Path("$.data.attributes.permissions").Object() 437 perms.Keys().Length().Equal(1) 438 perms.Path("$.whatever.type").String().Equal("io.cozy.files") 439 }) 440 441 t.Run("PatchChangesCodes", func(t *testing.T) { 442 e := testutils.CreateTestClient(t, ts.URL) 443 444 id, codes, err := createTestSubPermissions(e, token, "john,jane") 445 require.NoError(t, err) 446 447 codes.Value("john").String().NotEmpty() 448 janeToken := codes.Value("jane").String().NotEmpty().Raw() 449 450 e.PATCH("/permissions/"+id). 451 WithHeader("Authorization", "Bearer "+janeToken). 452 WithHeader("Content-Type", "application/json"). 453 WithBytes([]byte(`{ 454 "data": { 455 "attributes": { 456 "codes": { 457 "john": "set-token" 458 } 459 } 460 } 461 }`)). 462 Expect().Status(403) 463 464 obj := e.PATCH("/permissions/"+id). 465 WithHeader("Authorization", "Bearer "+token). 466 WithHeader("Content-Type", "application/json"). 467 WithBytes([]byte(`{ 468 "data": { 469 "attributes": { 470 "codes": { 471 "john": "set-token" 472 } 473 } 474 } 475 }`)). 476 Expect().Status(200). 477 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 478 Object() 479 480 obj.Path("$.data.id").String().Equal(id) 481 482 codes = obj.Path("$.data.attributes.codes").Object() 483 codes.Value("john").String().NotEmpty() 484 codes.NotContainsKey("jane") 485 }) 486 487 t.Run("Revoke", func(t *testing.T) { 488 e := testutils.CreateTestClient(t, ts.URL) 489 490 id, codes, err := createTestSubPermissions(e, token, "igor") 491 require.NoError(t, err) 492 493 igorToken := codes.Value("igor").String().NotEmpty().Raw() 494 495 e.DELETE("/permissions/"+id). 496 WithHeader("Authorization", "Bearer "+igorToken). 497 WithHeader("Content-Type", "application/json"). 498 Expect().Status(403) 499 500 e.DELETE("/permissions/"+id). 501 WithHeader("Authorization", "Bearer "+token). 502 WithHeader("Content-Type", "application/json"). 503 Expect().Status(204) 504 }) 505 506 t.Run("RevokeByAnotherApp", func(t *testing.T) { 507 e := testutils.CreateTestClient(t, ts.URL) 508 509 id, _, err := createTestSubPermissions(e, token, "roger") 510 require.NoError(t, err) 511 512 installer, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), &app.InstallerOptions{ 513 Operation: app.Install, 514 Type: consts.WebappType, 515 SourceURL: "registry://notes", 516 Slug: "notes", 517 Registries: testInstance.Registries(), 518 }) 519 assert.NoError(t, err) 520 _, err = installer.RunSync() 521 require.NoError(t, err) 522 523 notesToken, err := testInstance.MakeJWT(consts.AppAudience, "notes", "", "", time.Now()) 524 assert.NoError(t, err) 525 526 e.DELETE("/permissions/"+id). 527 WithHeader("Authorization", "Bearer "+notesToken). 528 Expect().Status(204) 529 530 // Cleaning 531 uninstaller, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), 532 &app.InstallerOptions{ 533 Operation: app.Delete, 534 Type: consts.WebappType, 535 Slug: "notes", 536 SourceURL: "registry://notes", 537 Registries: testInstance.Registries(), 538 }, 539 ) 540 assert.NoError(t, err) 541 _, err = uninstaller.RunSync() 542 assert.NoError(t, err) 543 }) 544 545 t.Run("GetPermissionsWithShortCode", func(t *testing.T) { 546 e := testutils.CreateTestClient(t, ts.URL) 547 548 id, _, _ := createTestSubPermissions(e, token, "daniel") 549 perm, _ := permission.GetByID(testInstance, id) 550 551 assert.NotNil(t, perm.ShortCodes) 552 553 e.GET("/permissions/self"). 554 WithHeader("Authorization", "Bearer "+perm.ShortCodes["daniel"]). 555 Expect().Status(200) 556 }) 557 558 t.Run("GetPermissionsWithBadShortCode", func(t *testing.T) { 559 e := testutils.CreateTestClient(t, ts.URL) 560 561 id, _, _ := createTestSubPermissions(e, token, "alice") 562 perm, _ := permission.GetByID(testInstance, id) 563 564 assert.NotNil(t, perm.ShortCodes) 565 566 e.GET("/permissions/self"). 567 WithHeader("Authorization", "Bearer foobar"). 568 Expect().Status(400) 569 }) 570 571 t.Run("GetTokenFromShortCode", func(t *testing.T) { 572 e := testutils.CreateTestClient(t, ts.URL) 573 574 id, _, _ := createTestSubPermissions(e, token, "alice") 575 perm, _ := permission.GetByID(testInstance, id) 576 577 tok, _ := permission.GetTokenFromShortcode(testInstance, perm.ShortCodes["alice"]) 578 assert.Equal(t, perm.Codes["alice"], tok) 579 }) 580 581 t.Run("GetBadShortCode", func(t *testing.T) { 582 e := testutils.CreateTestClient(t, ts.URL) 583 584 _, _, err := createTestSubPermissions(e, token, "alice") 585 assert.NoError(t, err) 586 shortcode := "coincoin" 587 588 tok, err := permission.GetTokenFromShortcode(testInstance, shortcode) 589 assert.Empty(t, tok) 590 assert.NotNil(t, err) 591 assert.Contains(t, err.Error(), "no permission doc for shortcode") 592 }) 593 594 t.Run("GetMultipleShortCode", func(t *testing.T) { 595 e := testutils.CreateTestClient(t, ts.URL) 596 597 id, _, _ := createTestSubPermissions(e, token, "alice") 598 id2, _, _ := createTestSubPermissions(e, token, "alice") 599 perm, _ := permission.GetByID(testInstance, id) 600 perm2, _ := permission.GetByID(testInstance, id2) 601 602 perm2.ShortCodes["alice"] = perm.ShortCodes["alice"] 603 assert.NoError(t, couchdb.UpdateDoc(testInstance, perm2)) 604 605 _, err := permission.GetTokenFromShortcode(testInstance, perm.ShortCodes["alice"]) 606 607 assert.NotNil(t, err) 608 assert.Contains(t, err.Error(), "several permission docs for shortcode") 609 }) 610 611 t.Run("CannotFindToken", func(t *testing.T) { 612 e := testutils.CreateTestClient(t, ts.URL) 613 614 id, _, _ := createTestSubPermissions(e, token, "alice") 615 perm, _ := permission.GetByID(testInstance, id) 616 perm.Codes = map[string]string{} 617 assert.NoError(t, couchdb.UpdateDoc(testInstance, perm)) 618 619 _, err := permission.GetTokenFromShortcode(testInstance, perm.ShortCodes["alice"]) 620 assert.NotNil(t, err) 621 assert.Contains(t, err.Error(), "Cannot find token for shortcode") 622 }) 623 624 t.Run("TinyShortCodeOK", func(t *testing.T) { 625 e := testutils.CreateTestClient(t, ts.URL) 626 627 id, codes, _ := createTestTinyCode(e, token, "elise", "30m") 628 code := codes.Value("elise").String().NotEmpty().Raw() 629 assert.Len(t, code, 6) 630 631 perm, _ := permission.GetByID(testInstance, id) 632 assert.Equal(t, code, perm.ShortCodes["elise"]) 633 634 assert.NotNil(t, perm.ShortCodes) 635 636 e.GET("/permissions/self"). 637 WithHeader("Authorization", "Bearer "+perm.ShortCodes["elise"]). 638 Expect().Status(200) 639 640 tok, _ := permission.GetTokenFromShortcode(testInstance, perm.ShortCodes["elise"]) 641 assert.Equal(t, perm.Codes["elise"], tok) 642 }) 643 644 t.Run("TinyShortCodeInvalid", func(t *testing.T) { 645 e := testutils.CreateTestClient(t, ts.URL) 646 647 _, codes, _ := createTestTinyCode(e, token, "fanny", "24h") 648 649 code := codes.Value("fanny").String().NotEmpty().Raw() 650 assert.Len(t, code, 12) 651 }) 652 653 t.Run("GetForOauth", func(t *testing.T) { 654 // Install app 655 installer, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), &app.InstallerOptions{ 656 Operation: app.Install, 657 Type: consts.WebappType, 658 SourceURL: "registry://settings", 659 Slug: "settings", 660 Registries: testInstance.Registries(), 661 }) 662 assert.NoError(t, err) 663 installer.Run() 664 665 // Get app manifest 666 manifest, err := app.GetBySlug(testInstance, "settings", consts.WebappType) 667 assert.NoError(t, err) 668 669 // Create OAuth client 670 var oauthClient oauth.Client 671 672 u := "https://example.org/oauth/callback" 673 674 oauthClient.RedirectURIs = []string{u} 675 oauthClient.ClientName = "cozy-test-2" 676 oauthClient.SoftwareID = "registry://settings" 677 oauthClient.Create(testInstance) 678 679 parent, err := middlewares.GetForOauth(testInstance, &permission.Claims{ 680 RegisteredClaims: jwt.RegisteredClaims{ 681 Audience: jwt.ClaimStrings{consts.AccessTokenAudience}, 682 Issuer: testInstance.Domain, 683 IssuedAt: jwt.NewNumericDate(time.Now()), 684 Subject: clientID, 685 }, 686 Scope: "@io.cozy.apps/settings", 687 }, &oauthClient) 688 assert.NoError(t, err) 689 assert.True(t, parent.Permissions.HasSameRules(manifest.Permissions())) 690 }) 691 692 t.Run("ListPermission", func(t *testing.T) { 693 e := testutils.CreateTestClient(t, ts.URL) 694 695 ev1, _ := createTestEvent(testInstance) 696 ev2, _ := createTestEvent(testInstance) 697 ev3, _ := createTestEvent(testInstance) 698 699 parent, _ := middlewares.GetForOauth(testInstance, &permission.Claims{ 700 RegisteredClaims: jwt.RegisteredClaims{ 701 Audience: jwt.ClaimStrings{consts.AccessTokenAudience}, 702 Issuer: testInstance.Domain, 703 IssuedAt: jwt.NewNumericDate(time.Now()), 704 Subject: clientID, 705 }, 706 Scope: "io.cozy.events", 707 }, clientVal) 708 709 p1 := permission.Set{ 710 permission.Rule{ 711 Type: "io.cozy.events", 712 Verbs: permission.Verbs(permission.DELETE, permission.PATCH), 713 Values: []string{ev1.ID()}, 714 }, 715 } 716 p2 := permission.Set{ 717 permission.Rule{ 718 Type: "io.cozy.events", 719 Verbs: permission.Verbs(permission.GET), 720 Values: []string{ev2.ID()}, 721 }, 722 } 723 724 perm1 := permission.Permission{ 725 Permissions: p1, 726 } 727 perm2 := permission.Permission{ 728 Permissions: p2, 729 } 730 codes := map[string]string{"bob": "secret"} 731 _, _ = permission.CreateShareSet(testInstance, parent, parent.SourceID, codes, nil, perm1, nil) 732 _, _ = permission.CreateShareSet(testInstance, parent, parent.SourceID, codes, nil, perm2, nil) 733 734 obj := e.POST("/permissions/exists"). 735 WithHeader("Authorization", "Bearer "+token). 736 WithHeader("Content-Type", "application/json"). 737 WithBytes([]byte(`{ 738 "data": [ 739 { "type": "io.cozy.events", "id": "` + ev1.ID() + `" }, 740 { "type": "io.cozy.events", "id": "` + ev2.ID() + `" }, 741 { "type": "io.cozy.events", "id": "non-existing-id" }, 742 { "type": "io.cozy.events", "id": "another-fake-id" }, 743 { "type": "io.cozy.events", "id": "` + ev3.ID() + `" } 744 ] 745 }`)). 746 Expect().Status(200). 747 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 748 Object() 749 750 data := obj.Value("data").Array() 751 data.Length().Equal(2) 752 753 res := data.Find(func(_ int, value *httpexpect.Value) bool { 754 value.Object().ValueEqual("id", ev1.ID()) 755 return true 756 }) 757 res.Object().ValueEqual("type", "io.cozy.events") 758 res.Object().ValueEqual("verbs", []string{"PATCH", "DELETE"}) 759 760 res = data.Find(func(_ int, value *httpexpect.Value) bool { 761 value.Object().ValueEqual("id", ev2.ID()) 762 return true 763 }) 764 res.Object().ValueEqual("type", "io.cozy.events") 765 res.Object().ValueEqual("verbs", []string{"GET"}) 766 767 obj = e.GET("/permissions/doctype/io.cozy.events/shared-by-link"). 768 WithHeader("Authorization", "Bearer "+token). 769 Expect().Status(200). 770 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 771 Object() 772 773 data = obj.Value("data").Array() 774 data.Length().Equal(2) 775 data.Element(0).Object().Value("id").String(). 776 NotEqual(data.Element(1).Object().Value("id").String().Raw()) 777 778 obj = e.GET("/permissions/doctype/io.cozy.events/shared-by-link"). 779 WithQuery("page[limit]", 1). 780 WithHeader("Authorization", "Bearer "+token). 781 Expect().Status(200). 782 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 783 Object() 784 785 data = obj.Value("data").Array() 786 data.Length().Equal(1) 787 obj.Path("$.links.next").String().NotEmpty() 788 }) 789 790 t.Run("ShowPermissions", func(t *testing.T) { 791 e := testutils.CreateTestClient(t, ts.URL) 792 id, _, _ := createTestSubPermissions(e, token, "alice") 793 794 obj := e.GET("/permissions/"+id). 795 WithHeader("Authorization", "Bearer "+token). 796 Expect().Status(200). 797 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 798 Object() 799 800 perms := obj.Path("$.data.attributes.permissions").Object() 801 802 for _, r := range perms.Iter() { 803 r.Object().ValueEqual("type", "io.cozy.files") 804 r.Object().ValueEqual("verbs", []interface{}{"GET"}) 805 } 806 }) 807 808 t.Run("ShowPermissionsFail", func(t *testing.T) { 809 e := testutils.CreateTestClient(t, ts.URL) 810 id, _, _ := createTestSubPermissions(e, token, "alice") 811 _, otherToken := setup.GetTestClient("io.cozy.tags") 812 813 e.GET("/permissions/"+id). 814 WithHeader("Authorization", "Bearer "+otherToken). 815 Expect().Status(403) 816 }) 817 818 t.Run("CreatePermissionWithoutMetadata", func(t *testing.T) { 819 e := testutils.CreateTestClient(t, ts.URL) 820 821 // Install the app 822 installer, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), &app.InstallerOptions{ 823 Operation: app.Install, 824 Type: consts.WebappType, 825 SourceURL: "registry://drive", 826 Slug: "drive", 827 Registries: testInstance.Registries(), 828 }) 829 assert.NoError(t, err) 830 _, err = installer.RunSync() 831 assert.NoError(t, err) 832 833 tok, err := testInstance.MakeJWT(permission.TypeWebapp, 834 "drive", "io.cozy.files", "", time.Now()) 835 assert.NoError(t, err) 836 837 // Request to create a permission 838 obj := e.POST("/permissions"). 839 WithHeader("Authorization", "Bearer "+tok). 840 WithHeader("Content-Type", "application/json"). 841 WithHost(testInstance.Domain). 842 WithBytes([]byte(`{ 843 "data": { 844 "type": "io.cozy.permissions", 845 "attributes": { 846 "permissions": { 847 "files": { 848 "type": "io.cozy.files", 849 "verbs": ["GET"] 850 } 851 } 852 } 853 } 854 }`)). 855 Expect().Status(200). 856 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 857 Object() 858 859 // Assert a cozyMetadata has been added 860 meta := obj.Path("$.data.attributes.cozyMetadata").Object() 861 meta.ValueEqual("createdByApp", "drive") 862 meta.ValueEqual("doctypeVersion", "1") 863 meta.ValueEqual("metadataVersion", 1) 864 meta.Value("createdAt").String().AsDateTime(time.RFC3339). 865 InRange(time.Now().Add(-5*time.Second), time.Now().Add(5*time.Second)) 866 867 // Clean 868 uninstaller, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), 869 &app.InstallerOptions{ 870 Operation: app.Delete, 871 Type: consts.WebappType, 872 Slug: "drive", 873 SourceURL: "registry://drive", 874 Registries: testInstance.Registries(), 875 }, 876 ) 877 assert.NoError(t, err) 878 879 _, err = uninstaller.RunSync() 880 assert.NoError(t, err) 881 }) 882 883 t.Run("CreatePermissionWithMetadata", func(t *testing.T) { 884 e := testutils.CreateTestClient(t, ts.URL) 885 886 // Install the app 887 installer, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), &app.InstallerOptions{ 888 Operation: app.Install, 889 Type: consts.WebappType, 890 SourceURL: "registry://drive", 891 Slug: "drive", 892 Registries: testInstance.Registries(), 893 }) 894 assert.NoError(t, err) 895 _, err = installer.RunSync() 896 assert.NoError(t, err) 897 898 tok, err := testInstance.MakeJWT(permission.TypeWebapp, 899 "drive", "io.cozy.files", "", time.Now()) 900 assert.NoError(t, err) 901 902 // Request to create a permission 903 obj := e.POST("/permissions"). 904 WithHeader("Authorization", "Bearer "+tok). 905 WithHeader("Content-Type", "application/json"). 906 WithHost(testInstance.Domain). 907 WithBytes([]byte(`{ 908 "data": { 909 "type":"io.cozy.permissions", 910 "attributes":{ 911 "permissions":{ 912 "files":{ 913 "type":"io.cozy.files", 914 "verbs":["GET"] 915 } 916 }, 917 "cozyMetadata":{"createdByApp":"foobar"} 918 } 919 } 920 }`)). 921 Expect().Status(200). 922 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 923 Object() 924 925 // Assert a cozyMetadata has been added 926 meta := obj.Path("$.data.attributes.cozyMetadata").Object() 927 meta.ValueEqual("createdByApp", "foobar") 928 meta.ValueEqual("doctypeVersion", "1") 929 meta.ValueEqual("metadataVersion", 1) 930 931 // Clean 932 uninstaller, err := app.NewInstaller(testInstance, app.Copier(consts.WebappType, testInstance), 933 &app.InstallerOptions{ 934 Operation: app.Delete, 935 Type: consts.WebappType, 936 Slug: "drive", 937 SourceURL: "registry://drive", 938 Registries: testInstance.Registries(), 939 }, 940 ) 941 assert.NoError(t, err) 942 943 _, err = uninstaller.RunSync() 944 assert.NoError(t, err) 945 }) 946 } 947 948 func createTestSubPermissions(e *httpexpect.Expect, tok string, codes string) (string, *httpexpect.Object, error) { 949 obj := e.POST("/permissions"). 950 WithQuery("codes", codes). 951 WithHeader("Authorization", "Bearer "+tok). 952 WithHeader("Content-Type", "application/json"). 953 WithBytes([]byte(`{ 954 "data": { 955 "type": "io.cozy.permissions", 956 "attributes": { 957 "permissions": { 958 "whatever": { 959 "type": "io.cozy.files", 960 "verbs": ["GET"], 961 "values": ["io.cozy.music"] 962 }, 963 "otherrule": { 964 "type": "io.cozy.files", 965 "verbs": ["GET"], 966 "values": ["some-other-dir"] 967 } 968 } 969 } 970 } 971 }`)). 972 Expect().Status(200). 973 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 974 Object() 975 976 data := obj.Value("data").Object() 977 id := data.Value("id").String() 978 result := obj.Path("$.data.attributes.codes").Object() 979 980 return id.Raw(), result, nil 981 } 982 983 func createTestTinyCode(e *httpexpect.Expect, tok string, codes string, ttl string) (string, *httpexpect.Object, error) { 984 obj := e.POST("/permissions"). 985 WithQuery("codes", codes). 986 WithQuery("tiny", true). 987 WithQuery("ttl", ttl). 988 WithHeader("Authorization", "Bearer "+tok). 989 WithHeader("Content-Type", "application/json"). 990 WithBytes([]byte(`{ 991 "data": { 992 "type": "io.cozy.permissions", 993 "attributes": { 994 "permissions": { 995 "whatever": { 996 "type": "io.cozy.files", 997 "verbs": ["GET"], 998 "values": ["id.` + codes + `"] 999 } 1000 } 1001 } 1002 } 1003 }`)). 1004 Expect().Status(200). 1005 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 1006 Object() 1007 1008 data := obj.Value("data").Object() 1009 id := data.Value("id").String() 1010 result := obj.Path("$.data.attributes.shortcodes").Object() 1011 1012 return id.Raw(), result, nil 1013 } 1014 1015 func createTestEvent(i *instance.Instance) (*couchdb.JSONDoc, error) { 1016 e := &couchdb.JSONDoc{ 1017 Type: "io.cozy.events", 1018 M: map[string]interface{}{"test": "value"}, 1019 } 1020 err := couchdb.CreateDoc(i, e) 1021 return e, err 1022 }