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  }