Skip to content

Commit

Permalink
BUGFIX Respecting api_access on has_one relations in RestfulServer
Browse files Browse the repository at this point in the history
BUGFIX Limiting fields according to api_access on relation object (rather than the "root" object) in RestfulServer
BUGFIX Limit listing of has_one relations in RestfulServer to actual relation (was listing all objects before)
BUGFIX Creating correct object instances in RestfulServer->getHandler() for relation queries
  • Loading branch information
chillu committed Nov 13, 2011
1 parent ec47cc1 commit d30b4b1
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 19 deletions.
53 changes: 37 additions & 16 deletions api/RestfulServer.php
Expand Up @@ -214,7 +214,7 @@ protected function getHandler($className, $id, $relationName) {


$params = $this->request->getVars(); $params = $this->request->getVars();


$responseFormatter = $this->getResponseDataFormatter(); $responseFormatter = $this->getResponseDataFormatter($className);
if(!$responseFormatter) return $this->unsupportedMediaType(); if(!$responseFormatter) return $this->unsupportedMediaType();


// $obj can be either a DataObject or a SS_List, // $obj can be either a DataObject or a SS_List,
Expand All @@ -229,6 +229,9 @@ protected function getHandler($className, $id, $relationName) {
if($relationName) { if($relationName) {
$obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName); $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
if(!$obj) return $this->notFound(); if(!$obj) return $this->notFound();

// TODO Avoid creating data formatter again for relation class (see above)
$responseFormatter = $this->getResponseDataFormatter($obj->dataClass());
} }


} else { } else {
Expand Down Expand Up @@ -270,24 +273,25 @@ protected function getSearchQuery($className, $params = null, $sort = null, $lim
} else { } else {
$searchContext = singleton($className)->getDefaultSearchContext(); $searchContext = singleton($className)->getDefaultSearchContext();
} }

return $searchContext->getQuery($params, $sort, $limit, $existingQuery);
return $searchContext->getQuery($params, $sort, $limit, $existingQuery);
} }


