
Previously, the bulk api endpoint were explicitly filtered out. This meant that you couldn't use them with api tokens. This change adds them to their "parent" token types as another option, allowing users to select and use them when creating api tokens. Resolves https://community.vikunja.io/t/help-with-bulk-api-complete/2461
292 lines
7.7 KiB
Go
292 lines
7.7 KiB
Go
// 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 (
|
|
"net/http"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"code.vikunja.io/api/pkg/log"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
var apiTokenRoutes = map[string]APITokenRoute{}
|
|
|
|
func init() {
|
|
apiTokenRoutes = make(map[string]APITokenRoute)
|
|
}
|
|
|
|
type APITokenRoute map[string]*RouteDetail
|
|
|
|
type RouteDetail struct {
|
|
Path string `json:"path"`
|
|
Method string `json:"method"`
|
|
}
|
|
|
|
func getRouteGroupName(path string) (finalName string, filteredParts []string) {
|
|
parts := strings.Split(strings.TrimPrefix(path, "/api/v1/"), "/")
|
|
filteredParts = []string{}
|
|
for _, part := range parts {
|
|
if strings.HasPrefix(part, ":") {
|
|
continue
|
|
}
|
|
|
|
filteredParts = append(filteredParts, part)
|
|
}
|
|
|
|
finalName = strings.Join(filteredParts, "_")
|
|
switch finalName {
|
|
case "projects_tasks":
|
|
fallthrough
|
|
case "tasks_all":
|
|
return "tasks", []string{"tasks"}
|
|
default:
|
|
return finalName, filteredParts
|
|
}
|
|
}
|
|
|
|
func getRouteDetail(route echo.Route) (method string, detail *RouteDetail) {
|
|
if strings.Contains(route.Name, "CreateWeb") {
|
|
return "create", &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
if strings.Contains(route.Name, "ReadOneWeb") {
|
|
return "read_one", &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
if strings.Contains(route.Name, "ReadAllWeb") {
|
|
return "read_all", &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
if strings.Contains(route.Name, "UpdateWeb") {
|
|
return "update", &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
if strings.Contains(route.Name, "DeleteWeb") {
|
|
return "delete", &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
|
|
return "", &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
|
|
// CollectRoutesForAPITokenUsage gets called for every added APITokenRoute and builds a list of all routes we can use for the api tokens.
|
|
func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.MiddlewareFunc) {
|
|
|
|
if route.Method == "echo_route_not_found" {
|
|
return
|
|
}
|
|
|
|
seenJWT := false
|
|
for _, middleware := range middlewares {
|
|
if strings.Contains(runtime.FuncForPC(reflect.ValueOf(middleware).Pointer()).Name(), "github.com/labstack/echo-jwt/") {
|
|
seenJWT = true
|
|
}
|
|
}
|
|
|
|
if !seenJWT {
|
|
return
|
|
}
|
|
|
|
routeGroupName, routeParts := getRouteGroupName(route.Path)
|
|
|
|
if routeGroupName == "user" ||
|
|
routeGroupName == "tokenTest" ||
|
|
routeGroupName == "subscriptions" ||
|
|
routeGroupName == "tokens" ||
|
|
routeGroupName == "*" ||
|
|
strings.HasPrefix(routeGroupName, "user_") {
|
|
return
|
|
}
|
|
|
|
if !strings.Contains(route.Name, "(*WebHandler)") && !strings.Contains(route.Name, "Attachment") {
|
|
routeDetail := &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
// We're trying to add routes to the routes of a matching "parent" - for
|
|
// example, projects_background should show up under "projects".
|
|
// To do this, we check if the route is a sub route of some other route
|
|
// and if that's the case, add it to its parent instead.
|
|
// Otherwise, we add it to the "other" key.
|
|
if len(routeParts) == 1 {
|
|
if _, has := apiTokenRoutes["other"]; !has {
|
|
apiTokenRoutes["other"] = make(APITokenRoute)
|
|
}
|
|
|
|
_, exists := apiTokenRoutes["other"][routeGroupName]
|
|
if exists {
|
|
routeGroupName += "_" + strings.ToLower(route.Method)
|
|
}
|
|
apiTokenRoutes["other"][routeGroupName] = routeDetail
|
|
return
|
|
}
|
|
|
|
subkey := strings.Join(routeParts[1:], "_")
|
|
|
|
if _, has := apiTokenRoutes[routeParts[0]]; !has {
|
|
apiTokenRoutes[routeParts[0]] = make(APITokenRoute)
|
|
}
|
|
|
|
if _, has := apiTokenRoutes[routeParts[0]][subkey]; has {
|
|
subkey += "_" + strings.ToLower(route.Method)
|
|
}
|
|
|
|
apiTokenRoutes[routeParts[0]][subkey] = routeDetail
|
|
|
|
return
|
|
}
|
|
|
|
if strings.HasSuffix(routeGroupName, "_bulk") {
|
|
parent := strings.TrimSuffix(routeGroupName, "_bulk")
|
|
_, has := apiTokenRoutes[parent]
|
|
if !has {
|
|
apiTokenRoutes[parent] = make(APITokenRoute)
|
|
}
|
|
|
|
method, routeDetail := getRouteDetail(route)
|
|
apiTokenRoutes[parent][method+"_bulk"] = routeDetail
|
|
return
|
|
}
|
|
|
|
_, has := apiTokenRoutes[routeGroupName]
|
|
if !has {
|
|
apiTokenRoutes[routeGroupName] = make(APITokenRoute)
|
|
}
|
|
|
|
method, routeDetail := getRouteDetail(route)
|
|
if method != "" {
|
|
apiTokenRoutes[routeGroupName][method] = routeDetail
|
|
}
|
|
|
|
if routeGroupName == "tasks_attachments" {
|
|
if strings.Contains(route.Name, "UploadTaskAttachment") {
|
|
apiTokenRoutes[routeGroupName]["create"] = &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
if strings.Contains(route.Name, "GetTaskAttachment") {
|
|
apiTokenRoutes[routeGroupName]["read_one"] = &RouteDetail{
|
|
Path: route.Path,
|
|
Method: route.Method,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetAvailableAPIRoutesForToken returns a list of all API routes which are available for token usage.
|
|
// @Summary Get a list of all token api routes
|
|
// @Description Returns a list of all API routes which are available to use with an api token, not a user login.
|
|
// @tags api
|
|
// @Produce json
|
|
// @Security JWTKeyAuth
|
|
// @Success 200 {array} models.APITokenRoute "The list of all routes."
|
|
// @Router /routes [get]
|
|
func GetAvailableAPIRoutesForToken(c echo.Context) error {
|
|
return c.JSON(http.StatusOK, apiTokenRoutes)
|
|
}
|
|
|
|
// CanDoAPIRoute checks if a token is allowed to use the current api route
|
|
func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) {
|
|
path := c.Path()
|
|
if path == "" {
|
|
// c.Path() is empty during testing, but returns the path which
|
|
// the route used during registration which is what we need.
|
|
path = c.Request().URL.Path
|
|
}
|
|
|
|
routeGroupName, routeParts := getRouteGroupName(path)
|
|
|
|
routeGroupName = strings.TrimSuffix(routeGroupName, "_bulk")
|
|
|
|
group, hasGroup := token.Permissions[routeGroupName]
|
|
if !hasGroup {
|
|
group, hasGroup = token.Permissions[routeParts[0]]
|
|
if !hasGroup {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var route string
|
|
routes, has := apiTokenRoutes[routeGroupName]
|
|
if !has {
|
|
routes, has = apiTokenRoutes[routeParts[0]]
|
|
if !has {
|
|
return false
|
|
}
|
|
route = strings.Join(routeParts[1:], "_")
|
|
}
|
|
|
|
// The tasks read_all route is available as /:project/tasks and /tasks/all - therefore we need this workaround here.
|
|
if routeGroupName == "tasks" && path == "/api/v1/projects/:project/tasks" && c.Request().Method == http.MethodGet {
|
|
route = "read_all"
|
|
}
|
|
|
|
for _, p := range group {
|
|
if route == "" && routes[p] != nil && routes[p].Path == path && routes[p].Method == c.Request().Method {
|
|
return true
|
|
}
|
|
if route != "" && p == route {
|
|
return true
|
|
}
|
|
}
|
|
|
|
log.Debugf("[auth] Token %d tried to use route %s which requires permission %s but has only %v", token.ID, path, route, token.Permissions)
|
|
|
|
return false
|
|
}
|
|
|
|
func PermissionsAreValid(permissions APIPermissions) (err error) {
|
|
|
|
for key, methods := range permissions {
|
|
routes, has := apiTokenRoutes[key]
|
|
if !has {
|
|
return &ErrInvalidAPITokenPermission{
|
|
Group: key,
|
|
}
|
|
}
|
|
|
|
for _, method := range methods {
|
|
if routes[method] == nil {
|
|
return &ErrInvalidAPITokenPermission{
|
|
Group: key,
|
|
Permission: method,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|