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}',