Refactored out claim check logic to its own class
This commit is contained in:
		
							
								
								
									
										46
									
								
								src/Broker/Security/ClaimChecker.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/Broker/Security/ClaimChecker.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\Mercureact\Broker\Security;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use NoccyLabs\SimpleJWT\JWTToken;
 | 
				
			||||||
 | 
					use Rize\UriTemplate\UriTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ClaimChecker
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private UriTemplate $uriTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->uriTemplate = new UriTemplate();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function matchAll(array $topics, array $claims): bool
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $matched = 0;
 | 
				
			||||||
 | 
					        foreach ((array)$topics as $match) {
 | 
				
			||||||
 | 
					            foreach ($claims as $claim) {
 | 
				
			||||||
 | 
					                if (($claim === "*")
 | 
				
			||||||
 | 
					                    || ($claim === $match)
 | 
				
			||||||
 | 
					                    || ($this->uriTemplate->extract($claim, $match, true))) {
 | 
				
			||||||
 | 
					                    $matched++;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return ($matched == count($topics));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function matchOne(array $topics, array $claims): bool
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        foreach ((array)$topics as $match) {
 | 
				
			||||||
 | 
					            foreach ($claims as $claim) {
 | 
				
			||||||
 | 
					                if (($claim === "*")
 | 
				
			||||||
 | 
					                    || ($claim === $match)
 | 
				
			||||||
 | 
					                    || ($this->uriTemplate->extract($claim, $match, true))) {
 | 
				
			||||||
 | 
					                    return true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -30,6 +30,11 @@ class SseSubscriber implements SubscriberInterface
 | 
				
			|||||||
        return $this->request->getAttribute('authorized');
 | 
					        return $this->request->getAttribute('authorized');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getMercureClaims(): ?array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->request->getAttribute('mercure.claims');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getPayload(): array
 | 
					    public function getPayload(): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->request->getAttribute('mercure.payload')??[];
 | 
					        return $this->request->getAttribute('mercure.payload')??[];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,13 @@ interface SubscriberInterface
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    public function isAuthenticated(): bool;
 | 
					    public function isAuthenticated(): bool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the content of the JWT mercure claim if present.
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return array|null
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getMercureClaims(): ?array;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Returns the content of the JWT mercure.payload claim if present.
 | 
					     * Returns the content of the JWT mercure.payload claim if present.
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,13 +21,8 @@ class WsSubscriber implements SubscriberInterface, EventEmitterInterface
 | 
				
			|||||||
    const EVENT_UNSUBSCRIBE = 'unsubscribe';
 | 
					    const EVENT_UNSUBSCRIBE = 'unsubscribe';
 | 
				
			||||||
    const EVENT_ERROR = 'error';
 | 
					    const EVENT_ERROR = 'error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const STATE_UNAUTHORIZED = 0;
 | 
					 | 
				
			||||||
    const STATE_AUTHORIZED = 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private string $id;
 | 
					    private string $id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private int $state = self::STATE_UNAUTHORIZED;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function __construct(
 | 
					    public function __construct(
 | 
				
			||||||
        private WebSocketConnection $stream,
 | 
					        private WebSocketConnection $stream,
 | 
				
			||||||
        private ServerRequestInterface $request,
 | 
					        private ServerRequestInterface $request,
 | 
				
			||||||
@@ -73,6 +68,11 @@ class WsSubscriber implements SubscriberInterface, EventEmitterInterface
 | 
				
			|||||||
        return $this->token && $this->token->isValid();
 | 
					        return $this->token && $this->token->isValid();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getMercureClaims(): ?array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->request->getAttribute('mercure.claims');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getPayload(): array
 | 
					    public function getPayload(): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->request->getAttribute('mercure.payload')??[];
 | 
					        return $this->request->getAttribute('mercure.payload')??[];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@
 | 
				
			|||||||
namespace NoccyLabs\Mercureact\Http\Middleware;
 | 
					namespace NoccyLabs\Mercureact\Http\Middleware;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use NoccyLabs\Mercureact\Broker\Message;
 | 
					use NoccyLabs\Mercureact\Broker\Message;
 | 
				
			||||||
 | 
					use NoccyLabs\Mercureact\Broker\Security\ClaimChecker;
 | 
				
			||||||
use NoccyLabs\Mercureact\Broker\Subscriber\SseSubscriber;
 | 
					use NoccyLabs\Mercureact\Broker\Subscriber\SseSubscriber;
 | 
				
			||||||
use NoccyLabs\Mercureact\Broker\TopicManager;
 | 
					use NoccyLabs\Mercureact\Broker\TopicManager;
 | 
				
			||||||
use NoccyLabs\Mercureact\Configuration;
 | 
					use NoccyLabs\Mercureact\Configuration;
 | 
				
			||||||
@@ -27,6 +28,8 @@ class MercureHandler
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private int $seenIdHistorySize = 100;
 | 
					    private int $seenIdHistorySize = 100;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private ClaimChecker $claimChecker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __construct(
 | 
					    public function __construct(
 | 
				
			||||||
        private Configuration $config,
 | 
					        private Configuration $config,
 | 
				
			||||||
        private TopicManager $topicManager,
 | 
					        private TopicManager $topicManager,
 | 
				
			||||||
@@ -35,6 +38,7 @@ class MercureHandler
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->loop = $loop ?? Loop::get();
 | 
					        $this->loop = $loop ?? Loop::get();
 | 
				
			||||||
        $this->seenIdHistorySize = $this->config->getDuplicateIdHistorySize();
 | 
					        $this->seenIdHistorySize = $this->config->getDuplicateIdHistorySize();
 | 
				
			||||||
 | 
					        $this->claimChecker = new ClaimChecker();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -94,7 +98,7 @@ class MercureHandler
 | 
				
			|||||||
        // Grab the JWT token from the requests authorization attribute
 | 
					        // Grab the JWT token from the requests authorization attribute
 | 
				
			||||||
        if ($request->getAttribute('authorized')) {
 | 
					        if ($request->getAttribute('authorized')) {
 | 
				
			||||||
            $claims = $request->getAttribute('mercure.subscribe');
 | 
					            $claims = $request->getAttribute('mercure.subscribe');
 | 
				
			||||||
            if (!$this->checkTopicClaims($topics, $claims)) {
 | 
					            if (!$this->claimChecker->matchAll($topics, $claims)) {
 | 
				
			||||||
                throw new SecurityException(
 | 
					                throw new SecurityException(
 | 
				
			||||||
                    message: "Insufficient permissions for subscribe", 
 | 
					                    message: "Insufficient permissions for subscribe", 
 | 
				
			||||||
                    code: SecurityException::ERR_NO_PERMISSION
 | 
					                    code: SecurityException::ERR_NO_PERMISSION
 | 
				
			||||||
@@ -112,6 +116,7 @@ class MercureHandler
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        $this->topicManager->subscribe($subscriber, $topics);
 | 
					        $this->topicManager->subscribe($subscriber, $topics);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $responseStream->on('close', function () use ($subscriber, $topics) {
 | 
					        $responseStream->on('close', function () use ($subscriber, $topics) {
 | 
				
			||||||
            $this->topicManager->unsubscribe($subscriber, $topics);
 | 
					            $this->topicManager->unsubscribe($subscriber, $topics);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@@ -156,7 +161,7 @@ class MercureHandler
 | 
				
			|||||||
        if ($request->getAttribute('authorized')) {
 | 
					        if ($request->getAttribute('authorized')) {
 | 
				
			||||||
            $claims = $request->getAttribute('mercure.publish');
 | 
					            $claims = $request->getAttribute('mercure.publish');
 | 
				
			||||||
            // check topic against publishClaims
 | 
					            // check topic against publishClaims
 | 
				
			||||||
            if (!$this->checkTopicClaims($data['topic']??[], $claims)) {
 | 
					            if (!$this->claimChecker->matchAll($data['topic']??[], $claims)) {
 | 
				
			||||||
                throw new SecurityException(
 | 
					                throw new SecurityException(
 | 
				
			||||||
                    message: "Insufficient permissions for publish", 
 | 
					                    message: "Insufficient permissions for publish", 
 | 
				
			||||||
                    code: SecurityException::ERR_NO_PERMISSION
 | 
					                    code: SecurityException::ERR_NO_PERMISSION
 | 
				
			||||||
@@ -195,23 +200,4 @@ class MercureHandler
 | 
				
			|||||||
        return Response::plaintext($message->id."\n");
 | 
					        return Response::plaintext($message->id."\n");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function checkTopicClaims(string|array $topic, array $claims): bool
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $matched = 0;
 | 
					 | 
				
			||||||
        foreach ((array)$topic as $match) {
 | 
					 | 
				
			||||||
            foreach ($claims as $claim) {
 | 
					 | 
				
			||||||
                if (($claim === "*") || ($claim === $match)) {
 | 
					 | 
				
			||||||
                    $matched++;
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                // TODO make sure that UriTemplate parsing works
 | 
					 | 
				
			||||||
                if ((new UriTemplate())->extract($claim, $match, true)) {
 | 
					 | 
				
			||||||
                    $matched++;
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return ($matched == count($topic));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -56,6 +56,7 @@ abstract class _Subscriber implements SubscriberInterface {
 | 
				
			|||||||
    public array $messages = [];
 | 
					    public array $messages = [];
 | 
				
			||||||
    public function isAuthenticated():bool { return false; }
 | 
					    public function isAuthenticated():bool { return false; }
 | 
				
			||||||
    public function deliver(Message $message):void { $this->messages[] = $message; }
 | 
					    public function deliver(Message $message):void { $this->messages[] = $message; }
 | 
				
			||||||
 | 
					    public function getMercureClaims(): ?array { return []; }
 | 
				
			||||||
    public function getPayload(): ?array { return null; }
 | 
					    public function getPayload(): ?array { return null; }
 | 
				
			||||||
    public function getId(): string { return ""; }
 | 
					    public function getId(): string { return ""; }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
		Reference in New Issue
	
	Block a user