Return rights when reading a single item #626

Merged
konrad merged 16 commits from feature/show-rights into master 2020-08-10 12:11:45 +00:00
36 changed files with 165 additions and 68 deletions

3
go.mod
View File

@ -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

13
go.sum
View File

@ -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=

View File

@ -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"))
})
})
})

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
})

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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"] {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 <jwt-token>`-header to authenticate successfully.
// @description

View File

@ -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 <jwt-token>`-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n<!-- ReDoc-Inject: <security-definitions> -->",
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 <jwt-token>`-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n<!-- ReDoc-Inject: <security-definitions> -->",
}
type s struct{}

View File

@ -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",

View File

@ -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 <jwt-token>`-header to authenticate successfully.