Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,9 @@ rm-dev-docker: build ## Delete the docker containers including DB volumes.
init-dev-docker: build-dev-docker ## Delete the docker containers including DB volumes.
cd dev; \
docker compose run --rm backend sh -c "make dist && ./listmonk --install --idempotent --yes --config dev/config.toml"

# Test a db migration in the dev docker suite.
.PHONY: init-dev-docker
upgrade-dev-docker: build-dev-docker ## Delete the docker containers including DB volumes.
cd dev; \
docker compose run --rm backend sh -c "make dist && ./listmonk --upgrade --idempotent --yes --config dev/config.toml"
51 changes: 39 additions & 12 deletions cmd/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"
"syscall"
"time"

"github.com/knadh/listmonk/internal/auth"
"github.com/knadh/listmonk/internal/messenger/email"
"github.com/labstack/echo/v4"
null "gopkg.in/volatiletech/null.v6"
)

type messengerConfig struct {
Name string `json:"name"`
DefaultFromEmail string `json:"from_email"`
}

type serverConfig struct {
RootURL string `json:"root_url"`
FromEmail string `json:"from_email"`
Expand All @@ -19,14 +27,14 @@ type serverConfig struct {
CaptchaEnabled bool `json:"captcha_enabled"`
CaptchaKey null.String `json:"captcha_key"`
} `json:"public_subscription"`
Messengers []string `json:"messengers"`
Langs []i18nLang `json:"langs"`
Lang string `json:"lang"`
Permissions json.RawMessage `json:"permissions"`
Update *AppUpdate `json:"update"`
NeedsRestart bool `json:"needs_restart"`
HasLegacyUser bool `json:"has_legacy_user"`
Version string `json:"version"`
Messengers []messengerConfig `json:"messengers"`
Langs []i18nLang `json:"langs"`
Lang string `json:"lang"`
Permissions json.RawMessage `json:"permissions"`
Update *AppUpdate `json:"update"`
NeedsRestart bool `json:"needs_restart"`
HasLegacyUser bool `json:"has_legacy_user"`
Version string `json:"version"`
}

// GetServerConfig returns general server config.
Expand All @@ -37,6 +45,7 @@ func (a *App) GetServerConfig(c echo.Context) error {
Lang: a.cfg.Lang,
Permissions: a.cfg.PermissionsRaw,
HasLegacyUser: a.cfg.HasLegacyUser,
Messengers: []messengerConfig{},
}
out.PublicSubscription.Enabled = a.cfg.EnablePublicSubPage
if a.cfg.Security.EnableCaptcha {
Expand All @@ -52,9 +61,27 @@ func (a *App) GetServerConfig(c echo.Context) error {
}
out.Langs = langList

out.Messengers = make([]string, 0, len(a.messengers))
for _, m := range a.messengers {
out.Messengers = append(out.Messengers, m.Name())
// List messengers user has access to.
if u, ok := c.Get(auth.UserHTTPCtxKey).(auth.User); ok {
hasPerm := u.HasPerm(auth.PermMessengersGetAll)
if hasPerm || len(u.Messengers) > 0 {
out.Messengers = make([]messengerConfig, 0, len(a.messengers))
for _, m := range a.messengers {
if hasPerm || slices.Contains(u.Messengers, m.Name()) {
msgr := messengerConfig{Name: m.Name()}
if msgr.Name == emailMsgr {
msgr.DefaultFromEmail = a.cfg.FromEmail
} else if em, ok := m.(*email.Emailer); ok {
msgr.DefaultFromEmail = em.DefaultFromEmail
}
out.Messengers = append(out.Messengers, msgr)
}
}
}
}
// If messengers were renamed, or user has no messengers, fall back to default.
if len(out.Messengers) == 0 {
out.Messengers = []messengerConfig{{Name: emailMsgr, DefaultFromEmail: a.cfg.FromEmail}}
}

a.Lock()
Expand All @@ -66,7 +93,7 @@ func (a *App) GetServerConfig(c echo.Context) error {
return c.JSON(http.StatusOK, okResp{out})
}

// GetDashboardCharts returns chart data points to render ont he dashboard.
// GetDashboardCharts returns chart data points to render on the dashboard.
func (a *App) GetDashboardCharts(c echo.Context) error {
// Get the chart data from the DB.
out, err := a.core.GetDashboardCharts()
Expand Down
2 changes: 1 addition & 1 deletion cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
// Individual list permissions are applied directly within handleGetLists.
g.GET("/api/lists", a.GetLists)
g.GET("/api/lists/:id", hasID(a.GetList))
g.POST("/api/lists", pm(a.CreateList, "lists:manage_all"))
g.POST("/api/lists", pm(a.CreateList, "lists:create"))
g.PUT("/api/lists/:id", hasID(a.UpdateList))
g.DELETE("/api/lists/:id", hasID(a.DeleteLists))

Expand Down
11 changes: 11 additions & 0 deletions cmd/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/knadh/listmonk/internal/auth"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
)

// GetLists retrieves lists with additional metadata like subscriber counts.
Expand Down Expand Up @@ -106,6 +107,16 @@ func (a *App) CreateList(c echo.Context) error {
return err
}

if user := auth.GetUser(c); user.ListRoleID != nil {
if err := a.core.UpsertListPermissions(*user.ListRoleID, append(user.ListRole.Lists, auth.ListPermission{
ID: out.ID,
Name: out.Name,
Permissions: pq.StringArray{auth.PermListManage, auth.PermListGet},
})); err != nil {
return err
}
}

return c.JSON(http.StatusOK, okResp{out})
}

Expand Down
5 changes: 5 additions & 0 deletions cmd/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ func (a *App) validateListRole(r auth.ListRole) error {
if !strHasLen(r.Name.String, 1, stdInputMaxLen) {
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidFields", "name", "name"))
}
for _, p := range r.Messengers {
if !strHasLen(p, 1, stdInputMaxLen) {
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("messenger: %s", p)))
}
}

