From 85f0179d80656ebb280edfeffd5ab2a38756256a Mon Sep 17 00:00:00 2001 From: Yannik Schmidt Date: Mon, 19 Feb 2024 18:19:54 +0100 Subject: [PATCH] feat: direct token auth & webhook path auth support --- client/dispatch-query.py | 17 ++++++++--------- server/interface.py | 32 +++++++++++++++++++++++++++++++- signal-query-dispatch.py | 4 ++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/client/dispatch-query.py b/client/dispatch-query.py index 43704b6..0bebb21 100755 --- a/client/dispatch-query.py +++ b/client/dispatch-query.py @@ -12,7 +12,7 @@ import json HTTP_NOT_FOUND = 404 DISPATCH_SERVER = None -AUTH = None +DISPATCH_ACCESS_TOKEN = None def debug_send(uuid, data, fail_it=False): '''Dummy function to print and ack a dispatch for debugging''' @@ -92,7 +92,7 @@ def report_failed_dispatch(uuid, error): '''Inform the server that the dispatch has failed''' payload = [{ "uuid" : uuid, "error" : error }] - response = requests.post(DISPATCH_SERVER + "/report-dispatch-failed", json=payload, auth=AUTH) + response = requests.post(DISPATCH_SERVER + "/report-dispatch-failed", json=payload) if response.status_code not in [200, 204]: print("Failed to report back failed dispatch for {} ({})".format( @@ -102,7 +102,7 @@ def confirm_dispatch(uuid): '''Confirm to server that message has been dispatched and can be removed''' payload = [{ "uuid" : uuid }] - response = requests.post(DISPATCH_SERVER + "/confirm-dispatch", json=payload, auth=AUTH) + response = requests.post(DISPATCH_SERVER + "/confirm-dispatch", json=payload) if response.status_code not in [200, 204]: print("Failed to confirm dispatch with server for {} ({})".format( @@ -115,8 +115,7 @@ if __name__ == "__main__": formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--dispatch-server') - parser.add_argument('--dispatch-user') - parser.add_argument('--dispatch-password') + parser.add_argument('--dispatch-access-token') parser.add_argument('--ntfy-api-server') parser.add_argument('--ntfy-api-token') @@ -136,12 +135,11 @@ if __name__ == "__main__": dispatch_server = args.dispatch_server or os.environ.get("DISPATCH_SERVER") - dispatch_user = args.dispatch_user or os.environ.get("DISPATCH_USER") - dispatch_password = args.dispatch_password or os.environ.get("DISPATCH_PASSWORD") + dispatch_access_token = args.dispatch_access_token or os.environ.get("DISPATCH_ACCESS_TOKEN") # set dispatch server & authentication global # DISPATCH_SERVER = dispatch_server - AUTH = (dispatch_user, dispatch_password) + DISPATCH_ACCESS_TOKEN = dispatch_access_token ntfy_api_server = args.ntfy_api_server or os.environ.get("NTFY_API_SERVER") ntfy_api_token = args.ntfy_api_token or os.environ.get("NTFY_API_TOKEN") @@ -159,7 +157,8 @@ if __name__ == "__main__": while args.loop or first_run: # request dispatches # - response = requests.get(dispatch_server + "/get-dispatch?method=all&timeout=0", auth=AUTH) + response = requests.get(dispatch_server + + "/get-dispatch?method=all&timeout=0&dispatch-access-token={}".format(DISPATCH_ACCESS_TOKEN)) # check status # if response.status_code == HTTP_NOT_FOUND: diff --git a/server/interface.py b/server/interface.py index 579e3e3..eefb525 100755 --- a/server/interface.py +++ b/server/interface.py @@ -25,6 +25,15 @@ app = flask.Flask("Signal Notification Gateway") app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///sqlite.db" db = SQLAlchemy(app) +BAD_DISPATCH_ACCESS_TOKEN = "Invalid or missing dispatch-access-token parameter in URL" + +class WebHookPaths(db.Model): + + __tablename__ = "webhook_paths" + + username = Column(String, primary_key=True) + path = Column(String, primary_key=True) + class UserSettings(db.Model): __tablename__ = "user_settings" @@ -153,6 +162,10 @@ def get_dispatch(): timeout = flask.request.args.get("timeout") or 5 # timeout in seconds timeout = int(timeout) + dispatch_acces_token = flask.request.args.get("dispatch-access-token") or "" + if dispatch_acces_token != app.config["DISPATCH_ACCESS_TOKEN"]: + return (BAD_DISPATCH_ACCESS_TOKEN, 401) + if not method: return (500, "Missing Dispatch Target (signal|email|phone|ntfy|all|any)") @@ -259,8 +272,9 @@ def confirm_dispatch(): return ("", 204) +@app.route('/smart-send/', methods=["POST"]) @app.route('/smart-send', methods=["POST"]) -def smart_send_to_clients(): +def smart_send_to_clients(path=None): '''Send to clients based on querying the LDAP requests MAY include: - list of usernames under key "users" @@ -280,6 +294,19 @@ def smart_send_to_clients(): title = instructions.get("title") method = instructions.get("method") + # authenticated by access token or webhook path # + dispatch_acces_token = flask.request.args.get("dispatch-access-token") or "" + print(path) + if path: + webhook_path = db.session.query(WebHookPaths).filter(WebHookPaths.path==path).first() + if webhook_path: + users = webhook_path.username + groups = None + else: + return ("Invalid Webhook path", 401) + elif dispatch_acces_token != app.config["DISPATCH_ACCESS_TOKEN"]: + return (BAD_DISPATCH_ACCESS_TOKEN, 401) + # allow single use string instead of array # if type(users) == str: users = [users] @@ -346,6 +373,7 @@ def create_app(): } app.config["LDAP_ARGS"] = ldap_args app.config["SETTINGS_ACCESS_TOKEN"] = os.environ["SETTINGS_ACCESS_TOKEN"] + app.config["DISPATCH_ACCESS_TOKEN"] = os.environ["DISPATCH_ACCESS_TOKEN"] if __name__ == "__main__": @@ -363,6 +391,7 @@ if __name__ == "__main__": parser.add_argument('--ldap-manager-password') parser.add_argument('--settings-access-token') + parser.add_argument('--dispatch-access-token') args = parser.parse_args() @@ -376,6 +405,7 @@ if __name__ == "__main__": app.config["LDAP_NO_READ_ENV"] = True app.config["SETTINGS_ACCESS_TOKEN"] = args.settings_access_token + app.config["DISPATCH_ACCESS_TOKEN"] = args.dispatch_access_token if not any([value is None for value in ldap_args.values()]): app.config["LDAP_ARGS"] = ldap_args diff --git a/signal-query-dispatch.py b/signal-query-dispatch.py index 73eb90b..e8310b4 100755 --- a/signal-query-dispatch.py +++ b/signal-query-dispatch.py @@ -51,8 +51,8 @@ if __name__ == "__main__": signal_cli_bin = args.signal_cli_bin # request dispatches # - response = requests.get(args.target + "/get-dispatch?method={}".format(args.method), - auth=(args.user, args.password)) + response = requests.get(args.target + + "/get-dispatch?method={}&dispatch-access-token={}".format(args.method), args.password) # check status # if response.status_code == HTTP_NOT_FOUND: