From e3dac1639821d55acb0720cb324c35aed87cf742 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 1 Sep 2023 08:52:57 +0200 Subject: [PATCH] feat(api tokens): check permissions when saving --- pkg/{routes => models}/api_routes.go | 57 +++++++++++++++++++++++++--- pkg/models/api_tokens.go | 4 +- pkg/models/error.go | 28 ++++++++++++++ pkg/routes/routes.go | 6 +-- 4 files changed, 85 insertions(+), 10 deletions(-) rename pkg/{routes => models}/api_routes.go (78%) diff --git a/pkg/routes/api_routes.go b/pkg/models/api_routes.go similarity index 78% rename from pkg/routes/api_routes.go rename to pkg/models/api_routes.go index d25e68480bb..560564d7c9e 100644 --- a/pkg/routes/api_routes.go +++ b/pkg/models/api_routes.go @@ -14,14 +14,12 @@ // You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . -package routes +package models import ( "net/http" "strings" - "code.vikunja.io/api/pkg/models" - "github.com/labstack/echo/v4" ) @@ -64,8 +62,8 @@ func getRouteGroupName(path string) string { } } -// 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) { +// 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) { if !strings.Contains(route.Name, "(*WebHandler)") { return @@ -130,7 +128,7 @@ 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 *models.APIToken) (can bool) { +func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) { routeGroupName := getRouteGroupName(c.Path()) group, hasGroup := token.Permissions[routeGroupName] @@ -168,3 +166,50 @@ func CanDoAPIRoute(c echo.Context, token *models.APIToken) (can bool) { 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 method == "create" && routes.Create == nil { + return &ErrInvalidAPITokenPermission{ + Group: key, + Permission: method, + } + } + if method == "read_one" && routes.ReadOne == nil { + return &ErrInvalidAPITokenPermission{ + Group: key, + Permission: method, + } + } + if method == "read_all" && routes.ReadAll == nil { + return &ErrInvalidAPITokenPermission{ + Group: key, + Permission: method, + } + } + if method == "update" && routes.Update == nil { + return &ErrInvalidAPITokenPermission{ + Group: key, + Permission: method, + } + } + if method == "delete" && routes.Delete == nil { + return &ErrInvalidAPITokenPermission{ + Group: key, + Permission: method, + } + } + } + } + + return nil +} diff --git a/pkg/models/api_tokens.go b/pkg/models/api_tokens.go index 2794daa03df..f31ea4ff508 100644 --- a/pkg/models/api_tokens.go +++ b/pkg/models/api_tokens.go @@ -102,7 +102,9 @@ func (t *APIToken) Create(s *xorm.Session, a web.Auth) (err error) { t.OwnerID = a.GetID() - // TODO: validate permissions + if err := PermissionsAreValid(t.Permissions); err != nil { + return err + } _, err = s.Insert(t) return err diff --git a/pkg/models/error.go b/pkg/models/error.go index 6bc62d58d2f..73866ad7a87 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1685,3 +1685,31 @@ func (err ErrAPITokenInvalid) HTTPError() web.HTTPError { Message: "The provided api token is invalid.", } } + +// ErrInvalidAPITokenPermission represents an error where an api token is invalid +type ErrInvalidAPITokenPermission struct { + Group string + Permission string +} + +// IsErrInvalidAPITokenPermission checks if an error is ErrInvalidAPITokenPermission. +func IsErrInvalidAPITokenPermission(err error) bool { + _, ok := err.(*ErrInvalidAPITokenPermission) + return ok +} + +func (err *ErrInvalidAPITokenPermission) Error() string { + return fmt.Sprintf("API token permission %s of group %s is invalid", err.Permission, err.Group) +} + +// ErrCodeInvalidAPITokenPermission holds the unique world-error code of this error +const ErrCodeInvalidAPITokenPermission = 14002 + +// HTTPError holds the http error description +func (err ErrInvalidAPITokenPermission) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusBadRequest, + Code: ErrCodeInvalidAPITokenPermission, + Message: fmt.Sprintf("The permission %s of group %s is invalid.", err.Permission, err.Group), + } +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index de54cf42ae0..04ad658222f 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -204,7 +204,7 @@ func RegisterRoutes(e *echo.Echo) { // API Routes a := e.Group("/api/v1") e.OnAddRouteHandler = func(host string, route echo.Route, handler echo.HandlerFunc, middleware []echo.MiddlewareFunc) { - collectRoutesForAPITokenUsage(route) + models.CollectRoutesForAPITokenUsage(route) } registerAPIRoutes(a) } @@ -316,7 +316,7 @@ func registerAPIRoutes(a *echo.Group) { return echo.NewHTTPError(http.StatusUnauthorized) } - if !CanDoAPIRoute(c, token) { + if !models.CanDoAPIRoute(c, token) { return echo.NewHTTPError(http.StatusUnauthorized) } @@ -333,7 +333,7 @@ func registerAPIRoutes(a *echo.Group) { setupMetricsMiddleware(a) a.POST("/tokenTest", apiv1.CheckToken) - a.GET("/routes", GetAvailableAPIRoutesForToken) + a.GET("/routes", models.GetAvailableAPIRoutesForToken) // User stuff u := a.Group("/user")