From d9bfcdab8e20074cbf752fa98537619892059926 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 1 Sep 2023 10:19:31 +0200 Subject: [PATCH] feat(api tokens): add tests --- pkg/db/fixtures/api_tokens.yml | 33 ++++++++ pkg/integrations/api_tokens_test.go | 113 +++++++++++++++++++++++++++ pkg/models/api_routes.go | 13 ++-- pkg/models/api_tokens.go | 16 ++-- pkg/models/api_tokens_test.go | 117 ++++++++++++++++++++++++++++ pkg/models/models.go | 1 + pkg/models/unit_tests.go | 1 + 7 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 pkg/db/fixtures/api_tokens.yml create mode 100644 pkg/integrations/api_tokens_test.go create mode 100644 pkg/models/api_tokens_test.go diff --git a/pkg/db/fixtures/api_tokens.yml b/pkg/db/fixtures/api_tokens.yml new file mode 100644 index 00000000000..1a0f38b4a96 --- /dev/null +++ b/pkg/db/fixtures/api_tokens.yml @@ -0,0 +1,33 @@ +- id: 1 + title: 'test token 1' + token_salt: iC1Qbpf7H1 + token_hash: a1813a558185d99f5197d2d549e4dd91292376aa00210229d70f77b57e165f6613fd12c1f790aa6493548cb9bceff33b45b4 + token_last_eight: 75f29d2e + permissions: '{"tasks":["read_all","update"]}' + expires_at: 2099-01-01 00:00:00 + owner_id: 1 + created: 2023-09-01 07:00:00 + updated: 2023-09-01 07:00:00 + # token in plaintext is tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e +- id: 2 + title: 'test token 2' + token_salt: EtwMsqDfOA + token_hash: 5c4d80c58947f21295064d473937709f1159ab09085eb59e38783da6032181069ec2e1d236486533b66999f9f4ac375b45f5 + token_last_eight: 235008c8 + permissions: '{"tasks":["read_all","update"]}' + expires_at: 2023-01-01 00:00:00 + owner_id: 1 + created: 2023-09-01 07:00:00 + updated: 2023-09-01 07:00:00 + # token in plaintext is tk_a5e6f92ddbad68f49ee2c63e52174db0235008c8 +- id: 3 + title: 'test token 3' + token_salt: AHeetyp1aB + token_hash: da4b9c3aa72633274c37ab3419fbfbe4c5b79310b76027ac36f85e4c5ad0c2342a1d9e1c9b72ca07ec0a66ad2ee3505539af + token_last_eight: 0b8dcb7c + permissions: '{"tasks":["read_all","update"]}' + expires_at: 2099-01-01 00:00:00 + owner_id: 2 + created: 2023-09-01 07:00:00 + updated: 2023-09-01 07:00:00 + # token in plaintext is tk_5e29ae2ae079781ff73b0a3e0fe4d75a0b8dcb7c diff --git a/pkg/integrations/api_tokens_test.go b/pkg/integrations/api_tokens_test.go new file mode 100644 index 00000000000..4ccde721f59 --- /dev/null +++ b/pkg/integrations/api_tokens_test.go @@ -0,0 +1,113 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present 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 Affero General Public Licensee 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 Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package integrations + +import ( + "net/http" + "net/http/httptest" + "testing" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/modules/auth" + "code.vikunja.io/api/pkg/routes" + "code.vikunja.io/api/pkg/user" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestAPIToken(t *testing.T) { + t.Run("valid token", func(t *testing.T) { + e, err := setupTestEnv() + assert.NoError(t, err) + req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil) + res := httptest.NewRecorder() + c := e.NewContext(req, res) + h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + u, err := auth.GetAuthFromClaims(c) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } + + return c.JSON(http.StatusOK, u) + }) + + req.Header.Set(echo.HeaderAuthorization, "Bearer tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e") // Token 1 + assert.NoError(t, h(c)) + // check if the request handlers "see" the request as if it came directly from that user + assert.Contains(t, res.Body.String(), `"username":"user1"`) + }) + t.Run("invalid token", func(t *testing.T) { + e, err := setupTestEnv() + assert.NoError(t, err) + req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil) + res := httptest.NewRecorder() + c := e.NewContext(req, res) + h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + return c.String(http.StatusOK, "test") + }) + + req.Header.Set(echo.HeaderAuthorization, "Bearer tk_loremipsumdolorsitamet") + assert.Error(t, h(c)) + }) + t.Run("expired token", func(t *testing.T) { + e, err := setupTestEnv() + assert.NoError(t, err) + req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil) + res := httptest.NewRecorder() + c := e.NewContext(req, res) + h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + return c.String(http.StatusOK, "test") + }) + + req.Header.Set(echo.HeaderAuthorization, "Bearer tk_a5e6f92ddbad68f49ee2c63e52174db0235008c8") // Token 2 + assert.Error(t, h(c)) + }) + t.Run("valid token, invalid scope", func(t *testing.T) { + e, err := setupTestEnv() + assert.NoError(t, err) + req := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil) + res := httptest.NewRecorder() + c := e.NewContext(req, res) + h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + return c.String(http.StatusOK, "test") + }) + + req.Header.Set(echo.HeaderAuthorization, "Bearer tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e") + assert.Error(t, h(c)) + }) + t.Run("jwt", func(t *testing.T) { + e, err := setupTestEnv() + assert.NoError(t, err) + req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil) + res := httptest.NewRecorder() + c := e.NewContext(req, res) + h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + return c.String(http.StatusOK, "test") + }) + + s := db.NewSession() + defer s.Close() + u, err := user.GetUserByID(s, 1) + assert.NoError(t, err) + jwt, err := auth.NewUserJWTAuthtoken(u, false) + assert.NoError(t, err) + + req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwt) + assert.NoError(t, h(c)) + }) +} diff --git a/pkg/models/api_routes.go b/pkg/models/api_routes.go index 560564d7c9e..4336a8b8fa7 100644 --- a/pkg/models/api_routes.go +++ b/pkg/models/api_routes.go @@ -129,7 +129,8 @@ func GetAvailableAPIRoutesForToken(c echo.Context) error { // CanDoAPIRoute checks if a token is allowed to use the current api route func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) { - routeGroupName := getRouteGroupName(c.Path()) + path := c.Request().URL.Path + routeGroupName := getRouteGroupName(path) group, hasGroup := token.Permissions[routeGroupName] if !hasGroup { @@ -142,19 +143,19 @@ func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) { return false } - if routes.Create != nil && routes.Create.Path == c.Path() && routes.Create.Method == c.Request().Method { + if routes.Create != nil && routes.Create.Path == path && routes.Create.Method == c.Request().Method { route = "create" } - if routes.ReadOne != nil && routes.ReadOne.Path == c.Path() && routes.ReadOne.Method == c.Request().Method { + if routes.ReadOne != nil && routes.ReadOne.Path == path && routes.ReadOne.Method == c.Request().Method { route = "read_one" } - if routes.ReadAll != nil && routes.ReadAll.Path == c.Path() && routes.ReadAll.Method == c.Request().Method { + if routes.ReadAll != nil && routes.ReadAll.Path == path && routes.ReadAll.Method == c.Request().Method { route = "read_all" } - if routes.Update != nil && routes.Update.Path == c.Path() && routes.Update.Method == c.Request().Method { + if routes.Update != nil && routes.Update.Path == path && routes.Update.Method == c.Request().Method { route = "update" } - if routes.Delete != nil && routes.Delete.Path == c.Path() && routes.Delete.Method == c.Request().Method { + if routes.Delete != nil && routes.Delete.Path == path && routes.Delete.Method == c.Request().Method { route = "delete" } diff --git a/pkg/models/api_tokens.go b/pkg/models/api_tokens.go index f31ea4ff508..b1a4c98efa6 100644 --- a/pkg/models/api_tokens.go +++ b/pkg/models/api_tokens.go @@ -21,6 +21,7 @@ import ( "crypto/subtle" "encoding/hex" "time" + "xorm.io/builder" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/utils" @@ -132,19 +133,24 @@ func (t *APIToken) ReadAll(s *xorm.Session, a web.Auth, search string, page int, tokens := []*APIToken{} - query := s.Where("owner_id = ?", a.GetID()). - Limit(getLimitFromPageIndex(page, perPage)) + var where builder.Cond = builder.Eq{"owner_id": a.GetID()} if search != "" { - query = query.Where(db.ILIKE("title", search)) + where = builder.And( + where, + db.ILIKE("title", search), + ) } - err = query.Find(&tokens) + err = s. + Where(where). + Limit(getLimitFromPageIndex(page, perPage)). + Find(&tokens) if err != nil { return nil, 0, 0, err } - totalCount, err := query.Count(&APIToken{}) + totalCount, err := s.Where(where).Count(&APIToken{}) return tokens, len(tokens), totalCount, err } diff --git a/pkg/models/api_tokens_test.go b/pkg/models/api_tokens_test.go new file mode 100644 index 00000000000..9b7bc10ae2f --- /dev/null +++ b/pkg/models/api_tokens_test.go @@ -0,0 +1,117 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present 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 Affero General Public Licensee 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 Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package models + +import ( + "testing" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/user" + + "github.com/stretchr/testify/assert" +) + +func TestAPIToken_ReadAll(t *testing.T) { + u := &user.User{ID: 1} + token := &APIToken{} + s := db.NewSession() + defer s.Close() + db.LoadAndAssertFixtures(t) + + // Checking if the user only sees their own tokens + + result, count, total, err := token.ReadAll(s, u, "", 1, 50) + assert.NoError(t, err) + tokens, is := result.([]*APIToken) + assert.Truef(t, is, "tokens are not of type []*APIToken") + assert.Len(t, tokens, 2) + assert.Equal(t, count, len(tokens)) + assert.Equal(t, int64(2), total) + assert.Equal(t, int64(1), tokens[0].ID) + assert.Equal(t, int64(2), tokens[1].ID) +} + +func TestAPIToken_CanDelete(t *testing.T) { + t.Run("own token", func(t *testing.T) { + u := &user.User{ID: 1} + token := &APIToken{ID: 1} + s := db.NewSession() + defer s.Close() + db.LoadAndAssertFixtures(t) + + can, err := token.CanDelete(s, u) + assert.NoError(t, err) + assert.True(t, can) + }) + t.Run("noneixsting token", func(t *testing.T) { + u := &user.User{ID: 1} + token := &APIToken{ID: 999} + s := db.NewSession() + defer s.Close() + db.LoadAndAssertFixtures(t) + + can, err := token.CanDelete(s, u) + assert.NoError(t, err) + assert.False(t, can) + }) + t.Run("token of another user", func(t *testing.T) { + u := &user.User{ID: 2} + token := &APIToken{ID: 1} + s := db.NewSession() + defer s.Close() + db.LoadAndAssertFixtures(t) + + can, err := token.CanDelete(s, u) + assert.NoError(t, err) + assert.False(t, can) + }) +} + +func TestAPIToken_Create(t *testing.T) { + t.Run("normal", func(t *testing.T) { + u := &user.User{ID: 1} + token := &APIToken{} + s := db.NewSession() + defer s.Close() + db.LoadAndAssertFixtures(t) + + err := token.Create(s, u) + }) +} + +func TestAPIToken_GetTokenFromTokenString(t *testing.T) { + t.Run("valid token", func(t *testing.T) { + s := db.NewSession() + defer s.Close() + db.LoadAndAssertFixtures(t) + + token, err := GetTokenFromTokenString(s, "tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e") // Token 1 + + assert.NoError(t, err) + assert.Equal(t, int64(1), token.ID) + }) + t.Run("invalid token", func(t *testing.T) { + s := db.NewSession() + defer s.Close() + db.LoadAndAssertFixtures(t) + + _, err := GetTokenFromTokenString(s, "tk_loremipsum") + + assert.Error(t, err) + assert.True(t, IsErrAPITokenInvalid(err)) + }) +} diff --git a/pkg/models/models.go b/pkg/models/models.go index 4e212bdee47..ec2e92f7c91 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -58,6 +58,7 @@ func GetTables() []interface{} { &SavedFilter{}, &Subscription{}, &Favorite{}, + &APIToken{}, } } diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go index fb383ed91b9..6dd66990b34 100644 --- a/pkg/models/unit_tests.go +++ b/pkg/models/unit_tests.go @@ -64,6 +64,7 @@ func SetupTests() { "saved_filters", "subscriptions", "favorites", + "api_tokens", ) if err != nil { log.Fatal(err)