From 88bf239eb1c91b77e9cdf1c218ee9252521cfcef Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Mon, 11 Mar 2024 00:36:34 +0100 Subject: [PATCH] Added anonymous/private logic --- src/Broker/SseSubscriber.php | 9 +++-- src/Broker/SubscriberInterface.php | 2 +- src/Broker/Topic.php | 10 +++++- src/Broker/TopicManager.php | 3 +- src/Http/Middleware/MercureHandler.php | 38 +++++++++++++++++----- src/Http/Middleware/ResponseMiddleware.php | 6 ++-- 6 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/Broker/SseSubscriber.php b/src/Broker/SseSubscriber.php index 51a8fca..d778e78 100644 --- a/src/Broker/SseSubscriber.php +++ b/src/Broker/SseSubscriber.php @@ -2,12 +2,15 @@ namespace NoccyLabs\Mercureact\Broker; +use NoccyLabs\SimpleJWT\JWTToken; +use Psr\Http\Message\ServerRequestInterface; use React\Stream\WritableStreamInterface; class SseSubscriber implements SubscriberInterface { public function __construct( - private WritableStreamInterface $stream + private WritableStreamInterface $stream, + private ServerRequestInterface $request, ) { } @@ -17,8 +20,8 @@ class SseSubscriber implements SubscriberInterface $this->stream->write($message->toString()); } - public function isAuthorized(string $topics): bool + public function isAuthorized(): bool { - return true; + return $this->request->getAttribute('authorization') instanceof JWTToken; } } \ No newline at end of file diff --git a/src/Broker/SubscriberInterface.php b/src/Broker/SubscriberInterface.php index 54c2322..af1107c 100644 --- a/src/Broker/SubscriberInterface.php +++ b/src/Broker/SubscriberInterface.php @@ -6,5 +6,5 @@ interface SubscriberInterface { public function deliver(Message $message): void; - public function isAuthorized(string $topics): bool; + public function isAuthorized(): bool; } \ No newline at end of file diff --git a/src/Broker/Topic.php b/src/Broker/Topic.php index ca35f75..23e3dc8 100644 --- a/src/Broker/Topic.php +++ b/src/Broker/Topic.php @@ -29,9 +29,17 @@ class Topic public function publish(Message $message) { // TODO check if message id has already been published + if (isset($this->messages[$message->id])) return; + + $this->messages[$message->id] = $message; + foreach ($this->subscribers as $subscriber) { - // Deliver to all subscribers + // Skip sending private messages to unauthorized subscribers + if ($message->private && !$subscriber->isAuthorized()) { + continue; + } + // Deliver to the subscriber $subscriber->deliver($message); } } diff --git a/src/Broker/TopicManager.php b/src/Broker/TopicManager.php index af8bee4..45ea6ee 100644 --- a/src/Broker/TopicManager.php +++ b/src/Broker/TopicManager.php @@ -34,8 +34,7 @@ class TopicManager public function subscribe(SubscriberInterface $subscriber, array $topics): void { foreach ($topics as $topic) { - if ($subscriber->isAuthorized($topic)) - $this->getTopic($topic)->addSubscriber($subscriber); + $this->getTopic($topic)->addSubscriber($subscriber); } $this->subscribers->attach($subscriber); } diff --git a/src/Http/Middleware/MercureHandler.php b/src/Http/Middleware/MercureHandler.php index 6ad4fc6..4a16971 100644 --- a/src/Http/Middleware/MercureHandler.php +++ b/src/Http/Middleware/MercureHandler.php @@ -94,9 +94,23 @@ class MercureHandler ); [ $name, $value ] = array_map('urldecode', explode("=", $param, 2)); if ($name === 'topic') $topics[] = $value; - // TODO check claims for access } + // Grab the JWT token from the requests authorization attribute + $tok = $request->getAttribute('authorization'); + if ($tok instanceof JWTToken) { + $claims = $tok->claims->getAll(); + if (isset($claims['mercure']['subscribe'])) { + $subscribeClaims = $claims['mercure']['subscribe']; + // TODO check topic against publishClaims + if (!$this->checkTopicClaims($topics, $subscribeClaims)) { + throw new SecurityException("Insufficient permissions for subscribe", SecurityException::ERR_NO_PERMISSION); + } + } + } else { + // TODO add option to allow/disallow anonymous acess. should still respect + } + $this->topicManager->subscribe($subscriber, $topics); //$this->eventClients->attach($responseStream, $request); $responseStream->on('close', function () use ($subscriber, $topics) { @@ -142,13 +156,17 @@ class MercureHandler $claims = $tok->claims->getAll(); if (isset($claims['mercure']['publish'])) { $publishClaims = $claims['mercure']['publish']; - // TODO check topic against publishClaims + // check topic against publishClaims if (!$this->checkTopicClaims($data['topic']??[], $publishClaims)) { throw new SecurityException("Insufficient permissions for publish", SecurityException::ERR_NO_PERMISSION); } } } else { - // FIXME reject if access denied + // reject if access denied + throw new SecurityException( + message: "Access denied", + code: SecurityException::ERR_ACCESS_DENIED + ); } // Put an id in there if none already @@ -167,17 +185,19 @@ class MercureHandler return Response::plaintext("urn:uuid:".$message->id."\n"); } - private function checkTopicClaims(string|array $topic, array $claims, bool $all=false): bool + private function checkTopicClaims(string|array $topic, array $claims): bool { - // TODO match all topics if $all, reject on mismatch + $matched = 0; foreach ((array)$topic as $match) { foreach ($claims as $claim) { - if ($claim === "*") return true; - if ($claim === $match) return true; - // TODO implement full matching + // TODO implement matching of URI Templates + if (($claim === "*") || ($claim === $match)) { + $matched++; + break; + } } } - return false; + return ($matched == count($topic)); } /** diff --git a/src/Http/Middleware/ResponseMiddleware.php b/src/Http/Middleware/ResponseMiddleware.php index d4d3b0c..eb5e0a7 100644 --- a/src/Http/Middleware/ResponseMiddleware.php +++ b/src/Http/Middleware/ResponseMiddleware.php @@ -3,7 +3,7 @@ namespace NoccyLabs\Mercureact\Http\Middleware; use NoccyLabs\Mercureact\Configuration; -use NoccyLabs\Mercureact\Http\Exeption\SecurityException; +use NoccyLabs\Mercureact\Http\Exception\SecurityException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; @@ -68,8 +68,8 @@ class ResponseMiddleware strlen($response->getBody()) ); return $response - ->withAddedHeader('Link', '; rel="mercure"') - ->withAddedHeader('Link', '; rel="mercure+ws"') + // ->withAddedHeader('Link', '; rel="mercure"') + // ->withAddedHeader('Link', '; rel="mercure+ws"') ->withHeader('Access-Control-Allow-Origin', '*') ->withHeader('Content-Security-Policy', "default-src * 'self' http: 'unsafe-eval' 'unsafe-inline'; connect-src * 'self'") ->withHeader('Cache-Control', 'must-revalidate')