diff --git a/go.mod b/go.mod index eb167f215..028ff4b40 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 a9b6b3fbf..8b2b61123 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 75a91c82e..a4486257d 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 d3ced45a4..73eb1197a 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 60f26061c..86d2cb6e2 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 68b1afe1c..f3d24c181 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 17d5742c1..85d70adf1 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 e0a59cd25..2849fb012 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 790ef3e05..5ffdcf6cb 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 7a70fd912..ed263ed65 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 2aab2357f..b8635f78c 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 81d0c259d..47816b6d2 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 89bd542e9..4c2f8afa0 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 e38a204ae..066fbcfc6 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 fe14b4ce5..cb69d92c0 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 f5b88ab33..b25d32f0a 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 115bc81dc..3e5c4ffe3 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 f762d7b6d..f74f50dab 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 6b4eea98c..5e9bbb6f9 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 c1306a794..1edad9c32 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 19b32a112..082b2f378 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 b1fec2d53..b81f72d22 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 98658dc45..568a5fe59 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 5239b2846..ca4e00b7a 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 8eb138347..49ee1ceaf 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 0ba1ffd1f..0a926fa1d 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 bba8fc2c3..700ebcc10 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 c4a462e6f..9c482ab1d 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 b103ff59e..3142b1d98 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 95fc7d368..b524408ee 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 ad22d5697..1f9061deb 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 7691a321e..1abc683d0 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 ad384d423..810ed1b72 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 949d99a5e..8e1c856aa 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 46ca36fee..9bcb5a3ed 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 58e919a2c..0b44c392f 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.