diff --git a/NEWS b/NEWS
index 7a124bf4..302a6546 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,7 @@
1.8.9.dev0
* #311: Add custom dump/restore command options for MySQL and MariaDB.
+ * #811: Add an "access_token" option to the ntfy monitoring hook for authenticating
+ without username/password.
* #827: When the "--json" flag is given, suppress console escape codes so as not to
interfere with JSON output.
* #829: Fix "--override" values containing deprecated section headers not actually overriding
diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml
index 65a775ed..61bdb6e4 100644
--- a/borgmatic/config/schema.yaml
+++ b/borgmatic/config/schema.yaml
@@ -1318,6 +1318,12 @@ properties:
description: |
The password used for authentication.
example: fakepassword
+ access_token:
+ type: string
+ description: |
+ An ntfy access token to authenticate with instead of
+ username/password.
+ example: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
start:
type: object
properties:
diff --git a/borgmatic/hooks/ntfy.py b/borgmatic/hooks/ntfy.py
index c1be2a1b..fbbae88e 100644
--- a/borgmatic/hooks/ntfy.py
+++ b/borgmatic/hooks/ntfy.py
@@ -50,9 +50,16 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
username = hook_config.get('username')
password = hook_config.get('password')
-
+ access_token = hook_config.get('access_token')
auth = None
- if (username and password) is not None:
+
+ if access_token is not None:
+ if username or password:
+ logger.warning(
+ f'{config_filename}: ntfy access_token is set but so is username/password, only using access_token'
+ )
+ auth = requests.auth.HTTPBasicAuth('', access_token)
+ elif (username and password) is not None:
auth = requests.auth.HTTPBasicAuth(username, password)
logger.info(f'{config_filename}: Using basic auth with user {username} for ntfy')
elif username is not None:
diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md
index f485aebd..d867aa44 100644
--- a/docs/how-to/monitor-your-backups.md
+++ b/docs/how-to/monitor-your-backups.md
@@ -298,22 +298,22 @@ platforms including [web](https://ntfy.sh/stats),
[iOS](https://apps.apple.com/us/app/ntfy/id1625396347).
Since push notifications for regular events might soon become quite annoying,
-this hook only fires on any errors by default in order to instantly alert you to issues.
-The `states` list can override this.
+this hook only fires on any errors by default in order to instantly alert you
+to issues. The `states` list can override this. Each state can have its own
+custom messages, priorities and tags or, if none are provided, will use the
+default.
-As ntfy is unauthenticated, it isn't a suitable channel for any private information
-so the default messages are intentionally generic. These can be overridden, depending
-on your risk assessment. Each `state` can have its own custom messages, priorities and tags
-or, if none are provided, will use the default.
-
-An example configuration is shown here, with all the available options, including
-[priorities](https://ntfy.sh/docs/publish/#message-priority) and
+An example configuration is shown here with all the available options,
+including [priorities](https://ntfy.sh/docs/publish/#message-priority) and
[tags](https://ntfy.sh/docs/publish/#tags-emojis):
```yaml
ntfy:
topic: my-unique-topic
server: https://ntfy.my-domain.com
+ username: myuser
+ password: secret
+
start:
title: A borgmatic backup started
message: Watch this space...
@@ -338,6 +338,16 @@ ntfy:
Prior to version 1.8.0 Put
the `ntfy:` option in the `hooks:` section of your configuration.
+New in version 1.8.9 Instead of
+`username`/`password`, you can specify an [ntfy access
+token](https://docs.ntfy.sh/config/#access-tokens):
+
+```yaml
+ntfy:
+ topic: my-unique-topic
+ server: https://ntfy.my-domain.com
+ access_token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+````
## Loki hook
diff --git a/tests/unit/hooks/test_ntfy.py b/tests/unit/hooks/test_ntfy.py
index 2d790fa2..0f815b36 100644
--- a/tests/unit/hooks/test_ntfy.py
+++ b/tests/unit/hooks/test_ntfy.py
@@ -52,7 +52,52 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
)
-def test_ping_monitor_with_auth_hits_hosted_ntfy_on_fail():
+def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
+ hook_config = {
+ 'topic': topic,
+ 'access_token': 'abc123',
+ }
+ flexmock(module.requests).should_receive('post').with_args(
+ f'{default_base_url}/{topic}',
+ headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+ auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
+ ).and_return(flexmock(ok=True)).once()
+
+ module.ping_monitor(
+ hook_config,
+ {},
+ 'config.yaml',
+ borgmatic.hooks.monitor.State.FAIL,
+ monitoring_log_level=1,
+ dry_run=False,
+ )
+
+
+def test_ping_monitor_with_username_password_and_access_token_ignores_username_password():
+ hook_config = {
+ 'topic': topic,
+ 'username': 'testuser',
+ 'password': 'fakepassword',
+ 'access_token': 'abc123',
+ }
+ flexmock(module.requests).should_receive('post').with_args(
+ f'{default_base_url}/{topic}',
+ headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
+ auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
+ ).and_return(flexmock(ok=True)).once()
+ flexmock(module.logger).should_receive('warning').once()
+
+ module.ping_monitor(
+ hook_config,
+ {},
+ 'config.yaml',
+ borgmatic.hooks.monitor.State.FAIL,
+ monitoring_log_level=1,
+ dry_run=False,
+ )
+
+
+def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
hook_config = {
'topic': topic,
'username': 'testuser',
@@ -74,7 +119,7 @@ def test_ping_monitor_with_auth_hits_hosted_ntfy_on_fail():
)
-def test_ping_monitor_auth_with_no_username_warning():
+def test_ping_monitor_with_password_but_no_username_warns():
hook_config = {'topic': topic, 'password': 'fakepassword'}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
@@ -93,7 +138,7 @@ def test_ping_monitor_auth_with_no_username_warning():
)
-def test_ping_monitor_auth_with_no_password_warning():
+def test_ping_monitor_with_username_but_no_password_warns():
hook_config = {'topic': topic, 'username': 'testuser'}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',