From 8d57923a7d5131c41de7d054ee8dce17d70ac14c Mon Sep 17 00:00:00 2001 From: konrad Date: Sat, 31 Aug 2019 20:56:41 +0000 Subject: [PATCH] Sharing of lists via public links (#94) --- Featurecreep.md | 2 +- config.yml.sample | 2 + docs/content/doc/setup/config.md | 2 + docs/content/doc/usage/errors.md | 2 + pkg/config/config.go | 18 +- pkg/integrations/integrations.go | 75 +- pkg/integrations/link_sharing_test.go | 989 +++++++++++++++++++++++++ pkg/integrations/list_test.go | 132 ++-- pkg/integrations/task_test.go | 168 ++--- pkg/migration/20190818210133.go | 53 ++ pkg/models/error.go | 47 ++ pkg/models/fixtures/link_sharing.yml | 24 + pkg/models/fixtures/tasks.yml | 6 + pkg/models/label.go | 16 +- pkg/models/label_rights.go | 12 + pkg/models/link_sharing.go | 207 ++++++ pkg/models/link_sharing_rights.go | 61 ++ pkg/models/list.go | 16 +- pkg/models/list_rights.go | 23 +- pkg/models/list_users_rights.go | 17 +- pkg/models/models.go | 1 + pkg/models/namespace.go | 4 + pkg/models/namespace_rights.go | 9 + pkg/models/task_assignees.go | 18 +- pkg/models/task_assignees_rights.go | 2 +- pkg/models/task_readall_test.go | 9 + pkg/models/tasks.go | 55 +- pkg/models/tasks_rights.go | 4 +- pkg/models/team_list_rights.go | 5 + pkg/models/team_members_rights.go | 5 + pkg/models/teams.go | 4 + pkg/models/teams_rights.go | 9 + pkg/models/user.go | 5 + pkg/routes/api/v1/auth.go | 67 ++ pkg/routes/api/v1/info.go | 16 +- pkg/routes/api/v1/link_sharing_auth.go | 50 ++ pkg/routes/api/v1/login.go | 22 +- pkg/routes/routes.go | 34 +- pkg/swagger/docs.go | 652 +++++++++++++--- pkg/swagger/swagger.json | 672 +++++++++++++---- pkg/swagger/swagger.yaml | 500 ++++++++++--- 41 files changed, 3425 insertions(+), 590 deletions(-) create mode 100644 pkg/integrations/link_sharing_test.go create mode 100644 pkg/migration/20190818210133.go create mode 100644 pkg/models/fixtures/link_sharing.yml create mode 100644 pkg/models/link_sharing.go create mode 100644 pkg/models/link_sharing_rights.go create mode 100644 pkg/routes/api/v1/auth.go create mode 100644 pkg/routes/api/v1/link_sharing_auth.go diff --git a/Featurecreep.md b/Featurecreep.md index c8f6751930a..fc13d65c204 100644 --- a/Featurecreep.md +++ b/Featurecreep.md @@ -296,7 +296,7 @@ Sorry for some of them being in German, I'll tranlate them at some point. * [ ] User online status * [ ] More sharing features (all of these with the already existing permissions) * [ ] Invite users per mail - * [ ] Share a link with/without password + * [x] Share a link with/without password * [ ] Comments on tasks * [ ] @mention users in tasks or comments to get them notified * [ ] Summary of tasks to do in a configurable interval (every day/week or so) diff --git a/config.yml.sample b/config.yml.sample index 245e45f6dad..722d9a78d22 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -18,6 +18,8 @@ service: enablemetrics: false # Enable the caldav endpoint, see the docs for more details enablecaldav: true + # Enable sharing of lists via a link + enablelinksharing: true database: # Database type to use. Supported types are mysql and sqlite. diff --git a/docs/content/doc/setup/config.md b/docs/content/doc/setup/config.md index 1644408f8a8..406dca81804 100644 --- a/docs/content/doc/setup/config.md +++ b/docs/content/doc/setup/config.md @@ -61,6 +61,8 @@ service: enablemetrics: false # Enable the caldav endpoint, see the docs for more details enablecaldav: true + # Enable sharing of lists via a link + enablelinksharing: true database: # Database type to use. Supported types are mysql and sqlite. diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index 80aa256777f..54a61b34358 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -14,6 +14,7 @@ This document describes the different errors Vikunja can return. | ErrorCode | HTTP Status Code | Description | |-----------|------------------|-------------| +| 0001 | 403 | Generic forbidden error. | | 1001 | 400 | A user with this username already exists. | | 1002 | 400 | A user with this email address already exists. | | 1004 | 400 | No username and password specified. | @@ -31,6 +32,7 @@ This document describes the different errors Vikunja can return. | 3001 | 404 | The list does not exist. | | 3004 | 403 | The user needs to have read permissions on that list to perform that action. | | 3005 | 400 | The list title cannot be empty. | +| 3006 | 404 | The list share does not exist. | | 4001 | 400 | The list task text cannot be empty. | | 4002 | 404 | The list task does not exist. | | 4003 | 403 | All bulk editing tasks must belong to the same list. | diff --git a/pkg/config/config.go b/pkg/config/config.go index 7ceb82c12ca..e98f3d10244 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -33,14 +33,15 @@ type Key string // These constants hold all config value keys const ( - ServiceJWTSecret Key = `service.JWTSecret` - ServiceInterface Key = `service.interface` - ServiceFrontendurl Key = `service.frontendurl` - ServiceEnableCaldav Key = `service.enablecaldav` - ServiceRootpath Key = `service.rootpath` - ServicePageCount Key = `service.pagecount` - ServiceEnableMetrics Key = `service.enablemetrics` - ServiceMotd Key = `service.motd` + ServiceJWTSecret Key = `service.JWTSecret` + ServiceInterface Key = `service.interface` + ServiceFrontendurl Key = `service.frontendurl` + ServiceEnableCaldav Key = `service.enablecaldav` + ServiceRootpath Key = `service.rootpath` + ServicePageCount Key = `service.pagecount` + ServiceEnableMetrics Key = `service.enablemetrics` + ServiceMotd Key = `service.motd` + ServiceEnableLinkSharing Key = `service.enablelinksharing` DatabaseType Key = `database.type` DatabaseHost Key = `database.host` @@ -146,6 +147,7 @@ func InitConfig() { ServicePageCount.setDefault(50) ServiceEnableMetrics.setDefault(false) ServiceMotd.setDefault("") + ServiceEnableLinkSharing.setDefault(true) // Database DatabaseType.setDefault("sqlite") diff --git a/pkg/integrations/integrations.go b/pkg/integrations/integrations.go index fee47e44b34..98e304bd4d4 100644 --- a/pkg/integrations/integrations.go +++ b/pkg/integrations/integrations.go @@ -107,9 +107,9 @@ func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) return } -func addTokenToContext(t *testing.T, user *models.User, c echo.Context) { +func addUserTokenToContext(t *testing.T, user *models.User, c echo.Context) { // Get the token as a string - token, err := v1.CreateNewJWTTokenForUser(user) + token, err := v1.NewUserJWTAuthtoken(user) assert.NoError(t, err) // We send the string token through the parsing function to get a valid jwt.Token tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { @@ -119,8 +119,20 @@ func addTokenToContext(t *testing.T, user *models.User, c echo.Context) { c.Set("user", tken) } -func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *models.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { - c, rec := bootstrapTestRequest(t, method, payload, queryParams) +func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.Context) { + // Get the token as a string + token, err := v1.NewLinkShareJWTAuthtoken(share) + assert.NoError(t, err) + // We send the string token through the parsing function to get a valid jwt.Token + tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { + return []byte(config.ServiceJWTSecret.GetString()), nil + }) + assert.NoError(t, err) + c.Set("user", tken) +} + +func testRequestSetup(t *testing.T, method string, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, c echo.Context) { + c, rec = bootstrapTestRequest(t, method, payload, queryParams) var paramNames []string var paramValues []string @@ -130,8 +142,19 @@ func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFun } c.SetParamNames(paramNames...) c.SetParamValues(paramValues...) + return +} - addTokenToContext(t, user, c) +func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *models.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { + rec, c := testRequestSetup(t, method, payload, queryParams, urlParams) + addUserTokenToContext(t, user, c) + err = handler(c) + return +} + +func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.HandlerFunc, share *models.LinkSharing, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { + rec, c := testRequestSetup(t, method, payload, queryParams, urlParams) + addLinkShareTokenToContext(t, share, c) err = handler(c) return } @@ -155,9 +178,10 @@ func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) { } type webHandlerTest struct { - user *models.User - strFunc func() handler.CObject - t *testing.T + user *models.User + linkShare *models.LinkSharing + strFunc func() handler.CObject + t *testing.T } func (h *webHandlerTest) getHandler() handler.WebHandler { @@ -168,27 +192,52 @@ func (h *webHandlerTest) getHandler() handler.WebHandler { } } -func (h *webHandlerTest) testReadAll(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { +func (h *webHandlerTest) testReadAllWithUser(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { hndl := h.getHandler() return newTestRequestWithUser(h.t, http.MethodGet, hndl.ReadAllWeb, h.user, "", queryParams, urlParams) } -func (h *webHandlerTest) testReadOne(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { +func (h *webHandlerTest) testReadOneWithUser(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { hndl := h.getHandler() return newTestRequestWithUser(h.t, http.MethodGet, hndl.ReadOneWeb, h.user, "", queryParams, urlParams) } -func (h *webHandlerTest) testCreate(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { +func (h *webHandlerTest) testCreateWithUser(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { hndl := h.getHandler() return newTestRequestWithUser(h.t, http.MethodPut, hndl.CreateWeb, h.user, payload, queryParams, urlParams) } -func (h *webHandlerTest) testUpdate(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { +func (h *webHandlerTest) testUpdateWithUser(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { hndl := h.getHandler() return newTestRequestWithUser(h.t, http.MethodPost, hndl.UpdateWeb, h.user, payload, queryParams, urlParams) } -func (h *webHandlerTest) testDelete(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { +func (h *webHandlerTest) testDeleteWithUser(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { hndl := h.getHandler() return newTestRequestWithUser(h.t, http.MethodDelete, hndl.DeleteWeb, h.user, "", queryParams, urlParams) } + +func (h *webHandlerTest) testReadAllWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { + hndl := h.getHandler() + return newTestRequestWithLinkShare(h.t, http.MethodGet, hndl.ReadAllWeb, h.linkShare, "", queryParams, urlParams) +} + +func (h *webHandlerTest) testReadOneWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { + hndl := h.getHandler() + return newTestRequestWithLinkShare(h.t, http.MethodGet, hndl.ReadOneWeb, h.linkShare, "", queryParams, urlParams) +} + +func (h *webHandlerTest) testCreateWithLinkShare(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { + hndl := h.getHandler() + return newTestRequestWithLinkShare(h.t, http.MethodPut, hndl.CreateWeb, h.linkShare, payload, queryParams, urlParams) +} + +func (h *webHandlerTest) testUpdateWithLinkShare(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { + hndl := h.getHandler() + return newTestRequestWithLinkShare(h.t, http.MethodPost, hndl.UpdateWeb, h.linkShare, payload, queryParams, urlParams) +} + +func (h *webHandlerTest) testDeleteWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { + hndl := h.getHandler() + return newTestRequestWithLinkShare(h.t, http.MethodDelete, hndl.DeleteWeb, h.linkShare, "", queryParams, urlParams) +} diff --git a/pkg/integrations/link_sharing_test.go b/pkg/integrations/link_sharing_test.go new file mode 100644 index 00000000000..59d4b67d47c --- /dev/null +++ b/pkg/integrations/link_sharing_test.go @@ -0,0 +1,989 @@ +// Vikunja is a todo-list application to facilitate your life. +// Copyright 2019 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package integrations + +import ( + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/web/handler" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "net/url" + "testing" +) + +func TestLinkSharing(t *testing.T) { + + linkshareRead := &models.LinkSharing{ + ID: 1, + Hash: "test1", + ListID: 1, + Right: models.RightRead, + SharingType: models.SharingTypeWithoutPassword, + SharedByID: 1, + } + + linkShareWrite := &models.LinkSharing{ + ID: 2, + Hash: "test2", + ListID: 2, + Right: models.RightWrite, + SharingType: models.SharingTypeWithoutPassword, + SharedByID: 1, + } + + linkShareAdmin := &models.LinkSharing{ + ID: 3, + Hash: "test3", + ListID: 3, + Right: models.RightAdmin, + SharingType: models.SharingTypeWithoutPassword, + SharedByID: 1, + } + + t.Run("Lists", func(t *testing.T) { + testHandlerListReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.List{} + }, + t: t, + } + testHandlerListWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.List{} + }, + t: t, + } + testHandlerListAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.List{} + }, + t: t, + } + + t.Run("ReadAll", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(nil, nil) + assert.NoError(t, err) + // Should only return the shared list, nothing else + assert.Contains(t, rec.Body.String(), `Test1`) + assert.NotContains(t, rec.Body.String(), `Test2`) + assert.NotContains(t, rec.Body.String(), `Test3`) + assert.NotContains(t, rec.Body.String(), `Test4`) + assert.NotContains(t, rec.Body.String(), `Test5`) + }) + t.Run("Search", func(t *testing.T) { + rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(url.Values{"s": []string{"est1"}}, nil) + assert.NoError(t, err) + // Should only return the shared list, nothing else + assert.Contains(t, rec.Body.String(), `Test1`) + assert.NotContains(t, rec.Body.String(), `Test2`) + assert.NotContains(t, rec.Body.String(), `Test3`) + assert.NotContains(t, rec.Body.String(), `Test4`) + assert.NotContains(t, rec.Body.String(), `Test5`) + }) + }) + t.Run("ReadOne", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Test1"`) + assert.NotContains(t, rec.Body.String(), `"title":"Test2"`) + }) + t.Run("Nonexisting", func(t *testing.T) { + _, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "9999999"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) + }) + t.Run("Rights check", func(t *testing.T) { + t.Run("Forbidden", func(t *testing.T) { + // List 2, not shared with this token + _, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`) + }) + t.Run("Shared readonly", func(t *testing.T) { + rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Test1"`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerListWrite.testReadOneWithLinkShare(nil, map[string]string{"list": "2"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Test2"`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerListAdmin.testReadOneWithLinkShare(nil, map[string]string{"list": "3"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Test3"`) + }) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Nonexisting", func(t *testing.T) { + _, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "9999999"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) + }) + t.Run("Rights check", func(t *testing.T) { + t.Run("Forbidden", func(t *testing.T) { + _, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + }) + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Nonexisting", func(t *testing.T) { + _, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "9999999"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) + }) + t.Run("Rights check", func(t *testing.T) { + t.Run("Forbidden", func(t *testing.T) { + _, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerListAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + }) + }) + + // Creating a list should always be forbidden, since users need access to a namespace to create a list + t.Run("Create", func(t *testing.T) { + t.Run("Nonexisting", func(t *testing.T) { + _, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Rights check", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"Lorem","description":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerListAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"Lorem","description":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + }) + + t.Run("Right Management", func(t *testing.T) { + t.Run("Users", func(t *testing.T) { + testHandlerListUserReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.ListUser{} + }, + t: t, + } + testHandlerListUserWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.ListUser{} + }, + t: t, + } + testHandlerListUserAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.ListUser{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + rec, err := testHandlerListUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `[]`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerListUserWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `[]`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerListUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"username":"user1"`) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListUserReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListUserWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerListUserAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListUserWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerListUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListUserWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerListUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + }) + t.Run("Teams", func(t *testing.T) { + testHandlerListTeamReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.TeamList{} + }, + t: t, + } + testHandlerListTeamWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.TeamList{} + }, + t: t, + } + testHandlerListTeamAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.TeamList{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + rec, err := testHandlerListTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `[]`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerListTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `[]`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerListTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"name":"testteam1"`) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListTeamWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerListTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerListTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerListTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerListTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerListTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + }) + }) + }) + + t.Run("Tasks", func(t *testing.T) { + testHandlerTaskReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.Task{} + }, + t: t, + } + testHandlerTaskWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.Task{} + }, + t: t, + } + testHandlerTaskAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.Task{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + rec, err := testHandlerTaskReadOnly.testReadAllWithLinkShare(nil, nil) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `task #1`) + assert.Contains(t, rec.Body.String(), `task #2`) + assert.Contains(t, rec.Body.String(), `task #3`) + assert.Contains(t, rec.Body.String(), `task #4`) + assert.Contains(t, rec.Body.String(), `task #5`) + assert.Contains(t, rec.Body.String(), `task #6`) + assert.Contains(t, rec.Body.String(), `task #7`) + assert.Contains(t, rec.Body.String(), `task #8`) + assert.Contains(t, rec.Body.String(), `task #9`) + assert.Contains(t, rec.Body.String(), `task #10`) + assert.Contains(t, rec.Body.String(), `task #11`) + assert.Contains(t, rec.Body.String(), `task #12`) + assert.NotContains(t, rec.Body.String(), `task #13`) + assert.NotContains(t, rec.Body.String(), `task #14`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerTaskWrite.testReadAllWithLinkShare(nil, nil) + assert.NoError(t, err) + assert.NotContains(t, rec.Body.String(), `task #2`) + assert.NotContains(t, rec.Body.String(), `task #3`) + assert.NotContains(t, rec.Body.String(), `task #4`) + assert.NotContains(t, rec.Body.String(), `task #5`) + assert.NotContains(t, rec.Body.String(), `task #6`) + assert.NotContains(t, rec.Body.String(), `task #7`) + assert.NotContains(t, rec.Body.String(), `task #8`) + assert.NotContains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #10`) + assert.NotContains(t, rec.Body.String(), `task #11`) + assert.NotContains(t, rec.Body.String(), `task #12`) + assert.Contains(t, rec.Body.String(), `task #13`) + assert.NotContains(t, rec.Body.String(), `task #14`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerTaskAdmin.testReadAllWithLinkShare(nil, nil) + assert.NoError(t, err) + assert.NotContains(t, rec.Body.String(), `task #2`) + assert.NotContains(t, rec.Body.String(), `task #4`) + assert.NotContains(t, rec.Body.String(), `task #5`) + assert.NotContains(t, rec.Body.String(), `task #6`) + assert.NotContains(t, rec.Body.String(), `task #7`) + assert.NotContains(t, rec.Body.String(), `task #8`) + assert.NotContains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #10`) + assert.NotContains(t, rec.Body.String(), `task #11`) + assert.NotContains(t, rec.Body.String(), `task #12`) + assert.NotContains(t, rec.Body.String(), `task #13`) + assert.NotContains(t, rec.Body.String(), `task #14`) + assert.Contains(t, rec.Body.String(), `task #32`) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"text":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"text":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"listtask": "13"}, `{"text":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"listtask": "32"}, `{"text":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerTaskReadOnly.testDeleteWithLinkShare(nil, map[string]string{"listtask": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerTaskWrite.testDeleteWithLinkShare(nil, map[string]string{"listtask": "13"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `Successfully deleted.`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerTaskAdmin.testDeleteWithLinkShare(nil, map[string]string{"listtask": "32"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `Successfully deleted.`) + }) + }) + }) + + t.Run("Teams", func(t *testing.T) { + testHandlerTeamReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.Team{} + }, + t: t, + } + testHandlerTeamWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.Team{} + }, + t: t, + } + testHandlerTeamAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.Team{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerTeamReadOnly.testReadAllWithLinkShare(nil, nil) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerTeamWrite.testReadAllWithLinkShare(nil, nil) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerTeamAdmin.testReadAllWithLinkShare(nil, nil) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"team": "1"}, `{"name":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"team": "2"}, `{"name":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"team": "3"}, `{"name":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"team": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"team": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"team": "3"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + }) + + t.Run("Linkshare Management", func(t *testing.T) { + testHandlerLinkShareReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.LinkSharing{} + }, + t: t, + } + testHandlerLinkShareWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.LinkSharing{} + }, + t: t, + } + testHandlerLinkShareAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.LinkSharing{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + rec, err := testHandlerLinkShareReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"hash":"test"`) + }) + t.Run("Shared write", func(t *testing.T) { + rec, err := testHandlerLinkShareWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"hash":"test2"`) + }) + t.Run("Shared admin", func(t *testing.T) { + rec, err := testHandlerLinkShareAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"hash":"test3"`) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerLinkShareReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerLinkShareAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerLinkShareReadOnly.testUpdateWithLinkShare(nil, map[string]string{"share": "1"}, `{}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerLinkShareWrite.testUpdateWithLinkShare(nil, map[string]string{"share": "2"}, `{}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerLinkShareAdmin.testUpdateWithLinkShare(nil, map[string]string{"share": "3"}, `{}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerLinkShareReadOnly.testDeleteWithLinkShare(nil, map[string]string{"share": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerLinkShareWrite.testDeleteWithLinkShare(nil, map[string]string{"share": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerLinkShareAdmin.testDeleteWithLinkShare(nil, map[string]string{"share": "3"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + }) + + t.Run("Namespace", func(t *testing.T) { + testHandlerNamespaceReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.Namespace{} + }, + t: t, + } + testHandlerNamespaceWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.Namespace{} + }, + t: t, + } + testHandlerNamespaceAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.Namespace{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceReadOnly.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceWrite.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceAdmin.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"name":"LoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"name":"LoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"name":"LoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + + t.Run("Right Management", func(t *testing.T) { + t.Run("Users", func(t *testing.T) { + testHandlerNamespaceUserReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.NamespaceUser{} + }, + t: t, + } + testHandlerNamespaceUserWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.NamespaceUser{} + }, + t: t, + } + testHandlerNamespaceUserAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.NamespaceUser{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceUserWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceUserReadOnly.testCreateWithLinkShare(nil, nil, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceUserWrite.testCreateWithLinkShare(nil, nil, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceUserAdmin.testCreateWithLinkShare(nil, nil, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceUserWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"userID":"user1"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceUserWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + }) + t.Run("Teams", func(t *testing.T) { + testHandlerNamespaceTeamReadOnly := webHandlerTest{ + linkShare: linkshareRead, + strFunc: func() handler.CObject { + return &models.TeamNamespace{} + }, + t: t, + } + testHandlerNamespaceTeamWrite := webHandlerTest{ + linkShare: linkShareWrite, + strFunc: func() handler.CObject { + return &models.TeamNamespace{} + }, + t: t, + } + testHandlerNamespaceTeamAdmin := webHandlerTest{ + linkShare: linkShareAdmin, + strFunc: func() handler.CObject { + return &models.TeamNamespace{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceTeamWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"teamID":1}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Shared readonly", func(t *testing.T) { + _, err := testHandlerNamespaceTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared write", func(t *testing.T) { + _, err := testHandlerNamespaceTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared admin", func(t *testing.T) { + _, err := testHandlerNamespaceTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + }) + }) + }) + }) +} diff --git a/pkg/integrations/list_test.go b/pkg/integrations/list_test.go index d935ef0f696..9caf5292eb6 100644 --- a/pkg/integrations/list_test.go +++ b/pkg/integrations/list_test.go @@ -35,7 +35,7 @@ func TestList(t *testing.T) { } t.Run("ReadAll", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - rec, err := testHandler.testReadAll(nil, nil) + rec, err := testHandler.testReadAllWithUser(nil, nil) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Test1`) assert.NotContains(t, rec.Body.String(), `Test2`) @@ -44,7 +44,7 @@ func TestList(t *testing.T) { assert.NotContains(t, rec.Body.String(), `Test5`) }) t.Run("Search", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"s": []string{"Test1"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"Test1"}}, nil) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Test1`) assert.NotContains(t, rec.Body.String(), `Test2`) @@ -55,7 +55,7 @@ func TestList(t *testing.T) { }) t.Run("ReadOne", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "1"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "1"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test1"`) assert.NotContains(t, rec.Body.String(), `"title":"Test2"`) @@ -64,77 +64,77 @@ func TestList(t *testing.T) { assert.Contains(t, rec.Body.String(), `"tasks":[{"id":1,"text":"task #1",`) }) t.Run("Nonexisting", func(t *testing.T) { - _, err := testHandler.testReadOne(nil, map[string]string{"list": "9999"}) + _, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9999"}) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) }) t.Run("Rights check", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { // Owned by user13 - _, err := testHandler.testReadOne(nil, map[string]string{"list": "20"}) + _, 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`) }) t.Run("Shared Via Team readonly", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "6"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "6"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test6"`) }) t.Run("Shared Via Team write", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "7"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "7"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test7"`) }) t.Run("Shared Via Team admin", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "8"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "8"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test8"`) }) t.Run("Shared Via User readonly", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "9"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test9"`) }) t.Run("Shared Via User write", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "10"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "10"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test10"`) }) t.Run("Shared Via User admin", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "11"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "11"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test11"`) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "12"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "12"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test12"`) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "13"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "13"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test13"`) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "14"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "14"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test14"`) }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "15"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "15"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test15"`) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "16"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "16"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test16"`) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { - rec, err := testHandler.testReadOne(nil, map[string]string{"list": "17"}) + rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "17"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Test17"`) }) @@ -142,106 +142,106 @@ func TestList(t *testing.T) { }) t.Run("Update", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - // Check the list was loaded successfully afterwards, see testReadOne - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`) + // Check the list was loaded successfully afterwards, see testReadOneWithUser + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) // The description should not be updated but returned correctly assert.Contains(t, rec.Body.String(), `description":"Lorem Ipsum`) }) t.Run("Nonexisting", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "9999"}, `{"title":"TestLoremIpsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"TestLoremIpsum"}`) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) }) t.Run("Normal with updating the description", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`) }) t.Run("Empty title", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":""}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":""}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required") }) t.Run("Almost empty title", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"nn"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"nn"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") }) t.Run("Title too long", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") }) t.Run("Rights check", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { // Owned by user13 - _, err := testHandler.testUpdate(nil, map[string]string{"list": "20"}, `{"title":"TestLoremIpsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "20"}, `{"title":"TestLoremIpsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "6"}, `{"title":"TestLoremIpsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "6"}, `{"title":"TestLoremIpsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) t.Run("Shared Via Team admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) t.Run("Shared Via User readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "9"}, `{"title":"TestLoremIpsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "9"}, `{"title":"TestLoremIpsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via User write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) t.Run("Shared Via User admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "12"}, `{"title":"TestLoremIpsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "12"}, `{"title":"TestLoremIpsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"list": "15"}, `{"title":"TestLoremIpsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "15"}, `{"title":"TestLoremIpsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) }) @@ -249,82 +249,82 @@ func TestList(t *testing.T) { }) t.Run("Delete", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"list": "1"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "1"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) }) t.Run("Nonexisting", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "999"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "999"}) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) }) t.Run("Rights check", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { // Owned by user13 - _, err := testHandler.testDelete(nil, map[string]string{"list": "20"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "20"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "6"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "6"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team write", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "7"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "7"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"list": "8"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "8"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) }) t.Run("Shared Via User readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "9"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "9"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via User write", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "10"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "10"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via User admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"list": "11"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "11"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "12"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "12"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "13"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "13"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"list": "14"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "14"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "15"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "15"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"list": "16"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "16"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"list": "17"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "17"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) }) @@ -332,8 +332,8 @@ func TestList(t *testing.T) { }) t.Run("Create", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - // Check the list was loaded successfully after update, see testReadOne - rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`) + // Check the list was loaded successfully after update, see testReadOneWithUser + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"description":""`) @@ -341,7 +341,7 @@ func TestList(t *testing.T) { assert.Contains(t, rec.Body.String(), `"tasks":null`) }) t.Run("Normal with description", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum"`) @@ -349,22 +349,22 @@ func TestList(t *testing.T) { assert.Contains(t, rec.Body.String(), `"tasks":null`) }) t.Run("Nonexisting Namespace", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeNamespaceDoesNotExist) }) t.Run("Empty title", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":""}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":""}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required") }) t.Run("Almost empty title", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"nn"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"nn"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") }) t.Run("Title too long", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") }) @@ -372,18 +372,18 @@ func TestList(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { // Owned by user13 - _, err := testHandler.testCreate(nil, map[string]string{"namespace": "15"}, `{"title":"Lorem"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "15"}, `{"title":"Lorem"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"namespace": "7"}, `{"title":"Lorem"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "7"}, `{"title":"Lorem"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "8"}, `{"title":"Lorem"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "8"}, `{"title":"Lorem"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"description":""`) @@ -391,7 +391,7 @@ func TestList(t *testing.T) { assert.Contains(t, rec.Body.String(), `"tasks":null`) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "9"}, `{"title":"Lorem"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "9"}, `{"title":"Lorem"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"description":""`) @@ -400,12 +400,12 @@ func TestList(t *testing.T) { }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"namespace": "10"}, `{"title":"Lorem"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "10"}, `{"title":"Lorem"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "11"}, `{"title":"Lorem"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "11"}, `{"title":"Lorem"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"description":""`) @@ -413,7 +413,7 @@ func TestList(t *testing.T) { assert.Contains(t, rec.Body.String(), `"tasks":null`) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"description":""`) diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go index 3c238e0aecb..b2100cd5f41 100644 --- a/pkg/integrations/task_test.go +++ b/pkg/integrations/task_test.go @@ -37,7 +37,7 @@ func TestTask(t *testing.T) { // ^TestTask$/^Update$/^Update_task_items$/^Removing_Assignees_null$ t.Run("ReadAll", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - rec, err := testHandler.testReadAll(nil, nil) + rec, err := testHandler.testReadAllWithUser(nil, nil) assert.NoError(t, err) // Not using assert.Equal to avoid having the tests break every time we add new fixtures assert.Contains(t, rec.Body.String(), `task #1`) @@ -58,7 +58,7 @@ func TestTask(t *testing.T) { // has at least read access }) t.Run("Search", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"s": []string{"task #6"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, nil) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) assert.NotContains(t, rec.Body.String(), `task #2`) @@ -78,39 +78,39 @@ func TestTask(t *testing.T) { t.Run("Sort Order", func(t *testing.T) { // should equal priority desc t.Run("by priority", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priority"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"priority"}}, nil) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) }) t.Run("by priority desc", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"sort": []string{"prioritydesc"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"prioritydesc"}}, nil) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priorityasc"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"priorityasc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":31,"text":"task #31 with color","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"f0f0f0","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":32,"text":"task #32","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":3,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) // should equal duedate desc t.Run("by duedate", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedate"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedate"}}, nil) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) }) t.Run("by duedate desc", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedatedesc"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedatedesc"}}, nil) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) }) t.Run("by duedate asc", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedateasc"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedateasc"}}, nil) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) t.Run("invalid parameter", func(t *testing.T) { // Invalid parameter should not sort at all - rec, err := testHandler.testReadAll(url.Values{"sort": []string{"loremipsum"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, nil) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":1`) assert.NotContains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}}]`) @@ -120,7 +120,7 @@ func TestTask(t *testing.T) { }) t.Run("Date range", func(t *testing.T) { t.Run("start and end date", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"startdate": []string{"1540000000"}, "enddate": []string{"1544700001"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}, "enddate": []string{"1544700001"}}, nil) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) assert.NotContains(t, rec.Body.String(), `task #2`) @@ -138,7 +138,7 @@ func TestTask(t *testing.T) { assert.NotContains(t, rec.Body.String(), `task #14`) }) t.Run("start date only", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"startdate": []string{"1540000000"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}}, nil) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) assert.NotContains(t, rec.Body.String(), `task #2`) @@ -156,7 +156,7 @@ func TestTask(t *testing.T) { assert.NotContains(t, rec.Body.String(), `task #14`) }) t.Run("end date only", func(t *testing.T) { - rec, err := testHandler.testReadAll(url.Values{"enddate": []string{"1544700001"}}, nil) + rec, err := testHandler.testReadAllWithUser(url.Values{"enddate": []string{"1544700001"}}, nil) assert.NoError(t, err) // If no start date but an end date is specified, this should be null // since we don't have any tasks in the fixtures with an end date > @@ -168,162 +168,162 @@ func TestTask(t *testing.T) { t.Run("Update", func(t *testing.T) { t.Run("Update task items", func(t *testing.T) { t.Run("Text", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.NotContains(t, rec.Body.String(), `"text":"task #1"`) }) t.Run("Description", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"description":"Dolor sit amet"`) assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`) }) t.Run("Description to empty", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"description":""}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":""}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"description":""`) assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`) }) t.Run("Done", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"done":true}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"done":true}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"done":true`) assert.NotContains(t, rec.Body.String(), `"done":false`) }) t.Run("Undone", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "2"}, `{"done":false}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "2"}, `{"done":false}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"done":false`) assert.NotContains(t, rec.Body.String(), `"done":true`) }) t.Run("Due date", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"dueDate": 123456}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"dueDate": 123456}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"dueDate":123456`) assert.NotContains(t, rec.Body.String(), `"dueDate":0`) }) t.Run("Due date unset", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "5"}, `{"dueDate": 0}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "5"}, `{"dueDate": 0}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"dueDate":0`) assert.NotContains(t, rec.Body.String(), `"dueDate":1543636724`) }) t.Run("Reminders", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"reminderDates": [1555508227,1555511000]}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminderDates": [1555508227,1555511000]}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminderDates":[1555508227,1555511000]`) assert.NotContains(t, rec.Body.String(), `"reminderDates": null`) }) t.Run("Reminders unset to empty array", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "27"}, `{"reminderDates": []}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminderDates": []}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminderDates":null`) assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`) }) t.Run("Reminders unset to null", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "27"}, `{"reminderDates": null}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminderDates": null}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminderDates":null`) assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`) }) t.Run("Repeat after", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"repeatAfter":3600}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"repeatAfter":3600}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"repeatAfter":3600`) assert.NotContains(t, rec.Body.String(), `"repeatAfter":0`) }) t.Run("Repeat after unset", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "28"}, `{"repeatAfter":0}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"repeatAfter":0}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"repeatAfter":0`) assert.NotContains(t, rec.Body.String(), `"repeatAfter":3600`) }) t.Run("Repeat after update done", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "28"}, `{"done":true}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"done":true}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"done":false`) assert.NotContains(t, rec.Body.String(), `"done":true`) }) t.Run("Parent task", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":2}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":2}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"parentTaskID":2`) assert.NotContains(t, rec.Body.String(), `"parentTaskID":0`) }) t.Run("Parent task same task", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":1}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":1}`) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeParentTaskCannotBeTheSame) }) t.Run("Parent task unset", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "29"}, `{"parentTaskID":0}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "29"}, `{"parentTaskID":0}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"parentTaskID":0`) assert.NotContains(t, rec.Body.String(), `"parentTaskID":1`) }) t.Run("Assignees", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"assignees":[{"id":1}]}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"assignees":[{"id":1}]}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"assignees":[{"id":1`) assert.NotContains(t, rec.Body.String(), `"assignees":[]`) }) t.Run("Removing Assignees empty array", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "30"}, `{"assignees":[]}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":[]}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"assignees":null`) assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`) }) t.Run("Removing Assignees null", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "30"}, `{"assignees":null}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":null}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"assignees":null`) assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`) }) t.Run("Priority", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"priority":100}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"priority":100}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"priority":100`) assert.NotContains(t, rec.Body.String(), `"priority":0`) }) t.Run("Priority to 0", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "3"}, `{"priority":0}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "3"}, `{"priority":0}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"priority":0`) assert.NotContains(t, rec.Body.String(), `"priority":100`) }) t.Run("Start date", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"startDate":1234567}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"startDate":1234567}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"startDate":1234567`) assert.NotContains(t, rec.Body.String(), `"startDate":0`) }) t.Run("Start date unset", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "7"}, `{"startDate":0}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "7"}, `{"startDate":0}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"startDate":0`) assert.NotContains(t, rec.Body.String(), `"startDate":1544600000`) }) t.Run("End date", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"endDate":123456}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"endDate":123456}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"endDate":123456`) assert.NotContains(t, rec.Body.String(), `"endDate":0`) }) t.Run("End date unset", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "8"}, `{"endDate":0}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "8"}, `{"endDate":0}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"endDate":0`) assert.NotContains(t, rec.Body.String(), `"endDate":1544700000`) }) t.Run("Color", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"hexColor":"f0f0f0"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"hexColor":"f0f0f0"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"hexColor":"f0f0f0"`) assert.NotContains(t, rec.Body.String(), `"hexColor":""`) }) t.Run("Color unset", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "31"}, `{"hexColor":""}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "31"}, `{"hexColor":""}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"hexColor":""`) assert.NotContains(t, rec.Body.String(), `"hexColor":"f0f0f0"`) @@ -331,76 +331,76 @@ func TestTask(t *testing.T) { }) t.Run("Nonexisting", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "99999"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "99999"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist) }) t.Run("Rights check", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "14"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "14"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "15"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "15"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "16"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "16"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via Team admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "17"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "17"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via User readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "18"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "18"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via User write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "19"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "19"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via User admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "20"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "20"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "21"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "21"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "22"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "22"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "23"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "23"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { - _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "24"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "24"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "25"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "25"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { - rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "26"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "26"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) @@ -408,81 +408,81 @@ func TestTask(t *testing.T) { }) t.Run("Delete", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "1"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "1"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Nonexisting", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"listtask": "99999"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "99999"}) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist) }) t.Run("Rights check", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"listtask": "14"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "14"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"listtask": "15"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "15"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team write", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "16"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "16"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Shared Via Team admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "17"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "17"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Shared Via User readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"listtask": "18"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "18"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via User write", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "19"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "19"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Shared Via User admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "20"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "20"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"listtask": "21"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "21"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "22"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "22"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "23"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "23"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { - _, err := testHandler.testDelete(nil, map[string]string{"listtask": "24"}) + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "24"}) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "25"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "25"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { - rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "26"}) + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "26"}) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `Successfully deleted.`) }) @@ -490,82 +490,82 @@ func TestTask(t *testing.T) { }) t.Run("Create", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Nonexisting", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"list": "9999"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) }) t.Run("Rights check", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) { // Owned by user13 - _, err := testHandler.testCreate(nil, map[string]string{"list": "20"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team readonly", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"list": "6"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via Team write", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "7"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via Team admin", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "8"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via User readonly", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"list": "9"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via User write", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "10"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via User admin", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "11"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"list": "12"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "13"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "14"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { - _, err := testHandler.testCreate(nil, map[string]string{"list": "15"}, `{"text":"Lorem Ipsum"}`) + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"text":"Lorem Ipsum"}`) assert.Error(t, err) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) }) t.Run("Shared Via NamespaceUser write", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "16"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { - rec, err := testHandler.testCreate(nil, map[string]string{"list": "17"}, `{"text":"Lorem Ipsum"}`) + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"text":"Lorem Ipsum"}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) diff --git a/pkg/migration/20190818210133.go b/pkg/migration/20190818210133.go new file mode 100644 index 00000000000..87c8f52af28 --- /dev/null +++ b/pkg/migration/20190818210133.go @@ -0,0 +1,53 @@ +// Copyright 2019 Vikunja and contriubtors. All rights reserved. +// +// This file is part of Vikunja. +// +// Vikunja is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Vikunja is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Vikunja. If not, see . + +package migration + +import ( + "code.vikunja.io/api/pkg/models" + "github.com/go-xorm/xorm" + "src.techknowlogick.com/xormigrate" +) + +type linkSharing20190818210133 struct { + ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"` + Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"` + ListID int64 `xorm:"int(11) not null" json:"list_id"` + Right models.Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` + SharingType models.SharingType `xorm:"int(11) INDEX not null default 0" json:"sharing_type" valid:"length(0|2)" maximum:"2" default:"0"` + SharedByID int64 `xorm:"int(11) INDEX not null"` + Created int64 `xorm:"created not null" json:"created"` + Updated int64 `xorm:"updated not null" json:"updated"` +} + +// TableName holds the table name for this share +func (linkSharing20190818210133) TableName() string { + return "link_sharing" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20190818210133", + Description: "Add link sharing table", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(linkSharing20190818210133{}) + }, + Rollback: func(tx *xorm.Engine) error { + return tx.DropTables(linkSharing20190818210133{}) + }, + }) +} diff --git a/pkg/models/error.go b/pkg/models/error.go index 469d7eebbdb..43e304620ac 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -22,6 +22,29 @@ import ( "net/http" ) +// Generic + +// ErrGenericForbidden represents a "UsernameAlreadyExists" kind of error. +type ErrGenericForbidden struct{} + +// IsErrGenericForbidden checks if an error is a ErrGenericForbidden. +func IsErrGenericForbidden(err error) bool { + _, ok := err.(ErrGenericForbidden) + return ok +} + +func (err ErrGenericForbidden) Error() string { + return fmt.Sprintf("Forbidden") +} + +// ErrorCodeGenericForbidden holds the unique world-error code of this error +const ErrorCodeGenericForbidden = 0001 + +// HTTPError holds the http error description +func (err ErrGenericForbidden) HTTPError() web.HTTPError { + return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrorCodeGenericForbidden, Message: "You're not allowed to do this."} +} + // ===================== // User Operation Errors // ===================== @@ -423,6 +446,30 @@ func (err ErrListTitleCannotBeEmpty) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTitleCannotBeEmpty, Message: "You must provide at least a list title."} } +// ErrListShareDoesNotExist represents a "ErrListShareDoesNotExist" kind of error. Used if the list share does not exist. +type ErrListShareDoesNotExist struct { + ID int64 + Hash string +} + +// IsErrListShareDoesNotExist checks if an error is a ErrListShareDoesNotExist. +func IsErrListShareDoesNotExist(err error) bool { + _, ok := err.(ErrListShareDoesNotExist) + return ok +} + +func (err ErrListShareDoesNotExist) Error() string { + return fmt.Sprintf("List share does not exist.") +} + +// ErrCodeListShareDoesNotExist holds the unique world-error code of this error +const ErrCodeListShareDoesNotExist = 3006 + +// HTTPError holds the http error description +func (err ErrListShareDoesNotExist) HTTPError() web.HTTPError { + return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListShareDoesNotExist, Message: "The list share does not exist."} +} + // ================ // List task errors // ================ diff --git a/pkg/models/fixtures/link_sharing.yml b/pkg/models/fixtures/link_sharing.yml new file mode 100644 index 00000000000..f1c80857eda --- /dev/null +++ b/pkg/models/fixtures/link_sharing.yml @@ -0,0 +1,24 @@ +- id: 1 + hash: test + list_id: 1 + right: 0 + sharing_type: 1 + shared_by_id: 1 + created: 0 + updated: 0 +- id: 2 + hash: test2 + list_id: 2 + right: 1 + sharing_type: 1 + shared_by_id: 1 + created: 0 + updated: 0 +- id: 3 + hash: test3 + list_id: 3 + right: 2 + sharing_type: 1 + shared_by_id: 1 + created: 0 + updated: 0 diff --git a/pkg/models/fixtures/tasks.yml b/pkg/models/fixtures/tasks.yml index a57c79f785c..285565ae29e 100644 --- a/pkg/models/fixtures/tasks.yml +++ b/pkg/models/fixtures/tasks.yml @@ -198,4 +198,10 @@ hex_color: f0f0f0 created: 1543626724 updated: 1543626724 +- id: 32 + text: 'task #32' + created_by_id: 1 + list_id: 3 + created: 1543626724 + updated: 1543626724 diff --git a/pkg/models/label.go b/pkg/models/label.go index 7c8b3aadbb4..eb2cb7afe9a 100644 --- a/pkg/models/label.go +++ b/pkg/models/label.go @@ -131,6 +131,9 @@ func (l *Label) Delete() (err error) { // @Failure 500 {object} models.Message "Internal error" // @Router /labels [get] func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, err error) { + if _, is := a.(*LinkSharing); is { + return nil, ErrGenericForbidden{} + } u := &User{ID: a.GetID()} @@ -192,7 +195,18 @@ func getLabelByIDSimple(labelID int64) (*Label, error) { // Helper method to get all task ids a user has func getUserTaskIDs(u *User) (taskIDs []int64, err error) { - tasks, err := GetTasksByUser("", u, -1, SortTasksByUnsorted, time.Unix(0, 0), time.Unix(0, 0)) + + // Get all lists + lists, err := getRawListsForUser("", u, -1) + if err != nil { + return nil, err + } + + tasks, err := getTasksForLists(lists, &taskOptions{ + startDate: time.Unix(0, 0), + endDate: time.Unix(0, 0), + sortby: SortTasksByUnsorted, + }) if err != nil { return nil, err } diff --git a/pkg/models/label_rights.go b/pkg/models/label_rights.go index bd0821d4124..1787439b868 100644 --- a/pkg/models/label_rights.go +++ b/pkg/models/label_rights.go @@ -39,10 +39,19 @@ func (l *Label) CanRead(a web.Auth) (bool, error) { // CanCreate checks if the user can create a label // Currently a dummy. func (l *Label) CanCreate(a web.Auth) (bool, error) { + if _, is := a.(*LinkSharing); is { + return false, nil + } + return true, nil } func (l *Label) isLabelOwner(a web.Auth) (bool, error) { + + if _, is := a.(*LinkSharing); is { + return false, nil + } + lorig, err := getLabelByIDSimple(l.ID) if err != nil { return false, err @@ -52,6 +61,9 @@ 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) { + + // TODO: add an extra check for link share handling + // Get all tasks taskIDs, err := getUserTaskIDs(&User{ID: a.GetID()}) if err != nil { diff --git a/pkg/models/link_sharing.go b/pkg/models/link_sharing.go new file mode 100644 index 00000000000..c093c5418f4 --- /dev/null +++ b/pkg/models/link_sharing.go @@ -0,0 +1,207 @@ +// Copyright 2019 Vikunja and contriubtors. All rights reserved. +// +// This file is part of Vikunja. +// +// Vikunja is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Vikunja is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Vikunja. If not, see . + +package models + +import ( + "code.vikunja.io/api/pkg/utils" + "code.vikunja.io/web" + "github.com/dgrijalva/jwt-go" +) + +// SharingType holds the sharing type +type SharingType int + +// These consts represent all valid link sharing types +const ( + SharingTypeUnknown SharingType = iota + SharingTypeWithoutPassword + SharingTypeWithPassword +) + +// LinkSharing represents a shared list +type LinkSharing struct { + // The ID of the shared thing + ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"share"` + // The public id to get this shared list + Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"` + // The ID of the shared list + ListID int64 `xorm:"int(11) not null" json:"-" param:"list"` + // The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. + Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` + + List *List `xorm:"-" json:"list" param:"fullist"` + + // The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented). + SharingType SharingType `xorm:"int(11) INDEX not null default 0" json:"sharing_type" valid:"length(0|2)" maximum:"2" default:"0"` + + // The user who shared this list + SharedBy *User `xorm:"-" json:"shared_by"` + SharedByID int64 `xorm:"int(11) INDEX not null" json:"-"` + + // A unix timestamp when this list was shared. You cannot change this value. + Created int64 `xorm:"created not null" json:"created"` + // A unix timestamp when this share was last updated. You cannot change this value. + Updated int64 `xorm:"updated not null" json:"updated"` + + web.CRUDable `xorm:"-" json:"-"` + web.Rights `xorm:"-" json:"-"` +} + +// TableName holds the table name +func (LinkSharing) TableName() string { + return "link_sharing" +} + +// GetID returns the ID of the links sharing object +func (share *LinkSharing) GetID() int64 { + return share.ID +} + +// GetLinkShareFromClaims builds a link sharing object from jwt claims +func GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error) { + share = &LinkSharing{} + share.ID = int64(claims["id"].(float64)) + share.Hash = claims["hash"].(string) + share.ListID = int64(claims["listID"].(float64)) + share.Right = Right(claims["right"].(float64)) + share.SharedByID = int64(claims["sharedByID"].(float64)) + return +} + +// Create creates a new link share for a given list +// @Summary Share a list via link +// @Description Share a list via link. The user needs to have write-access to the list to be able do this. +// @tags sharing +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param list path int true "List ID" +// @Param label body models.LinkSharing true "The new link share object" +// @Success 200 {object} models.LinkSharing "The created link share object." +// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid link share object provided." +// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to add the list share." +// @Failure 404 {object} code.vikunja.io/web.HTTPError "The list does not exist." +// @Failure 500 {object} models.Message "Internal error" +// @Router /lists/{list}/shares [put] +func (share *LinkSharing) Create(a web.Auth) (err error) { + share.SharedByID = a.GetID() + share.Hash = utils.MakeRandomString(40) + _, err = x.Insert(share) + return +} + +// ReadOne returns one share +// @Summary Get one link shares for a list +// @Description Returns one link share by its ID. +// @tags sharing +// @Accept json +// @Produce json +// @Param list path int true "List ID" +// @Param share path int true "Share ID" +// @Security JWTKeyAuth +// @Success 200 {object} models.LinkSharing "The share links" +// @Failure 403 {object} code.vikunja.io/web.HTTPError "No access to the list" +// @Failure 404 {object} code.vikunja.io/web.HTTPError "Share Link not found." +// @Failure 500 {object} models.Message "Internal error" +// @Router /lists/{list}/shares/{share} [get] +func (share *LinkSharing) ReadOne() (err error) { + exists, err := x.Where("id = ?", share.ID).Get(share) + if err != nil { + return err + } + if !exists { + return ErrListShareDoesNotExist{ID: share.ID, Hash: share.Hash} + } + return +} + +// ReadAll returns all shares for a given list +// @Summary Get all link shares for a list +// @Description Returns all link shares which exist for a given list +// @tags sharing +// @Accept json +// @Produce json +// @Param list path int true "List ID" +// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned." +// @Param s query string false "Search shares by hash." +// @Security JWTKeyAuth +// @Success 200 {array} models.LinkSharing "The share links" +// @Failure 500 {object} models.Message "Internal error" +// @Router /lists/{list}/shares [get] +func (share *LinkSharing) ReadAll(search string, a web.Auth, page int) (interface{}, error) { + list := &List{ID: share.ListID} + can, err := list.CanRead(a) + if err != nil { + return nil, err + } + if !can { + return nil, ErrGenericForbidden{} + } + + var shares []*LinkSharing + err = x. + Where("list_id = ? AND hash LIKE ?", share.ListID, "%"+search+"%"). + Limit(getLimitFromPageIndex(page)). + Find(&shares) + return shares, err +} + +// Delete removes a link share +// @Summary Remove a link share +// @Description Remove a link share. The user needs to have write-access to the list to be able do this. +// @tags sharing +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param list path int true "List ID" +// @Param share path int true "Share Link ID" +// @Success 200 {object} models.Message "The link was successfully removed." +// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to remove the link." +// @Failure 404 {object} code.vikunja.io/web.HTTPError "Share Link not found." +// @Failure 500 {object} models.Message "Internal error" +// @Router /lists/{list}/shares/{share} [delete] +func (share *LinkSharing) Delete() (err error) { + _, err = x.Where("id = ?", share.ID).Delete(share) + return +} + +// GetLinkShareByHash returns a link share by hash +func GetLinkShareByHash(hash string) (share *LinkSharing, err error) { + share = &LinkSharing{} + has, err := x.Where("hash = ?", hash).Get(share) + if err != nil { + return + } + if !has { + return share, ErrListShareDoesNotExist{Hash: hash} + } + share.List = &List{ID: share.ListID} + return +} + +// GetListByShareHash returns a link share by its hash +func GetListByShareHash(hash string) (list *List, err error) { + share, err := GetLinkShareByHash(hash) + if err != nil { + return + } + + list = &List{ID: share.ListID} + err = list.GetSimpleByID() + return +} diff --git a/pkg/models/link_sharing_rights.go b/pkg/models/link_sharing_rights.go new file mode 100644 index 00000000000..64379e4b77f --- /dev/null +++ b/pkg/models/link_sharing_rights.go @@ -0,0 +1,61 @@ +// Vikunja is a todo-list application to facilitate your life. +// Copyright 2019 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +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) { + // Don't allow creating link shares if the user itself authenticated with a link share + if _, is := a.(*LinkSharing); is { + return false, nil + } + + l, err := GetListByShareHash(share.Hash) + if err != nil { + return false, err + } + return l.CanRead(a) +} + +// CanDelete implements the delete right check for a link share +func (share *LinkSharing) CanDelete(a web.Auth) (bool, error) { + return share.canDoLinkShare(a) +} + +// CanUpdate implements the update right check for a link share +func (share *LinkSharing) CanUpdate(a web.Auth) (bool, error) { + return share.canDoLinkShare(a) +} + +// CanCreate implements the create right check for a link share +func (share *LinkSharing) CanCreate(a web.Auth) (bool, error) { + return share.canDoLinkShare(a) +} + +func (share *LinkSharing) canDoLinkShare(a web.Auth) (bool, error) { + // Don't allow creating link shares if the user itself authenticated with a link share + if _, is := a.(*LinkSharing); is { + return false, nil + } + + l, err := GetListByShareHash(share.Hash) + if err != nil { + return false, err + } + return l.CanWrite(a) +} diff --git a/pkg/models/list.go b/pkg/models/list.go index 05022fc6f6a..f8f1867e1f0 100644 --- a/pkg/models/list.go +++ b/pkg/models/list.go @@ -85,14 +85,26 @@ func GetListsByNamespaceID(nID int64, doer *User) (lists []*List, err error) { // @Failure 500 {object} models.Message "Internal error" // @Router /lists [get] func (l *List) ReadAll(search string, a web.Auth, page int) (interface{}, error) { + // Check if we're dealing with a share auth + shareAuth, ok := a.(*LinkSharing) + if ok { + shareAuth.List = &List{ID: shareAuth.ListID} + err := shareAuth.List.GetSimpleByID() + if err != nil { + return nil, err + } + lists := []*List{shareAuth.List} + err = AddListDetails(lists) + return lists, err + } + lists, err := getRawListsForUser(search, &User{ID: a.GetID()}, page) if err != nil { return nil, err } // Add more list details - AddListDetails(lists) - + err = AddListDetails(lists) return lists, err } diff --git a/pkg/models/list_rights.go b/pkg/models/list_rights.go index efefbf15a23..09ee9aeb4b1 100644 --- a/pkg/models/list_rights.go +++ b/pkg/models/list_rights.go @@ -31,6 +31,13 @@ func (l *List) CanWrite(a web.Auth) (bool, error) { return false, err } + // Check if we're dealing with a share auth + shareAuth, ok := a.(*LinkSharing) + if ok { + return originalList.ID == shareAuth.ListID && + (shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), nil + } + // Check if the user is either owner or can write to the list if originalList.isOwner(&User{ID: a.GetID()}) { return true, nil @@ -45,6 +52,14 @@ func (l *List) CanRead(a web.Auth) (bool, error) { if err := l.GetSimpleByID(); err != nil { return false, 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 + } + if l.isOwner(&User{ID: a.GetID()}) { return true, nil } @@ -61,7 +76,7 @@ func (l *List) CanDelete(a web.Auth) (bool, error) { return l.IsAdmin(a) } -// CanCreate checks if the user can update a list +// CanCreate checks if the user can create a list func (l *List) CanCreate(a web.Auth) (bool, error) { // A user can create a list if he has write access to the namespace n := &Namespace{ID: l.NamespaceID} @@ -76,6 +91,12 @@ func (l *List) IsAdmin(a web.Auth) (bool, error) { return false, err } + // Check if we're dealing with a share auth + shareAuth, ok := a.(*LinkSharing) + if ok { + return originalList.ID == shareAuth.ListID && shareAuth.Right == RightAdmin, nil + } + // Check all the things // Check if the user is either owner or can write to the list // Owners are always admins diff --git a/pkg/models/list_users_rights.go b/pkg/models/list_users_rights.go index 6876ef1bc0a..634e53c5d2c 100644 --- a/pkg/models/list_users_rights.go +++ b/pkg/models/list_users_rights.go @@ -22,20 +22,25 @@ import ( // CanCreate checks if the user can create a new user <-> list relation func (lu *ListUser) CanCreate(a web.Auth) (bool, error) { - // Get the list and check if the user has write access on it - l := List{ID: lu.ListID} - return l.CanWrite(a) + return lu.canDoListUser(a) } // CanDelete checks if the user can delete a user <-> list relation func (lu *ListUser) CanDelete(a web.Auth) (bool, error) { - // Get the list and check if the user has write access on it - l := List{ID: lu.ListID} - return l.CanWrite(a) + return lu.canDoListUser(a) } // CanUpdate checks if the user can update a user <-> list relation func (lu *ListUser) CanUpdate(a web.Auth) (bool, error) { + return lu.canDoListUser(a) +} + +func (lu *ListUser) canDoListUser(a web.Auth) (bool, error) { + // Link shares aren't allowed to do anything + if _, is := a.(*LinkSharing); is { + return false, nil + } + // Get the list and check if the user has write access on it l := List{ID: lu.ListID} return l.CanWrite(a) diff --git a/pkg/models/models.go b/pkg/models/models.go index a07c9790c6b..5ce084505b9 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -48,6 +48,7 @@ func GetTables() []interface{} { &Label{}, &LabelTask{}, &TaskReminder{}, + &LinkSharing{}, } } diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go index e2ae85dc0f1..5e049ed4af4 100644 --- a/pkg/models/namespace.go +++ b/pkg/models/namespace.go @@ -138,6 +138,10 @@ type NamespaceWithLists struct { // @Failure 500 {object} models.Message "Internal error" // @Router /namespaces [get] func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, error) { + if _, is := a.(*LinkSharing); is { + return nil, ErrGenericForbidden{} + } + doer, err := getUserWithError(a) if err != nil { return nil, err diff --git a/pkg/models/namespace_rights.go b/pkg/models/namespace_rights.go index d584235e10b..cbc1701d836 100644 --- a/pkg/models/namespace_rights.go +++ b/pkg/models/namespace_rights.go @@ -48,12 +48,21 @@ func (n *Namespace) CanDelete(a web.Auth) (bool, error) { // CanCreate checks if the user can create a new namespace func (n *Namespace) CanCreate(a web.Auth) (bool, error) { + if _, is := a.(*LinkSharing); is { + return false, nil + } + // This is currently a dummy function, later on we could imagine global limits etc. return true, nil } func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) { + // If the auth is a link share, don't do anything + if _, is := a.(*LinkSharing); is { + return false, nil + } + // Get the namespace and check the right err := n.GetSimpleByID() if err != nil { diff --git a/pkg/models/task_assignees.go b/pkg/models/task_assignees.go index efa070b65d8..d4c747a4574 100644 --- a/pkg/models/task_assignees.go +++ b/pkg/models/task_assignees.go @@ -221,13 +221,27 @@ func (t *Task) addNewAssigneeByID(newAssigneeID int64, list *List) (err error) { // @Produce json // @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned." // @Param s query string false "Search assignees by their username." +// @Param taskID path int true "Task ID" // @Security JWTKeyAuth // @Success 200 {array} models.User "The assignees" // @Failure 500 {object} models.Message "Internal error" -// @Router /labels [get] +// @Router /tasks/{taskID}/assignees [get] func (la *TaskAssginee) ReadAll(search string, a web.Auth, page int) (interface{}, error) { + task, err := GetListSimplByTaskID(la.TaskID) + if err != nil { + return nil, err + } + + can, err := task.CanRead(a) + if err != nil { + return nil, err + } + if !can { + return nil, ErrGenericForbidden{} + } + var taskAssignees []*User - err := x.Table("task_assignees"). + err = x.Table("task_assignees"). Select("users.*"). Join("INNER", "users", "task_assignees.user_id = users.id"). Where("task_id = ? AND users.username LIKE ?", la.TaskID, "%"+search+"%"). diff --git a/pkg/models/task_assignees_rights.go b/pkg/models/task_assignees_rights.go index 4eeb254a79b..53389c395c7 100644 --- a/pkg/models/task_assignees_rights.go +++ b/pkg/models/task_assignees_rights.go @@ -41,5 +41,5 @@ func canDoTaskAssingee(taskID int64, a web.Auth) (bool, error) { if err != nil { return false, err } - return list.CanCreate(a) + return list.CanUpdate(a) } diff --git a/pkg/models/task_readall_test.go b/pkg/models/task_readall_test.go index 25bc69d12a1..0190faa9417 100644 --- a/pkg/models/task_readall_test.go +++ b/pkg/models/task_readall_test.go @@ -317,6 +317,15 @@ func sortTasksForTesting(by SortBy) (tasks []*Task) { Created: 1543626724, Updated: 1543626724, }, + { + ID: 32, + Text: "task #32", + CreatedByID: 1, + CreatedBy: user1, + ListID: 3, + Created: 1543626724, + Updated: 1543626724, + }, } switch by { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 720d5407b38..db32e012e13 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -147,17 +147,40 @@ func (t *Task) ReadAll(search string, a web.Auth, page int) (interface{}, error) sortby = SortTasksByUnsorted } - return GetTasksByUser(search, &User{ID: a.GetID()}, page, sortby, time.Unix(t.StartDateSortUnix, 0), time.Unix(t.EndDateSortUnix, 0)) -} + taskopts := &taskOptions{ + search: search, + sortby: sortby, + startDate: time.Unix(t.StartDateSortUnix, 0), + endDate: time.Unix(t.EndDateSortUnix, 0), + } -//GetTasksByUser returns all tasks for a user -func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate time.Time, endDate time.Time) ([]*Task, error) { - // Get all lists - lists, err := getRawListsForUser("", u, page) + shareAuth, is := a.(*LinkSharing) + if is { + shareAuth.List = &List{ID: shareAuth.ListID} + err := shareAuth.List.GetSimpleByID() + if err != nil { + return nil, err + } + return getTasksForLists([]*List{shareAuth.List}, taskopts) + } + + // Get all lists for the user + lists, err := getRawListsForUser("", &User{ID: a.GetID()}, page) if err != nil { return nil, err } + return getTasksForLists(lists, taskopts) +} + +type taskOptions struct { + search string + sortby SortBy + startDate time.Time + endDate time.Time +} + +func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, err error) { // Get all list IDs and get the tasks var listIDs []int64 for _, l := range lists { @@ -165,7 +188,7 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t } var orderby string - switch sortby { + switch opts.sortby { case SortTasksByPriorityDesc: orderby = "priority desc" case SortTasksByPriorityAsc: @@ -179,20 +202,20 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t taskMap := make(map[int64]*Task) // Then return all tasks for that lists - if startDate.Unix() != 0 || endDate.Unix() != 0 { + if opts.startDate.Unix() != 0 || opts.endDate.Unix() != 0 { startDateUnix := time.Now().Unix() - if startDate.Unix() != 0 { - startDateUnix = startDate.Unix() + if opts.startDate.Unix() != 0 { + startDateUnix = opts.startDate.Unix() } endDateUnix := time.Now().Unix() - if endDate.Unix() != 0 { - endDateUnix = endDate.Unix() + if opts.endDate.Unix() != 0 { + endDateUnix = opts.endDate.Unix() } if err := x.In("list_id", listIDs). - Where("text LIKE ?", "%"+search+"%"). + Where("text LIKE ?", "%"+opts.search+"%"). And("((due_date_unix BETWEEN ? AND ?) OR "+ "(start_date_unix BETWEEN ? and ?) OR "+ "(end_date_unix BETWEEN ? and ?))", startDateUnix, endDateUnix, startDateUnix, endDateUnix, startDateUnix, endDateUnix). @@ -203,7 +226,7 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t } } else { if err := x.In("list_id", listIDs). - Where("text LIKE ?", "%"+search+"%"). + Where("text LIKE ?", "%"+opts.search+"%"). And("(parent_task_id = 0 OR parent_task_id IS NULL)"). OrderBy(orderby). Find(&taskMap); err != nil { @@ -211,13 +234,13 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t } } - tasks, err := addMoreInfoToTasks(taskMap) + tasks, err = addMoreInfoToTasks(taskMap) if err != nil { return nil, err } // Because the list is sorted by id which we don't want (since we're dealing with maps) // we have to manually sort the tasks again here. - sortTasks(tasks, sortby) + sortTasks(tasks, opts.sortby) return tasks, err } diff --git a/pkg/models/tasks_rights.go b/pkg/models/tasks_rights.go index 0e1d67f9ecc..edae548b35d 100644 --- a/pkg/models/tasks_rights.go +++ b/pkg/models/tasks_rights.go @@ -47,8 +47,8 @@ func (t *Task) CanRead(a web.Auth) (canRead bool, err error) { } // A user can read a task if it has access to the list - list := &List{ID: t.ListID} - return list.CanRead(a) + l := &List{ID: t.ListID} + return l.CanRead(a) } // Helper function to check if a user can do stuff on a list task diff --git a/pkg/models/team_list_rights.go b/pkg/models/team_list_rights.go index c3728fa1128..aef30cb4160 100644 --- a/pkg/models/team_list_rights.go +++ b/pkg/models/team_list_rights.go @@ -36,6 +36,11 @@ func (tl *TeamList) CanUpdate(a web.Auth) (bool, error) { } func (tl *TeamList) canDoTeamList(a web.Auth) (bool, error) { + // Link shares aren't allowed to do anything + if _, is := a.(*LinkSharing); is { + return false, nil + } + l := List{ID: tl.ListID} return l.IsAdmin(a) } diff --git a/pkg/models/team_members_rights.go b/pkg/models/team_members_rights.go index 5fcb8d9a17c..63cfd4605eb 100644 --- a/pkg/models/team_members_rights.go +++ b/pkg/models/team_members_rights.go @@ -32,6 +32,11 @@ func (tm *TeamMember) CanDelete(a web.Auth) (bool, error) { // IsAdmin checks if the user is team admin func (tm *TeamMember) IsAdmin(a web.Auth) (bool, error) { + // Don't allow anything if we're dealing with a list share here + if _, is := a.(*LinkSharing); is { + return false, nil + } + // A user can add a member to a team if he is admin of that team exists, err := x.Where("user_id = ? AND team_id = ? AND admin = ?", a.GetID(), tm.TeamID, true). Get(&TeamMember{}) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 0d9c77e5a48..e53b1efac59 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -142,6 +142,10 @@ func (t *Team) ReadOne() (err error) { // @Failure 500 {object} models.Message "Internal error" // @Router /teams [get] func (t *Team) ReadAll(search string, a web.Auth, page int) (interface{}, error) { + if _, is := a.(*LinkSharing); is { + return nil, ErrGenericForbidden{} + } + all := []*Team{} err := x.Select("teams.*"). Table("teams"). diff --git a/pkg/models/teams_rights.go b/pkg/models/teams_rights.go index 6d038be59fd..321141f6a0e 100644 --- a/pkg/models/teams_rights.go +++ b/pkg/models/teams_rights.go @@ -22,6 +22,10 @@ import ( // CanCreate checks if the user can create a new team func (t *Team) CanCreate(a web.Auth) (bool, error) { + if _, is := a.(*LinkSharing); is { + return false, nil + } + // This is currently a dummy function, later on we could imagine global limits etc. return true, nil } @@ -38,6 +42,11 @@ func (t *Team) CanDelete(a web.Auth) (bool, error) { // IsAdmin returns true when the user is admin of a team func (t *Team) IsAdmin(a web.Auth) (bool, error) { + // Don't do anything if we're deadling with a link share auth here + if _, is := a.(*LinkSharing); is { + return false, nil + } + // Check if the team exists to be able to return a proper error message if not _, err := GetTeamByID(t.ID) if err != nil { diff --git a/pkg/models/user.go b/pkg/models/user.go index 7bfa8b5bb8f..b6c73dbe8e2 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -189,6 +189,11 @@ func CheckUserCredentials(u *UserLogin) (*User, error) { func GetCurrentUser(c echo.Context) (user *User, err error) { jwtinf := c.Get("user").(*jwt.Token) claims := jwtinf.Claims.(jwt.MapClaims) + return GetUserFromClaims(claims) +} + +// GetUserFromClaims Returns a new user from jwt claims +func GetUserFromClaims(claims jwt.MapClaims) (user *User, err error) { userID, ok := claims["id"].(float64) if !ok { return user, ErrCouldNotGetUserID{} diff --git a/pkg/routes/api/v1/auth.go b/pkg/routes/api/v1/auth.go new file mode 100644 index 00000000000..d3f48aed7c4 --- /dev/null +++ b/pkg/routes/api/v1/auth.go @@ -0,0 +1,67 @@ +// Vikunja is a todo-list application to facilitate your life. +// Copyright 2019 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package v1 + +import ( + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/models" + "github.com/dgrijalva/jwt-go" + "time" +) + +// These are all valid auth types +const ( + AuthTypeUnknown int = iota + AuthTypeUser + AuthTypeLinkShare +) + +// NewUserJWTAuthtoken generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests. +func NewUserJWTAuthtoken(user *models.User) (token string, err error) { + t := jwt.New(jwt.SigningMethodHS256) + + // Set claims + claims := t.Claims.(jwt.MapClaims) + claims["type"] = AuthTypeUser + claims["id"] = user.ID + claims["username"] = user.Username + claims["email"] = user.Email + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + + claims["avatar"] = user.AvatarURL + + // Generate encoded token and send it as response. + return t.SignedString([]byte(config.ServiceJWTSecret.GetString())) +} + +// NewLinkShareJWTAuthtoken creates a new jwt token from a link share +func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err error) { + t := jwt.New(jwt.SigningMethodHS256) + + // Set claims + claims := t.Claims.(jwt.MapClaims) + claims["type"] = AuthTypeLinkShare + claims["id"] = share.ID + claims["hash"] = share.Hash + claims["listID"] = share.ListID + claims["right"] = share.Right + claims["sharedByID"] = share.SharedByID + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + + // Generate encoded token and send it as response. + return t.SignedString([]byte(config.ServiceJWTSecret.GetString())) +} diff --git a/pkg/routes/api/v1/info.go b/pkg/routes/api/v1/info.go index 7adf36335a4..ee22985f5c3 100644 --- a/pkg/routes/api/v1/info.go +++ b/pkg/routes/api/v1/info.go @@ -24,22 +24,24 @@ import ( ) type vikunjaInfos struct { - Version string `json:"version"` - FrontendURL string `json:"frontend_url"` - Motd string `json:"motd"` + Version string `json:"version"` + FrontendURL string `json:"frontend_url"` + Motd string `json:"motd"` + LinkSharingEnabled bool `json:"link_sharing_enabled"` } // Info is the handler to get infos about this vikunja instance // @Summary Info -// @Description Returns the version, frontendurl and motd of Vikunja +// @Description Returns the version, frontendurl, motd and various settings of Vikunja // @tags service // @Produce json // @Success 200 {object} v1.vikunjaInfos // @Router /info [get] func Info(c echo.Context) error { return c.JSON(http.StatusOK, vikunjaInfos{ - Version: version.Version, - FrontendURL: config.ServiceFrontendurl.GetString(), - Motd: config.ServiceMotd.GetString(), + Version: version.Version, + FrontendURL: config.ServiceFrontendurl.GetString(), + Motd: config.ServiceMotd.GetString(), + LinkSharingEnabled: config.ServiceEnableLinkSharing.GetBool(), }) } diff --git a/pkg/routes/api/v1/link_sharing_auth.go b/pkg/routes/api/v1/link_sharing_auth.go new file mode 100644 index 00000000000..bdc0a2b8e2f --- /dev/null +++ b/pkg/routes/api/v1/link_sharing_auth.go @@ -0,0 +1,50 @@ +// Vikunja is a todo-list application to facilitate your life. +// Copyright 2019 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package v1 + +import ( + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/web/handler" + "github.com/labstack/echo/v4" + "net/http" +) + +// AuthenticateLinkShare gives a jwt auth token for valid share hashes +// @Summary Get an auth token for a share +// @Description Get a jwt auth token for a shared list from a share hash. +// @tags sharing +// @Accept json +// @Produce json +// @Param share path string true "The share hash" +// @Success 200 {object} v1.Token "The valid jwt auth token." +// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid link share object provided." +// @Failure 500 {object} models.Message "Internal error" +// @Router /shares/{share}/auth [post] +func AuthenticateLinkShare(c echo.Context) error { + hash := c.Param("share") + share, err := models.GetLinkShareByHash(hash) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + t, err := NewLinkShareJWTAuthtoken(share) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + return c.JSON(http.StatusOK, Token{Token: t}) +} diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go index 0db483b6f41..6826944f854 100644 --- a/pkg/routes/api/v1/login.go +++ b/pkg/routes/api/v1/login.go @@ -17,13 +17,10 @@ package v1 import ( - "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/models" "code.vikunja.io/web/handler" - "github.com/dgrijalva/jwt-go" "github.com/labstack/echo/v4" "net/http" - "time" ) // Token represents an authentification token @@ -55,27 +52,10 @@ func Login(c echo.Context) error { } // Create token - t, err := CreateNewJWTTokenForUser(user) + t, err := NewUserJWTAuthtoken(user) if err != nil { return err } return c.JSON(http.StatusOK, Token{Token: t}) } - -// CreateNewJWTTokenForUser generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests. -func CreateNewJWTTokenForUser(user *models.User) (token string, err error) { - t := jwt.New(jwt.SigningMethodHS256) - - // Set claims - claims := t.Claims.(jwt.MapClaims) - claims["username"] = user.Username - claims["email"] = user.Email - claims["id"] = user.ID - claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - - claims["avatar"] = user.AvatarURL - - // Generate encoded token and send it as response. - return t.SignedString([]byte(config.ServiceJWTSecret.GetString())) -} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 38ca54937b6..a7216839308 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -48,9 +48,11 @@ import ( "code.vikunja.io/web" "code.vikunja.io/web/handler" "github.com/asaskevich/govalidator" + "github.com/dgrijalva/jwt-go" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" elog "github.com/labstack/gommon/log" + "net/http" "strings" ) @@ -67,11 +69,11 @@ func (cv *CustomValidator) Validate(i interface{}) error { } httperr := models.ValidationHTTPError{ - web.HTTPError{ + HTTPError: web.HTTPError{ Code: models.ErrCodeInvalidData, Message: "Invalid Data", }, - errs, + InvalidFields: errs, } return httperr @@ -108,7 +110,16 @@ func NewEcho() *echo.Echo { // Handler config handler.SetAuthProvider(&web.Auths{ AuthObject: func(c echo.Context) (web.Auth, error) { - return models.GetCurrentUser(c) + jwtinf := c.Get("user").(*jwt.Token) + claims := jwtinf.Claims.(jwt.MapClaims) + typ := int(claims["type"].(float64)) + if typ == apiv1.AuthTypeLinkShare && config.ServiceEnableLinkSharing.GetBool() { + return models.GetLinkShareFromClaims(claims) + } + if typ == apiv1.AuthTypeUser { + return models.GetUserFromClaims(claims) + } + return nil, echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."}) }, }) handler.SetLoggingProvider(log.GetLogger()) @@ -165,6 +176,11 @@ func registerAPIRoutes(a *echo.Group) { // Info endpoint a.GET("/info", apiv1.Info) + // Link share auth + if config.ServiceEnableLinkSharing.GetBool() { + a.POST("/shares/:share/auth", apiv1.AuthenticateLinkShare) + } + // ===== Routes with Authetication ===== // Authetification a.Use(middleware.JWT([]byte(config.ServiceJWTSecret.GetString()))) @@ -194,6 +210,18 @@ func registerAPIRoutes(a *echo.Group) { a.PUT("/namespaces/:namespace/lists", listHandler.CreateWeb) a.GET("/lists/:list/listusers", apiv1.ListUsersForList) + if config.ServiceEnableLinkSharing.GetBool() { + listSharingHandler := &handler.WebHandler{ + EmptyStruct: func() handler.CObject { + return &models.LinkSharing{} + }, + } + a.PUT("/lists/:list/shares", listSharingHandler.CreateWeb) + a.GET("/lists/:list/shares", listSharingHandler.ReadAllWeb) + a.GET("/lists/:list/shares/:share", listSharingHandler.ReadOneWeb) + a.DELETE("/lists/:list/shares/:share", listSharingHandler.DeleteWeb) + } + taskHandler := &handler.WebHandler{ EmptyStruct: func() handler.CObject { return &models.Task{} diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index ce87770b024..6ed359a30d8 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,6 +1,6 @@ // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // This file was generated by swaggo/swag at -// 2019-07-21 23:56:59.485456403 +0200 CEST m=+0.091837160 +// 2019-08-31 22:48:49.201391811 +0200 CEST m=+0.228973511 package swagger @@ -32,7 +32,7 @@ var doc = `{ "paths": { "/info": { "get": { - "description": "Returns the version, frontendurl and motd of Vikunja", + "description": "Returns the version, frontendurl, motd and various settings of Vikunja", "produces": [ "application/json" ], @@ -58,7 +58,7 @@ var doc = `{ "JWTKeyAuth": [] } ], - "description": "Returns an array with all assignees for this task.", + "description": "Returns all labels which are either created by the user or associated with a task the user has at least read-access to.", "consumes": [ "application/json" ], @@ -66,9 +66,9 @@ var doc = `{ "application/json" ], "tags": [ - "assignees" + "labels" ], - "summary": "Get all assignees for a task", + "summary": "Get all labels a user has access to", "parameters": [ { "type": "integer", @@ -78,18 +78,18 @@ var doc = `{ }, { "type": "string", - "description": "Search assignees by their username.", + "description": "Search labels by label text.", "name": "s", "in": "query" } ], "responses": { "200": { - "description": "The assignees", + "description": "The labels", "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.User" + "$ref": "#/definitions/models.Label" } } }, @@ -1266,6 +1266,269 @@ var doc = `{ } } }, + "/lists/{list}/shares": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns all link shares which exist for a given list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Get all link shares for a list", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "p", + "in": "query" + }, + { + "type": "string", + "description": "Search shares by hash.", + "name": "s", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The share links", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LinkSharing" + } + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + }, + "put": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Share a list via link. The user needs to have write-access to the list to be able do this.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Share a list via link", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "description": "The new link share object", + "name": "label", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/models.LinkSharing" + } + } + ], + "responses": { + "200": { + "description": "The created link share object.", + "schema": { + "type": "object", + "$ref": "#/definitions/models.LinkSharing" + } + }, + "400": { + "description": "Invalid link share object provided.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "403": { + "description": "Not allowed to add the list share.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "404": { + "description": "The list does not exist.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + } + }, + "/lists/{list}/shares/{share}": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns one link share by its ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Get one link shares for a list", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Share ID", + "name": "share", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The share links", + "schema": { + "type": "object", + "$ref": "#/definitions/models.LinkSharing" + } + }, + "403": { + "description": "No access to the list", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "404": { + "description": "Share Link not found.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + }, + "delete": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Remove a link share. The user needs to have write-access to the list to be able do this.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Remove a link share", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Share Link ID", + "name": "share", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The link was successfully removed.", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + }, + "403": { + "description": "Not allowed to remove the link.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "404": { + "description": "Share Link not found.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/login": { "post": { "description": "Logs a user in. Returns a JWT-Token to authenticate further requests.", @@ -2337,6 +2600,53 @@ var doc = `{ } } }, + "/shares/{share}/auth": { + "post": { + "description": "Get a jwt auth token for a shared list from a share hash.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Get an auth token for a share", + "parameters": [ + { + "type": "string", + "description": "The share hash", + "name": "share", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The valid jwt auth token.", + "schema": { + "type": "object", + "$ref": "#/definitions/v1.Token" + } + }, + "400": { + "description": "Invalid link share object provided.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io.web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/tasks/all": { "get": { "security": [ @@ -2593,6 +2903,63 @@ var doc = `{ } }, "/tasks/{taskID}/assignees": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns an array with all assignees for this task.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "assignees" + ], + "summary": "Get all assignees for a task", + "parameters": [ + { + "type": "integer", + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "p", + "in": "query" + }, + { + "type": "string", + "description": "Search assignees by their username.", + "name": "s", + "in": "query" + }, + { + "type": "integer", + "description": "Task ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The assignees", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + }, "put": { "security": [ { @@ -3896,6 +4263,48 @@ var doc = `{ } } }, + "models.LinkSharing": { + "type": "object", + "properties": { + "created": { + "description": "A unix timestamp when this list was shared. You cannot change this value.", + "type": "integer" + }, + "hash": { + "description": "The public id to get this shared list", + "type": "string" + }, + "id": { + "description": "The ID of the shared thing", + "type": "integer" + }, + "list": { + "type": "object", + "$ref": "#/definitions/models.List" + }, + "right": { + "description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", + "default": 0, + "maximum": 2 + }, + "shared_by": { + "description": "The user who shared this list", + "type": "object", + "$ref": "#/definitions/models.User" + }, + "sharing_type": { + "description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).", + "type": "integer", + "default": 0, + "maximum": 2 + }, + "updated": { + "description": "A unix timestamp when this share was last updated. You cannot change this value.", + "type": "integer" + } + } + }, "models.List": { "type": "object", "properties": { @@ -3935,118 +4344,6 @@ var doc = `{ } } }, - "models.Task": { - "type": "object", - "properties": { - "assignees": { - "description": "An array of users who are assigned to this task", - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "created": { - "description": "A unix timestamp when this task was created. You cannot change this value.", - "type": "integer" - }, - "createdBy": { - "description": "The user who initially created the task.", - "type": "object", - "$ref": "#/definitions/models.User" - }, - "description": { - "description": "The task description.", - "type": "string" - }, - "done": { - "description": "Whether a task is done or not.", - "type": "boolean" - }, - "doneAt": { - "description": "The unix timestamp when a task was marked as done.", - "type": "integer" - }, - "dueDate": { - "description": "A unix timestamp when the task is due.", - "type": "integer" - }, - "endDate": { - "description": "When this task ends.", - "type": "integer" - }, - "hexColor": { - "description": "The task color in hex", - "type": "string", - "maxLength": 6 - }, - "id": { - "description": "The unique, numeric id of this task.", - "type": "integer" - }, - "labels": { - "description": "An array of labels which are associated with this task.", - "type": "array", - "items": { - "$ref": "#/definitions/models.Label" - } - }, - "listID": { - "description": "The list this task belongs to.", - "type": "integer" - }, - "parentTaskID": { - "description": "If the task is a subtask, this is the id of its parent.", - "type": "integer" - }, - "priority": { - "description": "The task priority. Can be anything you want, it is possible to sort by this later.", - "type": "integer" - }, - "reminderDates": { - "description": "An array of unix timestamps when the user wants to be reminded of the task.", - "type": "array", - "items": { - "type": "integer" - } - }, - "repeatAfter": { - "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", - "type": "integer" - }, - "startDate": { - "description": "When this task starts.", - "type": "integer" - }, - "subtasks": { - "description": "An array of subtasks.", - "type": "array", - "items": { - "$ref": "#/definitions/models.Task" - } - }, - "text": { - "description": "The task text. This is what you'll see in the list.", - "type": "string", - "maxLength": 250, - "minLength": 3 - }, - "updated": { - "description": "A unix timestamp when this task was last updated. You cannot change this value.", - "type": "integer" - } - } - }, - "models.TaskAssginee": { - "type": "object", - "properties": { - "created": { - "type": "integer" - }, - "user_id": { - "type": "integer" - } - } - }, "models.ListUser": { "type": "object", "properties": { @@ -4202,6 +4499,118 @@ var doc = `{ } } }, + "models.Task": { + "type": "object", + "properties": { + "assignees": { + "description": "An array of users who are assigned to this task", + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + }, + "created": { + "description": "A unix timestamp when this task was created. You cannot change this value.", + "type": "integer" + }, + "createdBy": { + "description": "The user who initially created the task.", + "type": "object", + "$ref": "#/definitions/models.User" + }, + "description": { + "description": "The task description.", + "type": "string" + }, + "done": { + "description": "Whether a task is done or not.", + "type": "boolean" + }, + "doneAt": { + "description": "The unix timestamp when a task was marked as done.", + "type": "integer" + }, + "dueDate": { + "description": "A unix timestamp when the task is due.", + "type": "integer" + }, + "endDate": { + "description": "When this task ends.", + "type": "integer" + }, + "hexColor": { + "description": "The task color in hex", + "type": "string", + "maxLength": 6 + }, + "id": { + "description": "The unique, numeric id of this task.", + "type": "integer" + }, + "labels": { + "description": "An array of labels which are associated with this task.", + "type": "array", + "items": { + "$ref": "#/definitions/models.Label" + } + }, + "listID": { + "description": "The list this task belongs to.", + "type": "integer" + }, + "parentTaskID": { + "description": "If the task is a subtask, this is the id of its parent.", + "type": "integer" + }, + "priority": { + "description": "The task priority. Can be anything you want, it is possible to sort by this later.", + "type": "integer" + }, + "reminderDates": { + "description": "An array of unix timestamps when the user wants to be reminded of the task.", + "type": "array", + "items": { + "type": "integer" + } + }, + "repeatAfter": { + "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", + "type": "integer" + }, + "startDate": { + "description": "When this task starts.", + "type": "integer" + }, + "subtasks": { + "description": "An array of subtasks.", + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } + }, + "text": { + "description": "The task text. This is what you'll see in the list.", + "type": "string", + "maxLength": 250, + "minLength": 3 + }, + "updated": { + "description": "A unix timestamp when this task was last updated. You cannot change this value.", + "type": "integer" + } + } + }, + "models.TaskAssginee": { + "type": "object", + "properties": { + "created": { + "type": "integer" + }, + "user_id": { + "type": "integer" + } + } + }, "models.Team": { "type": "object", "properties": { @@ -4499,6 +4908,9 @@ var doc = `{ "frontend_url": { "type": "string" }, + "link_sharing_enabled": { + "type": "boolean" + }, "motd": { "type": "string" }, diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 094349ce7ca..ab13676d27d 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -19,7 +19,7 @@ "paths": { "/info": { "get": { - "description": "Returns the version, frontendurl and motd of Vikunja", + "description": "Returns the version, frontendurl, motd and various settings of Vikunja", "produces": [ "application/json" ], @@ -45,7 +45,7 @@ "JWTKeyAuth": [] } ], - "description": "Returns an array with all assignees for this task.", + "description": "Returns all labels which are either created by the user or associated with a task the user has at least read-access to.", "consumes": [ "application/json" ], @@ -53,9 +53,9 @@ "application/json" ], "tags": [ - "assignees" + "labels" ], - "summary": "Get all assignees for a task", + "summary": "Get all labels a user has access to", "parameters": [ { "type": "integer", @@ -65,18 +65,18 @@ }, { "type": "string", - "description": "Search assignees by their username.", + "description": "Search labels by label text.", "name": "s", "in": "query" } ], "responses": { "200": { - "description": "The assignees", + "description": "The labels", "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.User" + "$ref": "#/definitions/models.Label" } } }, @@ -475,7 +475,7 @@ "required": true, "schema": { "type": "object", - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } } ], @@ -484,7 +484,7 @@ "description": "The created task object.", "schema": { "type": "object", - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } }, "400": { @@ -1253,6 +1253,269 @@ } } }, + "/lists/{list}/shares": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns all link shares which exist for a given list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Get all link shares for a list", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "p", + "in": "query" + }, + { + "type": "string", + "description": "Search shares by hash.", + "name": "s", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The share links", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LinkSharing" + } + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + }, + "put": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Share a list via link. The user needs to have write-access to the list to be able do this.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Share a list via link", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "description": "The new link share object", + "name": "label", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/models.LinkSharing" + } + } + ], + "responses": { + "200": { + "description": "The created link share object.", + "schema": { + "type": "object", + "$ref": "#/definitions/models.LinkSharing" + } + }, + "400": { + "description": "Invalid link share object provided.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "403": { + "description": "Not allowed to add the list share.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "404": { + "description": "The list does not exist.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + } + }, + "/lists/{list}/shares/{share}": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns one link share by its ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Get one link shares for a list", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Share ID", + "name": "share", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The share links", + "schema": { + "type": "object", + "$ref": "#/definitions/models.LinkSharing" + } + }, + "403": { + "description": "No access to the list", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "404": { + "description": "Share Link not found.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + }, + "delete": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Remove a link share. The user needs to have write-access to the list to be able do this.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Remove a link share", + "parameters": [ + { + "type": "integer", + "description": "List ID", + "name": "list", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Share Link ID", + "name": "share", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The link was successfully removed.", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + }, + "403": { + "description": "Not allowed to remove the link.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "404": { + "description": "Share Link not found.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/login": { "post": { "description": "Logs a user in. Returns a JWT-Token to authenticate further requests.", @@ -2324,6 +2587,53 @@ } } }, + "/shares/{share}/auth": { + "post": { + "description": "Get a jwt auth token for a shared list from a share hash.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sharing" + ], + "summary": "Get an auth token for a share", + "parameters": [ + { + "type": "string", + "description": "The share hash", + "name": "share", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The valid jwt auth token.", + "schema": { + "type": "object", + "$ref": "#/definitions/v1.Token" + } + }, + "400": { + "description": "Invalid link share object provided.", + "schema": { + "type": "object", + "$ref": "#/definitions/code.vikunja.io/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/tasks/all": { "get": { "security": [ @@ -2380,7 +2690,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } } }, @@ -2429,7 +2739,7 @@ "description": "The updated task object.", "schema": { "type": "object", - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } }, "400": { @@ -2489,7 +2799,7 @@ "required": true, "schema": { "type": "object", - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } } ], @@ -2498,7 +2808,7 @@ "description": "The updated task object.", "schema": { "type": "object", - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } }, "400": { @@ -2580,6 +2890,63 @@ } }, "/tasks/{taskID}/assignees": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns an array with all assignees for this task.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "assignees" + ], + "summary": "Get all assignees for a task", + "parameters": [ + { + "type": "integer", + "description": "The page number. Used for pagination. If not provided, the first page of results is returned.", + "name": "p", + "in": "query" + }, + { + "type": "string", + "description": "Search assignees by their username.", + "name": "s", + "in": "query" + }, + { + "type": "integer", + "description": "Task ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The assignees", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + } + }, + "500": { + "description": "Internal error", + "schema": { + "type": "object", + "$ref": "#/definitions/models.Message" + } + } + } + }, "put": { "security": [ { @@ -2605,7 +2972,7 @@ "required": true, "schema": { "type": "object", - "$ref": "#/definitions/models.ListTaskAssginee" + "$ref": "#/definitions/models.TaskAssginee" } }, { @@ -2621,7 +2988,7 @@ "description": "The created assingee object.", "schema": { "type": "object", - "$ref": "#/definitions/models.ListTaskAssginee" + "$ref": "#/definitions/models.TaskAssginee" } }, "400": { @@ -2683,7 +3050,7 @@ "description": "The created assingees object.", "schema": { "type": "object", - "$ref": "#/definitions/models.ListTaskAssginee" + "$ref": "#/definitions/models.TaskAssginee" } }, "400": { @@ -3789,7 +4156,7 @@ "description": "An array of subtasks.", "type": "array", "items": { - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } }, "task_ids": { @@ -3882,6 +4249,48 @@ } } }, + "models.LinkSharing": { + "type": "object", + "properties": { + "created": { + "description": "A unix timestamp when this list was shared. You cannot change this value.", + "type": "integer" + }, + "hash": { + "description": "The public id to get this shared list", + "type": "string" + }, + "id": { + "description": "The ID of the shared thing", + "type": "integer" + }, + "list": { + "type": "object", + "$ref": "#/definitions/models.List" + }, + "right": { + "description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", + "default": 0, + "maximum": 2 + }, + "shared_by": { + "description": "The user who shared this list", + "type": "object", + "$ref": "#/definitions/models.User" + }, + "sharing_type": { + "description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).", + "type": "integer", + "default": 0, + "maximum": 2 + }, + "updated": { + "description": "A unix timestamp when this share was last updated. You cannot change this value.", + "type": "integer" + } + } + }, "models.List": { "type": "object", "properties": { @@ -3906,7 +4315,7 @@ "description": "An array of tasks which belong to the list.", "type": "array", "items": { - "$ref": "#/definitions/models.ListTask" + "$ref": "#/definitions/models.Task" } }, "title": { @@ -3921,118 +4330,6 @@ } } }, - "models.ListTask": { - "type": "object", - "properties": { - "assignees": { - "description": "An array of users who are assigned to this task", - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "created": { - "description": "A unix timestamp when this task was created. You cannot change this value.", - "type": "integer" - }, - "createdBy": { - "description": "The user who initially created the task.", - "type": "object", - "$ref": "#/definitions/models.User" - }, - "description": { - "description": "The task description.", - "type": "string" - }, - "done": { - "description": "Whether a task is done or not.", - "type": "boolean" - }, - "doneAt": { - "description": "The unix timestamp when a task was marked as done.", - "type": "integer" - }, - "dueDate": { - "description": "A unix timestamp when the task is due.", - "type": "integer" - }, - "endDate": { - "description": "When this task ends.", - "type": "integer" - }, - "hexColor": { - "description": "The task color in hex", - "type": "string", - "maxLength": 6 - }, - "id": { - "description": "The unique, numeric id of this task.", - "type": "integer" - }, - "labels": { - "description": "An array of labels which are associated with this task.", - "type": "array", - "items": { - "$ref": "#/definitions/models.Label" - } - }, - "listID": { - "description": "The list this task belongs to.", - "type": "integer" - }, - "parentTaskID": { - "description": "If the task is a subtask, this is the id of its parent.", - "type": "integer" - }, - "priority": { - "description": "The task priority. Can be anything you want, it is possible to sort by this later.", - "type": "integer" - }, - "reminderDates": { - "description": "An array of unix timestamps when the user wants to be reminded of the task.", - "type": "array", - "items": { - "type": "integer" - } - }, - "repeatAfter": { - "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", - "type": "integer" - }, - "startDate": { - "description": "When this task starts.", - "type": "integer" - }, - "subtasks": { - "description": "An array of subtasks.", - "type": "array", - "items": { - "$ref": "#/definitions/models.ListTask" - } - }, - "text": { - "description": "The task text. This is what you'll see in the list.", - "type": "string", - "maxLength": 250, - "minLength": 3 - }, - "updated": { - "description": "A unix timestamp when this task was last updated. You cannot change this value.", - "type": "integer" - } - } - }, - "models.ListTaskAssginee": { - "type": "object", - "properties": { - "created": { - "type": "integer" - }, - "user_id": { - "type": "integer" - } - } - }, "models.ListUser": { "type": "object", "properties": { @@ -4188,6 +4485,118 @@ } } }, + "models.Task": { + "type": "object", + "properties": { + "assignees": { + "description": "An array of users who are assigned to this task", + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + }, + "created": { + "description": "A unix timestamp when this task was created. You cannot change this value.", + "type": "integer" + }, + "createdBy": { + "description": "The user who initially created the task.", + "type": "object", + "$ref": "#/definitions/models.User" + }, + "description": { + "description": "The task description.", + "type": "string" + }, + "done": { + "description": "Whether a task is done or not.", + "type": "boolean" + }, + "doneAt": { + "description": "The unix timestamp when a task was marked as done.", + "type": "integer" + }, + "dueDate": { + "description": "A unix timestamp when the task is due.", + "type": "integer" + }, + "endDate": { + "description": "When this task ends.", + "type": "integer" + }, + "hexColor": { + "description": "The task color in hex", + "type": "string", + "maxLength": 6 + }, + "id": { + "description": "The unique, numeric id of this task.", + "type": "integer" + }, + "labels": { + "description": "An array of labels which are associated with this task.", + "type": "array", + "items": { + "$ref": "#/definitions/models.Label" + } + }, + "listID": { + "description": "The list this task belongs to.", + "type": "integer" + }, + "parentTaskID": { + "description": "If the task is a subtask, this is the id of its parent.", + "type": "integer" + }, + "priority": { + "description": "The task priority. Can be anything you want, it is possible to sort by this later.", + "type": "integer" + }, + "reminderDates": { + "description": "An array of unix timestamps when the user wants to be reminded of the task.", + "type": "array", + "items": { + "type": "integer" + } + }, + "repeatAfter": { + "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", + "type": "integer" + }, + "startDate": { + "description": "When this task starts.", + "type": "integer" + }, + "subtasks": { + "description": "An array of subtasks.", + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } + }, + "text": { + "description": "The task text. This is what you'll see in the list.", + "type": "string", + "maxLength": 250, + "minLength": 3 + }, + "updated": { + "description": "A unix timestamp when this task was last updated. You cannot change this value.", + "type": "integer" + } + } + }, + "models.TaskAssginee": { + "type": "object", + "properties": { + "created": { + "type": "integer" + }, + "user_id": { + "type": "integer" + } + } + }, "models.Team": { "type": "object", "properties": { @@ -4485,6 +4894,9 @@ "frontend_url": { "type": "string" }, + "link_sharing_enabled": { + "type": "boolean" + }, "motd": { "type": "string" }, diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 8074892161f..df06020f8a3 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -98,7 +98,7 @@ definitions: subtasks: description: An array of subtasks. items: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: array task_ids: description: A list of task ids to update @@ -170,6 +170,42 @@ definitions: $ref: '#/definitions/models.Label' type: array type: object + models.LinkSharing: + properties: + created: + description: A unix timestamp when this list was shared. You cannot change + this value. + type: integer + hash: + description: The public id to get this shared list + type: string + id: + description: The ID of the shared thing + type: integer + list: + $ref: '#/definitions/models.List' + type: object + right: + default: 0 + description: The right this list is shared with. 0 = Read only, 1 = Read & + Write, 2 = Admin. See the docs for more details. + maximum: 2 + type: integer + shared_by: + $ref: '#/definitions/models.User' + description: The user who shared this list + type: object + sharing_type: + default: 0 + description: The kind of this link. 0 = undefined, 1 = without password, 2 + = with password (currently not implemented). + maximum: 2 + type: integer + updated: + description: A unix timestamp when this share was last updated. You cannot + change this value. + type: integer + type: object models.List: properties: created: @@ -189,7 +225,7 @@ definitions: tasks: description: An array of tasks which belong to the list. items: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: array title: description: The title of the list. You'll see this in the namespace overview. @@ -201,94 +237,6 @@ definitions: change this value. type: integer type: object - models.ListTask: - properties: - assignees: - description: An array of users who are assigned to this task - items: - $ref: '#/definitions/models.User' - type: array - created: - description: A unix timestamp when this task was created. You cannot change - this value. - type: integer - createdBy: - $ref: '#/definitions/models.User' - description: The user who initially created the task. - type: object - description: - description: The task description. - type: string - done: - description: Whether a task is done or not. - type: boolean - doneAt: - description: The unix timestamp when a task was marked as done. - type: integer - dueDate: - description: A unix timestamp when the task is due. - type: integer - endDate: - description: When this task ends. - type: integer - hexColor: - description: The task color in hex - maxLength: 6 - type: string - id: - description: The unique, numeric id of this task. - type: integer - labels: - description: An array of labels which are associated with this task. - items: - $ref: '#/definitions/models.Label' - type: array - listID: - description: The list this task belongs to. - type: integer - parentTaskID: - description: If the task is a subtask, this is the id of its parent. - type: integer - priority: - description: The task priority. Can be anything you want, it is possible to - sort by this later. - type: integer - reminderDates: - description: An array of unix timestamps when the user wants to be reminded - of the task. - items: - type: integer - type: array - repeatAfter: - description: An amount in seconds this task repeats itself. If this is set, - when marking the task as done, it will mark itself as "undone" and then - increase all remindes and the due date by its amount. - type: integer - startDate: - description: When this task starts. - type: integer - subtasks: - description: An array of subtasks. - items: - $ref: '#/definitions/models.ListTask' - type: array - text: - description: The task text. This is what you'll see in the list. - maxLength: 250 - minLength: 3 - type: string - updated: - description: A unix timestamp when this task was last updated. You cannot - change this value. - type: integer - type: object - models.ListTaskAssginee: - properties: - created: - type: integer - user_id: - type: integer - type: object models.ListUser: properties: created: @@ -412,6 +360,94 @@ definitions: maxLength: 250 type: string type: object + models.Task: + properties: + assignees: + description: An array of users who are assigned to this task + items: + $ref: '#/definitions/models.User' + type: array + created: + description: A unix timestamp when this task was created. You cannot change + this value. + type: integer + createdBy: + $ref: '#/definitions/models.User' + description: The user who initially created the task. + type: object + description: + description: The task description. + type: string + done: + description: Whether a task is done or not. + type: boolean + doneAt: + description: The unix timestamp when a task was marked as done. + type: integer + dueDate: + description: A unix timestamp when the task is due. + type: integer + endDate: + description: When this task ends. + type: integer + hexColor: + description: The task color in hex + maxLength: 6 + type: string + id: + description: The unique, numeric id of this task. + type: integer + labels: + description: An array of labels which are associated with this task. + items: + $ref: '#/definitions/models.Label' + type: array + listID: + description: The list this task belongs to. + type: integer + parentTaskID: + description: If the task is a subtask, this is the id of its parent. + type: integer + priority: + description: The task priority. Can be anything you want, it is possible to + sort by this later. + type: integer + reminderDates: + description: An array of unix timestamps when the user wants to be reminded + of the task. + items: + type: integer + type: array + repeatAfter: + description: An amount in seconds this task repeats itself. If this is set, + when marking the task as done, it will mark itself as "undone" and then + increase all remindes and the due date by its amount. + type: integer + startDate: + description: When this task starts. + type: integer + subtasks: + description: An array of subtasks. + items: + $ref: '#/definitions/models.Task' + type: array + text: + description: The task text. This is what you'll see in the list. + maxLength: 250 + minLength: 3 + type: string + updated: + description: A unix timestamp when this task was last updated. You cannot + change this value. + type: integer + type: object + models.TaskAssginee: + properties: + created: + type: integer + user_id: + type: integer + type: object models.Team: properties: created: @@ -652,6 +688,8 @@ definitions: properties: frontend_url: type: string + link_sharing_enabled: + type: boolean motd: type: string version: @@ -678,7 +716,8 @@ info: paths: /info: get: - description: Returns the version, frontendurl and motd of Vikunja + description: Returns the version, frontendurl, motd and various settings of + Vikunja produces: - application/json responses: @@ -694,14 +733,15 @@ paths: get: consumes: - application/json - description: Returns an array with all assignees for this task. + description: Returns all labels which are either created by the user or associated + with a task the user has at least read-access to. parameters: - description: The page number. Used for pagination. If not provided, the first page of results is returned. in: query name: p type: integer - - description: Search assignees by their username. + - description: Search labels by label text. in: query name: s type: string @@ -709,10 +749,10 @@ paths: - application/json responses: "200": - description: The assignees + description: The labels schema: items: - $ref: '#/definitions/models.User' + $ref: '#/definitions/models.Label' type: array "500": description: Internal error @@ -721,9 +761,9 @@ paths: type: object security: - JWTKeyAuth: [] - summary: Get all assignees for a task + summary: Get all labels a user has access to tags: - - assignees + - labels put: consumes: - application/json @@ -1057,7 +1097,7 @@ paths: name: task required: true schema: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: object produces: - application/json @@ -1065,7 +1105,7 @@ paths: "200": description: The created task object. schema: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: object "400": description: Invalid task object provided. @@ -1320,6 +1360,184 @@ paths: summary: Add a user to a list tags: - sharing + /lists/{list}/shares: + get: + consumes: + - application/json + description: Returns all link shares which exist for a given list + parameters: + - description: List ID + in: path + name: list + required: true + type: integer + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. + in: query + name: p + type: integer + - description: Search shares by hash. + in: query + name: s + type: string + produces: + - application/json + responses: + "200": + description: The share links + schema: + items: + $ref: '#/definitions/models.LinkSharing' + type: array + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + type: object + security: + - JWTKeyAuth: [] + summary: Get all link shares for a list + tags: + - sharing + put: + consumes: + - application/json + description: Share a list via link. The user needs to have write-access to the + list to be able do this. + parameters: + - description: List ID + in: path + name: list + required: true + type: integer + - description: The new link share object + in: body + name: label + required: true + schema: + $ref: '#/definitions/models.LinkSharing' + type: object + produces: + - application/json + responses: + "200": + description: The created link share object. + schema: + $ref: '#/definitions/models.LinkSharing' + type: object + "400": + description: Invalid link share object provided. + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "403": + description: Not allowed to add the list share. + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "404": + description: The list does not exist. + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + type: object + security: + - JWTKeyAuth: [] + summary: Share a list via link + tags: + - sharing + /lists/{list}/shares/{share}: + delete: + consumes: + - application/json + description: Remove a link share. The user needs to have write-access to the + list to be able do this. + parameters: + - description: List ID + in: path + name: list + required: true + type: integer + - description: Share Link ID + in: path + name: share + required: true + type: integer + produces: + - application/json + responses: + "200": + description: The link was successfully removed. + schema: + $ref: '#/definitions/models.Message' + type: object + "403": + description: Not allowed to remove the link. + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "404": + description: Share Link not found. + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + type: object + security: + - JWTKeyAuth: [] + summary: Remove a link share + tags: + - sharing + get: + consumes: + - application/json + description: Returns one link share by its ID. + parameters: + - description: List ID + in: path + name: list + required: true + type: integer + - description: Share ID + in: path + name: share + required: true + type: integer + produces: + - application/json + responses: + "200": + description: The share links + schema: + $ref: '#/definitions/models.LinkSharing' + type: object + "403": + description: No access to the list + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "404": + description: Share Link not found. + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + type: object + security: + - JWTKeyAuth: [] + summary: Get one link shares for a list + tags: + - sharing /lists/{listID}/teams/{teamID}: delete: description: Delets a team from a list. The team won't have access to the list @@ -2232,6 +2450,38 @@ paths: summary: Register tags: - user + /shares/{share}/auth: + post: + consumes: + - application/json + description: Get a jwt auth token for a shared list from a share hash. + parameters: + - description: The share hash + in: path + name: share + required: true + type: string + produces: + - application/json + responses: + "200": + description: The valid jwt auth token. + schema: + $ref: '#/definitions/v1.Token' + type: object + "400": + description: Invalid link share object provided. + schema: + $ref: '#/definitions/code.vikunja.io/web.HTTPError' + type: object + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + type: object + summary: Get an auth token for a share + tags: + - sharing /tasks/{id}: delete: description: Deletes a task from a list. This does not mean "mark it done". @@ -2286,7 +2536,7 @@ paths: name: task required: true schema: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: object produces: - application/json @@ -2294,7 +2544,7 @@ paths: "200": description: The updated task object. schema: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: object "400": description: Invalid task object provided. @@ -2452,6 +2702,44 @@ paths: tags: - labels /tasks/{taskID}/assignees: + get: + consumes: + - application/json + description: Returns an array with all assignees for this task. + parameters: + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. + in: query + name: p + type: integer + - description: Search assignees by their username. + in: query + name: s + type: string + - description: Task ID + in: path + name: taskID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: The assignees + schema: + items: + $ref: '#/definitions/models.User' + type: array + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + type: object + security: + - JWTKeyAuth: [] + summary: Get all assignees for a task + tags: + - assignees put: consumes: - application/json @@ -2463,7 +2751,7 @@ paths: name: assignee required: true schema: - $ref: '#/definitions/models.ListTaskAssginee' + $ref: '#/definitions/models.TaskAssginee' type: object - description: Task ID in: path @@ -2476,7 +2764,7 @@ paths: "200": description: The created assingee object. schema: - $ref: '#/definitions/models.ListTaskAssginee' + $ref: '#/definitions/models.TaskAssginee' type: object "400": description: Invalid assignee object provided. @@ -2559,7 +2847,7 @@ paths: "200": description: The created assingees object. schema: - $ref: '#/definitions/models.ListTaskAssginee' + $ref: '#/definitions/models.TaskAssginee' type: object "400": description: Invalid assignee object provided. @@ -2659,7 +2947,7 @@ paths: description: The tasks schema: items: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: array "500": description: Internal error @@ -2693,7 +2981,7 @@ paths: "200": description: The updated task object. schema: - $ref: '#/definitions/models.ListTask' + $ref: '#/definitions/models.Task' type: object "400": description: Invalid task object provided.