/** /**
* Returns a dataformatter instance based on the request * Returns a dataformatter instance based on the request
* extension or mimetype. Falls back to {@link self::$default_extension}. * extension or mimetype. Falls back to {@link self::$default_extension}.
* *
* @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers
* @param String Classname of a DataObject
* @return DataFormatter * @return DataFormatter
*/ */
protected function getDataFormatter($includeAcceptHeader = false) { protected function getDataFormatter($includeAcceptHeader = false, $className = null) {
$extension = $this->request->getExtension(); $extension = $this->request->getExtension();
$contentTypeWithEncoding = $this->request->getHeader('Content-Type'); $contentTypeWithEncoding = $this->request->getHeader('Content-Type');
preg_match('/([^;]*)/',$contentTypeWithEncoding, $contentTypeMatches); preg_match('/([^;]*)/',$contentTypeWithEncoding, $contentTypeMatches);
$contentType = $contentTypeMatches[0]; $contentType = $contentTypeMatches[0];
$accept = $this->request->getHeader('Accept'); $accept = $this->request->getHeader('Accept');
$mimetypes = $this->request->getAcceptMimetypes(); $mimetypes = $this->request->getAcceptMimetypes();
if(!$className) $className = $this->urlParams['ClassName'];


// get formatter // get formatter
if(!empty($extension)) { if(!empty($extension)) {
Expand All @@ -306,9 +310,9 @@ protected function getDataFormatter($includeAcceptHeader = false) {
// set custom fields // set custom fields
if($customAddFields = $this->request->getVar('add_fields')) $formatter->setCustomAddFields(explode(',',$customAddFields)); if($customAddFields = $this->request->getVar('add_fields')) $formatter->setCustomAddFields(explode(',',$customAddFields));
if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields)); if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields));
$formatter->setCustomRelations($this->getAllowedRelations($this->urlParams['ClassName'])); $formatter->setCustomRelations($this->getAllowedRelations($className));


$apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access'); $apiAccess = singleton($className)->stat('api_access');
if(is_array($apiAccess)) { if(is_array($apiAccess)) {
$formatter->setCustomAddFields(array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view'])); $formatter->setCustomAddFields(array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view']));
if($formatter->getCustomFields()) { if($formatter->getCustomFields()) {
Expand All @@ -331,12 +335,20 @@ protected function getDataFormatter($includeAcceptHeader = false) {
return $formatter; return $formatter;
} }


protected function getRequestDataFormatter() { /**
return $this->getDataFormatter(false); * @param String Classname of a DataObject
* @return DataFormatter
*/
protected function getRequestDataFormatter($className = null) {
return $this->getDataFormatter(false, $className);
} }


protected function getResponseDataFormatter() { /**
return $this->getDataFormatter(true); * @param String Classname of a DataObject
* @return DataFormatter
*/
protected function getResponseDataFormatter($className = null) {
return $this->getDataFormatter(true, $className);
} }


/** /**
Expand All @@ -361,10 +373,10 @@ protected function putHandler($className, $id) {
if(!$obj) return $this->notFound(); if(!$obj) return $this->notFound();
if(!$obj->canEdit()) return $this->permissionFailure(); if(!$obj->canEdit()) return $this->permissionFailure();


$reqFormatter = $this->getRequestDataFormatter(); $reqFormatter = $this->getRequestDataFormatter($className);
if(!$reqFormatter) return $this->unsupportedMediaType(); if(!$reqFormatter) return $this->unsupportedMediaType();


$responseFormatter = $this->getResponseDataFormatter(); $responseFormatter = $this->getResponseDataFormatter($className);
if(!$responseFormatter) return $this->unsupportedMediaType(); if(!$responseFormatter) return $this->unsupportedMediaType();


$obj = $this->updateDataObject($obj, $reqFormatter); $obj = $this->updateDataObject($obj, $reqFormatter);
Expand Down Expand Up @@ -419,10 +431,10 @@ protected function postHandler($className, $id, $relation) {
if(!singleton($className)->canCreate()) return $this->permissionFailure(); if(!singleton($className)->canCreate()) return $this->permissionFailure();
$obj = new $className(); $obj = new $className();


$reqFormatter = $this->getRequestDataFormatter(); $reqFormatter = $this->getRequestDataFormatter($className);
if(!$reqFormatter) return $this->unsupportedMediaType(); if(!$reqFormatter) return $this->unsupportedMediaType();


$responseFormatter = $this->getResponseDataFormatter(); $responseFormatter = $this->getResponseDataFormatter($className);


$obj = $this->updateDataObject($obj, $reqFormatter); $obj = $this->updateDataObject($obj, $reqFormatter);


Expand Down Expand Up @@ -520,8 +532,17 @@ protected function getObjectsQuery($className, $params, $sort, $limit) {
protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) { protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) {
// The relation method will return a DataList, that getSearchQuery subsequently manipulates // The relation method will return a DataList, that getSearchQuery subsequently manipulates
if($obj->hasMethod($relationName)) { if($obj->hasMethod($relationName)) {
$query = $obj->$relationName(); if($relationClass = $obj->has_one($relationName)) {
return $this->getSearchQuery($query->dataClass(), $params, $sort, $limit, $query); $joinField = $relationName . 'ID';
$list = DataList::create($relationClass)->byIDs(array($obj->$joinField));

This comment has been minimized.

Copy link
@sminnee

sminnee Nov 13, 2011

Member

I don't like this, but it stems from a hole in the ORM API. A discussion has been kicked off here:
http://groups.google.com/group/silverstripe-dev/t/276f592cdb74e7c5

} else {
$list = $obj->$relationName();
}

$apiAccess = singleton($list->dataClass())->stat('api_access');
if(!$apiAccess) return false;

return $this->getSearchQuery($list->dataClass(), $params, $sort, $limit, $list);
} }
} }


Expand Down
32 changes: 29 additions & 3 deletions tests/api/RestfulServerTest.php
Expand Up @@ -305,6 +305,7 @@ public function testXMLValueFormatting() {
} }


public function testApiAccessFieldRestrictions() { public function testApiAccessFieldRestrictions() {
$author1 = $this->objFromFixture('RestfulServerTest_Author','author1');
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1'); $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1');


$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
Expand Down Expand Up @@ -332,15 +333,40 @@ public function testApiAccessFieldRestrictions() {
$this->assertNotContains('<SecretRelation>', $response->getBody(), $this->assertNotContains('<SecretRelation>', $response->getBody(),
'"fields" URL parameter filters out disallowed relations from $api_access' '"fields" URL parameter filters out disallowed relations from $api_access'
); );

$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . '/Ratings';
$response = Director::test($url, null, null, 'GET');
$this->assertContains('<Rating>', $response->getBody(),
'Relation viewer shows fields allowed through $api_access'
);
$this->assertNotContains('<SecretField>', $response->getBody(),
'Relation viewer on has-many filters out disallowed fields from $api_access'
);
} }


public function testApiAccessRelationRestrictions() { public function testApiAccessRelationRestrictionsInline() {
$author1 = $this->objFromFixture('RestfulServerTest_Author','author1'); $author1 = $this->objFromFixture('RestfulServerTest_Author','author1');


$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID; $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
$response = Director::test($url, null, null, 'GET'); $response = Director::test($url, null, null, 'GET');
$this->assertNotContains('<RelatedPages', $response->getBody()); $this->assertNotContains('<RelatedPages', $response->getBody(), 'Restricts many-many with api_access=false');
$this->assertNotContains('<PublishedPages', $response->getBody()); $this->assertNotContains('<PublishedPages', $response->getBody(), 'Restricts has-many with api_access=false');
}

public function testApiAccessRelationRestrictionsOnEndpoint() {
$author1 = $this->objFromFixture('RestfulServerTest_Author','author1');

$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/ProfilePage";
$response = Director::test($url, null, null, 'GET');
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false');

$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/RelatedPages";
$response = Director::test($url, null, null, 'GET');
$this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false');

$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/PublishedPages";
$response = Director::test($url, null, null, 'GET');
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false');
} }


public function testApiAccessWithPUT() { public function testApiAccessWithPUT() {
Expand Down
2 changes: 2 additions & 0 deletions tests/api/RestfulServerTest.yml
Expand Up @@ -56,9 +56,11 @@ RestfulServerTest_AuthorRating:
WriteProtectedField: Dont overwrite me WriteProtectedField: Dont overwrite me
SecretField: Dont look at me! SecretField: Dont look at me!
Author: =>RestfulServerTest_Author.author1 Author: =>RestfulServerTest_Author.author1
SecretRelation: =>RestfulServerTest_Author.author1
rating2: rating2:
Rating: 5 Rating: 5
Author: =>RestfulServerTest_Author.author1 Author: =>RestfulServerTest_Author.author1
SecretRelation: =>RestfulServerTest_Author.author1
RestfulServerTest_SecretThing: RestfulServerTest_SecretThing:
thing1: thing1:
Name: Unspeakable Name: Unspeakable

0 comments on commit d30b4b1

Please sign in to comment.