listmonk/internal/core/users.go
Kailash Nadh a271bf54d5 Introduce per-campaign filter permissions. Closes #2325.
This patch introduces new `campaigns:get_all` and `campaigns:manage_all`
permissions which alter the behaviour of the the old `campaigns:get` and
`campaigns:manage` permissions. This is a subtle breaking behavioural change.

Old:

- `campaigns:get` -> View all campaigns irrespective of a user's list
  permissions.
- `campaigns:manage` -> Manage all campaigns irrespective of a user's list
  permissions.

New:

- `campaigns:get_all` -> View all campaigns irrespective of a user's list
  permissions.
- `campaigns:manage_all` -> Manage all campaigns irrespective of a user's list
  permissions.
- `campaigns:get` -> View only the campaigns that have at least one list to
  which which a user has get or manage access.
- `campaigns:manage` -> Manage only the campaigns that have at list one list
  to which a user has get or manage access.

In addition, this patch refactors and cleans up certain permission related
logic and functions.
2025-03-31 16:39:42 +05:30

214 lines
6.7 KiB
Go

package core
import (
"database/sql"
"encoding/json"
"net/http"
"github.com/knadh/listmonk/internal/utils"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
"gopkg.in/volatiletech/null.v6"
)
func (c *Core) GetUsers() ([]models.User, error) {
out := []models.User{}
if err := c.q.GetUsers.Select(&out); err != nil {
return nil, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
}
return c.setupUserFields(out), nil
}
// GetUser retrieves a specific user based on any one given identifier.
func (c *Core) GetUser(id int, username, email string) (models.User, error) {
var out models.User
if err := c.q.GetUser.Get(&out, id, username, email); err != nil {
if err == sql.ErrNoRows {
return out, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.user}"))
}
return out, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
}
return c.setupUserFields([]models.User{out})[0], nil
}
// CreateUser creates a new user.
func (c *Core) CreateUser(u models.User) (models.User, error) {
var id int
// If it's an API user, generate a random token for password
// and set the e-mail to default.
if u.Type == models.UserTypeAPI {
// Generate a random admin password.
tk, err := utils.GenerateRandomString(32)
if err != nil {
return models.User{}, err
}
u.Email = null.String{String: u.Username + "@api", Valid: true}
u.PasswordLogin = false
u.Password = null.String{String: tk, Valid: true}
}
if err := c.q.CreateUser.Get(&id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.UserRoleID, u.ListRoleID, u.Status); err != nil {
return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
}
// Hide the password field in the response except for when the user type is an API token,
// where the frontend shows the token on the UI just once.
if u.Type != models.UserTypeAPI {
u.Password = null.String{Valid: false}
}
out, err := c.GetUser(id, "", "")
return out, err
}
// UpdateUser updates a given user.
func (c *Core) UpdateUser(id int, u models.User) (models.User, error) {
listRoleID := 0
if u.ListRoleID == nil {
listRoleID = -1
} else {
listRoleID = *u.ListRoleID
}
res, err := c.q.UpdateUser.Exec(id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.UserRoleID, listRoleID, u.Status)
if err != nil {
return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
}
if n, _ := res.RowsAffected(); n == 0 {
return models.User{}, echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("users.needSuper"))
}
out, err := c.GetUser(id, "", "")
return out, err
}
// UpdateUserProfile updates the basic fields of a given uesr (name, email, password).
func (c *Core) UpdateUserProfile(id int, u models.User) (models.User, error) {
res, err := c.q.UpdateUserProfile.Exec(id, u.Name, u.Email, u.PasswordLogin, u.Password)
if err != nil {
return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
}
if n, _ := res.RowsAffected(); n == 0 {
return models.User{}, echo.NewHTTPError(http.StatusBadRequest,
c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.user}"))
}
return c.GetUser(id, "", "")
}
// UpdateUserLogin updates a user's record post-login.
func (c *Core) UpdateUserLogin(id int, avatar string) error {
if _, err := c.q.UpdateUserLogin.Exec(id, avatar); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
}
return nil
}
// DeleteUsers deletes a given user.
func (c *Core) DeleteUsers(ids []int) error {
res, err := c.q.DeleteUsers.Exec(pq.Array(ids))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
}
if num, err := res.RowsAffected(); err != nil || num == 0 {
return echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("users.needSuper"))
}
return nil
}
// LoginUser attempts to log the given user_id in by matching the password.
func (c *Core) LoginUser(username, password string) (models.User, error) {
var out models.User
if err := c.q.LoginUser.Get(&out, username, password); err != nil {
if err == sql.ErrNoRows {
return out, echo.NewHTTPError(http.StatusForbidden,
c.i18n.T("users.invalidLogin"))
}
return out, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
}
return out, nil
}
// setupUserFields prepares and sets up various user fields.
func (c *Core) setupUserFields(users []models.User) []models.User {
for n, u := range users {
u := u
if u.Password.String != "" {
u.HasPassword = true
u.PasswordLogin = true
}
if u.Type == models.UserTypeAPI {
u.Email = null.String{}
}
u.UserRole.ID = u.UserRoleID
u.UserRole.Name = u.UserRoleName
u.UserRole.Permissions = u.UserRolePerms
u.UserRoleID = 0
// Prepare lookup maps.
u.ListPermissionsMap = make(map[int]map[string]struct{})
u.PermissionsMap = make(map[string]struct{})
for _, p := range u.UserRolePerms {
u.PermissionsMap[p] = struct{}{}
}
if u.ListRoleID != nil {
// Unmarshall the raw list perms map.
var listPerms []models.ListPermission
if u.ListsPermsRaw != nil {
if err := json.Unmarshal(*u.ListsPermsRaw, &listPerms); err != nil {
c.log.Printf("error unmarshalling list permissions for role %d: %v", u.ID, err)
}
}
u.ListRole = &models.ListRolePermissions{ID: *u.ListRoleID, Name: u.ListRoleName.String, Lists: listPerms}
// Iterate each list in the list permissions and setup get/manage list IDs.
for _, p := range listPerms {
u.ListPermissionsMap[p.ID] = make(map[string]struct{})
for _, perm := range p.Permissions {
u.ListPermissionsMap[p.ID][perm] = struct{}{}
// List IDs with get / manage permissions.
if perm == models.PermListGet {
u.GetListIDs = append(u.GetListIDs, p.ID)
}
if perm == models.PermListManage {
u.ManageListIDs = append(u.ManageListIDs, p.ID)
}
}
}
}
users[n] = u
}
return users
}