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

@ -279,7 +279,7 @@ $ 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) |
@ -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:
**Message**:
| Field | Required | Type | Example | Description | | Field | Required | Type | Example | Description |
|---|---|---|---|---| |--------------|----------|---------------------------------------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier | | `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
| `time` | ✔️ | *int* | `1635528741` | Message date time, as Unix time stamp | | `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
| `event` | ✔️ | `open`, `keepalive` or `message` | `message` | Message type, typically you'd be only interested in `message` | | `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
| `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 | | `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 |
| `message` | - | *string* | `Some message` | Message body; always present in `message` events | | `message` | - | *string* | `Some message` | Message body; always present in `message` events |
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` | | `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 | | `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 | | `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,12 +395,23 @@ 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 |

View file

@ -104,7 +104,7 @@ these are environment variables, you typically don't have to worry about quoting
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 |