feat(api): bot token support via /tokens CRUD and bot_users_enabled flag

This commit is contained in:
kolaente 2026-04-05 20:00:15 +02:00 committed by kolaente
parent 3415981d1c
commit 05acc2b660
3 changed files with 47 additions and 12 deletions

View File

@ -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
}

View File

@ -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) {

View File

@ -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{