From 9299c9fc792d5527d30aefe3638a6d69a4d58ee7 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 18 Jan 2017 21:13:23 +0100 Subject: [PATCH] LDAP OCS API (Part 1) LDAP OCS Api for create config Signed-off-by: Arthur Schiwon LDAP OCS Api for delete config Signed-off-by: Arthur Schiwon fix and extend behat tests Signed-off-by: Arthur Schiwon add LDAP OCS Api for modifying a configuration Signed-off-by: Arthur Schiwon test against OCS v2 instead Signed-off-by: Arthur Schiwon LDAP OCS Api for show config Signed-off-by: Arthur Schiwon also, let ldap integration tests run Signed-off-by: Arthur Schiwon fix indentation Signed-off-by: Morris Jobke enable user_ldap app for tests Signed-off-by: Arthur Schiwon LDAP PUT command now supports setting multiple keys at once Signed-off-by: Arthur Schiwon fix deletion for configIDs < s10 Also move ensureConfigIDExists checks into try, it might throw DB related exceptions Signed-off-by: Arthur Schiwon remove outdated scenario covered by "Delete a non-existing configuration" Signed-off-by: Arthur Schiwon --- .drone.yml | 11 + apps/user_ldap/appinfo/routes.php | 10 + .../lib/Command/CreateEmptyConfig.php | 28 +- apps/user_ldap/lib/Configuration.php | 9 +- .../lib/Controller/ConfigAPIController.php | 322 ++++++++++++++++++ apps/user_ldap/lib/Helper.php | 19 ++ build/integration/config/behat.yml | 10 + .../features/bootstrap/LDAPContext.php | 85 +++++ .../ldap_features/ldap-ocs.feature | 70 ++++ 9 files changed, 545 insertions(+), 19 deletions(-) create mode 100644 apps/user_ldap/lib/Controller/ConfigAPIController.php create mode 100644 build/integration/features/bootstrap/LDAPContext.php create mode 100644 build/integration/ldap_features/ldap-ocs.feature diff --git a/.drone.yml b/.drone.yml index 85b83eb20c121..e97590d4db6e6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -365,6 +365,16 @@ pipeline: when: matrix: TESTS: integration-filesdrop-features + integration-ldap-features: + image: nextcloudci/integration-php7.0:integration-php7.0-3 + commands: + - ./occ maintenance:install --admin-pass=admin + - ./occ app:enable user_ldap + - cd build/integration + - ./run.sh ldap_features/ldap-ocs.feature + when: + matrix: + TESTS: integration-ldap-features nodb-codecov: image: nextcloudci/php7.0:php7.0-7 commands: @@ -410,6 +420,7 @@ matrix: - TESTS: integration-sharees-features - TESTS: integration-setup-features - TESTS: integration-filesdrop-features + - TESTS: integration-ldap-features - TESTS: jsunit - TESTS: check-autoloader - TESTS: app-check-code diff --git a/apps/user_ldap/appinfo/routes.php b/apps/user_ldap/appinfo/routes.php index c01a3c114725b..45b43c214092e 100644 --- a/apps/user_ldap/appinfo/routes.php +++ b/apps/user_ldap/appinfo/routes.php @@ -36,3 +36,13 @@ ->actionInclude('user_ldap/ajax/testConfiguration.php'); $this->create('user_ldap_ajax_wizard', 'ajax/wizard.php') ->actionInclude('user_ldap/ajax/wizard.php'); + +$application = new \OCP\AppFramework\App('user_ldap'); +$application->registerRoutes($this, [ + 'ocs' => [ + ['name' => 'ConfigAPI#create', 'url' => '/api/v1/config', 'verb' => 'POST'], + ['name' => 'ConfigAPI#show', 'url' => '/api/v1/config/{configID}', 'verb' => 'GET'], + ['name' => 'ConfigAPI#modify', 'url' => '/api/v1/config/{configID}', 'verb' => 'PUT'], + ['name' => 'ConfigAPI#delete', 'url' => '/api/v1/config/{configID}', 'verb' => 'DELETE'], + ] +]); diff --git a/apps/user_ldap/lib/Command/CreateEmptyConfig.php b/apps/user_ldap/lib/Command/CreateEmptyConfig.php index 28d3a1d8bffab..38d3192058c5b 100644 --- a/apps/user_ldap/lib/Command/CreateEmptyConfig.php +++ b/apps/user_ldap/lib/Command/CreateEmptyConfig.php @@ -29,6 +29,7 @@ use OCA\User_LDAP\Helper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class CreateEmptyConfig extends Command { @@ -47,29 +48,24 @@ protected function configure() { $this ->setName('ldap:create-empty-config') ->setDescription('creates an empty LDAP configuration') + ->addOption( + 'only-print-prefix', + 'p', + InputOption::VALUE_NONE, + 'outputs only the prefix' + ) ; } protected function execute(InputInterface $input, OutputInterface $output) { - $configPrefix = $this->getNewConfigurationPrefix(); - $output->writeln("Created new configuration with configID '{$configPrefix}'"); - + $configPrefix = $this->helper->getNextServerConfigurationPrefix(); $configHolder = new Configuration($configPrefix); $configHolder->saveConfiguration(); - } - protected function getNewConfigurationPrefix() { - $serverConnections = $this->helper->getServerConfigurationPrefixes(); - - // first connection uses no prefix - if(sizeof($serverConnections) == 0) { - return ''; + $prose = ''; + if(!$input->getOption('only-print-prefix')) { + $prose = 'Created new configuration with configID '; } - - sort($serverConnections); - $lastKey = array_pop($serverConnections); - $lastNumber = intval(str_replace('s', '', $lastKey)); - $nextPrefix = 's' . str_pad($lastNumber + 1, 2, '0', STR_PAD_LEFT); - return $nextPrefix; + $output->writeln($prose . "{$configPrefix}"); } } diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php index eb4fcd3fbe604..65ee9c7080791 100644 --- a/apps/user_ldap/lib/Configuration.php +++ b/apps/user_ldap/lib/Configuration.php @@ -393,9 +393,12 @@ protected function setRawValue($varName, $value) { * @return bool */ protected function saveValue($varName, $value) { - return \OCP\Config::setAppValue('user_ldap', - $this->configPrefix.$varName, - $value); + \OC::$server->getConfig()->setAppValue( + 'user_ldap', + $this->configPrefix.$varName, + $value + ); + return true; } /** diff --git a/apps/user_ldap/lib/Controller/ConfigAPIController.php b/apps/user_ldap/lib/Controller/ConfigAPIController.php new file mode 100644 index 0000000000000..7d51b0aafe4be --- /dev/null +++ b/apps/user_ldap/lib/Controller/ConfigAPIController.php @@ -0,0 +1,322 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program 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 (at your option) 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 . + * + */ + +namespace OCA\User_LDAP\Controller; + +use OC\CapabilitiesManager; +use OC\Core\Controller\OCSController; +use OC\Security\Bruteforce\Throttler; +use OC\Security\IdentityProof\Manager; +use OCA\User_LDAP\Configuration; +use OCA\User_LDAP\Helper; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\ILogger; +use OCP\IRequest; +use OCP\IUserManager; +use OCP\IUserSession; + +class ConfigAPIController extends OCSController { + + /** @var Helper */ + private $ldapHelper; + + /** @var ILogger */ + private $logger; + + public function __construct( + $appName, + IRequest $request, + CapabilitiesManager $capabilitiesManager, + IUserSession $userSession, + IUserManager $userManager, + Throttler $throttler, + Manager $keyManager, + Helper $ldapHelper, + ILogger $logger + ) { + parent::__construct( + $appName, + $request, + $capabilitiesManager, + $userSession, + $userManager, + $throttler, + $keyManager + ); + + + $this->ldapHelper = $ldapHelper; + $this->logger = $logger; + } + + /** + * creates a new (empty) configuration and returns the resulting prefix + * + * Example: curl -X POST -H "OCS-APIREQUEST: true" -u $admin:$password \ + * https://nextcloud.server/ocs/v2.php/apps/user_ldap/api/v1/config + * + * results in: + * + * + * + * + * ok + * 200 + * OK + * + * + * s40 + * + * + * + * Failing example: if an exception is thrown (e.g. Database connection lost) + * the detailed error will be logged. The output will then look like: + * + * + * + * + * failure + * 999 + * An issue occurred when creating the new config. + * + * + * + * + * For JSON output provide the format=json parameter + * + * @return DataResponse + * @throws OCSException + */ + public function create() { + try { + $configPrefix = $this->ldapHelper->getNextServerConfigurationPrefix(); + $configHolder = new Configuration($configPrefix); + $configHolder->saveConfiguration(); + } catch (\Exception $e) { + $this->logger->logException($e); + throw new OCSException('An issue occurred when creating the new config.'); + } + return new DataResponse(['configID' => $configPrefix]); + } + + /** + * Deletes a LDAP configuration, if present. + * + * Example: + * curl -X DELETE -H "OCS-APIREQUEST: true" -u $admin:$password \ + * https://nextcloud.server/ocs/v2.php/apps/user_ldap/api/v1/config/s60 + * + * + * + * + * ok + * 200 + * OK + * + * + * + * + * @param string $configID + * @return DataResponse + * @throws OCSBadRequestException + * @throws OCSException + */ + public function delete($configID) { + try { + $this->ensureConfigIDExists($configID); + if(!$this->ldapHelper->deleteServerConfiguration($configID)) { + throw new OCSException('Could not delete configuration'); + } + } catch(OCSException $e) { + throw $e; + } catch(\Exception $e) { + $this->logger->logException($e); + throw new OCSException('An issue occurred when deleting the config.'); + } + + return new DataResponse(); + } + + /** + * modifies a configuration + * + * Example: + * curl -X PUT -d "configData[ldapHost]=ldaps://my.ldap.server&configData[ldapPort]=636" \ + * -H "OCS-APIREQUEST: true" -u $admin:$password \ + * https://nextcloud.server/ocs/v2.php/apps/user_ldap/api/v1/config/s60 + * + * + * + * + * ok + * 200 + * OK + * + * + * + * + * @param string $configID + * @param array $configData + * @return DataResponse + * @throws OCSException + */ + public function modify($configID, $configData) { + try { + $this->ensureConfigIDExists($configID); + + if(!is_array($configData)) { + throw new OCSBadRequestException('configData is not properly set'); + } + + $configuration = new Configuration($configID); + $configKeys = $configuration->getConfigTranslationArray(); + + foreach ($configKeys as $i => $key) { + if(isset($configData[$key])) { + $configuration->$key = $configData[$key]; + } + } + + $configuration->saveConfiguration(); + } catch(OCSException $e) { + throw $e; + } catch (\Exception $e) { + $this->logger->logException($e); + throw new OCSException('An issue occurred when modifying the config.'); + } + + return new DataResponse(); + } + + /** + * retrieves a configuration + * + * + * + * + * ok + * 200 + * OK + * + * + * ldaps://my.ldap.server + * 7770 + * + * + * ou=small,dc=my,dc=ldap,dc=server + * ou=users,ou=small,dc=my,dc=ldap,dc=server + * ou=small,dc=my,dc=ldap,dc=server + * cn=root,dc=my,dc=ldap,dc=server + * clearTextWithShowPassword=1 + * 1 + * 0 + * + * displayname + * uid + * inetOrgPerson + * + * (&(objectclass=nextcloudUser)(nextcloudEnabled=TRUE)) + * 1 + * (&(|(objectclass=nextcloudGroup))) + * 0 + * nextcloudGroup + * + * cn + * memberUid + * (&(|(objectclass=inetOrgPerson))(uid=%uid)) + * 0 + * 0 + * 1 + * + * + * + * mail + * 20 + * auto + * auto + * + * 1 + * uid;sn;givenname + * + * 0 + * + * + * + * 1 + * uid + * uid + * + * 0 + * 0 + * 500 + * 1 + * + * + * + * + * @param string $configID + * @param bool|string $showPassword + * @return DataResponse + * @throws OCSException + */ + public function show($configID, $showPassword = false) { + try { + $this->ensureConfigIDExists($configID); + + $config = new Configuration($configID); + $data = $config->getConfiguration(); + if(!boolval(intval($showPassword))) { + $data['ldapAgentPassword'] = '***'; + } + foreach ($data as $key => $value) { + if(is_array($value)) { + $value = implode(';', $value); + $data[$key] = $value; + } + } + } catch(OCSException $e) { + throw $e; + } catch (\Exception $e) { + $this->logger->logException($e); + throw new OCSException('An issue occurred when modifying the config.'); + } + + return new DataResponse($data); + } + + /** + * if the given config ID is not available, an exception is thrown + * + * @param string $configID + * @throws OCSNotFoundException + */ + private function ensureConfigIDExists($configID) { + $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(); + if(!in_array($configID, $prefixes, true)) { + throw new OCSNotFoundException('Config ID not found'); + } + } +} diff --git a/apps/user_ldap/lib/Helper.php b/apps/user_ldap/lib/Helper.php index b48b4001f9d28..f1186ffa31045 100644 --- a/apps/user_ldap/lib/Helper.php +++ b/apps/user_ldap/lib/Helper.php @@ -105,6 +105,25 @@ public function getServerConfigurationHosts() { return $result; } + /** + * return the next available configuration prefix + * + * @return string + */ + public function getNextServerConfigurationPrefix() { + $serverConnections = $this->getServerConfigurationPrefixes(); + + if(count($serverConnections) === 0) { + return 's01'; + } + + sort($serverConnections); + $lastKey = array_pop($serverConnections); + $lastNumber = intval(str_replace('s', '', $lastKey)); + $nextPrefix = 's' . str_pad($lastNumber + 1, 2, '0', STR_PAD_LEFT); + return $nextPrefix; + } + private function getServersConfig($value) { $regex = '/' . $value . '$/S'; diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index 9ef36f083e93b..3abd2a19fd8df 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -72,6 +72,16 @@ default: - admin - admin regular_user_password: 123456 + ldap: + paths: + - %paths.base%/../ldap_features + contexts: + - LDAPContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: what_for diff --git a/build/integration/features/bootstrap/LDAPContext.php b/build/integration/features/bootstrap/LDAPContext.php new file mode 100644 index 0000000000000..f23de6f47cde9 --- /dev/null +++ b/build/integration/features/bootstrap/LDAPContext.php @@ -0,0 +1,85 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program 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 (at your option) 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\Gherkin\Node\TableNode; + +class LDAPContext implements Context { + use BasicStructure; + + protected $configID; + + protected $apiUrl; + + /** + * @Given /^the response should contain a tag "([^"]*)"$/ + */ + public function theResponseShouldContainATag($arg1) { + $configID = $this->response->xml()->data[0]->$arg1; + PHPUnit_Framework_Assert::assertInstanceOf(SimpleXMLElement::class, $configID[0]); + } + + /** + * @Given /^creating an LDAP configuration at "([^"]*)"$/ + */ + public function creatingAnLDAPConfigurationAt($apiUrl) { + $this->apiUrl = $apiUrl; + $this->sendingToWith('POST', $this->apiUrl, null); + $configElements = $this->response->xml()->data[0]->configID; + $this->configID = $configElements[0]; + } + + /** + * @When /^deleting the LDAP configuration$/ + */ + public function deletingTheLDAPConfiguration() { + $this->sendingToWith('DELETE', $this->apiUrl . '/' . $this->configID, null); + } + + /** + * @Given /^the response should contain a tag "([^"]*)" with value "([^"]*)"$/ + */ + public function theResponseShouldContainATagWithValue($tagName, $expectedValue) { + $data = $this->response->xml()->data[0]->$tagName; + PHPUnit_Framework_Assert::assertEquals($expectedValue, $data[0]); + } + + /** + * @When /^getting the LDAP configuration with showPassword "([^"]*)"$/ + */ + public function gettingTheLDAPConfigurationWithShowPassword($showPassword) { + $this->sendingToWith( + 'GET', + $this->apiUrl . '/' . $this->configID . '?showPassword=' . $showPassword, + null + ); + } + + /** + * @Given /^setting the LDAP configuration to$/ + */ + public function settingTheLDAPConfigurationTo(TableNode $configData) { + $this->sendingToWith('PUT', $this->apiUrl . '/' . $this->configID, $configData); + } +} diff --git a/build/integration/ldap_features/ldap-ocs.feature b/build/integration/ldap_features/ldap-ocs.feature new file mode 100644 index 0000000000000..a9ad047870255 --- /dev/null +++ b/build/integration/ldap_features/ldap-ocs.feature @@ -0,0 +1,70 @@ +Feature: LDAP + Background: + Given using api version "2" + + Scenario: Creating an new, empty configuration + Given As an "admin" + When sending "POST" to "/apps/user_ldap/api/v1/config" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + And the response should contain a tag "configID" + + Scenario: Delete a non-existing configuration + Given As an "admin" + When sending "DELETE" to "/apps/user_ldap/api/v1/config/s666" + Then the OCS status code should be "404" + And the HTTP status code should be "404" + + Scenario: Create and delete a configuration + Given As an "admin" + And creating an LDAP configuration at "/apps/user_ldap/api/v1/config" + When deleting the LDAP configuration + Then the OCS status code should be "200" + And the HTTP status code should be "200" + + Scenario: Create and modify a configuration + Given As an "admin" + And creating an LDAP configuration at "/apps/user_ldap/api/v1/config" + When setting the LDAP configuration to + | configData[ldapHost] | ldaps://my.ldap.server | + Then the OCS status code should be "200" + And the HTTP status code should be "200" + + Scenario: Modifying a non-existing configuration + Given As an "admin" + When sending "PUT" to "/apps/user_ldap/api/v1/config/s666" with + | configData[ldapHost] | ldaps://my.ldap.server | + Then the OCS status code should be "404" + And the HTTP status code should be "404" + + Scenario: Modifying an existing configuration with malformed configData + Given As an "admin" + And creating an LDAP configuration at "/apps/user_ldap/api/v1/config" + When setting the LDAP configuration to + | configData | ldapHost=ldaps://my.ldap.server | + Then the OCS status code should be "400" + And the HTTP status code should be "400" + + Scenario: create, modify and get a configuration + Given As an "admin" + And creating an LDAP configuration at "/apps/user_ldap/api/v1/config" + And setting the LDAP configuration to + | configData[ldapHost] | ldaps://my.ldap.server | + | configData[ldapLoginFilter] | (&(\|(objectclass=inetOrgPerson))(uid=%uid)) | + | configData[ldapAgentPassword] | psst,secret | + When getting the LDAP configuration with showPassword "0" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + And the response should contain a tag "ldapHost" with value "ldaps://my.ldap.server" + And the response should contain a tag "ldapLoginFilter" with value "(&(|(objectclass=inetOrgPerson))(uid=%uid))" + And the response should contain a tag "ldapAgentPassword" with value "***" + + Scenario: receiving password in plain text + Given As an "admin" + And creating an LDAP configuration at "/apps/user_ldap/api/v1/config" + And setting the LDAP configuration to + | configData[ldapAgentPassword] | psst,secret | + When getting the LDAP configuration with showPassword "1" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + And the response should contain a tag "ldapAgentPassword" with value "psst,secret"