feat(models): add BotUser CRUD wrapper
This commit is contained in:
parent
74af7af2e3
commit
3415981d1c
|
|
@ -0,0 +1,133 @@
|
|||
// 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 License 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 License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// BotUser is a thin wrapper around user.User that implements CRUDable + Permissions
|
||||
// for bot-management endpoints. Ownership lives on users.bot_owner_id, so there is
|
||||
// no separate table.
|
||||
type BotUser struct {
|
||||
// Status shadows user.User.Status so it is included in JSON responses
|
||||
// (the original has json:"-").
|
||||
Status user.Status `xorm:"-" json:"status"`
|
||||
|
||||
user.User `xorm:"extends"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Permissions `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// Create creates a new bot user.
|
||||
func (b *BotUser) Create(s *xorm.Session, a web.Auth) error {
|
||||
owner, ok := a.(*user.User)
|
||||
if !ok {
|
||||
return ErrGenericForbidden{}
|
||||
}
|
||||
b.ID = 0
|
||||
created, err := user.CreateBotUser(s, &b.User, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.User = *created
|
||||
b.Status = created.Status
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadAll returns all bots owned by the calling user.
|
||||
func (b *BotUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result any, resultCount int, numberOfTotalItems int64, err error) {
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
var bots []*BotUser
|
||||
q := s.Where("bot_owner_id = ?", a.GetID())
|
||||
if search != "" {
|
||||
q = q.And("(username LIKE ? OR name LIKE ?)", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
if limit > 0 {
|
||||
q = q.Limit(limit, start)
|
||||
}
|
||||
total, err := q.FindAndCount(&bots)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
for _, bot := range bots {
|
||||
bot.Status = bot.User.Status
|
||||
}
|
||||
return bots, len(bots), total, nil
|
||||
}
|
||||
|
||||
// ReadOne returns a single bot user.
|
||||
// Ownership is verified in CanRead.
|
||||
func (b *BotUser) ReadOne(s *xorm.Session, _ web.Auth) error {
|
||||
u, err := user.GetUserByID(s, b.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.User = *u
|
||||
b.Status = u.Status
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update allows a narrow set of fields to be changed on an owned bot.
|
||||
// Ownership is verified in CanUpdate.
|
||||
func (b *BotUser) Update(s *xorm.Session, _ web.Auth) error {
|
||||
existing, err := user.GetUserByID(s, b.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cols := []string{"name"}
|
||||
existing.Name = b.Name
|
||||
|
||||
if b.Status == user.StatusDisabled {
|
||||
existing.Status = b.Status
|
||||
cols = append(cols, "status")
|
||||
} else if b.Status == user.StatusActive && existing.Status != user.StatusActive {
|
||||
existing.Status = b.Status
|
||||
cols = append(cols, "status")
|
||||
}
|
||||
if b.Username != "" && b.Username != existing.Username {
|
||||
if !strings.HasPrefix(b.Username, "bot-") {
|
||||
return &user.ErrBotUsernameMustHavePrefix{Username: b.Username}
|
||||
}
|
||||
existing.Username = b.Username
|
||||
cols = append(cols, "username")
|
||||
}
|
||||
|
||||
if len(cols) > 0 {
|
||||
_, err = s.ID(existing.ID).Cols(cols...).Update(existing)
|
||||
}
|
||||
b.User = *existing
|
||||
b.Status = existing.Status
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete completely removes the bot user and all associated data.
|
||||
// Ownership is verified in CanDelete.
|
||||
func (b *BotUser) Delete(s *xorm.Session, _ web.Auth) error {
|
||||
existing, err := user.GetUserByID(s, b.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return DeleteUser(s, existing)
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// 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 License 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 License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if a user can create a bot user.
|
||||
func (b *BotUser) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
|
||||
u, ok := a.(*user.User)
|
||||
if !ok || u.IsBot() {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CanRead checks if a user can read a bot user.
|
||||
func (b *BotUser) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
ok, err := b.isOwner(s, a)
|
||||
return ok, 0, err
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user can update a bot user.
|
||||
func (b *BotUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { return b.isOwner(s, a) }
|
||||
|
||||
// CanDelete checks if a user can delete a bot user.
|
||||
func (b *BotUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { return b.isOwner(s, a) }
|
||||
|
||||
func (b *BotUser) isOwner(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
u, err := user.GetUserByID(s, b.ID)
|
||||
if err != nil {
|
||||
if user.IsErrUserDoesNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return u.BotOwnerID == a.GetID(), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// 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 License 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 License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// 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"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBotUser_Create(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
owner, err := user.GetUserByID(s, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
bot := &BotUser{User: user.User{Username: "bot-model-success"}}
|
||||
require.NoError(t, bot.Create(s, owner))
|
||||
assert.True(t, bot.IsBot())
|
||||
assert.Equal(t, owner.ID, bot.BotOwnerID)
|
||||
})
|
||||
t.Run("bot cannot create bot", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
botOwner := &user.User{ID: 555, BotOwnerID: 1}
|
||||
bot := &BotUser{User: user.User{Username: "bot-child"}}
|
||||
err := bot.Create(s, botOwner)
|
||||
require.Error(t, err)
|
||||
assert.True(t, user.IsErrBotNotOwned(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBotUser_ReadAll(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
owner, err := user.GetUserByID(s, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
bot := &BotUser{User: user.User{Username: "bot-readall"}}
|
||||
require.NoError(t, bot.Create(s, owner))
|
||||
|
||||
list := &BotUser{}
|
||||
result, _, _, err := list.ReadAll(s, owner, "", 1, 50)
|
||||
require.NoError(t, err)
|
||||
bots, ok := result.([]*BotUser)
|
||||
require.True(t, ok)
|
||||
found := false
|
||||
for _, u := range bots {
|
||||
if u.Username == "bot-readall" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestBotUser_CanRead_NotOwned(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
owner, err := user.GetUserByID(s, 1)
|
||||
require.NoError(t, err)
|
||||
other, err := user.GetUserByID(s, 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
bot := &BotUser{User: user.User{Username: "bot-notowned"}}
|
||||
require.NoError(t, bot.Create(s, owner))
|
||||
|
||||
view := &BotUser{User: user.User{ID: bot.ID}}
|
||||
canRead, _, err := view.CanRead(s, other)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, canRead)
|
||||
}
|
||||
|
||||
func TestBotUser_Update_Status(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
owner, err := user.GetUserByID(s, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
bot := &BotUser{User: user.User{Username: "bot-update"}}
|
||||
require.NoError(t, bot.Create(s, owner))
|
||||
|
||||
upd := &BotUser{Status: user.StatusDisabled, User: user.User{ID: bot.ID, Name: "Renamed"}}
|
||||
require.NoError(t, upd.Update(s, owner))
|
||||
assert.Equal(t, user.StatusDisabled, upd.Status)
|
||||
assert.Equal(t, "Renamed", upd.Name)
|
||||
}
|
||||
|
||||
func TestBotUser_Delete(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
owner, err := user.GetUserByID(s, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
bot := &BotUser{User: user.User{Username: "bot-delete"}}
|
||||
require.NoError(t, bot.Create(s, owner))
|
||||
|
||||
del := &BotUser{User: user.User{ID: bot.ID}}
|
||||
require.NoError(t, del.Delete(s, owner))
|
||||
}
|
||||
|
|
@ -128,6 +128,17 @@ func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Pro
|
|||
// This action is irrevocable.
|
||||
// Public to allow deletion from the CLI.
|
||||
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
|
||||
// Delete any bot users owned by this user first (cascades their data too).
|
||||
var ownedBots []*user.User
|
||||
if err = s.Where("bot_owner_id = ?", u.ID).Find(&ownedBots); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, bot := range ownedBots {
|
||||
if err = DeleteUser(s, bot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
projectsToDelete, err := getProjectsToDelete(s, u)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ type vikunjaInfos struct {
|
|||
DemoModeEnabled bool `json:"demo_mode_enabled"`
|
||||
WebhooksEnabled bool `json:"webhooks_enabled"`
|
||||
PublicTeamsEnabled bool `json:"public_teams_enabled"`
|
||||
BotUsersEnabled bool `json:"bot_users_enabled"`
|
||||
EnabledProFeatures []license.Feature `json:"enabled_pro_features"`
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +108,7 @@ func Info(c *echo.Context) error {
|
|||
DemoModeEnabled: config.ServiceDemoMode.GetBool(),
|
||||
WebhooksEnabled: config.WebhooksEnabled.GetBool(),
|
||||
PublicTeamsEnabled: config.ServiceEnablePublicTeams.GetBool(),
|
||||
BotUsersEnabled: config.ServiceEnableBotUsers.GetBool(),
|
||||
EnabledProFeatures: license.EnabledProFeatures(),
|
||||
AvailableMigrators: []string{
|
||||
(&vikunja_file.FileMigrator{}).Name(),
|
||||
|
|
|
|||
|
|
@ -481,6 +481,24 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
u.POST("/deletion/cancel", apiv1.UserCancelDeletion)
|
||||
}
|
||||
|
||||
// Bot users
|
||||
if config.ServiceEnableBotUsers.GetBool() {
|
||||
botHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.BotUser{}
|
||||
},
|
||||
}
|
||||
u.PUT("/bots", botHandler.CreateWeb)
|
||||
u.GET("/bots", botHandler.ReadAllWeb)
|
||||
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{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Project{}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ const (
|
|||
// User holds information about an user
|
||||
type User struct {
|
||||
// The unique, numeric id of this user.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"bot"`
|
||||
// The full name of the user.
|
||||
Name string `xorm:"text null" json:"name"`
|
||||
// The username of the user. Is always unique.
|
||||
|
|
|
|||
Loading…
Reference in New Issue