Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #75: WOPI proof #79

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open

Issue #75: WOPI proof #79

wants to merge 23 commits into from

Conversation

donquixote
Copy link
Collaborator

Fixes #75

This also contains some refactoring of WopiController, moving access checks to the routing layer.
I could split this out but I would prefer not to..

@donquixote donquixote force-pushed the issue-75-wopi-proof branch 6 times, most recently from f37d4ff to 9789290 Compare December 19, 2024 14:59
@donquixote donquixote force-pushed the issue-75-wopi-proof branch 3 times, most recently from a58c0f4 to 6fa5037 Compare December 19, 2024 15:10
Comment on lines 71 to 73
// Unfortunately, is_numeric() confuses the IDE's static analysis, so use
// regular expression instead.
if (!preg_match('#^[1-9]\d+$#', $wopi_ticks_str)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The IDE should never be the driver for how we write the code. Let's put the is_numeric() call.

*
* Note that the X-WOPI-Timestamp is in DotNet ticks.
*/
class WopiTimeoutAccessCheck implements AccessInterface {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the end, the timestamp is used also in the other class, and we basically need one single method from here, while we need to duplicate quite a bit of code. Let's merge all into a single class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One motivation for the split was the distinct service dependencies.
We can fully keep out the crypto stuff from the timeout checker, and we can fully keep out the dotnet time conversion from the proof checker. We get two independent buckets of control flow.
The shared code is the config check and the timestamp header value.

@@ -10,5 +10,6 @@ cool:
access_token_ttl: 86400
# Disable cert checks.
disable_cert_check: false
wopi_proof: true
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should provide a post_update hook to set this value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was not sure if we have an upgrade path.
But I also don't mind to add it.

protected function getSignatures(Request $request): array {
// Get the current and old proof header.
// Remove empty values.
// If both are the same, keep only the current one.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this actually a possible scenario? Like, is it written in some documentation?
Because this piece of code makes me think that it's part of the WOPI protocol.
What would be the implications if we left not-unique headers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If the keys are not changed regularly, then we will have identical old and new key, and identical old and new signature. You will see that in the existing discovery.xml.
The only implication of keeping duplicates is that we will do redundant checks. So not a big deal, but also easy enough to avoid.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The redundant checks would only happen in the non-happy path, so it is not really the main scenario to optimize for.
The wopi-lib has explicit vars for old and new key and proof and does the 3 checks explicitly with &&.
We could do that.

Currently with the array_filter() and then foreach() we silently ignore if an old key is missing in discovery.xml, or the old proof is missing in the request.
Instead, we could simply fail if the old key or proof is not present.
This would reveal if e.g. we are using the wrong xpath or wrong header name, or if the WOPI client behaves differently due to an update.

Comment on lines 63 to 65
$assert_session->fieldValueEquals('Disable TLS certificate check for COOL.', '');
$assert_session->fieldValueEquals('Verify proof header and timestamp in incoming WOPI requests.', '1');
$assert_session->fieldValueEquals('Allow COOL to use fullscreen mode.', '1');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's convert this to checkboxChecked/checkboxNotChecked.

Comment on lines 115 to 122
public static function provideAllMethods(): array {
return [
// Closures in data providers are ok in unit tests.
'getWopiClientURL' => [fn (CollaboraDiscoveryInterface $discovery) => $discovery->getWopiClientURL()],
'getProofKey' => [fn (CollaboraDiscoveryInterface $discovery) => $discovery->getProofKey()],
'getProofKeyOld' => [fn (CollaboraDiscoveryInterface $discovery) => $discovery->getProofKeyOld()],
];
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can simplify this:

return [
  'getWopiClientURL' => ['getWopiClientURL'],
  'getProofKey' => ['getProofKey'],
  'getProofKeyOld' => ['getProofKeyOld'],
];

And then just call_user_func([$discovery, $callback]);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It works as long as there are no arguments, which is ok for now.
It also means that static analysis would find usages of the methods.
But I think we can change it, overall it will become simpler, so ok.

Comment on lines +146 to +147
'current' => $this->discovery->getProofKey(),
'old' => $this->discovery->getProofKeyOld(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

These two methods make each an HTTP request to retrieve the XML. This happens for each request to the three routes.
I found some information about the fact that the proof key is rotated every certain amount of time (https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/online/discovery).
We can check for Collabora, but I wold say that implementing a caching system that keeps the discovery XML cached for one hour will already greatly reduce the amount of requests we make.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Absolutely. I think somewhere it was documented that this should be 20 minutes.
But I wanted to do a dedicated PR to add caching to the discovery.
If we implement caching, we need to determine when this should be reset. We can do the < 20 min TTL, and ofc the dependency on the config record. But perhaps we also need a different mechanism to force this reset.


/**
* Service to get a WOPI client url for a given MIME type.
* Service to get a WOPI client URL for a given MIME type.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not true now. This service is now a representation of the Collabora discovery file.

### Installation steps
### Collabora Online server preparation

It is recommended to generate a proof key as documented here:\
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think there might be a link missing.


/**
* Service to get a WOPI client url for a given MIME type.
* Service to get a WOPI client URL for a given MIME type.
*/
class CollaboraDiscovery implements CollaboraDiscoveryInterface {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to rethink a bit how this service works. Probably we should gather once per initialization the file from the fetcher?
I would like a class that completely wraps the XML, and the single methods return the parsed information from it, like a sort of value object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was thinking that too. But, once we have a value object, we can't have it respond to config changes etc.
Perhaps this is all ok, but I would like a separate issue to think about this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One idea I had was a proxy object that deals with the config etc and which then creates the value object to delegate calls to.
When the config changes, we could drop the value object and create a new one.
Or, do we actually need that? Do we care if config changes at runtime, or only between requests?

As a compromise we can have a property that has the cached parsed xml, and then a persistent cache for the xml string. TBD.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement WOPI proof
2 participants