Refactored out claim check logic to its own class
This commit is contained in:
parent
e61d0abb5d
commit
d8ae8ade70
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 ""; }
|
||||||
};
|
};
|
Loading…
Reference in New Issue
Block a user