diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index e09260ab..6dbf1a32 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1788,7 +1788,7 @@ class ItemViewSet( if request.user.is_authenticated and request.user.language else settings.LANGUAGE_CODE ) - launch_url = compute_wopi_launch_url(wopi_client["url"], get_file_info, language) + launch_url = compute_wopi_launch_url(wopi_client.get("launch_url"), get_file_info, language) return drf.response.Response( { diff --git a/src/backend/core/tests/items/test_api_items_wopi.py b/src/backend/core/tests/items/test_api_items_wopi.py index 2de89ce3..4b68b46b 100644 --- a/src/backend/core/tests/items/test_api_items_wopi.py +++ b/src/backend/core/tests/items/test_api_items_wopi.py @@ -40,11 +40,14 @@ def configure_wopi_settings(valid_mimetype, valid_wopi_launch_url): { "mimetypes": { valid_mimetype: { - "url": valid_wopi_launch_url, + "launch_url": valid_wopi_launch_url, "client": "vendorA", }, }, "extensions": {}, + "vendorA": { + "proof_keys": {}, + }, }, ) diff --git a/src/backend/wopi/tasks/configure_wopi.py b/src/backend/wopi/tasks/configure_wopi.py index 5c982890..fce4c4e1 100644 --- a/src/backend/wopi/tasks/configure_wopi.py +++ b/src/backend/wopi/tasks/configure_wopi.py @@ -1,11 +1,15 @@ """Task configuring WOPI using discovery url.""" +from base64 import b64decode + from django.conf import settings from django.core.cache import cache import requests from celery import Celery from celery.schedules import crontab +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers from defusedxml.ElementTree import fromstring from drive.celery_app import app as celery_app @@ -44,6 +48,19 @@ def configure_wopi_clients(): ) +def build_rsa_public_key(modulus, exponent): + """Build RSA public key from modulus and exponent.""" + mod = int(b64decode(modulus).hex(), 16) + exp = int(b64decode(exponent).hex(), 16) + + rsa_public_key = RSAPublicNumbers(exp, mod).public_key() + + return rsa_public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + def _configure_wopi_client_from_discovery(client, discovery_url): """Configure wopi client from discovery url.""" @@ -67,6 +84,27 @@ def _configure_wopi_client_from_discovery(client, discovery_url): if net_zone is None: raise RuntimeError(f"net-zone element not found in discovery url for wopi client {client}") + proof_key_node = root.find(".//proof-key") + proof_keys = {} + + if proof_key_node is not None: + # build current and old public key + current_public_key = build_rsa_public_key( + proof_key_node.get("modulus"), proof_key_node.get("exponent") + ) + old_public_key = build_rsa_public_key( + proof_key_node.get("oldmodulus"), proof_key_node.get("oldexponent") + ) + + proof_keys = { + "public_key": current_public_key, + "old_public_key": old_public_key, + } + + wopi_configuration[client] = { + "proof_keys": proof_keys, + } + # Iterate through all app elements for app in net_zone.findall(".//app"): app_name = app.get("name") @@ -85,7 +123,7 @@ def _configure_wopi_client_from_discovery(client, discovery_url): continue wopi_configuration["mimetypes"][mimetype] = { - "url": action.get("urlsrc"), + "launch_url": action.get("urlsrc"), "client": client, } @@ -96,7 +134,7 @@ def _configure_wopi_client_from_discovery(client, discovery_url): continue wopi_configuration["extensions"][extension] = { - "url": action.get("urlsrc"), + "launch_url": action.get("urlsrc"), "client": client, } diff --git a/src/backend/wopi/tests/tasks/test_configure_wopi.py b/src/backend/wopi/tests/tasks/test_configure_wopi.py index b9463185..a48a3657 100644 --- a/src/backend/wopi/tests/tasks/test_configure_wopi.py +++ b/src/backend/wopi/tests/tasks/test_configure_wopi.py @@ -49,16 +49,83 @@ def test_configure_wopi_clients(settings): assert cache.get(WOPI_CONFIGURATION_CACHE_KEY) == { "mimetypes": { "application/vnd.oasis.opendocument.text": { - "url": "http://localhost:9980/browser/0968141f2c/cool.html?", + "launch_url": "http://localhost:9980/browser/0968141f2c/cool.html?", "client": "vendorA", }, }, "extensions": { "odt": { - "url": "http://localhost:9980/browser/0968141f2c/cool.html?", + "launch_url": "http://localhost:9980/browser/0968141f2c/cool.html?", "client": "vendorA", }, }, + "vendorA": { + "proof_keys": {}, + }, + } + + +@responses.activate +def test_configure_wopi_clients_with_proof_keys(settings): + """Test the configure_wopi celery task with client using proof key.""" + + settings.WOPI_CLIENTS = ["vendorA"] + settings.WOPI_CLIENTS_CONFIGURATION = { + "vendorA": { + "discovery_url": "https://vendorA.com/hosting/discovery", + } + } + + # pylint: disable=line-too-long + responses.add( + responses.GET, + "https://vendorA.com/hosting/discovery", + body=""" + + + + + + + + + + + + +""", + ) + + assert cache.get(WOPI_CONFIGURATION_CACHE_KEY) is None + + configure_wopi_clients() + + # pylint: disable=line-too-long + assert cache.get(WOPI_CONFIGURATION_CACHE_KEY) == { + "mimetypes": { + "application/vnd.oasis.opendocument.text": { + "launch_url": "http://localhost:9980/browser/0968141f2c/cool.html?", + "client": "vendorA", + }, + }, + "extensions": { + "odt": { + "launch_url": "http://localhost:9980/browser/0968141f2c/cool.html?", + "client": "vendorA", + }, + }, + "vendorA": { + "proof_keys": { + "old_public_key": b"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA04SCCYR0PEvAAVGxNyua\neYwNQ9gOsnbQaC4jQF0CEPSkW9Aj/y8y6rwEoHRBJmGeoiemd+0XEWi1LPFeCo+7\nC98VsPqWS4kPPx4mVJWTGSR8Do9/CMm4Ezs/bxj2Y/aNIuEwMOdVTSlR8woDt73D\nkwOTDbpSOrOk5ras63yCNu4tLk5xexW6I8vkykRXjJtU59BCzrQzBwwSAlHUTkiZ\nyXW8tqtELEkRoMWMSaOE5y89v+SsJbmNpKqI8uIKXKgaY6vtjYpS+TjcED5M40J6\nJDjy50uc8zXTAy0UqUI+G5hho70fKSTgs4D+oljG2T1amUoFNyKCnVW8q1O6IeLm\n+wIDAQAB\n-----END PUBLIC KEY-----\n", + "public_key": b"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA04SCCYR0PEvAAVGxNyua\neYwNQ9gOsnbQaC4jQF0CEPSkW9Aj/y8y6rwEoHRBJmGeoiemd+0XEWi1LPFeCo+7\nC98VsPqWS4kPPx4mVJWTGSR8Do9/CMm4Ezs/bxj2Y/aNIuEwMOdVTSlR8woDt73D\nkwOTDbpSOrOk5ras63yCNu4tLk5xexW6I8vkykRXjJtU59BCzrQzBwwSAlHUTkiZ\nyXW8tqtELEkRoMWMSaOE5y89v+SsJbmNpKqI8uIKXKgaY6vtjYpS+TjcED5M40J6\nJDjy50uc8zXTAy0UqUI+G5hho70fKSTgs4D+oljG2T1amUoFNyKCnVW8q1O6IeLm\n+wIDAQAB\n-----END PUBLIC KEY-----\n", + }, + }, } diff --git a/src/backend/wopi/utils/__init__.py b/src/backend/wopi/utils/__init__.py index bf2834b0..1dadefe2 100644 --- a/src/backend/wopi/utils/__init__.py +++ b/src/backend/wopi/utils/__init__.py @@ -23,7 +23,7 @@ def is_item_wopi_supported(item, user): def get_wopi_client_config(item, user): - """make + """ Get the WOPI client configuration for an item. """ if ( @@ -33,7 +33,7 @@ def get_wopi_client_config(item, user): ): return None - wopi_configuration = cache.get(WOPI_CONFIGURATION_CACHE_KEY, default=WOPI_DEFAULT_CONFIGURATION) + wopi_configuration = get_wopi_configuration() if not wopi_configuration: return None @@ -50,6 +50,23 @@ def get_wopi_client_config(item, user): return result +def get_wopi_client_proof_keys(item, user): + """get the wopi proof keys for an item""" + wopi_client_config = get_wopi_client_config(item, user) + + if not wopi_client_config: + return None + + wopi_configuration = get_wopi_configuration() + + return wopi_configuration[wopi_client_config["client"]]["proof_keys"] + + +def get_wopi_configuration(): + """get the wopi configuration""" + return cache.get(WOPI_CONFIGURATION_CACHE_KEY, default=WOPI_DEFAULT_CONFIGURATION) + + def compute_wopi_launch_url(launch_url, get_file_info_path, lang=None): """ Compute the WOPI launch URL for an item.