diff --git a/pkg/models/api_tokens.go b/pkg/models/api_tokens.go index 80a5a10a4..ed7651b9e 100644 --- a/pkg/models/api_tokens.go +++ b/pkg/models/api_tokens.go @@ -54,7 +54,9 @@ type APIToken struct { // A timestamp when this api key was created. You cannot change this value. Created time.Time `xorm:"created not null" json:"created"` - OwnerID int64 `xorm:"bigint not null" json:"-"` + // The user ID of the token owner. When creating a token for a bot user, set this + // to the bot's ID. If omitted, defaults to the authenticated user. + OwnerID int64 `xorm:"bigint not null" json:"owner_id,omitempty"` web.Permissions `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"` @@ -103,6 +105,15 @@ func (t *APIToken) Create(s *xorm.Session, a web.Auth) (err error) { if t.OwnerID == 0 { t.OwnerID = a.GetID() + } else if t.OwnerID != a.GetID() { + // If OwnerID is set to someone else, verify it's a bot owned by the caller. + botUser, err := user.GetUserByID(s, t.OwnerID) + if err != nil { + return err + } + if !botUser.IsBot() || botUser.BotOwnerID != a.GetID() { + return &user.ErrBotNotOwned{UserID: t.OwnerID} + } } if err := PermissionsAreValid(t.APIPermissions); err != nil { @@ -135,7 +146,20 @@ func (t *APIToken) ReadAll(s *xorm.Session, a web.Auth, search string, page int, tokens := []*APIToken{} - var where builder.Cond = builder.Eq{"owner_id": a.GetID()} + ownerID := a.GetID() + if t.OwnerID != 0 && t.OwnerID != a.GetID() { + // If filtering by a different owner, verify it's a bot owned by the caller. + botUser, lookupErr := user.GetUserByID(s, t.OwnerID) + if lookupErr != nil { + return nil, 0, 0, lookupErr + } + if !botUser.IsBot() || botUser.BotOwnerID != a.GetID() { + return nil, 0, 0, &user.ErrBotNotOwned{UserID: t.OwnerID} + } + ownerID = t.OwnerID + } + + var where builder.Cond = builder.Eq{"owner_id": ownerID} if search != "" { where = builder.And( @@ -168,8 +192,9 @@ func (t *APIToken) ReadAll(s *xorm.Session, a web.Auth, search string, page int, // @Failure 404 {object} web.HTTPError "The token does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /tokens/{tokenID} [delete] -func (t *APIToken) Delete(s *xorm.Session, a web.Auth) (err error) { - _, err = s.Where("id = ? AND owner_id = ?", t.ID, a.GetID()).Delete(&APIToken{}) +func (t *APIToken) Delete(s *xorm.Session, _ web.Auth) (err error) { + // Ownership is verified in CanDelete; delete by ID only. + _, err = s.Where("id = ?", t.ID).Delete(&APIToken{}) return err } diff --git a/pkg/models/api_tokens_permissions.go b/pkg/models/api_tokens_permissions.go index fb8ed0424..5d2b1b415 100644 --- a/pkg/models/api_tokens_permissions.go +++ b/pkg/models/api_tokens_permissions.go @@ -17,6 +17,7 @@ package models import ( + "code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/web" "xorm.io/xorm" ) @@ -27,12 +28,25 @@ func (t *APIToken) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { return false, err } - if token.OwnerID != a.GetID() { - return false, nil + if token.OwnerID == a.GetID() { + *t = *token + return true, nil } - *t = *token - return true, nil + // Allow deletion if the token belongs to a bot owned by the caller. + botUser, err := user.GetUserByID(s, token.OwnerID) + if err != nil { + if user.IsErrUserDoesNotExist(err) { + return false, nil + } + return false, err + } + if botUser.IsBot() && botUser.BotOwnerID == a.GetID() { + *t = *token + return true, nil + } + + return false, nil } func (t *APIToken) CanCreate(_ *xorm.Session, _ web.Auth) (bool, error) { diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index a305c3dc6..36e8b8e3e 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -493,10 +493,6 @@ func registerAPIRoutes(a *echo.Group) { u.GET("/bots/:bot", botHandler.ReadOneWeb) u.POST("/bots/:bot", botHandler.UpdateWeb) u.DELETE("/bots/:bot", botHandler.DeleteWeb) - - u.PUT("/bots/:bot/tokens", apiv1.CreateBotToken) - u.GET("/bots/:bot/tokens", apiv1.ListBotTokens) - u.DELETE("/bots/:bot/tokens/:token", apiv1.DeleteBotToken) } projectHandler := &handler.WebHandler{