for _, l := range r.Lists {
for _, p := range l.Permissions {
Expand Down
1 change: 1 addition & 0 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var migList = []migFunc{
{"v4.0.0", migrations.V4_0_0},
{"v4.1.0", migrations.V4_1_0},
{"v5.0.0", migrations.V5_0_0},
{"v5.1.0-alpha1", migrations.V5_1_0},
}

// upgrade upgrades the database to the current version by running SQL migration files
Expand Down
4 changes: 2 additions & 2 deletions dev/app.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM golang:1.20 AS go
FROM golang:1.24.1 AS go

FROM node:16 AS node
FROM node:18 AS node

COPY --from=go /usr/local/go /usr/local/go
ENV GOPATH /go
Expand Down
2 changes: 0 additions & 2 deletions dev/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3"

services:
adminer:
image: adminer:4.8.1-standalone
Expand Down
54 changes: 49 additions & 5 deletions docs/docs/content/roles-and-permissions.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
listmonk supports (>= v4.0.0) creating systems users with granular permissions to various features, including list-specific permissions. Users can login with a username and password, or via an OIDC (OpenID Connect) handshake if an auth provider is connected. Various permissions can be grouped into "user roles", which can be assigned to users. List-specific permissions can be grouped into "list roles".
listmonk supports (>= v4.0.0) creating systems users with granular permissions
to various features, including list-specific permissions. Users can login with
a username and password, or via an OIDC (OpenID Connect) handshake if an auth
provider is connected. Users can be assigned a "user role" to grant generic
user and app permissions and a "list role" to grant per-list and per-messenger
permissions, managed per-user in the _Admin -> Users -> Users_ UI.

## User roles

A user role is a collection of user related permissions. User roles are attached to user accounts. User roles can be managed in `Admin -> Users -> User roles` The permissions are described below.
A user role is a collection of generic user and app permissions, described
below. User roles can be managed in the _Admin -> Users -> User roles_ UI.

| Group | Permission | Description |
| ----------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| lists | lists:get_all | Get details of all lists |
| | lists:manage_all | Create, update, and delete all lists |
| | lists:manage_all | Update and delete all lists |
| | lists:create | Create new lists |
| subscribers | subscribers:get | Get individual subscriber details |
| | subscribers:get_all | Get all subscribers and their details |
| | subscribers:manage | Add, update, and delete subscribers |
Expand All @@ -18,6 +25,7 @@ A user role is a collection of user related permissions. User roles are attached
| | campaigns:get_all | Get and view campaigns across all lists |
| | campaigns:get_analytics | Access campaign performance metrics |
| | campaigns:manage | Create, update, and delete campaigns |
| | messengers:get_all | Send campaigns to any/all configured email servers and custom senders |
| bounces | bounces:get | Get email bounce records |
| | bounces:manage | Process and handle bounced emails |
| | webhooks:post_bounce | Receive bounce notifications via webhook |
Expand All @@ -35,8 +43,44 @@ A user role is a collection of user related permissions. User roles are attached

## List roles

A list role is a collection of permissions assigned per list. Each list can be assigned a view (read) or manage (update) permission. List roles are attached to user accounts. Only the lists defined in a list role is accessible by the user, be it on the admin UI or via API calls. Do note that the `lists:get_all` and `lists:manage_all` permissions in user roles override all per-list permissions.
A list role is a collection of per-list and per-messenger permissions that can
be used to segment subscriber lists, messengers and campaigns to user groups.
List roles can be managed in the _Admin -> Users -> List roles_ UI.

### Subscribers

Each list can be assigned a view (read) or manage (update) permission. Users may
only access subscriber lists they have the view permission for, both within the
admin UI and via API calls. The `lists:get_all` and `lists:manage_all` user role
permissions override this behaviour, giving users access to all lists regardless.

Lists created by a user with the `lists:create` user role permission will be
added to that user's list role with both view and manage permissions granted.
If a user does not have a list role, they may be unable to view created lists.

- If you want users to be able to create lists shared with other users,
grant them `lists:create` and put them in the same list role.
- If you want users to be able to create lists hidden from other users,
grant them `lists:create` and put them in different list roles.
- If you do not want users to create/manage custom subscriber lists,
do not grant them `lists:create`.

### Messengers

Each named messenger can be enabled (checked) or disabled (unchecked).
Messengers can be configured in the _Admin -> Settings -> Settings -> SMTP_
and _Admin -> Settings -> Settings -> Messengers_ UIs. Users may only send
campaigns to subscriber lists via [messengers](https://listmonk.app/docs/messengers/)
enabled by their list role. The `messengers:get_all` user role permission
overrides this behaviour, allowing a user to send from any/all messengers.

A user must be able to send from at least one messenger. If a user has not been
granted any named messengers via their list role or use role permissions, they
will default to the generic `email` messenger.

## API users

A user account can be of two types, a regular user or an API user. API users are meant for intertacting with the listmonk APIs programmatically. Unlike regular user accounts that have custom passwords or OIDC for authentication, API users get an automatically generated secret token.
A user account can be of two types, a regular user or an API user. API users
are meant for intertacting with the listmonk APIs programmatically. Unlike
regular user accounts that have custom passwords or OIDC for authentication,
API users get an automatically generated secret token.
6 changes: 5 additions & 1 deletion frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ router.afterEach((to) => {

async function initConfig(app) {
// Load logged in user profile, server side config, and the language file before mounting the app.
const [profile, cfg] = await Promise.all([api.getUserProfile(), api.getServerConfig()]);
const [_profile, cfg] = await Promise.all([api.getUserProfile(), api.getServerConfig()]);
let profile = _profile;

const lang = await api.getLang(cfg.lang);
i18n.locale = cfg.lang;
Expand All @@ -42,6 +43,9 @@ async function initConfig(app) {
Vue.prototype.$utils = new Utils(i18n);
Vue.prototype.$api = api;
Vue.prototype.$events = app;
Vue.prototype.$refreshUser = async () => {
profile = await api.getUserProfile();
};

// $can('permission:name') is used in the UI to check whether the logged in user
// has a certain permission to toggle visibility of UI objects and UI functionality.
Expand Down
Loading