From 46d11f3d70c3e67f1214b384c3d271c0cda0da84 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Sun, 23 Mar 2025 00:11:04 +0100 Subject: [PATCH] Re #3045 - API Access should still work even when UI Password is enabled --- changedetectionio/api/api_v1.py | 2 - changedetectionio/api/auth.py | 4 +- changedetectionio/flask_app.py | 4 +- changedetectionio/tests/test_api.py | 56 +++++++++++++++++-- .../test_automatic_follow_ldjson_price.py | 4 +- changedetectionio/tests/util.py | 14 ----- 6 files changed, 58 insertions(+), 26 deletions(-) diff --git a/changedetectionio/api/api_v1.py b/changedetectionio/api/api_v1.py index fc09bb1b3..72598188e 100644 --- a/changedetectionio/api/api_v1.py +++ b/changedetectionio/api/api_v1.py @@ -285,8 +285,6 @@ class CreateWatch(Resource): list = {} tag_limit = request.args.get('tag', '').lower() - - for uuid, watch in self.datastore.data['watching'].items(): # Watch tags by name (replace the other calls?) tags = self.datastore.get_all_tags_for_watch(uuid=uuid) diff --git a/changedetectionio/api/auth.py b/changedetectionio/api/auth.py index 806a8ccd5..9f4c3b19b 100644 --- a/changedetectionio/api/auth.py +++ b/changedetectionio/api/auth.py @@ -12,7 +12,9 @@ def check_token(f): config_api_token_enabled = datastore.data['settings']['application'].get('api_access_token_enabled') if not config_api_token_enabled: - return + return make_response( + jsonify("API Access is disabled"), 403 + ) try: api_key_header = request.headers['x-api-key'] diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index 232ad9446..44707bd4d 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -4,7 +4,6 @@ import flask_login import locale import os -import pytz import queue import threading import time @@ -244,6 +243,9 @@ def changedetection_app(config=None, datastore_o=None): # RSS access with token is allowed elif request.endpoint and 'rss.feed' in request.endpoint: return None + # API routes - use their own auth mechanism (@auth.check_token) + elif request.path.startswith('/api/'): + return None else: return login_manager.unauthorized() diff --git a/changedetectionio/tests/test_api.py b/changedetectionio/tests/test_api.py index 097133fef..e05f96d79 100644 --- a/changedetectionio/tests/test_api.py +++ b/changedetectionio/tests/test_api.py @@ -2,7 +2,7 @@ import time from flask import url_for -from .util import live_server_setup, extract_api_key_from_UI, wait_for_all_checks +from .util import live_server_setup, wait_for_all_checks import json import uuid @@ -57,9 +57,9 @@ def test_setup(client, live_server, measure_memory_usage): def test_api_simple(client, live_server, measure_memory_usage): -# live_server_setup(live_server) + #live_server_setup(live_server) - api_key = extract_api_key_from_UI(client) + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') # Create a watch set_original_response() @@ -293,7 +293,7 @@ def test_access_denied(client, live_server, measure_memory_usage): def test_api_watch_PUT_update(client, live_server, measure_memory_usage): #live_server_setup(live_server) - api_key = extract_api_key_from_UI(client) + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') # Create a watch set_original_response() @@ -374,7 +374,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): def test_api_import(client, live_server, measure_memory_usage): #live_server_setup(live_server) - api_key = extract_api_key_from_UI(client) + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') res = client.post( url_for("import") + "?tag=import-test", @@ -392,4 +392,48 @@ def test_api_import(client, live_server, measure_memory_usage): # Should see the new tag in the tag/groups list res = client.get(url_for('tags.tags_overview_page')) assert b'import-test' in res.data - \ No newline at end of file + +def test_api_conflict_UI_password(client, live_server, measure_memory_usage): + + #live_server_setup(live_server) + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') + + # Enable password check and diff page access bypass + res = client.post( + url_for("settings.settings_page"), + data={"application-password": "foobar", # password is now set! API should still work! + "application-api_access_token_enabled": "y", + "requests-time_between_check-minutes": 180, + 'application-fetch_backend': "html_requests"}, + follow_redirects=True + ) + + assert b"Password protection enabled." in res.data + + # Create a watch + set_original_response() + test_url = url_for('test_endpoint', _external=True) + + # Create new + res = client.post( + url_for("createwatch"), + data=json.dumps({"url": test_url, "title": "My test URL" }), + headers={'content-type': 'application/json', 'x-api-key': api_key}, + follow_redirects=True + ) + + assert res.status_code == 201 + + + wait_for_all_checks(client) + url = url_for("createwatch") + # Get a listing, it will be the first one + res = client.get( + url, + headers={'x-api-key': api_key} + ) + assert res.status_code == 200 + + assert len(res.json) + + diff --git a/changedetectionio/tests/test_automatic_follow_ldjson_price.py b/changedetectionio/tests/test_automatic_follow_ldjson_price.py index 2e085d3f8..1996e72ca 100644 --- a/changedetectionio/tests/test_automatic_follow_ldjson_price.py +++ b/changedetectionio/tests/test_automatic_follow_ldjson_price.py @@ -2,7 +2,7 @@ import time from flask import url_for -from .util import live_server_setup, extract_UUID_from_client, extract_api_key_from_UI, wait_for_all_checks +from .util import live_server_setup, extract_UUID_from_client, wait_for_all_checks def set_response_with_ldjson(): @@ -110,7 +110,7 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage assert b'tracking-ldjson-price-data' in res.data # and last snapshop (via API) should be just the price - api_key = extract_api_key_from_UI(client) + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') res = client.get( url_for("watchsinglehistory", uuid=uuid, timestamp='latest'), headers={'x-api-key': api_key}, diff --git a/changedetectionio/tests/util.py b/changedetectionio/tests/util.py index 2ffe79d13..c4e6b3161 100644 --- a/changedetectionio/tests/util.py +++ b/changedetectionio/tests/util.py @@ -95,20 +95,6 @@ def wait_for_notification_endpoint_output(): return False - -# kinda funky, but works for now -def extract_api_key_from_UI(client): - import re - res = client.get( - url_for("settings.settings_page"), - ) - # {{api_key}} - - m = re.search('(.+?)', str(res.data)) - api_key = m.group(1) - return api_key.strip() - - # kinda funky, but works for now def get_UUID_for_tag_name(client, name): app_config = client.application.config.get('DATASTORE').data