feat(api tokens): add tests
This commit is contained in:
parent
e3dac16398
commit
d9bfcdab8e
33
pkg/db/fixtures/api_tokens.yml
Normal file
33
pkg/db/fixtures/api_tokens.yml
Normal file
@ -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
|
113
pkg/integrations/api_tokens_test.go
Normal file
113
pkg/integrations/api_tokens_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
@ -129,7 +129,8 @@ func GetAvailableAPIRoutesForToken(c echo.Context) error {
|
|||||||
|
|
||||||
// CanDoAPIRoute checks if a token is allowed to use the current api route
|
// CanDoAPIRoute checks if a token is allowed to use the current api route
|
||||||
func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) {
|
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]
|
group, hasGroup := token.Permissions[routeGroupName]
|
||||||
if !hasGroup {
|
if !hasGroup {
|
||||||
@ -142,19 +143,19 @@ func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) {
|
|||||||
return false
|
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"
|
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"
|
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"
|
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"
|
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"
|
route = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"time"
|
"time"
|
||||||
|
"xorm.io/builder"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"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{}
|
tokens := []*APIToken{}
|
||||||
|
|
||||||
query := s.Where("owner_id = ?", a.GetID()).
|
var where builder.Cond = builder.Eq{"owner_id": a.GetID()}
|
||||||
Limit(getLimitFromPageIndex(page, perPage))
|
|
||||||
|
|
||||||
if search != "" {
|
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 {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
totalCount, err := query.Count(&APIToken{})
|
totalCount, err := s.Where(where).Count(&APIToken{})
|
||||||
return tokens, len(tokens), totalCount, err
|
return tokens, len(tokens), totalCount, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
117
pkg/models/api_tokens_test.go
Normal file
117
pkg/models/api_tokens_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
@ -58,6 +58,7 @@ func GetTables() []interface{} {
|
|||||||
&SavedFilter{},
|
&SavedFilter{},
|
||||||
&Subscription{},
|
&Subscription{},
|
||||||
&Favorite{},
|
&Favorite{},
|
||||||
|
&APIToken{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ func SetupTests() {
|
|||||||
"saved_filters",
|
"saved_filters",
|
||||||
"subscriptions",
|
"subscriptions",
|
||||||
"favorites",
|
"favorites",
|
||||||
|
"api_tokens",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user