diff --git a/tests/TestHelpers/OcsApiHelper.php b/tests/TestHelpers/OcsApiHelper.php index 9646313c5647..66900123904c 100644 --- a/tests/TestHelpers/OcsApiHelper.php +++ b/tests/TestHelpers/OcsApiHelper.php @@ -38,11 +38,12 @@ class OcsApiHelper { * @param string $path * @param array $body array of key, value pairs e.g ['value' => 'yes'] * @param int $ocsApiVersion (1|2) default 2 + * @param array $headers * * @return ResponseInterface */ public static function sendRequest( - $baseUrl, $user, $password, $method, $path, $body = [], $ocsApiVersion = 2 + $baseUrl, $user, $password, $method, $path, $body = [], $ocsApiVersion = 2, $headers = [] ) { $fullUrl = $baseUrl; if (\substr($fullUrl, -1) !== '/') { @@ -50,6 +51,6 @@ public static function sendRequest( } $fullUrl .= "ocs/v{$ocsApiVersion}.php" . $path; - return HttpRequestHelper::sendRequest($fullUrl, $method, $user, $password, [], $body); + return HttpRequestHelper::sendRequest($fullUrl, $method, $user, $password, $headers, $body); } } diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml index 2eb7761e3af1..044cb86b96db 100644 --- a/tests/acceptance/config/behat.yml +++ b/tests/acceptance/config/behat.yml @@ -25,6 +25,7 @@ default: - '%paths.base%/../features/apiAuth' contexts: - FeatureContext: *common_feature_context_params + - CorsContext: apiCapabilities: paths: diff --git a/tests/acceptance/features/apiAuth/cors.feature b/tests/acceptance/features/apiAuth/cors.feature new file mode 100644 index 000000000000..f638827907cb --- /dev/null +++ b/tests/acceptance/features/apiAuth/cors.feature @@ -0,0 +1,123 @@ +@api @TestAlsoOnExternalUserBackend +Feature: CORS headers + Background: + Given user "user0" has been created with default attributes + And a new client token for "user0" has been generated + + Scenario Outline: CORS headers should be returned when setting CORS domain sending Origin header + Given using OCS API version "" + And user "user0" has added "https://aphno.badal" to the list of personal CORS domains + When user "user0" sends HTTP method "GET" to OCS API endpoint "" with headers + | header | value | + | Origin | https://aphno.badal | + Then the OCS status code should be "" + And the HTTP status code should be "" + Then the following headers should be set + | Access-Control-Allow-Headers | OC-Checksum,OC-Total-Length,OCS-APIREQUEST,X-OC-Mtime,Accept,Authorization,Brief,Content-Length,Content-Range,Content-Type,Date,Depth,Destination,Host,If,If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,Location,Lock-Token,Overwrite,Prefer,Range,Schedule-Reply,Timeout,User-Agent,X-Expected-Entity-Length,Accept-Language,Access-Control-Request-Method,Access-Control-Allow-Origin,ETag,OC-Autorename,OC-CalDav-Import,OC-Chunked,OC-Etag,OC-FileId,OC-LazyOps,OC-Total-File-Length,Origin,X-Request-ID,X-Requested-With | + | Access-Control-Expose-Headers | Content-Location,DAV,ETag,Link,Lock-Token,OC-ETag,OC-Checksum,OC-FileId,OC-JobStatus-Location,Vary,Webdav-Location,X-Sabre-Status | + | Access-Control-Allow-Origin | https://aphno.badal | + | Access-Control-Allow-Methods | GET,OPTIONS,POST,PUT,DELETE,MKCOL,PROPFIND,PATCH,PROPPATCH,REPORT | + Examples: + | ocs_api_version |endpoint | ocs-code | http-code | + | 1 |/apps/files_external/api/v1/mounts | 100 | 200 | + | 2 |/apps/files_external/api/v1/mounts | 200 | 200 | + | 1 |/apps/files_sharing/api/v1/remote_shares | 100 | 200 | + | 2 |/apps/files_sharing/api/v1/remote_shares | 200 | 200 | + | 1 |/apps/files_sharing/api/v1/remote_shares/pending | 100 | 200 | + | 2 |/apps/files_sharing/api/v1/remote_shares/pending | 200 | 200 | + | 1 |/apps/files_sharing/api/v1/shares | 100 | 200 | + | 2 |/apps/files_sharing/api/v1/shares | 200 | 200 | + | 1 |/privatedata/getattribute | 100 | 200 | + | 2 |/privatedata/getattribute | 200 | 200 | + + #merge into previous scenario when fixed + @issue-34664 + Scenario Outline: CORS headers should be returned when setting CORS domain sending Origin header + Given using OCS API version "" + And user "user0" has added "https://aphno.badal" to the list of personal CORS domains + When user "user0" sends HTTP method "GET" to OCS API endpoint "" with headers + | header | value | + | Origin | https://aphno.badal | + Then the OCS status code should be "" + And the HTTP status code should be "" + Then the following headers should not be set + | Access-Control-Allow-Headers | + | Access-Control-Expose-Headers | + | Access-Control-Allow-Origin | + | Access-Control-Allow-Methods | + Examples: + | ocs_api_version |endpoint | ocs-code | http-code | + | 1 |/config | 100 | 200 | + | 2 |/config | 200 | 200 | + + Scenario Outline: CORS headers should be returned when setting CORS domain sending Origin header (admin only endpoints) + Given using OCS API version "" + And the administrator has added "https://aphno.badal" to the list of personal CORS domains + When the administrator sends HTTP method "GET" to OCS API endpoint "" with headers + | header | value | + | Origin | https://aphno.badal | + Then the OCS status code should be "" + And the HTTP status code should be "" + Then the following headers should be set + | Access-Control-Allow-Headers | OC-Checksum,OC-Total-Length,OCS-APIREQUEST,X-OC-Mtime,Accept,Authorization,Brief,Content-Length,Content-Range,Content-Type,Date,Depth,Destination,Host,If,If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,Location,Lock-Token,Overwrite,Prefer,Range,Schedule-Reply,Timeout,User-Agent,X-Expected-Entity-Length,Accept-Language,Access-Control-Request-Method,Access-Control-Allow-Origin,ETag,OC-Autorename,OC-CalDav-Import,OC-Chunked,OC-Etag,OC-FileId,OC-LazyOps,OC-Total-File-Length,Origin,X-Request-ID,X-Requested-With | + | Access-Control-Expose-Headers | Content-Location,DAV,ETag,Link,Lock-Token,OC-ETag,OC-Checksum,OC-FileId,OC-JobStatus-Location,Vary,Webdav-Location,X-Sabre-Status | + | Access-Control-Allow-Origin | https://aphno.badal | + | Access-Control-Allow-Methods | GET,OPTIONS,POST,PUT,DELETE,MKCOL,PROPFIND,PATCH,PROPPATCH,REPORT | + Examples: + | ocs_api_version |endpoint | ocs-code | http-code | + | 1 |/cloud/apps | 100 | 200 | + | 2 |/cloud/apps | 200 | 200 | + | 1 |/cloud/groups | 100 | 200 | + | 2 |/cloud/groups | 200 | 200 | + | 1 |/cloud/users | 100 | 200 | + | 2 |/cloud/users | 200 | 200 | + + Scenario Outline: no CORS headers should be returned when CORS domain does not match Origin header + Given using OCS API version "" + And user "user0" has added "https://mero.badal" to the list of personal CORS domains + When user "user0" sends HTTP method "GET" to OCS API endpoint "" with headers + | header | value | + | Origin | https://aphno.badal | + Then the OCS status code should be "" + And the HTTP status code should be "" + Then the following headers should not be set + | Access-Control-Allow-Headers | + | Access-Control-Expose-Headers | + | Access-Control-Allow-Origin | + | Access-Control-Allow-Methods | + Examples: + | ocs_api_version |endpoint | ocs-code | http-code | + | 1 |/apps/files_external/api/v1/mounts | 100 | 200 | + | 2 |/apps/files_external/api/v1/mounts | 200 | 200 | + | 1 |/apps/files_sharing/api/v1/remote_shares | 100 | 200 | + | 2 |/apps/files_sharing/api/v1/remote_shares | 200 | 200 | + | 1 |/apps/files_sharing/api/v1/remote_shares/pending | 100 | 200 | + | 2 |/apps/files_sharing/api/v1/remote_shares/pending | 200 | 200 | + | 1 |/apps/files_sharing/api/v1/shares | 100 | 200 | + | 2 |/apps/files_sharing/api/v1/shares | 200 | 200 | + | 1 |/config | 100 | 200 | + | 2 |/config | 200 | 200 | + | 1 |/privatedata/getattribute | 100 | 200 | + | 2 |/privatedata/getattribute | 200 | 200 | + + Scenario Outline: no CORS headers should be returned when CORS domain does not match Origin header (admin only endpoints) + Given using OCS API version "" + And the administrator has added "https://mero.badal" to the list of personal CORS domains + When the administrator sends HTTP method "GET" to OCS API endpoint "" with headers + | header | value | + | Origin | https://aphno.badal | + Then the OCS status code should be "" + And the HTTP status code should be "" + Then the following headers should not be set + | Access-Control-Allow-Headers | + | Access-Control-Expose-Headers | + | Access-Control-Allow-Origin | + | Access-Control-Allow-Methods | + Examples: + | ocs_api_version |endpoint | ocs-code | http-code | + | 1 |/cloud/apps | 100 | 200 | + | 2 |/cloud/apps | 200 | 200 | + | 1 |/cloud/groups | 100 | 200 | + | 2 |/cloud/groups | 200 | 200 | + | 1 |/cloud/users | 100 | 200 | + | 2 |/cloud/users | 200 | 200 | diff --git a/tests/acceptance/features/bootstrap/BasicStructure.php b/tests/acceptance/features/bootstrap/BasicStructure.php index e671ab9d2e18..dfe4f3b6f367 100644 --- a/tests/acceptance/features/bootstrap/BasicStructure.php +++ b/tests/acceptance/features/bootstrap/BasicStructure.php @@ -801,6 +801,50 @@ public function userSendsToOcsApiEndpoint($user, $verb, $url, $password = null) ); } + /** + * @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers$/ + * + * @param string $user + * @param string $verb + * @param string $url + * @param TableNode $headersTable + * + * @return void + */ + public function userSendsToOcsApiEndpointWithHeaders( + $user, $verb, $url, TableNode $headersTable + ) { + $user = $this->getActualUsername($user); + $password = $this->getPasswordForUser($user); + + $headers = []; + foreach ($headersTable as $row) { + $headers[$row['header']] = $row ['value']; + } + + $this->response = OcsApiHelper::sendRequest( + $this->getBaseUrl(), $user, $password, $verb, + $url, [], $this->ocsApiVersion, $headers + ); + } + + /** + * @When /^the administrator sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers$/ + * + * @param string $verb + * @param string $url + * @param TableNode $headersTable + * + * @return void + */ + public function administratorSendsToOcsApiEndpointWithHeaders( + $verb, $url, TableNode $headersTable + ) { + $this->userSendsToOcsApiEndpointWithHeaders( + $this->getAdminUsername(), $verb, $url, $headersTable + ); + } + /** * @When the administrator sends HTTP method :verb to OCS API endpoint :url * @When the administrator sends HTTP method :verb to OCS API endpoint :url using password :password diff --git a/tests/acceptance/features/bootstrap/CorsContext.php b/tests/acceptance/features/bootstrap/CorsContext.php new file mode 100644 index 000000000000..c1843915526a --- /dev/null +++ b/tests/acceptance/features/bootstrap/CorsContext.php @@ -0,0 +1,165 @@ + + * @copyright Copyright (c) 2019, ownCloud GmbH + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Behat\Hook\Scope\AfterScenarioScope; + +require_once 'bootstrap.php'; +/** + * Steps that relate to CORS tests + */ +class CorsContext implements Context { + /** + * + * @var FeatureContext + */ + private $featureContext; + + private $originalAdminCorsDomains = null; + + /** + * @Given user :user has added :domain to the list of personal CORS domains + * + * @param string $user + * @param string $domain + * + * @return void + */ + public function addDomainToPrivateCORSLists($user, $domain) { + $this->featureContext->runOcc( + [ + 'user:setting', + $user, + 'core', + 'domains' + ] + ); + if ($this->featureContext->getExitStatusCodeOfOccCommand() === 0) { + $domainsJson = $this->featureContext->getStdOutOfOccCommand(); + $domains = \json_decode($domainsJson); + } else { + $domainsJson = ""; + $domains = []; + } + if ($user === $this->featureContext->getAdminUsername() + && $this->originalAdminCorsDomains === null + ) { + $this->originalAdminCorsDomains = $domainsJson; + } + + $domains[] = $domain; + $valueString = \json_encode($domains); + + $this->featureContext->runOcc( + [ + 'user:setting', + $user, + 'core', + 'domains', + '--value=\'' . $valueString . '\'' + ] + ); + if ($this->featureContext->getExitStatusCodeOfOccCommand() !== 0) { + throw new \Exception( + "could not set CORS domain. " . + $this->featureContext->getStdErrOfOccCommand() + ); + } + //double check if it was set + $this->featureContext->runOcc( + [ + 'user:setting', + $user, + 'core', + 'domains' + ] + ); + $domains = \json_decode($this->featureContext->getStdOutOfOccCommand()); + PHPUnit_Framework_Assert::assertContains( + $domain, $domains, "CORS domain was not added correctly" + ); + } + + /** + * @Given the administrator has added :domain to the list of personal CORS domains + * + * @param string $domain + * + * @return void + */ + public function adminAddDomainToPrivateCORSLists($domain) { + $this->addDomainToPrivateCORSLists( + $this->featureContext->getAdminUsername(), $domain + ); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope) { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } + + /** + * @AfterScenario + * + * @param AfterScenarioScope $scope + * + * @return void + */ + public function resetAdminCors(AfterScenarioScope $scope) { + if ($this->originalAdminCorsDomains !== null) { + if ($this->originalAdminCorsDomains !== "") { + $this->featureContext->runOcc( + [ + 'user:setting', + $this->featureContext->getAdminUsername(), + 'core', + 'domains', + '--value=\'' . $this->originalAdminCorsDomains . '\'' + ] + ); + } else { + $this->featureContext->runOcc( + [ + 'user:setting', + $this->featureContext->getAdminUsername(), + 'core', + 'domains', + '--delete' + ] + ); + } + } + } +}