Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to
- ✨(backend) add a quota_excluded flag on items
- ✨(backend) apply per-audience attributes to external api items
- ✨(backend) add a grant_unlimited_storage command
- ✨(wopi) verify the WOPI request proof signature

### Changed

Expand Down
2 changes: 1 addition & 1 deletion src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1788,7 +1788,7 @@ def wopi(self, request, *args, **kwargs):
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(
{
Expand Down
5 changes: 4 additions & 1 deletion src/backend/core/tests/items/test_api_items_wopi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
},
},
)

Expand Down
25 changes: 13 additions & 12 deletions src/backend/wopi/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,29 @@
from wopi.services.access import AccessError, AccessUserItemService


def get_access_token(request):
"""Look for the access_token in query params first, then headers."""
access_token = request.query_params.get("access_token")

if not access_token:
access_token = request.headers.get("Authorization", "")
if access_token.startswith("Bearer "):
access_token = access_token[7:]

return access_token


class WopiAccessTokenAuthentication(BaseAuthentication):
"""
WOPI access token authentication.
"""

def _get_access_token(self, request):
"""Look for the access_token in query params first, then headers."""
access_token = request.query_params.get("access_token")

if not access_token:
access_token = request.headers.get("Authorization", "")
if access_token.startswith("Bearer "):
access_token = access_token[7:]

return access_token

def authenticate(self, request):
"""
Authenticate the request.
"""
# First check if the access token is present in the request
access_token = self._get_access_token(request)
access_token = get_access_token(request)
if not access_token:
raise AuthenticationFailed("Access token not provided")

