More docs, more tests, more docs

This commit is contained in:
Philipp Heckel 2022-02-03 20:07:23 -05:00
parent 29c2fc5472
commit d714af43c9
6 changed files with 155 additions and 60 deletions

View file

@ -35,7 +35,7 @@ The command allows you to show the access control list, as well as change it, de
it is called. it is called.
Usage: Usage:
ntfy access # Shows the entire access control list ntfy access # Shows access control list (alias: 'ntfy user list')
ntfy access USERNAME # Shows access control entries for USERNAME ntfy access USERNAME # Shows access control entries for USERNAME
ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC
@ -50,7 +50,7 @@ Arguments:
- deny (alias: none) - deny (alias: none)
Examples: Examples:
ntfy access # Shows entire access control list ntfy access # Shows access control list (alias: 'ntfy user list')
ntfy access phil # Shows access for user phil ntfy access phil # Shows access for user phil
ntfy access phil mytopic rw # Allow read-write access to mytopic for user phil ntfy access phil mytopic rw # Allow read-write access to mytopic for user phil
ntfy access everyone mytopic rw # Allow anonymous read-write access to mytopic ntfy access everyone mytopic rw # Allow anonymous read-write access to mytopic
@ -82,6 +82,9 @@ func execUserAccess(c *cli.Context) error {
} }
return resetAccess(c, manager, username, topic) return resetAccess(c, manager, username, topic)
} else if perms == "" { } else if perms == "" {
if topic != "" {
return errors.New("invalid syntax, please check 'ntfy access --help' for usage details")
}
return showAccess(c, manager, username) return showAccess(c, manager, username)
} }
return changeAccess(c, manager, username, topic, perms) return changeAccess(c, manager, username, topic, perms)
@ -97,13 +100,13 @@ func changeAccess(c *cli.Context, manager auth.Manager, username string, topic s
return err return err
} }
if read && write { if read && write {
fmt.Fprintf(c.App.Writer, "Granted read-write access to topic %s\n\n", topic) fmt.Fprintf(c.App.ErrWriter, "Granted read-write access to topic %s\n\n", topic)
} else if read { } else if read {
fmt.Fprintf(c.App.Writer, "Granted read-only access to topic %s\n\n", topic) fmt.Fprintf(c.App.ErrWriter, "Granted read-only access to topic %s\n\n", topic)
} else if write { } else if write {
fmt.Fprintf(c.App.Writer, "Granted write-only access to topic %s\n\n", topic) fmt.Fprintf(c.App.ErrWriter, "Granted write-only access to topic %s\n\n", topic)
} else { } else {
fmt.Fprintf(c.App.Writer, "Revoked all access to topic %s\n\n", topic) fmt.Fprintf(c.App.ErrWriter, "Revoked all access to topic %s\n\n", topic)
} }
return showUserAccess(c, manager, username) return showUserAccess(c, manager, username)
} }
@ -121,7 +124,7 @@ func resetAllAccess(c *cli.Context, manager auth.Manager) error {
if err := manager.ResetAccess("", ""); err != nil { if err := manager.ResetAccess("", ""); err != nil {
return err return err
} }
fmt.Fprintln(c.App.Writer, "Reset access for all users") fmt.Fprintln(c.App.ErrWriter, "Reset access for all users")
return nil return nil
} }
@ -129,7 +132,7 @@ func resetUserAccess(c *cli.Context, manager auth.Manager, username string) erro
if err := manager.ResetAccess(username, ""); err != nil { if err := manager.ResetAccess(username, ""); err != nil {
return err return err
} }
fmt.Fprintf(c.App.Writer, "Reset access for user %s\n\n", username) fmt.Fprintf(c.App.ErrWriter, "Reset access for user %s\n\n", username)
return showUserAccess(c, manager, username) return showUserAccess(c, manager, username)
} }
@ -137,7 +140,7 @@ func resetUserTopicAccess(c *cli.Context, manager auth.Manager, username string,
if err := manager.ResetAccess(username, topic); err != nil { if err := manager.ResetAccess(username, topic); err != nil {
return err return err
} }
fmt.Fprintf(c.App.Writer, "Reset access for user %s and topic %s\n\n", username, topic) fmt.Fprintf(c.App.ErrWriter, "Reset access for user %s and topic %s\n\n", username, topic)
return showUserAccess(c, manager, username) return showUserAccess(c, manager, username)
} }
@ -158,7 +161,9 @@ func showAllAccess(c *cli.Context, manager auth.Manager) error {
func showUserAccess(c *cli.Context, manager auth.Manager, username string) error { func showUserAccess(c *cli.Context, manager auth.Manager, username string) error {
users, err := manager.User(username) users, err := manager.User(username)
if err != nil { if err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err return err
} }
return showUsers(c, manager, []*auth.User{users}) return showUsers(c, manager, []*auth.User{users})
@ -166,7 +171,7 @@ func showUserAccess(c *cli.Context, manager auth.Manager, username string) error
func showUsers(c *cli.Context, manager auth.Manager, users []*auth.User) error { func showUsers(c *cli.Context, manager auth.Manager, users []*auth.User) error {
for _, user := range users { for _, user := range users {
fmt.Fprintf(c.App.Writer, "User %s (%s)\n", user.Name, user.Role) fmt.Fprintf(c.App.ErrWriter, "User %s (%s)\n", user.Name, user.Role)
if user.Role == auth.RoleAdmin { if user.Role == auth.RoleAdmin {
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n") fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
} else if len(user.Grants) > 0 { } else if len(user.Grants) > 0 {

View file

@ -99,6 +99,13 @@ Example:
Usage: "Shows a list of users", Usage: "Shows a list of users",
Before: inheritRootReaderFunc, Before: inheritRootReaderFunc,
Action: execUserList, Action: execUserList,
Description: `Shows a list of all configured users, including the everyone ('*') user.
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
This command is an alias to calling 'ntfy access' (display access control list).
`,
}, },
}, },
Description: `Manage users of the ntfy server. Description: `Manage users of the ntfy server.
@ -111,7 +118,7 @@ The command allows you to add/remove/change users in the ntfy user database, as
passwords or roles. passwords or roles.
Examples: Examples:
ntfy user list # Shows list of users ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil ntfy user del phil # Delete user phil

View file

@ -28,7 +28,7 @@ func TestCLI_User_Add_Exists(t *testing.T) {
require.Nil(t, runUserCommand(app, conf, "add", "phil")) require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user") require.Contains(t, stderr.String(), "user phil added with role user")
app, stdin, _, stderr = newTestApp() app, stdin, _, _ = newTestApp()
stdin.WriteString("mypass\nmypass") stdin.WriteString("mypass\nmypass")
err := runUserCommand(app, conf, "add", "phil") err := runUserCommand(app, conf, "add", "phil")
require.Error(t, err) require.Error(t, err)
@ -73,6 +73,44 @@ func TestCLI_User_ChangePass(t *testing.T) {
require.Contains(t, stderr.String(), "changed password for user phil") require.Contains(t, stderr.String(), "changed password for user phil")
} }
func TestCLI_User_ChangeRole(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
// Add user
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
// Change role
app, _, _, stderr = newTestApp()
require.Nil(t, runUserCommand(app, conf, "change-role", "phil", "admin"))
require.Contains(t, stderr.String(), "changed role for user phil to admin")
}
func TestCLI_User_Delete(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
// Add user
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
// Delete user
app, _, _, stderr = newTestApp()
require.Nil(t, runUserCommand(app, conf, "del", "phil"))
require.Contains(t, stderr.String(), "user phil removed")
// Delete user again (does not exist)
app, _, _, _ = newTestApp()
err := runUserCommand(app, conf, "del", "phil")
require.Error(t, err)
require.Contains(t, err.Error(), "user phil does not exist")
}
func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, port int) { func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, port int) {
conf = server.NewConfig() conf = server.NewConfig()
conf.AuthFile = filepath.Join(t.TempDir(), "user.db") conf.AuthFile = filepath.Join(t.TempDir(), "user.db")

View file

@ -155,7 +155,7 @@ user with `ntfy user add --role=admin ...` and be done with all this (see [examp
**Example commands** (type `ntfy user --help` or `ntfy user COMMAND --help` for more details): **Example commands** (type `ntfy user --help` or `ntfy user COMMAND --help` for more details):
``` ```
ntfy user list # Shows list of users ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil ntfy user del phil # Delete user phil
@ -164,13 +164,13 @@ ntfy user change-role phil admin # Make user phil an admin
``` ```
### Access control list (ACL) ### Access control list (ACL)
The access control list (ACL) **manages access to topics for non-admin users, and for anonymous access**. Each entry The access control list (ACL) **manages access to topics for non-admin users, and for anonymous access (`everyone`/`*`)**.
represents the access permissions for a user to a specific topic or topic pattern. Each entry represents the access permissions for a user to a specific topic or topic pattern.
The ACL can be displayed or modified with the `ntfy access` command: The ACL can be displayed or modified with the `ntfy access` command:
``` ```
ntfy access # Shows the entire access control list ntfy access # Shows access control list (alias: 'ntfy user list')
ntfy access USERNAME # Shows access control entries for USERNAME ntfy access USERNAME # Shows access control entries for USERNAME
ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC
``` ```
@ -225,10 +225,11 @@ to topic `garagedoor` and all topics starting with the word `alerts` (wildcards)
### Example: Private instance ### Example: Private instance
The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`: The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`:
``` yaml === "/etc/ntfy/server.yml"
auth-file "/var/lib/ntfy/user.db" ``` yaml
auth-default-access: "deny-all" auth-file "/var/lib/ntfy/user.db"
``` auth-default-access: "deny-all"
```
After that, simply create an `admin` user: After that, simply create an `admin` user:

View file

@ -278,12 +278,12 @@ $ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error"
Available filters (all case-insensitive): Available filters (all case-insensitive):
| Filter variable | Alias | Example | Description | | Filter variable | Alias | Example | Description |
|---|---|---|---| |-----------------|---------------------------|------------------------------------|-------------------------------------------------------------------------|
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic?message=lalala` | Only return messages that match this exact message string | | `message` | `X-Message`, `m` | `ntfy.sh/mytopic?message=lalala` | Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic?title=some+title` | Only return messages that match this exact title string | | `title` | `X-Title`, `t` | `ntfy.sh/mytopic?title=some+title` | Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) | | `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) | | `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
### Subscribe to multiple topics ### Subscribe to multiple topics
It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics
@ -296,37 +296,70 @@ $ curl -s ntfy.sh/mytopic1,mytopic2/json
{"id":"Cm02DsxUHb","time":1637182643,"event":"message","topic":"mytopic2","message":"for topic 2"} {"id":"Cm02DsxUHb","time":1637182643,"event":"message","topic":"mytopic2","message":"for topic 2"}
``` ```
### Authentication
Depending on whether the server is configured to support [access control](../config.md#access-control), some topics
may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
To publish/subscribe to protected topics, you can use [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
with a valid username/password. For your self-hosted server, **be sure to use HTTPS to avoid eavesdropping** and exposing
your password.
```
curl -u phil:mypass -s "https://ntfy.example.com/mytopic/json"
```
## JSON message format ## JSON message format
Both the [`/json` endpoint](#subscribe-as-json-stream) and the [`/sse` endpoint](#subscribe-as-sse-stream) return a JSON Both the [`/json` endpoint](#subscribe-as-json-stream) and the [`/sse` endpoint](#subscribe-as-sse-stream) return a JSON
format of the message. It's very straight forward: format of the message. It's very straight forward:
| Field | Required | Type | Example | Description | **Message**:
|---|---|---|---|---|
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier | | Field | Required | Type | Example | Description |
| `time` | ✔️ | *int* | `1635528741` | Message date time, as Unix time stamp | |--------------|----------|---------------------------------------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| `event` | ✔️ | `open`, `keepalive` or `message` | `message` | Message type, typically you'd be only interested in `message` | | `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events | | `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
| `message` | - | *string* | `Some message` | Message body; always present in `message` events | | `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` | | `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis | | `message` | - | *string* | `Some message` | Message body; always present in `message` events |
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max | | `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
**Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
| Field | Required | Type | Example | Description |
|-----------|----------|-------------|--------------------------------|-----------------------------------------------------------------------------------------------------------|
| `name` | ✔️ | *string* | `attachment.jpg` | Name of the attachment, can be overridden with `X-Filename`, see [attachments](../publish.md#attachments) |
| `url` | ✔️ | *URL* | `https://example.com/file.jpg` | URL of the attachment |
| `type` | - | *mime type* | `image/jpeg` | Mime type of the attachment, only defined if attachment was uploaded to ntfy server |
| `size` | - | *number* | `33848` | Size of the attachment in bytes, only defined if attachment was uploaded to ntfy server |
| `expires` | - | *number* | `1635528741` | Attachment expiry date as Unix time stamp, only defined if attachment was uploaded to ntfy server |
Here's an example for each message type: Here's an example for each message type:
=== "Notification message" === "Notification message"
``` json ``` json
{ {
"id": "wze9zgqK41", "id": "sPs71M8A2T",
"time": 1638542110, "time": 1643935928,
"event": "message", "event": "message",
"topic": "phil_alerts", "topic": "mytopic",
"priority": 5, "priority": 5,
"tags": [ "tags": [
"warning", "warning",
"skull" "skull"
], ],
"click": "https://homecam.mynet.lan/incident/1234",
"attachment": {
"name": "camera.jpg",
"type": "image/png",
"size": 33848,
"expires": 1643946728,
"url": "https://ntfy.sh/file/sPs71M8A2T.png"
},
"title": "Unauthorized access detected", "title": "Unauthorized access detected",
"message": "Remote access to phils-laptop detected. Act right away." "message": "Movement detected in the yard. You better go check"
} }
``` ```
@ -362,15 +395,26 @@ Here's an example for each message type:
} }
``` ```
=== "Poll request message"
``` json
{
"id": "371sevb0pD",
"time": 1638542275,
"event": "poll_request",
"topic": "phil_alerts"
}
```
## List of all parameters ## List of all parameters
The following is a list of all parameters that can be passed when subscribing to a message. Parameter names are **case-insensitive**, The following is a list of all parameters that can be passed **when subscribing to a message**. Parameter names are **case-insensitive**,
and can be passed as **HTTP headers** or **query parameters in the URL**. They are listed in the table in their canonical form. and can be passed as **HTTP headers** or **query parameters in the URL**. They are listed in the table in their canonical form.
| Parameter | Aliases (case-insensitive) | Description | | Parameter | Aliases (case-insensitive) | Description |
|---|---|---| |-------------|----------------------------|---------------------------------------------------------------------------------|
| `poll` | `X-Poll`, `po` | Return cached messages and close connection | | `poll` | `X-Poll`, `po` | Return cached messages and close connection |
| `scheduled` | `X-Scheduled`, `sched` | Include scheduled/delayed messages in message list | | `scheduled` | `X-Scheduled`, `sched` | Include scheduled/delayed messages in message list |
| `message` | `X-Message`, `m` | Filter: Only return messages that match this exact message string | | `message` | `X-Message`, `m` | Filter: Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | Filter: Only return messages that match this exact title string | | `title` | `X-Title`, `t` | Filter: Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match *any priority listed* (comma-separated) | | `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match *any priority listed* (comma-separated) |
| `tags` | `X-Tags`, `tag`, `ta` | Filter: Only return messages that match *all listed tags* (comma-separated) | | `tags` | `X-Tags`, `tag`, `ta` | Filter: Only return messages that match *all listed tags* (comma-separated) |

View file

@ -103,16 +103,16 @@ The message fields are passed to the command as environment variables and can be
these are environment variables, you typically don't have to worry about quoting too much, as long as you enclose them these are environment variables, you typically don't have to worry about quoting too much, as long as you enclose them
in double-quotes, you should be fine: in double-quotes, you should be fine:
| Variable | Aliases | Description | | Variable | Aliases | Description |
|---|---|--- |------------------|----------------------------|----------------------------------------|
| `$NTFY_ID` | `$id` | Unique message ID | | `$NTFY_ID` | `$id` | Unique message ID |
| `$NTFY_TIME` | `$time` | Unix timestamp of the message delivery | | `$NTFY_TIME` | `$time` | Unix timestamp of the message delivery |
| `$NTFY_TOPIC` | `$topic` | Topic name | | `$NTFY_TOPIC` | `$topic` | Topic name |
| `$NTFY_MESSAGE` | `$message`, `$m` | Message body | | `$NTFY_MESSAGE` | `$message`, `$m` | Message body |
| `$NTFY_TITLE` | `$title`, `$t` | Message title | | `$NTFY_TITLE` | `$title`, `$t` | Message title |
| `$NTFY_PRIORITY` | `$priority`, `$prio`, `$p` | Message priority (1=min, 5=max) | | `$NTFY_PRIORITY` | `$priority`, `$prio`, `$p` | Message priority (1=min, 5=max) |
| `$NTFY_TAGS` | `$tags`, `$tag`, `$ta` | Message tags (comma separated list) | | `$NTFY_TAGS` | `$tags`, `$tag`, `$ta` | Message tags (comma separated list) |
| `$NTFY_RAW` | `$raw` | Raw JSON message | | `$NTFY_RAW` | `$raw` | Raw JSON message |
### Subscribe to multiple topics ### Subscribe to multiple topics
``` ```