diff --git a/frappe/realtime.py b/frappe/realtime.py index 216621b53b..7e8e31f1b0 100644 --- a/frappe/realtime.py +++ b/frappe/realtime.py @@ -121,9 +121,31 @@ def has_permission(doctype: str, name: str) -> bool: return True +SOCKETIO_SECRET_KEY = "socketio_auth_secret" + + +def get_socketio_secret(): + """Generate socket.io secret and store in redis""" + + from frappe.utils.background_jobs import get_redis_connection_without_auth + + r = get_redis_connection_without_auth() + secret = r.get(SOCKETIO_SECRET_KEY) + if secret: + return secret.decode() + + secret = frappe.generate_hash(length=32) + r.set(SOCKETIO_SECRET_KEY, secret) + return secret + + @frappe.whitelist(allow_guest=True) def get_user_info(): user_type = frappe.session.data.user_type + trusted_secret = get_socketio_secret() + provided_secret = frappe.get_request_header("X-Frappe-Socket-Secret") + if trusted_secret != provided_secret: + return {} # For requests with Bearer tokens, user_type is not set in the session data if not user_type: user_type = frappe.get_cached_value("User", frappe.session.user, "user_type") diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index fe3e551ae0..1e732c4cab 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -305,11 +305,11 @@ class TestMethodAPI(FrappeAPITestCase): self.assertEqual(response.json["message"], "pong") def test_get_user_info(self): - # test 3: test for /api/method/frappe.realtime.get_user_info + # test 3: test for /api/method/frappe.realtime.get_user_info (server-to-server only) response = self.get(self.method("frappe.realtime.get_user_info")) self.assertEqual(response.status_code, 200) - self.assertIsInstance(response.json, dict) - self.assertIn(response.json.get("message").get("user"), ("Administrator", "Guest")) + message = response.json.get("message") + self.assertEqual(message, {}) def test_auth_cycle(self): # test 4: Pass authorization token in request diff --git a/frappe/tests/test_api_v2.py b/frappe/tests/test_api_v2.py index cebc47b27b..b3777f4e87 100644 --- a/frappe/tests/test_api_v2.py +++ b/frappe/tests/test_api_v2.py @@ -161,10 +161,10 @@ class TestMethodAPIV2(FrappeAPITestCase): self.assertEqual(response.json["data"], "pong") def test_get_user_info(self): + # server-to-server only response = self.get(self.method("frappe.realtime.get_user_info")) self.assertEqual(response.status_code, 200) - self.assertIsInstance(response.json, dict) - self.assertIn(response.json.get("data").get("user"), ("Administrator", "Guest")) + self.assertEqual(response.json.get("data"), {}) def test_auth_cycle(self): global authorization_token diff --git a/realtime/middlewares/authenticate.js b/realtime/middlewares/authenticate.js index 3e521e52d2..e2c9d5794b 100644 --- a/realtime/middlewares/authenticate.js +++ b/realtime/middlewares/authenticate.js @@ -1,7 +1,14 @@ const cookie = require("cookie"); -const { get_conf } = require("../../node_utils"); +const { get_conf, get_redis_subscriber } = require("../../node_utils"); const { get_url } = require("../utils"); const conf = get_conf(); +const redisClient = get_redis_subscriber("redis_queue"); + +async function getSecretFromRedis() { + if (!redisClient.isOpen) await redisClient.connect(); + const val = await redisClient.get("socketio_auth_secret"); + return val; +} function authenticate_with_frappe(socket, next) { let namespace = socket.nsp.name; @@ -35,7 +42,7 @@ function authenticate_with_frappe(socket, next) { socket.sid = cookies.sid; socket.authorization_header = authorization_header; - socket.frappe_request = (path, args = {}, opts = {}) => { + socket.frappe_request = async (path, args = {}, opts = {}) => { let query_args = new URLSearchParams(args); if (query_args.toString()) { path = path + "?" + query_args.toString(); @@ -47,7 +54,10 @@ function authenticate_with_frappe(socket, next) { } else if (socket.sid) { headers["Cookie"] = `sid=${socket.sid}`; } - + const secret = await getSecretFromRedis(); + if (secret) { + headers["X-Frappe-Socket-Secret"] = secret; + } return fetch(get_url(socket, path), { ...opts, headers, @@ -57,10 +67,18 @@ function authenticate_with_frappe(socket, next) { socket .frappe_request("/api/method/frappe.realtime.get_user_info") .then((res) => res.json()) - .then(({ message }) => { + .then(async ({ message }) => { + if (socket.user !== "Guest" && !message.installed_apps) { + const retry_res = await socket.frappe_request( + "/api/method/frappe.realtime.get_user_info" + ); + const retry_data = await retry_res.json(); + message = retry_data.message; + } + socket.user = message.user; socket.user_type = message.user_type; - socket.installed_apps = message.installed_apps; + socket.installed_apps = message.installed_apps || []; next(); }) .catch((e) => {