From e927d88c23b731d9a9e121e15c12f826081d3219 Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Fri, 18 Jan 2019 17:13:46 -0500 Subject: [PATCH] Refactor ControllerResolver; implement auth check for GetObjectController --- src/ActivityPub.php | 3 +- src/Config/ActivityPubModule.php | 20 +++++- src/Controllers/GetObjectController.php | 56 ++++++++++++++++ ...nboxController.php => InboxController.php} | 6 +- ...boxController.php => OutboxController.php} | 4 +- src/Http/ControllerResolver.php | 64 ++++++------------- test/Controllers/GetObjectControllerTest.php | 47 ++++++++++++++ test/Http/ControllerResolverTest.php | 54 ++++++---------- 8 files changed, 164 insertions(+), 90 deletions(-) rename src/Controllers/{Inbox/DefaultInboxController.php => InboxController.php} (76%) rename src/Controllers/{Outbox/DefaultOutboxController.php => OutboxController.php} (87%) diff --git a/src/ActivityPub.php b/src/ActivityPub.php index b12c4af..5ba05ba 100644 --- a/src/ActivityPub.php +++ b/src/ActivityPub.php @@ -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( diff --git a/src/Config/ActivityPubModule.php b/src/Config/ActivityPubModule.php index 0a8dffd..c49cd18 100644 --- a/src/Config/ActivityPubModule.php +++ b/src/Config/ActivityPubModule.php @@ -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' ) ); } /** diff --git a/src/Controllers/GetObjectController.php b/src/Controllers/GetObjectController.php index 2605993..2b647e0 100644 --- a/src/Controllers/GetObjectController.php +++ b/src/Controllers/GetObjectController.php @@ -1,9 +1,11 @@ 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; + } } ?> diff --git a/src/Controllers/Inbox/DefaultInboxController.php b/src/Controllers/InboxController.php similarity index 76% rename from src/Controllers/Inbox/DefaultInboxController.php rename to src/Controllers/InboxController.php index 99df533..d891797 100644 --- a/src/Controllers/Inbox/DefaultInboxController.php +++ b/src/Controllers/InboxController.php @@ -1,13 +1,13 @@ 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(); } diff --git a/test/Controllers/GetObjectControllerTest.php b/test/Controllers/GetObjectControllerTest.php index 6988f15..106b793 100644 --- a/test/Controllers/GetObjectControllerTest.php +++ b/test/Controllers/GetObjectControllerTest.php @@ -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' ) ); + } } ?> diff --git a/test/Http/ControllerResolverTest.php b/test/Http/ControllerResolverTest.php index 9c7a209..3d41fee 100644 --- a/test/Http/ControllerResolverTest.php +++ b/test/Http/ControllerResolverTest.php @@ -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()