From bd8c1c3bb7d58c146530c32ddd8578eb72072f4a Mon Sep 17 00:00:00 2001 From: konrad Date: Mon, 10 Aug 2020 12:11:43 +0000 Subject: [PATCH] Return rights when reading a single item (#626) Fix lint Update docs Fix loading all rights (list & namespace) Add tests Update web framework Make tests run again Update all calls to CanRead methods Update task attachment & task comment & task rights to return the max right Update team rights to return the max right Update namespace rights to return the max right Update list rights to return the max right Update link share rights to return the max right Update label rights to return the max right Update web dependency Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/626 --- go.mod | 3 +- go.sum | 13 ++++++ pkg/integrations/list_test.go | 16 ++++++- pkg/models/label_rights.go | 29 ++++++++---- pkg/models/label_task.go | 4 +- pkg/models/label_task_rights.go | 2 +- pkg/models/label_test.go | 2 +- pkg/models/link_sharing.go | 2 +- pkg/models/link_sharing_rights.go | 6 +-- pkg/models/list_duplicate.go | 2 +- pkg/models/list_rights.go | 46 +++++++++++++++----- pkg/models/list_team.go | 2 +- pkg/models/list_users.go | 2 +- pkg/models/namespace_rights.go | 38 +++++++++++----- pkg/models/namespace_team.go | 2 +- pkg/models/namespace_test.go | 6 +-- pkg/models/namespace_users.go | 2 +- pkg/models/task_assignees.go | 4 +- pkg/models/task_attachment_rights.go | 2 +- pkg/models/task_attachment_test.go | 4 +- pkg/models/task_collection.go | 2 +- pkg/models/task_comment_rights.go | 2 +- pkg/models/task_comments.go | 2 +- pkg/models/task_relation_rights.go | 2 +- pkg/models/tasks_rights.go | 2 +- pkg/models/teams_rights.go | 14 ++++-- pkg/models/teams_rights_test.go | 2 +- pkg/modules/background/handler/background.go | 2 +- pkg/routes/api/v1/list_by_namespace.go | 2 +- pkg/routes/api/v1/task_attachment.go | 2 +- pkg/routes/api/v1/user_list.go | 2 +- pkg/routes/caldav/listStorageProvider.go | 2 +- pkg/routes/routes.go | 3 ++ pkg/swagger/docs.go | 2 +- pkg/swagger/swagger.json | 2 +- pkg/swagger/swagger.yaml | 3 ++ 36 files changed, 165 insertions(+), 68 deletions(-) diff --git a/go.mod b/go.mod index eb167f2157..028ff4b40d 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ module code.vikunja.io/api require ( 4d63.com/tz v1.1.0 - code.vikunja.io/web v0.0.0-20200618164749-a5f3d450d39a + code.vikunja.io/web v0.0.0-20200809154828-8767618f181f gitea.com/xorm/xorm-redis-cache v0.2.0 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a @@ -69,7 +69,6 @@ require ( golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/image v0.0.0-20200801110659-972c09e46d76 golang.org/x/lint v0.0.0-20200302205851-738671d3881b - golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect golang.org/x/text v0.3.3 // indirect golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index a9b6b3fbfc..8b2b611236 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,10 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= code.vikunja.io/web v0.0.0-20200618164749-a5f3d450d39a h1:RiLIcnTTBP43QlL7nL0ko+PkzaBUCp7NmgogPeZBx5I= code.vikunja.io/web v0.0.0-20200618164749-a5f3d450d39a/go.mod h1:q3to9xazLf9XoqIRk1Y+YCjGr5TYgpQFNSVclCKrmEQ= +code.vikunja.io/web v0.0.0-20200809150710-7e12686f28b9 h1:NWXOCZ+FI9pXwpBNISsZil9erTEn25AVfLKcw/J0Sw4= +code.vikunja.io/web v0.0.0-20200809150710-7e12686f28b9/go.mod h1:vDWiCtftF6LNCCrem7mjstPWMgzLUvMW/L4YwIQ1Voo= +code.vikunja.io/web v0.0.0-20200809154828-8767618f181f h1:Zgtk9lbJkGbKjdTC78mg/c2uNkesxDJs1YUIL9zGvco= +code.vikunja.io/web v0.0.0-20200809154828-8767618f181f/go.mod h1:vDWiCtftF6LNCCrem7mjstPWMgzLUvMW/L4YwIQ1Voo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= @@ -441,6 +445,8 @@ github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -663,6 +669,8 @@ github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8W github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.0 h1:y3yXRCoDvC2HTtIHvL2cc7Zd+bqA+zqDO6oQzsJO07E= +github.com/valyala/fasttemplate v1.2.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= @@ -705,6 +713,7 @@ golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -770,6 +779,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -821,6 +832,8 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9 h1:yi1hN8dcqI9l8klZfy4B8mJvFmmAxJEePIQQFNSd7Cs= +golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/integrations/list_test.go b/pkg/integrations/list_test.go index 75a91c82e9..a4486257d8 100644 --- a/pkg/integrations/list_test.go +++ b/pkg/integrations/list_test.go @@ -75,6 +75,7 @@ func TestList(t *testing.T) { assert.Contains(t, rec.Body.String(), `"owner":{"id":1,"username":"user1",`) assert.NotContains(t, rec.Body.String(), `"owner":{"id":2,"username":"user2",`) assert.NotContains(t, rec.Body.String(), `"tasks":`) + assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) // User 1 is owner so they should have admin rights. }) t.Run("Nonexisting", func(t *testing.T) { _, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9999"}) @@ -84,72 +85,85 @@ func TestList(t *testing.T) { t.Run("Rights check", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { // Owned by user13 - _, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "20"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "20"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`) + assert.Empty(t, rec.Result().Header.Get("x-max-rights")) }) t.Run("Shared Via Team readonly", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "6"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test6"`) + assert.Equal(t, "0", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via Team write", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "7"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test7"`) + assert.Equal(t, "1", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via Team admin", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "8"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test8"`) + assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via User readonly", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test9"`) + assert.Equal(t, "0", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via User write", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "10"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test10"`) + assert.Equal(t, "1", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via User admin", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "11"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test11"`) + assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "12"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test12"`) + assert.Equal(t, "0", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "13"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test13"`) + assert.Equal(t, "1", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "14"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test14"`) + assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "15"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test15"`) + assert.Equal(t, "0", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "16"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test16"`) + assert.Equal(t, "1", rec.Result().Header.Get("x-max-right")) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "17"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test17"`) + assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) }) }) }) diff --git a/pkg/models/label_rights.go b/pkg/models/label_rights.go index d3ced45a41..73eb1197a5 100644 --- a/pkg/models/label_rights.go +++ b/pkg/models/label_rights.go @@ -33,7 +33,7 @@ func (l *Label) CanDelete(a web.Auth) (bool, error) { } // CanRead checks if a user can read a label -func (l *Label) CanRead(a web.Auth) (bool, error) { +func (l *Label) CanRead(a web.Auth) (bool, int, error) { return l.hasAccessToLabel(a) } @@ -61,24 +61,37 @@ func (l *Label) isLabelOwner(a web.Auth) (bool, error) { } // Helper method to check if a user can see a specific label -func (l *Label) hasAccessToLabel(a web.Auth) (bool, error) { +func (l *Label) hasAccessToLabel(a web.Auth) (has bool, maxRight int, err error) { // TODO: add an extra check for link share handling // Get all tasks taskIDs, err := getUserTaskIDs(&user.User{ID: a.GetID()}) if err != nil { - return false, err + return false, 0, err } // Get all labels associated with these tasks - var labels []*Label - has, err := x.Table("labels"). - Select("labels.*"). + ll := &LabelTask{} + has, err = x.Table("labels"). + Select("label_task.*"). Join("LEFT", "label_task", "label_task.label_id = labels.id"). Where("label_task.label_id is not null OR labels.created_by_id = ?", a.GetID()). Or(builder.In("label_task.task_id", taskIDs)). And("labels.id = ?", l.ID). - Exist(&labels) - return has, err + Exist(ll) + if err != nil { + return + } + + // Since the right depends on the task the label is associated with, we need to check that too. + if ll.TaskID > 0 { + t := &Task{ID: ll.TaskID} + _, maxRight, err = t.CanRead(a) + if err != nil { + return + } + } + + return } diff --git a/pkg/models/label_task.go b/pkg/models/label_task.go index 60f26061c5..86d2cb6e20 100644 --- a/pkg/models/label_task.go +++ b/pkg/models/label_task.go @@ -113,7 +113,7 @@ func (lt *LabelTask) Create(a web.Auth) (err error) { func (lt *LabelTask) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { // Check if the user has the right to see the task task := Task{ID: lt.TaskID} - canRead, err := task.CanRead(a) + canRead, _, err := task.CanRead(a) if err != nil { return nil, 0, 0, err } @@ -291,7 +291,7 @@ func (t *Task) updateTaskLabels(creator web.Auth, labels []*Label) (err error) { } // Check if the user has the rights to see the label he is about to add - hasAccessToLabel, err := label.hasAccessToLabel(creator) + hasAccessToLabel, _, err := label.hasAccessToLabel(creator) if err != nil { return err } diff --git a/pkg/models/label_task_rights.go b/pkg/models/label_task_rights.go index 68b1afe1c8..f3d24c181a 100644 --- a/pkg/models/label_task_rights.go +++ b/pkg/models/label_task_rights.go @@ -27,7 +27,7 @@ func (lt *LabelTask) CanCreate(a web.Auth) (bool, error) { return false, err } - hasAccessTolabel, err := label.hasAccessToLabel(a) + hasAccessTolabel, _, err := label.hasAccessToLabel(a) if err != nil || !hasAccessTolabel { // If the user doesn't have access to the label, we can error out here return false, err } diff --git a/pkg/models/label_test.go b/pkg/models/label_test.go index 17d5742c11..85d70adf1a 100644 --- a/pkg/models/label_test.go +++ b/pkg/models/label_test.go @@ -240,7 +240,7 @@ func TestLabel_ReadOne(t *testing.T) { Rights: tt.fields.Rights, } - allowed, _ := l.CanRead(tt.auth) + allowed, _, _ := l.CanRead(tt.auth) if !allowed && !tt.wantForbidden { t.Errorf("Label.CanRead() forbidden, want %v", tt.wantForbidden) } diff --git a/pkg/models/link_sharing.go b/pkg/models/link_sharing.go index e0a59cd258..2849fb0129 100644 --- a/pkg/models/link_sharing.go +++ b/pkg/models/link_sharing.go @@ -153,7 +153,7 @@ func (share *LinkSharing) ReadOne() (err error) { // @Router /lists/{list}/shares [get] func (share *LinkSharing) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) { list := &List{ID: share.ListID} - can, err := list.CanRead(a) + can, _, err := list.CanRead(a) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/link_sharing_rights.go b/pkg/models/link_sharing_rights.go index 790ef3e05c..5ffdcf6cbd 100644 --- a/pkg/models/link_sharing_rights.go +++ b/pkg/models/link_sharing_rights.go @@ -19,15 +19,15 @@ package models import "code.vikunja.io/web" // CanRead implements the read right check for a link share -func (share *LinkSharing) CanRead(a web.Auth) (bool, error) { +func (share *LinkSharing) CanRead(a web.Auth) (bool, int, error) { // Don't allow creating link shares if the user itself authenticated with a link share if _, is := a.(*LinkSharing); is { - return false, nil + return false, 0, nil } l, err := GetListByShareHash(share.Hash) if err != nil { - return false, err + return false, 0, err } return l.CanRead(a) } diff --git a/pkg/models/list_duplicate.go b/pkg/models/list_duplicate.go index 7a70fd9129..ed263ed65d 100644 --- a/pkg/models/list_duplicate.go +++ b/pkg/models/list_duplicate.go @@ -41,7 +41,7 @@ type ListDuplicate struct { func (ld *ListDuplicate) CanCreate(a web.Auth) (canCreate bool, err error) { // List Exists + user has read access to list ld.List = &List{ID: ld.ListID} - canRead, err := ld.List.CanRead(a) + canRead, _, err := ld.List.CanRead(a) if err != nil || !canRead { return canRead, err } diff --git a/pkg/models/list_rights.go b/pkg/models/list_rights.go index 2aab2357f7..b8635f78c5 100644 --- a/pkg/models/list_rights.go +++ b/pkg/models/list_rights.go @@ -54,7 +54,7 @@ func (l *List) CanWrite(a web.Auth) (bool, error) { return canWrite, errIsArchived } - canWrite, err = originalList.checkRight(a, RightWrite, RightAdmin) + canWrite, _, err = originalList.checkRight(a, RightWrite, RightAdmin) if err != nil { return false, err } @@ -62,21 +62,21 @@ func (l *List) CanWrite(a web.Auth) (bool, error) { } // CanRead checks if a user has read access to a list -func (l *List) CanRead(a web.Auth) (bool, error) { +func (l *List) CanRead(a web.Auth) (bool, int, error) { // Check if the user is either owner or can read if err := l.GetSimpleByID(); err != nil { - return false, err + return false, 0, err } // Check if we're dealing with a share auth shareAuth, ok := a.(*LinkSharing) if ok { return l.ID == shareAuth.ListID && - (shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), nil + (shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil } if l.isOwner(&user.User{ID: a.GetID()}) { - return true, nil + return true, int(RightAdmin), nil } return l.checkRight(a, RightRead, RightWrite, RightAdmin) } @@ -123,7 +123,8 @@ func (l *List) IsAdmin(a web.Auth) (bool, error) { if originalList.isOwner(&user.User{ID: a.GetID()}) { return true, nil } - return originalList.checkRight(a, RightAdmin) + is, _, err := originalList.checkRight(a, RightAdmin) + return is, err } // Little helper function to check if a user is list owner @@ -132,7 +133,7 @@ func (l *List) isOwner(u *user.User) bool { } // Checks n different rights for any given user -func (l *List) checkRight(a web.Auth, rights ...Right) (bool, error) { +func (l *List) checkRight(a web.Auth, rights ...Right) (bool, int, error) { /* The following loop creates an sql condition like this one: @@ -174,7 +175,17 @@ func (l *List) checkRight(a web.Auth, rights ...Right) (bool, error) { // If the user is the owner of a namespace, it has any right, all the time conds = append(conds, builder.Eq{"n.owner_id": a.GetID()}) - exists, err := x.Select("l.*"). + type allListRights struct { + UserNamespace NamespaceUser `xorm:"extends"` + UserList ListUser `xorm:"extends"` + + TeamNamespace TeamNamespace `xorm:"extends"` + TeamList TeamList `xorm:"extends"` + } + + r := &allListRights{} + var maxRight = 0 + exists, err := x. Table("list"). Alias("l"). // User stuff @@ -193,6 +204,21 @@ func (l *List) checkRight(a web.Auth, rights ...Right) (bool, error) { ), builder.Eq{"l.id": l.ID}, )). - Exist(&List{}) - return exists, err + Get(r) + + // Figure out the max right and return it + if int(r.UserNamespace.Right) > maxRight { + maxRight = int(r.UserNamespace.Right) + } + if int(r.UserList.Right) > maxRight { + maxRight = int(r.UserList.Right) + } + if int(r.TeamNamespace.Right) > maxRight { + maxRight = int(r.TeamNamespace.Right) + } + if int(r.TeamList.Right) > maxRight { + maxRight = int(r.TeamList.Right) + } + + return exists, maxRight, err } diff --git a/pkg/models/list_team.go b/pkg/models/list_team.go index 81d0c259d9..47816b6d2f 100644 --- a/pkg/models/list_team.go +++ b/pkg/models/list_team.go @@ -168,7 +168,7 @@ func (tl *TeamList) Delete() (err error) { func (tl *TeamList) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) { // Check if the user can read the namespace l := &List{ID: tl.ListID} - canRead, err := l.CanRead(a) + canRead, _, err := l.CanRead(a) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/list_users.go b/pkg/models/list_users.go index 89bd542e98..4c2f8afa03 100644 --- a/pkg/models/list_users.go +++ b/pkg/models/list_users.go @@ -174,7 +174,7 @@ func (lu *ListUser) Delete() (err error) { func (lu *ListUser) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { // Check if the user has access to the list l := &List{ID: lu.ListID} - canRead, err := l.CanRead(a) + canRead, _, err := l.CanRead(a) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/namespace_rights.go b/pkg/models/namespace_rights.go index e38a204ae1..066fbcfc62 100644 --- a/pkg/models/namespace_rights.go +++ b/pkg/models/namespace_rights.go @@ -23,16 +23,18 @@ import ( // CanWrite checks if a user has write access to a namespace func (n *Namespace) CanWrite(a web.Auth) (bool, error) { - return n.checkRight(a, RightWrite, RightAdmin) + can, _, err := n.checkRight(a, RightWrite, RightAdmin) + return can, err } // IsAdmin returns true or false if the user is admin on that namespace or not func (n *Namespace) IsAdmin(a web.Auth) (bool, error) { - return n.checkRight(a, RightAdmin) + is, _, err := n.checkRight(a, RightAdmin) + return is, err } // CanRead checks if a user has read access to that namespace -func (n *Namespace) CanRead(a web.Auth) (bool, error) { +func (n *Namespace) CanRead(a web.Auth) (bool, int, error) { return n.checkRight(a, RightRead, RightWrite, RightAdmin) } @@ -56,22 +58,22 @@ func (n *Namespace) CanCreate(a web.Auth) (bool, error) { return true, nil } -func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) { +func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, int, error) { // If the auth is a link share, don't do anything if _, is := a.(*LinkSharing); is { - return false, nil + return false, 0, nil } // Get the namespace and check the right nn := &Namespace{ID: n.ID} err := nn.GetSimpleByID() if err != nil { - return false, err + return false, 0, err } if a.GetID() == n.OwnerID { - return true, nil + return true, int(RightAdmin), nil } /* @@ -104,7 +106,14 @@ func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) { )) } - exists, err := x.Select("namespaces.*"). + type allRights struct { + UserNamespace NamespaceUser `xorm:"extends"` + TeamNamespace TeamNamespace `xorm:"extends"` + } + + var maxRights = 0 + r := &allRights{} + exists, err := x.Select("*"). Table("namespaces"). // User stuff Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id"). @@ -118,6 +127,15 @@ func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) { ), builder.Eq{"namespaces.id": n.ID}, )). - Exist(&List{}) - return exists, err + Exist(r) + + // Figure out the max right and return it + if int(r.UserNamespace.Right) > maxRights { + maxRights = int(r.UserNamespace.Right) + } + if int(r.TeamNamespace.Right) > maxRights { + maxRights = int(r.TeamNamespace.Right) + } + + return exists, maxRights, err } diff --git a/pkg/models/namespace_team.go b/pkg/models/namespace_team.go index fe14b4ce5e..cb69d92c0b 100644 --- a/pkg/models/namespace_team.go +++ b/pkg/models/namespace_team.go @@ -153,7 +153,7 @@ func (tn *TeamNamespace) Delete() (err error) { func (tn *TeamNamespace) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { // Check if the user can read the namespace n := Namespace{ID: tn.NamespaceID} - canRead, err := n.CanRead(a) + canRead, _, err := n.CanRead(a) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go index f5b88ab337..b25d32f0af 100644 --- a/pkg/models/namespace_test.go +++ b/pkg/models/namespace_test.go @@ -47,7 +47,7 @@ func TestNamespace_Create(t *testing.T) { assert.NoError(t, err) // check if it really exists - allowed, err = dummynamespace.CanRead(doer) + allowed, _, err = dummynamespace.CanRead(doer) assert.NoError(t, err) assert.True(t, allowed) err = dummynamespace.ReadOne() @@ -78,7 +78,7 @@ func TestNamespace_Create(t *testing.T) { // Check if it was updated assert.Equal(t, "Dolor sit amet.", dummynamespace.Description) // Get it and check it again - allowed, err = dummynamespace.CanRead(doer) + allowed, _, err = dummynamespace.CanRead(doer) assert.NoError(t, err) assert.True(t, allowed) err = dummynamespace.ReadOne() @@ -116,7 +116,7 @@ func TestNamespace_Create(t *testing.T) { assert.True(t, IsErrNamespaceDoesNotExist(err)) // Check if it was successfully deleted - allowed, err = dummynamespace.CanRead(doer) + allowed, _, err = dummynamespace.CanRead(doer) assert.False(t, allowed) assert.Error(t, err) assert.True(t, IsErrNamespaceDoesNotExist(err)) diff --git a/pkg/models/namespace_users.go b/pkg/models/namespace_users.go index 115bc81dc4..3e5c4ffe33 100644 --- a/pkg/models/namespace_users.go +++ b/pkg/models/namespace_users.go @@ -160,7 +160,7 @@ func (nu *NamespaceUser) Delete() (err error) { func (nu *NamespaceUser) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { // Check if the user has access to the namespace l := Namespace{ID: nu.NamespaceID} - canRead, err := l.CanRead(a) + canRead, _, err := l.CanRead(a) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/task_assignees.go b/pkg/models/task_assignees.go index f762d7b6d2..f74f50dab6 100644 --- a/pkg/models/task_assignees.go +++ b/pkg/models/task_assignees.go @@ -207,7 +207,7 @@ func (t *Task) addNewAssigneeByID(newAssigneeID int64, list *List) (err error) { if err != nil { return err } - canRead, err := list.CanRead(newAssignee) + canRead, _, err := list.CanRead(newAssignee) if err != nil { return err } @@ -247,7 +247,7 @@ func (la *TaskAssginee) ReadAll(a web.Auth, search string, page int, perPage int return nil, 0, 0, err } - can, err := task.CanRead(a) + can, _, err := task.CanRead(a) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/task_attachment_rights.go b/pkg/models/task_attachment_rights.go index 6b4eea98cb..5e9bbb6f90 100644 --- a/pkg/models/task_attachment_rights.go +++ b/pkg/models/task_attachment_rights.go @@ -19,7 +19,7 @@ package models import "code.vikunja.io/web" // CanRead checks if the user can see an attachment -func (ta *TaskAttachment) CanRead(a web.Auth) (bool, error) { +func (ta *TaskAttachment) CanRead(a web.Auth) (bool, int, error) { t := &Task{ID: ta.TaskID} return t.CanRead(a) } diff --git a/pkg/models/task_attachment_test.go b/pkg/models/task_attachment_test.go index c1306a794d..1edad9c327 100644 --- a/pkg/models/task_attachment_test.go +++ b/pkg/models/task_attachment_test.go @@ -165,14 +165,14 @@ func TestTaskAttachment_Rights(t *testing.T) { t.Run("Allowed", func(t *testing.T) { db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 1} - can, err := ta.CanRead(u) + can, _, err := ta.CanRead(u) assert.NoError(t, err) assert.True(t, can) }) t.Run("Forbidden", func(t *testing.T) { db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 14} - can, err := ta.CanRead(u) + can, _, err := ta.CanRead(u) assert.NoError(t, err) assert.False(t, can) }) diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 19b32a112c..082b2f3789 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -166,7 +166,7 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i } else { // Check the list exists and the user has acess on it list := &List{ID: tf.ListID} - canRead, err := list.CanRead(a) + canRead, _, err := list.CanRead(a) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/task_comment_rights.go b/pkg/models/task_comment_rights.go index b1fec2d53e..b81f72d22e 100644 --- a/pkg/models/task_comment_rights.go +++ b/pkg/models/task_comment_rights.go @@ -20,7 +20,7 @@ package models import "code.vikunja.io/web" // CanRead checks if a user can read a comment -func (tc *TaskComment) CanRead(a web.Auth) (bool, error) { +func (tc *TaskComment) CanRead(a web.Auth) (bool, int, error) { t := Task{ID: tc.TaskID} return t.CanRead(a) } diff --git a/pkg/models/task_comments.go b/pkg/models/task_comments.go index 98658dc457..568a5fe590 100644 --- a/pkg/models/task_comments.go +++ b/pkg/models/task_comments.go @@ -165,7 +165,7 @@ func (tc *TaskComment) ReadOne() (err error) { func (tc *TaskComment) ReadAll(auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { // Check if the user has access to the task - canRead, err := tc.CanRead(auth) + canRead, _, err := tc.CanRead(auth) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/task_relation_rights.go b/pkg/models/task_relation_rights.go index 5239b2846d..ca4e00b7a3 100644 --- a/pkg/models/task_relation_rights.go +++ b/pkg/models/task_relation_rights.go @@ -42,7 +42,7 @@ func (rel *TaskRelation) CanCreate(a web.Auth) (bool, error) { // We explicitly don't check if the two tasks are on the same list. otherTask := &Task{ID: rel.OtherTaskID} - has, err = otherTask.CanRead(a) + has, _, err = otherTask.CanRead(a) if err != nil { return false, err } diff --git a/pkg/models/tasks_rights.go b/pkg/models/tasks_rights.go index 8eb1383477..49ee1ceafc 100644 --- a/pkg/models/tasks_rights.go +++ b/pkg/models/tasks_rights.go @@ -38,7 +38,7 @@ func (t *Task) CanCreate(a web.Auth) (bool, error) { } // CanRead determines if a user can read a task -func (t *Task) CanRead(a web.Auth) (canRead bool, err error) { +func (t *Task) CanRead(a web.Auth) (canRead bool, maxRight int, err error) { //return t.canDoTask(a) // Get the task, error out if it doesn't exist *t, err = GetTaskByIDSimple(t.ID) diff --git a/pkg/models/teams_rights.go b/pkg/models/teams_rights.go index 0ba1ffd1f0..0a926fa1d4 100644 --- a/pkg/models/teams_rights.go +++ b/pkg/models/teams_rights.go @@ -60,9 +60,17 @@ func (t *Team) IsAdmin(a web.Auth) (bool, error) { } // CanRead returns true if the user has read access to the team -func (t *Team) CanRead(a web.Auth) (bool, error) { +func (t *Team) CanRead(a web.Auth) (bool, int, error) { // Check if the user is in the team - return x.Where("team_id = ?", t.ID). + tm := &TeamMember{} + can, err := x.Where("team_id = ?", t.ID). And("user_id = ?", a.GetID()). - Get(&TeamMember{}) + Get(tm) + + maxRights := 0 + if tm.Admin { + maxRights = int(RightAdmin) + } + + return can, maxRights, err } diff --git a/pkg/models/teams_rights_test.go b/pkg/models/teams_rights_test.go index bba8fc2c3f..700ebcc10f 100644 --- a/pkg/models/teams_rights_test.go +++ b/pkg/models/teams_rights_test.go @@ -104,7 +104,7 @@ func TestTeam_CanDoSomething(t *testing.T) { if got, _ := tm.CanUpdate(tt.args.a); got != tt.want["CanUpdate"] { t.Errorf("Team.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"]) } - if got, _ := tm.CanRead(tt.args.a); got != tt.want["CanRead"] { + if got, _, _ := tm.CanRead(tt.args.a); got != tt.want["CanRead"] { t.Errorf("Team.CanRead() = %v, want %v", got, tt.want["CanRead"]) } if got, _ := tm.IsAdmin(tt.args.a); got != tt.want["IsAdmin"] { diff --git a/pkg/modules/background/handler/background.go b/pkg/modules/background/handler/background.go index c4a462e6fc..9c482ab1dc 100644 --- a/pkg/modules/background/handler/background.go +++ b/pkg/modules/background/handler/background.go @@ -191,7 +191,7 @@ func GetListBackground(c echo.Context) error { // Check if a background for this list exists + Rights list := &models.List{ID: listID} - can, err := list.CanRead(auth) + can, _, err := list.CanRead(auth) if err != nil { return handler.HandleHTTPError(err, c) } diff --git a/pkg/routes/api/v1/list_by_namespace.go b/pkg/routes/api/v1/list_by_namespace.go index b103ff59ec..3142b1d98b 100644 --- a/pkg/routes/api/v1/list_by_namespace.go +++ b/pkg/routes/api/v1/list_by_namespace.go @@ -79,7 +79,7 @@ func getNamespace(c echo.Context) (namespace *models.Namespace, err error) { return } namespace = &models.Namespace{ID: namespaceID} - canRead, err := namespace.CanRead(user) + canRead, _, err := namespace.CanRead(user) if err != nil { return namespace, err } diff --git a/pkg/routes/api/v1/task_attachment.go b/pkg/routes/api/v1/task_attachment.go index 95fc7d3688..b524408ee0 100644 --- a/pkg/routes/api/v1/task_attachment.go +++ b/pkg/routes/api/v1/task_attachment.go @@ -119,7 +119,7 @@ func GetTaskAttachment(c echo.Context) error { if err != nil { return handler.HandleHTTPError(err, c) } - can, err := taskAttachment.CanRead(auth) + can, _, err := taskAttachment.CanRead(auth) if err != nil { return handler.HandleHTTPError(err, c) } diff --git a/pkg/routes/api/v1/user_list.go b/pkg/routes/api/v1/user_list.go index ad22d56979..1f9061deb6 100644 --- a/pkg/routes/api/v1/user_list.go +++ b/pkg/routes/api/v1/user_list.go @@ -78,7 +78,7 @@ func ListUsersForList(c echo.Context) error { return handler.HandleHTTPError(err, c) } - canRead, err := list.CanRead(auth) + canRead, _, err := list.CanRead(auth) if err != nil { return handler.HandleHTTPError(err, c) } diff --git a/pkg/routes/caldav/listStorageProvider.go b/pkg/routes/caldav/listStorageProvider.go index 7691a321e2..1abc683d0a 100644 --- a/pkg/routes/caldav/listStorageProvider.go +++ b/pkg/routes/caldav/listStorageProvider.go @@ -383,7 +383,7 @@ func (vlra *VikunjaListResourceAdapter) GetModTime() time.Time { } func (vcls *VikunjaCaldavListStorage) getListRessource(isCollection bool) (rr VikunjaListResourceAdapter, err error) { - can, err := vcls.list.CanRead(vcls.user) + can, _, err := vcls.list.CanRead(vcls.user) if err != nil { return } diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index ad384d4230..810ed1b726 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -21,6 +21,9 @@ // @description Every endpoint capable of pagination will return two headers: // @description * `x-pagination-total-pages`: The total number of available pages for this request // @description * `x-pagination-result-count`: The number of items returned for this request. +// @description # Rights +// @description All endpoints which return a single item (list, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`. +// @description This can be used to show or hide ui elements based on the rights the user has. // @description # Authorization // @description **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully. // @description diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 949d99a5e1..8e1c856aa2 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7436,7 +7436,7 @@ var SwaggerInfo = swaggerInfo{ BasePath: "/api/v1", Schemes: []string{}, Title: "Vikunja API", - Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n", + Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (list, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n", } type s struct{} diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 46ca36feed..9bcb5a3ed7 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "description": "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer \u003cjwt-token\u003e`-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n\u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e", + "description": "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (list, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read \u0026 Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer \u003cjwt-token\u003e`-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n\u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e", "title": "Vikunja API", "contact": { "name": "General Vikunja contact", diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 58e919a2c4..0b44c392f0 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -987,6 +987,9 @@ info: Every endpoint capable of pagination will return two headers: * `x-pagination-total-pages`: The total number of available pages for this request * `x-pagination-result-count`: The number of items returned for this request. + # Rights + All endpoints which return a single item (list, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`. + This can be used to show or hide ui elements based on the rights the user has. # Authorization **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully.