Refactor ControllerResolver; implement auth check for GetObjectController
This commit is contained in:
parent
88dd8a7b8f
commit
e927d88c23
@ -4,7 +4,6 @@ namespace ActivityPub;
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use ActivityPub\Config\ActivityPubModule;
|
||||
use ActivityPub\Http\ControllerResolver;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\HttpKernel;
|
||||
@ -50,7 +49,7 @@ class ActivityPub
|
||||
$dispatcher->addSubscriber( $this->module->get( 'signatureListener' ) );
|
||||
$dispatcher->addSubscriber( new ExceptionListener() );
|
||||
|
||||
$controllerResolver = new ControllerResolver();
|
||||
$controllerResolver = $this->module->get( 'controllerResolver' );
|
||||
$argumentResolver = new ArgumentResolver();
|
||||
|
||||
$kernel = new HttpKernel(
|
||||
|
@ -3,8 +3,12 @@ namespace ActivityPub\Config;
|
||||
|
||||
use ActivityPub\Auth\AuthListener;
|
||||
use ActivityPub\Auth\SignatureListener;
|
||||
use ActivityPub\Controllers\GetObjectController;
|
||||
use ActivityPub\Controllers\InboxController;
|
||||
use ActivityPub\Controllers\OutboxController;
|
||||
use ActivityPub\Crypto\HttpSignatureService;
|
||||
use ActivityPub\Database\PrefixNamingStrategy;
|
||||
use ActivityPub\Http\ControllerResolver;
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
use ActivityPub\Utils\SimpleDateTimeProvider;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
@ -43,7 +47,6 @@ class ActivityPubModule
|
||||
$namingStrategy = new PrefixNamingStrategy( $options['dbPrefix'] );
|
||||
$dbConfig->setNamingStrategy( $namingStrategy );
|
||||
$dbParams = $options['dbOptions'];
|
||||
|
||||
$this->injector->register( 'entityManager', EntityManager::class )
|
||||
->setArguments( array( $dbParams, $dbConfig ) )
|
||||
->setFactory( array( EntityManager::class, 'create' ) );
|
||||
@ -67,6 +70,21 @@ class ActivityPubModule
|
||||
|
||||
$this->injector->register( 'authListener', AuthListener::class )
|
||||
->addArgument( $options['authFunction'] );
|
||||
|
||||
$this->injector->register( 'getObjectController', GetObjectController::class )
|
||||
->addArgument( new Reference( 'objectsService' ) );
|
||||
|
||||
$this->injector->register( 'inboxController', InboxController::class )
|
||||
->addArgument( new Reference( 'objectsService' ) );
|
||||
|
||||
$this->injector->register( 'outboxController', OutboxController::class )
|
||||
->addArgument( new Reference( 'objectsService' ) );
|
||||
|
||||
$this->injector->register( 'controllerResolver', ControllerResolver::class )
|
||||
->addArgument( new Reference( 'objectsService' ) )
|
||||
->addArgument( new Reference( 'getObjectController' ) )
|
||||
->addArgument( new Reference( 'inboxController' ) )
|
||||
->addArgument( new Reference( 'outboxController' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,9 +1,11 @@
|
||||
<?php
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use ActivityPub\Entities\ActivityPubObject;
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
@ -34,7 +36,61 @@ class GetObjectController
|
||||
if ( ! $object ) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
if ( ! $this->requestAuthorizedToView( $request, $object ) ) {
|
||||
throw new UnauthorizedHttpException(
|
||||
'Signature realm="ActivityPub",headers="(request-target) host date"'
|
||||
);
|
||||
}
|
||||
// TODO handle collections here
|
||||
return new JsonResponse( $object->asArray() );
|
||||
}
|
||||
|
||||
private function requestAuthorizedToView( Request $request,
|
||||
ActivityPubObject $object )
|
||||
{
|
||||
$audience = $this->getAudience( $object );
|
||||
if ( in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience ) ) {
|
||||
return true;
|
||||
}
|
||||
return $request->attributes->has( 'actor' ) &&
|
||||
in_array( $request->attributes->get( 'actor' ), $audience );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all of the $object's audience actors, i.e.
|
||||
* the contents of the to, bto, cc, bcc, and audience fields, as
|
||||
* well as the actor who created to object
|
||||
*
|
||||
* @param ActivityPubObject $object
|
||||
* @return array The audience members, collapsed to an array of ids
|
||||
*/
|
||||
private function getAudience( ActivityPubObject $object )
|
||||
{
|
||||
// TODO do I need to traverse the inReplyTo chain here?
|
||||
$objectArr = $object->asArray( 0 );
|
||||
$audience = array();
|
||||
if ( array_key_exists( 'to', $objectArr ) ) {
|
||||
$audience = array_merge( $audience, $objectArr['to'] );
|
||||
}
|
||||
if ( array_key_exists( 'bto', $objectArr ) ) {
|
||||
$audience = array_merge( $audience, $objectArr['bto'] );
|
||||
}
|
||||
if ( array_key_exists( 'cc', $objectArr ) ) {
|
||||
$audience = array_merge( $audience, $objectArr['cc'] );
|
||||
}
|
||||
if ( array_key_exists( 'bcc', $objectArr ) ) {
|
||||
$audience = array_merge( $audience, $objectArr['bcc'] );
|
||||
}
|
||||
if ( array_key_exists( 'audience', $objectArr ) ) {
|
||||
$audience = array_merge( $audience, $objectArr['audience'] );
|
||||
}
|
||||
if ( array_key_exists( 'attributedTo', $objectArr ) ) {
|
||||
$audience[] = $objectArr['attributedTo'];
|
||||
}
|
||||
if ( array_key_exists( 'actor', $objectArr ) ) {
|
||||
$audience[] = $objectArr['actor'];
|
||||
}
|
||||
return $audience;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<?php
|
||||
namespace ActivityPub\Controllers\Inbox;
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* The DefaultInboxController handles inbox requests not handled by other controllers
|
||||
* The InboxController handles POST requests to an inbox
|
||||
*/
|
||||
class DefaultInboxController
|
||||
class InboxController
|
||||
{
|
||||
private $ObjectsService;
|
||||
|
@ -1,10 +1,10 @@
|
||||
<?php
|
||||
namespace ActivityPub\Controllers\Outbox;
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class DefaultOutboxController
|
||||
class OutboxController
|
||||
{
|
||||
private $objectsService;
|
||||
|
@ -2,8 +2,8 @@
|
||||
namespace ActivityPub\Http;
|
||||
|
||||
use ActivityPub\Controllers\GetObjectController;
|
||||
use ActivityPub\Controllers\Inbox\DefaultInboxController;
|
||||
use ActivityPub\Controllers\Outbox\DefaultOutboxController;
|
||||
use ActivityPub\Controllers\InboxController;
|
||||
use ActivityPub\Controllers\OutboxController;
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
|
||||
@ -13,39 +13,22 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ControllerResolver implements ControllerResolverInterface
|
||||
{
|
||||
private $objectService;
|
||||
private $inboxControllers;
|
||||
private $outboxControllers;
|
||||
private $objectsService;
|
||||
private $getObjectController;
|
||||
private $inboxController;
|
||||
private $outboxController;
|
||||
|
||||
public function __construct( ObjectsService $objectService )
|
||||
public function __construct( ObjectsService $objectsService,
|
||||
GetObjectController $getObjectController,
|
||||
InboxController $inboxController,
|
||||
OutboxController $outboxController )
|
||||
{
|
||||
$this->objectService = $objectService;
|
||||
$this->inboxControllers = array();
|
||||
$this->outboxControllers = array();
|
||||
$this->objectsService = $objectsService;
|
||||
$this->getObjectController = $getObjectController;
|
||||
$this->inboxController = $inboxController;
|
||||
$this->outboxController = $outboxController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new controller to handle ActivityPub inbox requests of type $type
|
||||
*
|
||||
* @param Callable $controller The controller
|
||||
* @param string $type The Activity type this controller can handle
|
||||
*/
|
||||
public function registerInboxController( Callable $controller, string $type )
|
||||
{
|
||||
$this->inboxControllers[$type] = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new controller to handle ActivityPub outbox requests of type $type
|
||||
*
|
||||
* @param Callable $controller The controller
|
||||
* @param string $type The Activity type this controller can handle
|
||||
*/
|
||||
public function registerOutboxController( Callable $controller, string $type )
|
||||
{
|
||||
$this->outboxControllers[$type] = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an object with a field named $name with value $value exists
|
||||
*
|
||||
@ -55,14 +38,13 @@ class ControllerResolver implements ControllerResolverInterface
|
||||
*/
|
||||
private function objectWithFieldExists( string $name, string $value )
|
||||
{
|
||||
return count( $this->objectService->query( array( $name => $value ) ) ) > 0;
|
||||
return count( $this->objectsService->query( array( $name => $value ) ) ) > 0;
|
||||
}
|
||||
|
||||
public function getController( Request $request )
|
||||
{
|
||||
if ( $request->getMethod() == Request::METHOD_GET ) {
|
||||
$controller = new GetObjectController( $this->objectService );
|
||||
return array( $controller, 'handle' );
|
||||
return array( $this->getObjectController, 'handle' );
|
||||
} else if ( $request->getMethod() == Request::METHOD_POST ) {
|
||||
$uri = $request->getUri();
|
||||
if ( $this->objectWithFieldExists( 'inbox', $uri ) ) {
|
||||
@ -70,23 +52,13 @@ class ControllerResolver implements ControllerResolverInterface
|
||||
if ( ! isset( $activity->type ) ) {
|
||||
throw new BadRequestHttpException( '"type" field not found' );
|
||||
}
|
||||
if ( array_key_exists( $activity->type, $this->inboxControllers ) ) {
|
||||
return $this->inboxControllers[$activity->type];
|
||||
} else {
|
||||
$controller = new DefaultInboxController( $this->objectService );
|
||||
return array( $controller, 'handle' );
|
||||
}
|
||||
return array( $this->inboxController, 'handle' );
|
||||
} else if ( $this->objectWithFieldExists( 'outbox', $uri ) ) {
|
||||
$activity = json_decode( $request->getContent() );
|
||||
if ( ! isset( $activity->type ) ) {
|
||||
throw new BadRequestHttpException( '"type" field not found' );
|
||||
}
|
||||
if ( array_key_exists( $activity->type, $this->outboxControllers ) ) {
|
||||
return $this->outboxControllers[$activity->type];
|
||||
} else {
|
||||
$controller = new DefaultOutboxController( $this->objectService );
|
||||
return array( $controller, 'handle' );
|
||||
}
|
||||
return array( $this->outboxController, 'handle' );
|
||||
} else {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use ActivityPub\Entities\ActivityPubObject;
|
||||
use ActivityPub\Entities\Field;
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@ -18,8 +19,21 @@ class GetObjectControllerTest extends TestCase
|
||||
'id' => 'https://example.com/objects/2',
|
||||
'type' => 'Note',
|
||||
),
|
||||
'audience' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'type' => 'Create',
|
||||
),
|
||||
'https://example.com/objects/2' => array(
|
||||
'id' => 'https://example.com/objects/1',
|
||||
'object' => array(
|
||||
'id' => 'https://example.com/objects/2',
|
||||
'type' => 'Note',
|
||||
),
|
||||
'to' => array( 'https://example.com/actor/1' ),
|
||||
'type' => 'Create',
|
||||
'actor' => array(
|
||||
'id' => 'https://example.com/actor/2',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
private $getObjectController;
|
||||
@ -68,5 +82,38 @@ class GetObjectControllerTest extends TestCase
|
||||
$this->expectException( NotFoundHttpException::class );
|
||||
$this->getObjectController->handle( $request );
|
||||
}
|
||||
|
||||
public function testItDeniesAccess()
|
||||
{
|
||||
$request = Request::create( 'https://example.com/objects/2' );
|
||||
$this->expectException( UnauthorizedHttpException::class );
|
||||
$this->getObjectController->handle( $request );
|
||||
}
|
||||
|
||||
public function testItAllowsAccessToAuthedActor()
|
||||
{
|
||||
$request = Request::create( 'https://example.com/objects/2' );
|
||||
$request->attributes->set( 'actor', 'https://example.com/actor/1' );
|
||||
$response = $this->getObjectController->handle( $request );
|
||||
$this->assertNotNull( $response );
|
||||
$this->assertEquals(
|
||||
json_encode( self::OBJECTS['https://example.com/objects/2'] ),
|
||||
$response->getContent()
|
||||
);
|
||||
$this->assertEquals( 'application/json', $response->headers->get( 'Content-Type' ) );
|
||||
}
|
||||
|
||||
public function testItAllowsAccessToAttributedActor()
|
||||
{
|
||||
$request = Request::create( 'https://example.com/objects/2' );
|
||||
$request->attributes->set( 'actor', 'https://example.com/actor/2' );
|
||||
$response = $this->getObjectController->handle( $request );
|
||||
$this->assertNotNull( $response );
|
||||
$this->assertEquals(
|
||||
json_encode( self::OBJECTS['https://example.com/objects/2'] ),
|
||||
$response->getContent()
|
||||
);
|
||||
$this->assertEquals( 'application/json', $response->headers->get( 'Content-Type' ) );
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
@ -4,6 +4,8 @@ namespace ActivityPub\Test\Http;
|
||||
use ActivityPub\Controllers\Inbox\DefaultInboxController;
|
||||
use ActivityPub\Controllers\Outbox\DefaultOutboxController;
|
||||
use ActivityPub\Controllers\GetObjectController;
|
||||
use ActivityPub\Controllers\InboxController;
|
||||
use ActivityPub\Controllers\OutboxController;
|
||||
use ActivityPub\Http\ControllerResolver;
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -18,6 +20,9 @@ class ControllerResolverTest extends TestCase
|
||||
const OUTBOX_URI = 'https://example.com/outbox';
|
||||
|
||||
private $controllerResolver;
|
||||
private $getObjectController;
|
||||
private $inboxController;
|
||||
private $outboxController;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
@ -35,7 +40,15 @@ class ControllerResolverTest extends TestCase
|
||||
return array();
|
||||
})
|
||||
);
|
||||
$this->controllerResolver = new ControllerResolver( $objectsService );
|
||||
$this->getObjectController = $this->createMock( GetObjectController::class );
|
||||
$this->inboxController = $this->createMock( InboxController::class );
|
||||
$this->outboxController = $this->createMock( OutboxController::class );
|
||||
$this->controllerResolver = new ControllerResolver(
|
||||
$objectsService,
|
||||
$this->getObjectController,
|
||||
$this->inboxController,
|
||||
$this->outboxController
|
||||
);
|
||||
}
|
||||
|
||||
private function createRequestWithBody( $uri, $method, $body )
|
||||
@ -49,8 +62,7 @@ class ControllerResolverTest extends TestCase
|
||||
$request = Request::create( 'https://example.com/object', Request::METHOD_GET );
|
||||
$controller = $this->controllerResolver->getController( $request );
|
||||
$this->assertIsCallable( $controller );
|
||||
$this->assertInstanceOf( GetObjectController::class, $controller[0] );
|
||||
$this->assertEquals( 'handle', $controller[1] );
|
||||
$this->assertEquals( array( $this->getObjectController, 'handle' ), $controller );
|
||||
}
|
||||
|
||||
public function testItChecksForType()
|
||||
@ -60,15 +72,13 @@ class ControllerResolverTest extends TestCase
|
||||
$controller = $this->controllerResolver->getController( $request );
|
||||
}
|
||||
|
||||
public function testItReturnsDefaultInboxController()
|
||||
public function testItReturnsInboxController()
|
||||
{
|
||||
$request = $this->createRequestWithBody(
|
||||
'https://example.com/inbox', Request::METHOD_POST, array( 'type' => 'Foo' )
|
||||
);
|
||||
$controller = $this->controllerResolver->getController( $request );
|
||||
$this->assertIsCallable( $controller );
|
||||
$this->assertInstanceOf( DefaultInboxController::class, $controller[0] );
|
||||
$this->assertEquals( 'handle', $controller[1] );
|
||||
$this->assertEquals( array( $this->inboxController, 'handle' ), $controller );
|
||||
}
|
||||
|
||||
public function testItReturnsDefaultOutboxController()
|
||||
@ -77,35 +87,7 @@ class ControllerResolverTest extends TestCase
|
||||
'https://example.com/outbox', Request::METHOD_POST, array( 'type' => 'Foo' )
|
||||
);
|
||||
$controller = $this->controllerResolver->getController( $request );
|
||||
$this->assertIsCallable( $controller );
|
||||
$this->assertInstanceOf( DefaultOutboxController::class, $controller[0] );
|
||||
$this->assertEquals( 'handle', $controller[1] );
|
||||
}
|
||||
|
||||
public function testItRegistersNewInboxController()
|
||||
{
|
||||
$this->controllerResolver->registerInboxController( function() {
|
||||
return 'barCallable';
|
||||
}, 'Bar' );
|
||||
$request = $this->createRequestWithBody(
|
||||
'https://example.com/inbox', Request::METHOD_POST, array( 'type' => 'Bar' )
|
||||
);
|
||||
$controller = $this->controllerResolver->getController( $request );
|
||||
$this->assertIsCallable( $controller );
|
||||
$this->assertEquals( 'barCallable', call_user_func( $controller ) );
|
||||
}
|
||||
|
||||
public function testItRegistersNewOutboxController()
|
||||
{
|
||||
$this->controllerResolver->registerOutboxController( function() {
|
||||
return 'barCallable';
|
||||
}, 'Bar' );
|
||||
$request = $this->createRequestWithBody(
|
||||
'https://example.com/outbox', Request::METHOD_POST, array( 'type' => 'Bar' )
|
||||
);
|
||||
$controller = $this->controllerResolver->getController( $request );
|
||||
$this->assertIsCallable( $controller );
|
||||
$this->assertEquals( 'barCallable', call_user_func( $controller ) );
|
||||
$this->assertEquals( array( $this->outboxController, 'handle' ), $controller );
|
||||
}
|
||||
|
||||
public function testItDisallowsPostToInvalidUrl()
|
||||
|
Loading…
Reference in New Issue
Block a user