Expand Down
9 changes: 9 additions & 0 deletions src/backend/wopi/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""WOPI exceptions module."""


class WopiRequestSignatureError(Exception):
"""Exception for when a request signature is invalid."""

def __init__(self, message="Invalid request signature"):
self.message = message
super().__init__(self.message)
42 changes: 40 additions & 2 deletions src/backend/wopi/tasks/configure_wopi.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
)
Comment on lines +51 to +61

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I hesitate a lot between saving the modulus and exponent in redis and rebuild the key each time I need it or save it in a PEM format. PEM format take much more space and I don't know if it's faster to load a public key in PEM format or build it from its exponent and modulus



def _configure_wopi_client_from_discovery(client, discovery_url):
"""Configure wopi client from discovery url."""

Expand All @@ -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")
Expand All @@ -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,
Comment thread
kernicPanel marked this conversation as resolved.
}

Expand All @@ -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,
}

Expand Down
29 changes: 29 additions & 0 deletions src/backend/wopi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,38 @@

import pytest

from wopi.tasks.configure_wopi import WOPI_CONFIGURATION_CACHE_KEY


@pytest.fixture(autouse=True)
def clear_cache():
"""Fixture to clear the cache before each test."""
yield
cache.clear()


@pytest.fixture
def configure_wopi_clients():
"""Configure wopi clients."""

wopi_configuration = {
"mimetypes": {
"text/plain": {
"launch_url": "http://localhost:9980/browser/0968141f2c/cool.html?",
"client": "vendorA",
}
},
"extensions": {
"txt": {
"launch_url": "http://localhost:9980/browser/0968141f2c/cool.html?",
"client": "vendorA",
}
},
"vendorA": {
"proof_keys": {"public_key": b"public_proof_key\n"},
},
}
cache.set(WOPI_CONFIGURATION_CACHE_KEY, wopi_configuration)

yield wopi_configuration
cache.delete(WOPI_CONFIGURATION_CACHE_KEY)
71 changes: 69 additions & 2 deletions src/backend/wopi/tests/tasks/test_configure_wopi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="""
<wopi-discovery>
<net-zone name="external-http">
<app favIconUrl="http://localhost:9980/browser/0968141f2c/images/x-office-document.svg" name="writer">
<action default="true" ext="sxw" name="view" urlsrc="http://localhost:9980/browser/0968141f2c/cool.html?"/>
<action default="true" ext="odt" name="edit" urlsrc="http://localhost:9980/browser/0968141f2c/cool.html?"/>
</app>
<app name="application/vnd.oasis.opendocument.text">
<action default="true" ext="" name="edit" urlsrc="http://localhost:9980/browser/0968141f2c/cool.html?"/>
</app>
</net-zone>
<proof-key
oldvalue="BgIAAACkAABSU0ExAAgAAAEAAQD75uIhulOrvFWdgiI3BUqZWj3Zxlii/oCz4CQpH72jYZgbPkKpFC0D0zXznEvn8jgkekLjTD4Q3Dj5UoqN7atjGqhcCuLyiKqkjbklrOS/PS/nhKNJjMWgEUksRKu2vHXJmUhO1FECEgwHM7TOQtDnVJuMV0TK5MsjuhV7cU4uLe42gnzrrLbmpLM6UroNkwOTw723AwrzUSlNVecwMOEijfZj9hhvPzsTuMkIf48OfCQZk5VUJh4/D4lLlvqwFd8Lu48KXvEstWgRF+13pieinmEmQXSgBLzqMi//I9BbpPQQAl1AIy5o0HayDthDDYx5mis3sVEBwEs8dIQJgoTT"
oldmodulus="04SCCYR0PEvAAVGxNyuaeYwNQ9gOsnbQaC4jQF0CEPSkW9Aj/y8y6rwEoHRBJmGeoiemd+0XEWi1LPFeCo+7C98VsPqWS4kPPx4mVJWTGSR8Do9/CMm4Ezs/bxj2Y/aNIuEwMOdVTSlR8woDt73DkwOTDbpSOrOk5ras63yCNu4tLk5xexW6I8vkykRXjJtU59BCzrQzBwwSAlHUTkiZyXW8tqtELEkRoMWMSaOE5y89v+SsJbmNpKqI8uIKXKgaY6vtjYpS+TjcED5M40J6JDjy50uc8zXTAy0UqUI+G5hho70fKSTgs4D+oljG2T1amUoFNyKCnVW8q1O6IeLm+w=="
oldexponent="AQAB"
value="BgIAAACkAABSU0ExAAgAAAEAAQD75uIhulOrvFWdgiI3BUqZWj3Zxlii/oCz4CQpH72jYZgbPkKpFC0D0zXznEvn8jgkekLjTD4Q3Dj5UoqN7atjGqhcCuLyiKqkjbklrOS/PS/nhKNJjMWgEUksRKu2vHXJmUhO1FECEgwHM7TOQtDnVJuMV0TK5MsjuhV7cU4uLe42gnzrrLbmpLM6UroNkwOTw723AwrzUSlNVecwMOEijfZj9hhvPzsTuMkIf48OfCQZk5VUJh4/D4lLlvqwFd8Lu48KXvEstWgRF+13pieinmEmQXSgBLzqMi//I9BbpPQQAl1AIy5o0HayDthDDYx5mis3sVEBwEs8dIQJgoTT"
modulus="04SCCYR0PEvAAVGxNyuaeYwNQ9gOsnbQaC4jQF0CEPSkW9Aj/y8y6rwEoHRBJmGeoiemd+0XEWi1LPFeCo+7C98VsPqWS4kPPx4mVJWTGSR8Do9/CMm4Ezs/bxj2Y/aNIuEwMOdVTSlR8woDt73DkwOTDbpSOrOk5ras63yCNu4tLk5xexW6I8vkykRXjJtU59BCzrQzBwwSAlHUTkiZyXW8tqtELEkRoMWMSaOE5y89v+SsJbmNpKqI8uIKXKgaY6vtjYpS+TjcED5M40J6JDjy50uc8zXTAy0UqUI+G5hho70fKSTgs4D+oljG2T1amUoFNyKCnVW8q1O6IeLm+w=="
exponent="AQAB"/>
</wopi-discovery>
""",
)

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


Expand Down
Empty file.
Loading
Loading