mirror of
https://github.com/knadh/listmonk.git
synced 2025-11-09 17:22:26 +08:00
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.
This commit is contained in:
parent
a5f8b28cb1
commit
a271bf54d5
16 changed files with 394 additions and 191 deletions
141
cmd/campaigns.go
141
cmd/campaigns.go
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/knadh/listmonk/internal/auth"
|
||||||
"github.com/knadh/listmonk/models"
|
"github.com/knadh/listmonk/models"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
|
|
@ -52,8 +53,9 @@ var (
|
||||||
// handleGetCampaigns handles retrieval of campaigns.
|
// handleGetCampaigns handles retrieval of campaigns.
|
||||||
func handleGetCampaigns(c echo.Context) error {
|
func handleGetCampaigns(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
user = c.Get(auth.UserKey).(models.User)
|
||||||
|
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
||||||
|
|
||||||
status = c.QueryParams()["status"]
|
status = c.QueryParams()["status"]
|
||||||
tags = c.QueryParams()["tag"]
|
tags = c.QueryParams()["tag"]
|
||||||
|
|
@ -63,17 +65,31 @@ func handleGetCampaigns(c echo.Context) error {
|
||||||
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
||||||
)
|
)
|
||||||
|
|
||||||
res, total, err := app.core.QueryCampaigns(query, status, tags, orderBy, order, pg.Offset, pg.Limit)
|
var (
|
||||||
|
hasAllPerm = user.HasPerm(models.PermCampaignsGetAll)
|
||||||
|
permittedLists []int
|
||||||
|
)
|
||||||
|
|
||||||
|
if !hasAllPerm {
|
||||||
|
// Either the user has campaigns:get_all permissions and can view all campaigns,
|
||||||
|
// or the campaigns are filtered by the lists the user has get|manage access to.
|
||||||
|
hasAllPerm, permittedLists = user.GetPermittedLists(true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query and retrieve the campaigns.
|
||||||
|
res, total, err := app.core.QueryCampaigns(query, status, tags, orderBy, order, hasAllPerm, permittedLists, pg.Offset, pg.Limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the body from the response if requested.
|
||||||
if noBody {
|
if noBody {
|
||||||
for i := 0; i < len(res); i++ {
|
for i := range res {
|
||||||
res[i].Body = ""
|
res[i].Body = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paginate the response.
|
||||||
var out models.PageResults
|
var out models.PageResults
|
||||||
if len(res) == 0 {
|
if len(res) == 0 {
|
||||||
out.Results = []models.Campaign{}
|
out.Results = []models.Campaign{}
|
||||||
|
|
@ -93,11 +109,22 @@ func handleGetCampaigns(c echo.Context) error {
|
||||||
// handleGetCampaign handles retrieval of campaigns.
|
// handleGetCampaign handles retrieval of campaigns.
|
||||||
func handleGetCampaign(c echo.Context) error {
|
func handleGetCampaign(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
|
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if id < 1 {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has access to the campaign.
|
||||||
|
if err := checkCampaignPerm(id, true, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the campaign from the DB.
|
||||||
out, err := app.core.GetCampaign(id, "", "")
|
out, err := app.core.GetCampaign(id, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -113,7 +140,8 @@ func handleGetCampaign(c echo.Context) error {
|
||||||
// handlePreviewCampaign renders the HTML preview of a campaign body.
|
// handlePreviewCampaign renders the HTML preview of a campaign body.
|
||||||
func handlePreviewCampaign(c echo.Context) error {
|
func handlePreviewCampaign(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
|
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
tplID, _ = strconv.Atoi(c.FormValue("template_id"))
|
tplID, _ = strconv.Atoi(c.FormValue("template_id"))
|
||||||
)
|
)
|
||||||
|
|
@ -122,6 +150,12 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the user has access to the campaign.
|
||||||
|
if err := checkCampaignPerm(id, true, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the campaign body from the DB.
|
||||||
camp, err := app.core.GetCampaignForPreview(id, tplID)
|
camp, err := app.core.GetCampaignForPreview(id, tplID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -243,6 +277,12 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the user has access to the campaign.
|
||||||
|
if err := checkCampaignPerm(id, false, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the campaign from the DB.
|
||||||
cm, err := app.core.GetCampaign(id, "", "")
|
cm, err := app.core.GetCampaign(id, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -285,20 +325,26 @@ func handleUpdateCampaignStatus(c echo.Context) error {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var o struct {
|
// Check if the user has access to the campaign.
|
||||||
Status string `json:"status"`
|
if err := checkCampaignPerm(id, false, c); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Bind(&o); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := app.core.UpdateCampaignStatus(id, o.Status)
|
req := struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}{}
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the campaign status in the DB.
|
||||||
|
out, err := app.core.UpdateCampaignStatus(id, req.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.Status == models.CampaignStatusPaused || o.Status == models.CampaignStatusCancelled {
|
// If the campaign is being stopped, send the signal to the manager to stop it in flight.
|
||||||
|
if req.Status == models.CampaignStatusPaused || req.Status == models.CampaignStatusCancelled {
|
||||||
app.manager.StopCampaign(id)
|
app.manager.StopCampaign(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,14 +358,17 @@ func handleUpdateCampaignArchive(c echo.Context) error {
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check if the user has access to the campaign.
|
||||||
|
if err := checkCampaignPerm(id, false, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
req := struct {
|
req := struct {
|
||||||
Archive bool `json:"archive"`
|
Archive bool `json:"archive"`
|
||||||
TemplateID int `json:"archive_template_id"`
|
TemplateID int `json:"archive_template_id"`
|
||||||
Meta models.JSON `json:"archive_meta"`
|
Meta models.JSON `json:"archive_meta"`
|
||||||
ArchiveSlug string `json:"archive_slug"`
|
ArchiveSlug string `json:"archive_slug"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
// Get and validate fields.
|
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -351,6 +400,12 @@ func handleDeleteCampaign(c echo.Context) error {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the user has access to the campaign.
|
||||||
|
if err := checkCampaignPerm(id, false, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the campaign from the DB.
|
||||||
if err := app.core.DeleteCampaign(id); err != nil {
|
if err := app.core.DeleteCampaign(id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -401,17 +456,23 @@ func handleGetRunningCampaignStats(c echo.Context) error {
|
||||||
// arbitrary subscribers for testing.
|
// arbitrary subscribers for testing.
|
||||||
func handleTestCampaign(c echo.Context) error {
|
func handleTestCampaign(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
campID, _ = strconv.Atoi(c.Param("id"))
|
|
||||||
tplID, _ = strconv.Atoi(c.FormValue("template_id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
req campaignReq
|
tplID, _ = strconv.Atoi(c.FormValue("template_id"))
|
||||||
)
|
)
|
||||||
|
|
||||||
if campID < 1 {
|
if id < 1 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the user has access to the campaign.
|
||||||
|
if err := checkCampaignPerm(id, false, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get and validate fields.
|
// Get and validate fields.
|
||||||
|
var req campaignReq
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +498,7 @@ func handleTestCampaign(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The campaign.
|
// The campaign.
|
||||||
camp, err := app.core.GetCampaignForPreview(campID, tplID)
|
camp, err := app.core.GetCampaignForPreview(id, tplID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -644,3 +705,41 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) {
|
||||||
o.Body = b.String()
|
o.Body = b.String()
|
||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkCampaignPerm checks if the user has get or manage access to the given campaign.
|
||||||
|
func checkCampaignPerm(id int, isGet bool, c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
user = c.Get(auth.UserKey).(models.User)
|
||||||
|
)
|
||||||
|
|
||||||
|
perm := models.PermCampaignsGet
|
||||||
|
if isGet {
|
||||||
|
// It's a get request and there's a blanket get all permission.
|
||||||
|
if user.HasPerm(models.PermCampaignsGetAll) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a manage request and there's a blanket manage_all permission.
|
||||||
|
if user.HasPerm(models.PermCampaignsManageAll) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
perm = models.PermCampaignsManage
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are no *_all campaign permissions. Instead, check if the user access
|
||||||
|
// blanket get_all/manage_all list permissions. If yes, then the user can access
|
||||||
|
// all campaigns. If there are no *_all permissions, then ensure that the
|
||||||
|
// campaign belongs to the lists that the user has access to.
|
||||||
|
if hasAllPerm, permittedListIDs := user.GetPermittedLists(true, true); !hasAllPerm {
|
||||||
|
if ok, err := app.core.CampaignHasLists(id, permittedListIDs); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden,
|
||||||
|
app.i18n.Ts("globals.messages.permissionDenied", "name", perm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,20 +138,20 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
api.PUT("/api/lists/:id", listPerm(handleUpdateList))
|
api.PUT("/api/lists/:id", listPerm(handleUpdateList))
|
||||||
api.DELETE("/api/lists/:id", listPerm(handleDeleteLists))
|
api.DELETE("/api/lists/:id", listPerm(handleDeleteLists))
|
||||||
|
|
||||||
api.GET("/api/campaigns", pm(handleGetCampaigns, "campaigns:get"))
|
api.GET("/api/campaigns", pm(handleGetCampaigns, "campaigns:get_all", "campaigns:get"))
|
||||||
api.GET("/api/campaigns/running/stats", pm(handleGetRunningCampaignStats, "campaigns:get"))
|
api.GET("/api/campaigns/running/stats", pm(handleGetRunningCampaignStats, "campaigns:get_all", "campaigns:get"))
|
||||||
api.GET("/api/campaigns/:id", pm(handleGetCampaign, "campaigns:get"))
|
api.GET("/api/campaigns/:id", pm(handleGetCampaign, "campaigns:get_all", "campaigns:get"))
|
||||||
api.GET("/api/campaigns/analytics/:type", pm(handleGetCampaignViewAnalytics, "campaigns:get_analytics"))
|
api.GET("/api/campaigns/analytics/:type", pm(handleGetCampaignViewAnalytics, "campaigns:get_analytics"))
|
||||||
api.GET("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get"))
|
api.GET("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||||
api.POST("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get"))
|
api.POST("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||||
api.POST("/api/campaigns/:id/content", pm(handleCampaignContent, "campaigns:manage"))
|
api.POST("/api/campaigns/:id/content", pm(handleCampaignContent, "campaigns:manage_all", "campaigns:manage"))
|
||||||
api.POST("/api/campaigns/:id/text", pm(handlePreviewCampaign, "campaigns:manage"))
|
api.POST("/api/campaigns/:id/text", pm(handlePreviewCampaign, "campaigns:get"))
|
||||||
api.POST("/api/campaigns/:id/test", pm(handleTestCampaign, "campaigns:manage"))
|
api.POST("/api/campaigns/:id/test", pm(handleTestCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||||
api.POST("/api/campaigns", pm(handleCreateCampaign, "campaigns:manage"))
|
api.POST("/api/campaigns", pm(handleCreateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||||
api.PUT("/api/campaigns/:id", pm(handleUpdateCampaign, "campaigns:manage"))
|
api.PUT("/api/campaigns/:id", pm(handleUpdateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||||
api.PUT("/api/campaigns/:id/status", pm(handleUpdateCampaignStatus, "campaigns:manage"))
|
api.PUT("/api/campaigns/:id/status", pm(handleUpdateCampaignStatus, "campaigns:manage_all", "campaigns:manage"))
|
||||||
api.PUT("/api/campaigns/:id/archive", pm(handleUpdateCampaignArchive, "campaigns:manage"))
|
api.PUT("/api/campaigns/:id/archive", pm(handleUpdateCampaignArchive, "campaigns:manage_all", "campaigns:manage"))
|
||||||
api.DELETE("/api/campaigns/:id", pm(handleDeleteCampaign, "campaigns:manage"))
|
api.DELETE("/api/campaigns/:id", pm(handleDeleteCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||||
|
|
||||||
api.GET("/api/media", pm(handleGetMedia, "media:get"))
|
api.GET("/api/media", pm(handleGetMedia, "media:get"))
|
||||||
api.GET("/api/media/:id", pm(handleGetMedia, "media:get"))
|
api.GET("/api/media/:id", pm(handleGetMedia, "media:get"))
|
||||||
|
|
|
||||||
|
|
@ -1013,7 +1013,7 @@ func initAuth(db *sql.DB, ko *koanf.Koanf, co *core.Core) (bool, *auth.Auth) {
|
||||||
Status: models.UserStatusEnabled,
|
Status: models.UserStatusEnabled,
|
||||||
Type: models.UserTypeAPI,
|
Type: models.UserTypeAPI,
|
||||||
}
|
}
|
||||||
u.UserRole.ID = auth.SuperAdminRoleID
|
u.UserRole.ID = models.SuperAdminRoleID
|
||||||
a.CacheAPIUser(u)
|
a.CacheAPIUser(u)
|
||||||
|
|
||||||
lo.Println(`WARNING: Remove the admin_username and admin_password fields from the TOML configuration file. If you are using APIs, create and use new credentials. Users are now managed via the Admin -> Settings -> Users dashboard.`)
|
lo.Println(`WARNING: Remove the admin_username and admin_password fields from the TOML configuration file. If you are using APIs, create and use new credentials. Users are now managed via the Admin -> Settings -> Users dashboard.`)
|
||||||
|
|
|
||||||
23
cmd/lists.go
23
cmd/lists.go
|
|
@ -28,19 +28,12 @@ func handleGetLists(c echo.Context) error {
|
||||||
out models.PageResults
|
out models.PageResults
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// Get the list IDs (or blanket permission) the user has access to.
|
||||||
permittedIDs []int
|
hasAllPerm, permittedIDs := user.GetPermittedLists(true, false)
|
||||||
getAll = false
|
|
||||||
)
|
|
||||||
if _, ok := user.PermissionsMap[models.PermListGetAll]; ok {
|
|
||||||
getAll = true
|
|
||||||
} else {
|
|
||||||
permittedIDs = user.GetListIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal query simply returns the list of all lists without JOIN subscriber counts. This is fast.
|
// Minimal query simply returns the list of all lists without JOIN subscriber counts. This is fast.
|
||||||
if minimal {
|
if minimal {
|
||||||
res, err := app.core.GetLists("", getAll, permittedIDs)
|
res, err := app.core.GetLists("", hasAllPerm, permittedIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +51,7 @@ func handleGetLists(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full list query.
|
// Full list query.
|
||||||
res, total, err := app.core.QueryLists(query, typ, optin, tags, orderBy, order, getAll, permittedIDs, pg.Offset, pg.Limit)
|
res, total, err := app.core.QueryLists(query, typ, optin, tags, orderBy, order, hasAllPerm, permittedIDs, pg.Offset, pg.Limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +66,7 @@ func handleGetLists(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetList retrieves a single list by id.
|
// handleGetList retrieves a single list by id.
|
||||||
|
// It's permission checked by the listPerm middleware.
|
||||||
func handleGetList(c echo.Context) error {
|
func handleGetList(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
|
|
@ -112,6 +106,7 @@ func handleCreateList(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUpdateList handles list modification.
|
// handleUpdateList handles list modification.
|
||||||
|
// It's permission checked by the listPerm middleware.
|
||||||
func handleUpdateList(c echo.Context) error {
|
func handleUpdateList(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
|
|
@ -142,6 +137,7 @@ func handleUpdateList(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDeleteLists handles list deletion, either a single one (ID in the URI), or a list.
|
// handleDeleteLists handles list deletion, either a single one (ID in the URI), or a list.
|
||||||
|
// It's permission checked by the listPerm middleware.
|
||||||
func handleDeleteLists(c echo.Context) error {
|
func handleDeleteLists(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
|
|
@ -185,11 +181,12 @@ func listPerm(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user has permissions for all lists or the specific list.
|
// Check if the user has permissions for all lists or the specific list.
|
||||||
if _, ok := user.PermissionsMap[permAll]; ok {
|
if user.HasPerm(permAll) {
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if id > 0 {
|
if id > 0 {
|
||||||
if _, ok := user.ListPermissionsMap[id][perm]; ok {
|
if user.HasListPerm(id, perm) {
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -672,7 +672,7 @@ func sendOptinConfirmationHook(app *App) func(sub models.Subscriber, listIDs []i
|
||||||
// hasSubPerm checks whether the current user has permission to access the given list
|
// hasSubPerm checks whether the current user has permission to access the given list
|
||||||
// of subscriber IDs.
|
// of subscriber IDs.
|
||||||
func hasSubPerm(u models.User, subIDs []int, app *App) error {
|
func hasSubPerm(u models.User, subIDs []int, app *App) error {
|
||||||
if u.UserRoleID == auth.SuperAdminRoleID {
|
if u.UserRoleID == models.SuperAdminRoleID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div v-if="$can('campaigns:manage')" class="buttons">
|
<div v-if="canManage" class="buttons">
|
||||||
<b-field grouped v-if="isEditing && canEdit">
|
<b-field grouped v-if="isEditing && canEdit">
|
||||||
<b-field expanded>
|
<b-field expanded>
|
||||||
<b-button expanded @click="() => onSubmit('update')" :loading="loading.campaigns" type="is-primary"
|
<b-button expanded @click="() => onSubmit('update')" :loading="loading.campaigns" type="is-primary"
|
||||||
|
|
@ -157,7 +157,7 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$can('campaigns:manage')" class="column is-4 is-offset-1">
|
<div v-if="canManage" class="column is-4 is-offset-1">
|
||||||
<br />
|
<br />
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h3 class="title is-size-6">
|
<h3 class="title is-size-6">
|
||||||
|
|
@ -620,6 +620,10 @@ export default Vue.extend({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['serverConfig', 'loading', 'lists', 'templates']),
|
...mapState(['serverConfig', 'loading', 'lists', 'templates']),
|
||||||
|
|
||||||
|
canManage() {
|
||||||
|
return this.$can('campaigns:manage_all', 'campaigns:manage');
|
||||||
|
},
|
||||||
|
|
||||||
canEdit() {
|
canEdit() {
|
||||||
return this.isNew
|
return this.isNew
|
||||||
|| this.data.status === 'draft' || this.data.status === 'scheduled' || this.data.status === 'paused';
|
|| this.data.status === 'draft' || this.data.status === 'scheduled' || this.data.status === 'paused';
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,8 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// UserKey is the key on which the User profile is set on echo handlers.
|
// UserKey is the key on which the User profile is set on echo handlers.
|
||||||
UserKey = "auth_user"
|
UserKey = "auth_user"
|
||||||
SessionKey = "auth_session"
|
SessionKey = "auth_session"
|
||||||
SuperAdminRoleID = 1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -282,7 +281,7 @@ func (o *Auth) Perm(next echo.HandlerFunc, perms ...string) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current user is a Super Admin user, do no checks.
|
// If the current user is a Super Admin user, do no checks.
|
||||||
if u.UserRole.ID == SuperAdminRoleID {
|
if u.UserRole.ID == models.SuperAdminRoleID {
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const (
|
||||||
|
|
||||||
// QueryCampaigns retrieves paginated campaigns optionally filtering them by the given arbitrary
|
// QueryCampaigns retrieves paginated campaigns optionally filtering them by the given arbitrary
|
||||||
// query expression. It also returns the total number of records in the DB.
|
// query expression. It also returns the total number of records in the DB.
|
||||||
func (c *Core) QueryCampaigns(searchStr string, statuses, tags []string, orderBy, order string, offset, limit int) (models.Campaigns, int, error) {
|
func (c *Core) QueryCampaigns(searchStr string, statuses, tags []string, orderBy, order string, getAll bool, permittedLists []int, offset, limit int) (models.Campaigns, int, error) {
|
||||||
queryStr, stmt := makeSearchQuery(searchStr, orderBy, order, c.q.QueryCampaigns, campQuerySortFields)
|
queryStr, stmt := makeSearchQuery(searchStr, orderBy, order, c.q.QueryCampaigns, campQuerySortFields)
|
||||||
|
|
||||||
if statuses == nil {
|
if statuses == nil {
|
||||||
|
|
@ -36,7 +36,7 @@ func (c *Core) QueryCampaigns(searchStr string, statuses, tags []string, orderBy
|
||||||
|
|
||||||
// Unsafe to ignore scanning fields not present in models.Campaigns.
|
// Unsafe to ignore scanning fields not present in models.Campaigns.
|
||||||
var out models.Campaigns
|
var out models.Campaigns
|
||||||
if err := c.db.Select(&out, stmt, 0, pq.StringArray(statuses), pq.StringArray(tags), queryStr, offset, limit); err != nil {
|
if err := c.db.Select(&out, stmt, 0, pq.StringArray(statuses), pq.StringArray(tags), queryStr, getAll, pq.Array(permittedLists), offset, limit); err != nil {
|
||||||
c.log.Printf("error fetching campaigns: %v", err)
|
c.log.Printf("error fetching campaigns: %v", err)
|
||||||
return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
|
return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
|
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
|
||||||
|
|
@ -327,6 +327,18 @@ func (c *Core) DeleteCampaign(id int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CampaignHasLists checks if a campaign has any of the given list IDs.
|
||||||
|
func (c *Core) CampaignHasLists(id int, listIDs []int) (bool, error) {
|
||||||
|
has := false
|
||||||
|
if err := c.q.CampaignHasLists.Get(&has, id, pq.Array(listIDs)); err != nil {
|
||||||
|
c.log.Printf("error checking campaign lists: %v", err)
|
||||||
|
return false, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return has, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetRunningCampaignStats returns the progress stats of running campaigns.
|
// GetRunningCampaignStats returns the progress stats of running campaigns.
|
||||||
func (c *Core) GetRunningCampaignStats() ([]models.CampaignStats, error) {
|
func (c *Core) GetRunningCampaignStats() ([]models.CampaignStats, error) {
|
||||||
out := []models.CampaignStats{}
|
out := []models.CampaignStats{}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func (c *Core) GetUsers() ([]models.User, error) {
|
||||||
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
|
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.formatUsers(out), nil
|
return c.setupUserFields(out), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser retrieves a specific user based on any one given identifier.
|
// GetUser retrieves a specific user based on any one given identifier.
|
||||||
|
|
@ -36,7 +36,7 @@ func (c *Core) GetUser(id int, username, email string) (models.User, error) {
|
||||||
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
|
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.formatUsers([]models.User{out})[0], nil
|
return c.setupUserFields([]models.User{out})[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user.
|
// CreateUser creates a new user.
|
||||||
|
|
@ -152,7 +152,8 @@ func (c *Core) LoginUser(username, password string) (models.User, error) {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Core) formatUsers(users []models.User) []models.User {
|
// setupUserFields prepares and sets up various user fields.
|
||||||
|
func (c *Core) setupUserFields(users []models.User) []models.User {
|
||||||
for n, u := range users {
|
for n, u := range users {
|
||||||
u := u
|
u := u
|
||||||
|
|
||||||
|
|
@ -188,6 +189,7 @@ func (c *Core) formatUsers(users []models.User) []models.User {
|
||||||
|
|
||||||
u.ListRole = &models.ListRolePermissions{ID: *u.ListRoleID, Name: u.ListRoleName.String, Lists: listPerms}
|
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 {
|
for _, p := range listPerms {
|
||||||
u.ListPermissionsMap[p.ID] = make(map[string]struct{})
|
u.ListPermissionsMap[p.ID] = make(map[string]struct{})
|
||||||
|
|
||||||
|
|
@ -195,10 +197,10 @@ func (c *Core) formatUsers(users []models.User) []models.User {
|
||||||
u.ListPermissionsMap[p.ID][perm] = struct{}{}
|
u.ListPermissionsMap[p.ID][perm] = struct{}{}
|
||||||
|
|
||||||
// List IDs with get / manage permissions.
|
// List IDs with get / manage permissions.
|
||||||
if perm == "list:get" {
|
if perm == models.PermListGet {
|
||||||
u.GetListIDs = append(u.GetListIDs, p.ID)
|
u.GetListIDs = append(u.GetListIDs, p.ID)
|
||||||
}
|
}
|
||||||
if perm == "list:manage" {
|
if perm == models.PermListManage {
|
||||||
u.ManageListIDs = append(u.ManageListIDs, p.ID)
|
u.ManageListIDs = append(u.ManageListIDs, p.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ func V5_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Index of media filename lookup.
|
||||||
if _, err := db.Exec(`
|
if _, err := db.Exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_media_filename ON media(provider, filename);
|
CREATE INDEX IF NOT EXISTS idx_media_filename ON media(provider, filename);
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
|
|
@ -44,5 +45,13 @@ func V5_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert new default super admin permissions.
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
UPDATE roles SET permissions = permissions || '{campaigns:get_all}' WHERE id = 1 AND NOT permissions @> '{campaigns:get_all}';
|
||||||
|
UPDATE roles SET permissions = permissions || '{campaigns:manage_all}' WHERE id = 1 AND NOT permissions @> '{campaigns:manage_all}';
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
models/models.go
124
models/models.go
|
|
@ -150,88 +150,6 @@ type Base struct {
|
||||||
UpdatedAt null.Time `db:"updated_at" json:"updated_at"`
|
UpdatedAt null.Time `db:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// User represents an admin user.
|
|
||||||
type User struct {
|
|
||||||
Base
|
|
||||||
|
|
||||||
Username string `db:"username" json:"username"`
|
|
||||||
|
|
||||||
// For API users, this is the plaintext API token.
|
|
||||||
Password null.String `db:"password" json:"password,omitempty"`
|
|
||||||
PasswordLogin bool `db:"password_login" json:"password_login"`
|
|
||||||
Email null.String `db:"email" json:"email"`
|
|
||||||
Name string `db:"name" json:"name"`
|
|
||||||
Type string `db:"type" json:"type"`
|
|
||||||
Status string `db:"status" json:"status"`
|
|
||||||
Avatar null.String `db:"avatar" json:"avatar"`
|
|
||||||
LoggedInAt null.Time `db:"loggedin_at" json:"loggedin_at"`
|
|
||||||
|
|
||||||
// Role struct {
|
|
||||||
// ID int `db:"-" json:"id"`
|
|
||||||
// Name string `db:"-" json:"name"`
|
|
||||||
// Permissions []string `db:"-" json:"permissions"`
|
|
||||||
// Lists []ListPermission `db:"-" json:"lists"`
|
|
||||||
// } `db:"-" json:"role"`
|
|
||||||
|
|
||||||
// Filled post-retrieval.
|
|
||||||
UserRole struct {
|
|
||||||
ID int `db:"-" json:"id"`
|
|
||||||
Name string `db:"-" json:"name"`
|
|
||||||
Permissions []string `db:"-" json:"permissions"`
|
|
||||||
} `db:"-" json:"user_role"`
|
|
||||||
|
|
||||||
ListRole *ListRolePermissions `db:"-" json:"list_role"`
|
|
||||||
|
|
||||||
UserRoleID int `db:"user_role_id" json:"user_role_id,omitempty"`
|
|
||||||
UserRoleName string `db:"user_role_name" json:"-"`
|
|
||||||
ListRoleID *int `db:"list_role_id" json:"list_role_id,omitempty"`
|
|
||||||
ListRoleName null.String `db:"list_role_name" json:"-"`
|
|
||||||
UserRolePerms pq.StringArray `db:"user_role_permissions" json:"-"`
|
|
||||||
ListsPermsRaw *json.RawMessage `db:"list_role_perms" json:"-"`
|
|
||||||
|
|
||||||
PermissionsMap map[string]struct{} `db:"-" json:"-"`
|
|
||||||
ListPermissionsMap map[int]map[string]struct{} `db:"-" json:"-"`
|
|
||||||
GetListIDs []int `db:"-" json:"-"`
|
|
||||||
ManageListIDs []int `db:"-" json:"-"`
|
|
||||||
HasPassword bool `db:"-" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListPermission struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Permissions pq.StringArray `json:"permissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListRolePermissions struct {
|
|
||||||
ID int `db:"-" json:"id"`
|
|
||||||
Name string `db:"-" json:"name"`
|
|
||||||
Lists []ListPermission `db:"-" json:"lists"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Role struct {
|
|
||||||
Base
|
|
||||||
|
|
||||||
Type string `db:"type" json:"type"`
|
|
||||||
Name null.String `db:"name" json:"name"`
|
|
||||||
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
|
||||||
|
|
||||||
ListID null.Int `db:"list_id" json:"-"`
|
|
||||||
ParentID null.Int `db:"parent_id" json:"-"`
|
|
||||||
ListsRaw json.RawMessage `db:"list_permissions" json:"-"`
|
|
||||||
Lists []ListPermission `db:"-" json:"lists"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListRole struct {
|
|
||||||
Base
|
|
||||||
|
|
||||||
Name null.String `db:"name" json:"name"`
|
|
||||||
|
|
||||||
ListID null.Int `db:"list_id" json:"-"`
|
|
||||||
ParentID null.Int `db:"parent_id" json:"-"`
|
|
||||||
ListsRaw json.RawMessage `db:"list_permissions" json:"-"`
|
|
||||||
Lists []ListPermission `db:"-" json:"lists"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscriber represents an e-mail subscriber.
|
// Subscriber represents an e-mail subscriber.
|
||||||
type Subscriber struct {
|
type Subscriber struct {
|
||||||
Base
|
Base
|
||||||
|
|
@ -816,45 +734,3 @@ func (h Headers) Value() (driver.Value, error) {
|
||||||
|
|
||||||
return "[]", nil
|
return "[]", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) HasPerm(perm string) bool {
|
|
||||||
_, ok := u.PermissionsMap[perm]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterListsByPerm returns list IDs filtered by either of the given perms.
|
|
||||||
func (u *User) FilterListsByPerm(listIDs []int, get, manage bool) []int {
|
|
||||||
// If the user has full list management permission,
|
|
||||||
// no further checks are required.
|
|
||||||
if get {
|
|
||||||
if _, ok := u.PermissionsMap[PermListGetAll]; ok {
|
|
||||||
return listIDs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if manage {
|
|
||||||
if _, ok := u.PermissionsMap[PermListManageAll]; ok {
|
|
||||||
return listIDs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]int, 0, len(listIDs))
|
|
||||||
|
|
||||||
// Go through every list ID.
|
|
||||||
for _, id := range listIDs {
|
|
||||||
// Check if it exists in the map.
|
|
||||||
if l, ok := u.ListPermissionsMap[id]; ok {
|
|
||||||
// Check if any of the given permission exists for it.
|
|
||||||
if get {
|
|
||||||
if _, ok := l[PermListGet]; ok {
|
|
||||||
out = append(out, id)
|
|
||||||
}
|
|
||||||
} else if manage {
|
|
||||||
if _, ok := l[PermListManage]; ok {
|
|
||||||
out = append(out, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ const (
|
||||||
PermSubscribersSqlQuery = "subscribers:sql_query"
|
PermSubscribersSqlQuery = "subscribers:sql_query"
|
||||||
PermTxSend = "tx:send"
|
PermTxSend = "tx:send"
|
||||||
PermCampaignsGet = "campaigns:get"
|
PermCampaignsGet = "campaigns:get"
|
||||||
|
PermCampaignsGetAll = "campaigns:get_all"
|
||||||
PermCampaignsGetAnalytics = "campaigns:get_analytics"
|
PermCampaignsGetAnalytics = "campaigns:get_analytics"
|
||||||
PermCampaignsManage = "campaigns:manage"
|
PermCampaignsManage = "campaigns:manage"
|
||||||
|
PermCampaignsManageAll = "campaigns:manage_all"
|
||||||
PermBouncesGet = "bounces:get"
|
PermBouncesGet = "bounces:get"
|
||||||
PermBouncesManage = "bounces:manage"
|
PermBouncesManage = "bounces:manage"
|
||||||
PermWebhooksPostBounce = "webhooks:post_bounce"
|
PermWebhooksPostBounce = "webhooks:post_bounce"
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ type Queries struct {
|
||||||
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
||||||
GetCampaignStatus *sqlx.Stmt `query:"get-campaign-status"`
|
GetCampaignStatus *sqlx.Stmt `query:"get-campaign-status"`
|
||||||
GetArchivedCampaigns *sqlx.Stmt `query:"get-archived-campaigns"`
|
GetArchivedCampaigns *sqlx.Stmt `query:"get-archived-campaigns"`
|
||||||
|
CampaignHasLists *sqlx.Stmt `query:"campaign-has-lists"`
|
||||||
|
|
||||||
// These two queries are read as strings and based on settings.individual_tracking=on/off,
|
// These two queries are read as strings and based on settings.individual_tracking=on/off,
|
||||||
// are interpolated and copied to view and click counts. Same query, different tables.
|
// are interpolated and copied to view and click counts. Same query, different tables.
|
||||||
|
|
|
||||||
190
models/users.go
Normal file
190
models/users.go
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
null "gopkg.in/volatiletech/null.v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SuperAdminRoleID is the database ID of the primordial super admin role.
|
||||||
|
const SuperAdminRoleID = 1
|
||||||
|
|
||||||
|
// User represents an admin user.
|
||||||
|
type User struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
Username string `db:"username" json:"username"`
|
||||||
|
|
||||||
|
// For API users, this is the plaintext API token.
|
||||||
|
Password null.String `db:"password" json:"password,omitempty"`
|
||||||
|
|
||||||
|
PasswordLogin bool `db:"password_login" json:"password_login"`
|
||||||
|
Email null.String `db:"email" json:"email"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Type string `db:"type" json:"type"`
|
||||||
|
Status string `db:"status" json:"status"`
|
||||||
|
Avatar null.String `db:"avatar" json:"avatar"`
|
||||||
|
LoggedInAt null.Time `db:"loggedin_at" json:"loggedin_at"`
|
||||||
|
UserRoleID int `db:"user_role_id" json:"user_role_id,omitempty"`
|
||||||
|
UserRoleName string `db:"user_role_name" json:"-"`
|
||||||
|
ListRoleID *int `db:"list_role_id" json:"list_role_id,omitempty"`
|
||||||
|
ListRoleName null.String `db:"list_role_name" json:"-"`
|
||||||
|
UserRolePerms pq.StringArray `db:"user_role_permissions" json:"-"`
|
||||||
|
ListsPermsRaw *json.RawMessage `db:"list_role_perms" json:"-"`
|
||||||
|
|
||||||
|
// Non-DB fields filled post-retrieval.
|
||||||
|
UserRole struct {
|
||||||
|
ID int `db:"-" json:"id"`
|
||||||
|
Name string `db:"-" json:"name"`
|
||||||
|
Permissions []string `db:"-" json:"permissions"`
|
||||||
|
} `db:"-" json:"user_role"`
|
||||||
|
|
||||||
|
ListRole *ListRolePermissions `db:"-" json:"list_role"`
|
||||||
|
PermissionsMap map[string]struct{} `db:"-" json:"-"`
|
||||||
|
ListPermissionsMap map[int]map[string]struct{} `db:"-" json:"-"`
|
||||||
|
GetListIDs []int `db:"-" json:"-"`
|
||||||
|
ManageListIDs []int `db:"-" json:"-"`
|
||||||
|
HasPassword bool `db:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPermission struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Permissions pq.StringArray `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRolePermissions struct {
|
||||||
|
ID int `db:"-" json:"id"`
|
||||||
|
Name string `db:"-" json:"name"`
|
||||||
|
Lists []ListPermission `db:"-" json:"lists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
Type string `db:"type" json:"type"`
|
||||||
|
Name null.String `db:"name" json:"name"`
|
||||||
|
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
||||||
|
|
||||||
|
ListID null.Int `db:"list_id" json:"-"`
|
||||||
|
ParentID null.Int `db:"parent_id" json:"-"`
|
||||||
|
ListsRaw json.RawMessage `db:"list_permissions" json:"-"`
|
||||||
|
Lists []ListPermission `db:"-" json:"lists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRole struct {
|
||||||
|
Base
|
||||||
|
|
||||||
|
Name null.String `db:"name" json:"name"`
|
||||||
|
|
||||||
|
ListID null.Int `db:"list_id" json:"-"`
|
||||||
|
ParentID null.Int `db:"parent_id" json:"-"`
|
||||||
|
ListsRaw json.RawMessage `db:"list_permissions" json:"-"`
|
||||||
|
Lists []ListPermission `db:"-" json:"lists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPerm checks if the user has a specific permission.
|
||||||
|
func (u *User) HasPerm(perm string) bool {
|
||||||
|
// Short-circuit if the user is the primordial super admin.
|
||||||
|
if u.UserRoleID == SuperAdminRoleID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := u.PermissionsMap[perm]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) HasListPerm(listID int, perm string) bool {
|
||||||
|
// Short-circuit if the user is the primordial super admin.
|
||||||
|
if u.UserRoleID == SuperAdminRoleID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := u.ListPermissionsMap[listID]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := u.ListPermissionsMap[listID][perm]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermittedLists returns a list of IDs the user has access to based on
|
||||||
|
// the given get / manage permissions. If the user has the blanket "*_all"
|
||||||
|
// permission (or the user is a super admin), then the bool is set to true and
|
||||||
|
// the list is nil as all lists are permitted.
|
||||||
|
func (u *User) GetPermittedLists(get, manage bool) (bool, []int) {
|
||||||
|
// Short-circuit if the user is the primordial super admin.
|
||||||
|
if u.UserRoleID == SuperAdminRoleID {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has the list:get_all or list:manage_all permission, no
|
||||||
|
// further checks are required.
|
||||||
|
if get {
|
||||||
|
if _, ok := u.PermissionsMap[PermListGetAll]; ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if manage {
|
||||||
|
if _, ok := u.PermissionsMap[PermListManageAll]; ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if get {
|
||||||
|
// If the user has per-list permissions, return that. Otherwise, let the
|
||||||
|
// 'manage' permission check run.
|
||||||
|
if len(u.GetListIDs) > 0 {
|
||||||
|
out := make([]int, len(u.GetListIDs))
|
||||||
|
copy(out, u.GetListIDs)
|
||||||
|
return false, out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if manage {
|
||||||
|
// User has per-list permissions.
|
||||||
|
out := make([]int, len(u.ManageListIDs))
|
||||||
|
copy(out, u.ManageListIDs)
|
||||||
|
return false, out
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterListsByPerm returns list IDs filtered by either of the given perms.
|
||||||
|
func (u *User) FilterListsByPerm(listIDs []int, get, manage bool) []int {
|
||||||
|
// If the user has full list management permission,
|
||||||
|
// no further checks are required.
|
||||||
|
if get {
|
||||||
|
if _, ok := u.PermissionsMap[PermListGetAll]; ok {
|
||||||
|
return listIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if manage {
|
||||||
|
if _, ok := u.PermissionsMap[PermListManageAll]; ok {
|
||||||
|
return listIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]int, 0, len(listIDs))
|
||||||
|
|
||||||
|
// Go through every list ID.
|
||||||
|
for _, id := range listIDs {
|
||||||
|
// Check if it exists in the map.
|
||||||
|
if l, ok := u.ListPermissionsMap[id]; ok {
|
||||||
|
// Check if any of the given permission exists for it.
|
||||||
|
if get {
|
||||||
|
if _, ok := l[PermListGet]; ok {
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
} else if manage {
|
||||||
|
if _, ok := l[PermListManage]; ok {
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
@ -24,8 +24,10 @@
|
||||||
"permissions":
|
"permissions":
|
||||||
[
|
[
|
||||||
"campaigns:get",
|
"campaigns:get",
|
||||||
|
"campaigns:get_all",
|
||||||
"campaigns:get_analytics",
|
"campaigns:get_analytics",
|
||||||
"campaigns:manage"
|
"campaigns:manage",
|
||||||
|
"campaigns:manage_all"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
18
queries.sql
18
queries.sql
|
|
@ -557,7 +557,13 @@ WHERE ($1 = 0 OR id = $1)
|
||||||
AND (CARDINALITY($2::campaign_status[]) = 0 OR status = ANY($2))
|
AND (CARDINALITY($2::campaign_status[]) = 0 OR status = ANY($2))
|
||||||
AND (CARDINALITY($3::VARCHAR(100)[]) = 0 OR $3 <@ tags)
|
AND (CARDINALITY($3::VARCHAR(100)[]) = 0 OR $3 <@ tags)
|
||||||
AND ($4 = '' OR TO_TSVECTOR(CONCAT(name, ' ', subject)) @@ TO_TSQUERY($4) OR CONCAT(c.name, ' ', c.subject) ILIKE $4)
|
AND ($4 = '' OR TO_TSVECTOR(CONCAT(name, ' ', subject)) @@ TO_TSQUERY($4) OR CONCAT(c.name, ' ', c.subject) ILIKE $4)
|
||||||
ORDER BY %order% OFFSET $5 LIMIT (CASE WHEN $6 < 1 THEN NULL ELSE $6 END);
|
-- Get all campaigns or filter by list IDs.
|
||||||
|
AND (
|
||||||
|
$5 OR EXISTS (
|
||||||
|
SELECT 1 FROM campaign_lists WHERE campaign_id = c.id AND list_id = ANY($6::INT[])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ORDER BY %order% OFFSET $7 LIMIT (CASE WHEN $8 < 1 THEN NULL ELSE $8 END);
|
||||||
|
|
||||||
-- name: get-campaign
|
-- name: get-campaign
|
||||||
SELECT campaigns.*,
|
SELECT campaigns.*,
|
||||||
|
|
@ -640,9 +646,13 @@ LEFT JOIN templates ON (templates.id = (CASE WHEN $2=0 THEN campaigns.template_i
|
||||||
WHERE campaigns.id = $1;
|
WHERE campaigns.id = $1;
|
||||||
|
|
||||||
-- name: get-campaign-status
|
-- name: get-campaign-status
|
||||||
SELECT id, status, to_send, sent, started_at, updated_at
|
SELECT id, status, to_send, sent, started_at, updated_at FROM campaigns WHERE status=$1;
|
||||||
FROM campaigns
|
|
||||||
WHERE status=$1;
|
-- name: campaign-has-lists
|
||||||
|
-- Returns TRUE if the campaign $1 has any of the lists given in $2.
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT TRUE FROM campaign_lists WHERE campaign_id = $1 AND list_id = ANY($2::INT[])
|
||||||
|
);
|
||||||
|
|
||||||
-- name: next-campaigns
|
-- name: next-campaigns
|
||||||
-- Retreives campaigns that are running (or scheduled and the time's up) and need
|
-- Retreives campaigns that are running (or scheduled and the time's up) and need
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue