From 010de6f0c27d34d9b5e84355a0850900e4521189 Mon Sep 17 00:00:00 2001 From: Steve Bagwell Date: Fri, 14 Jan 2022 09:50:29 -0500 Subject: [PATCH 1/6] Add IDStore for SecureUser --- application/common/components/IdStoreBase.php | 3 + .../components/adapters/SecureUserIdStore.php | 167 ++++++++++++++++++ application/features/behat.yml | 7 + .../SecureUserIntegrationContext.php | 45 +++++ local.env.dist | 4 + 5 files changed, 226 insertions(+) create mode 100644 application/common/components/adapters/SecureUserIdStore.php create mode 100644 application/features/bootstrap/SecureUserIntegrationContext.php diff --git a/application/common/components/IdStoreBase.php b/application/common/components/IdStoreBase.php index 576673f..6c4ae44 100644 --- a/application/common/components/IdStoreBase.php +++ b/application/common/components/IdStoreBase.php @@ -4,6 +4,7 @@ use Sil\Idp\IdSync\common\components\adapters\GoogleSheetsIdStore; use Sil\Idp\IdSync\common\components\adapters\SagePeopleIdStore; use Sil\Idp\IdSync\common\components\adapters\WorkdayIdStore; +use Sil\Idp\IdSync\common\components\adapters\SecureUserIdStore; use Sil\Idp\IdSync\common\components\adapters\fakes\FakeIdStore; use Sil\Idp\IdSync\common\interfaces\IdStoreInterface; use Sil\Idp\IdSync\common\models\User; @@ -15,12 +16,14 @@ abstract class IdStoreBase extends Component implements IdStoreInterface const ADAPTER_GOOGLE_SHEETS = 'googlesheets'; const ADAPTER_WORKDAY = 'workday'; const ADAPTER_SAGE_PEOPLE = 'sagepeople'; + const ADAPTER_SECURE_USER = 'secureuser'; protected static $adapters = [ self::ADAPTER_FAKE => FakeIdStore::class, self::ADAPTER_GOOGLE_SHEETS => GoogleSheetsIdStore::class, self::ADAPTER_WORKDAY => WorkdayIdStore::class, self::ADAPTER_SAGE_PEOPLE => SagePeopleIdStore::class, + self::ADAPTER_SECURE_USER => SecureUserIdStore::class, ]; public static function getAdapterClassFor($adapterName) diff --git a/application/common/components/adapters/SecureUserIdStore.php b/application/common/components/adapters/SecureUserIdStore.php new file mode 100644 index 0000000..2378d6e --- /dev/null +++ b/application/common/components/adapters/SecureUserIdStore.php @@ -0,0 +1,167 @@ +$requiredProperty)) { + throw new InvalidArgumentException(sprintf( + 'No %s was provided.', + $requiredProperty + ), 1642083101); + } + } + + parent::init(); + } + + public static function getFieldNameMap(): array + { + return [ + // 'active' field isn't needed, since all Workday records returned are active. + 'employee_number' => User::EMPLOYEE_ID, + 'first_name' => User::FIRST_NAME, + 'last_name' => User::LAST_NAME, + 'display_name' => User::DISPLAY_NAME, + 'email' => User::EMAIL, + 'username' => User::USERNAME, + 'locked' => User::LOCKED, + 'manager_email' => User::MANAGER_EMAIL, + ]; + } + + /** + * Get the specified user's information. Note that inactive users will be + * treated as non-existent users. + * + * @param string $employeeId The Employee ID. + * @return User|null Information about the specified user, or null if no + * such active user was found. + * @throws Exception + */ + public function getActiveUser(string $employeeId) + { + $allUsers = $this->getAllActiveUsers(); + foreach ($allUsers as $user) { + if ((string)$user->getEmployeeId() === (string)$employeeId) { + return $user; + } + } + return null; + } + + /** + * Get a list of users' information (containing at least an Employee ID) for + * all users changed since the specified time. + * + * @param int $unixTimestamp The time (as a UNIX timestamp). + * @return User[] + * @throws Exception + */ + public function getUsersChangedSince(int $unixTimestamp) + { + throw new Exception(__FUNCTION__ . ' not yet implemented'); + } + + /** + * Get information about each of the (active) users. + * + * @return User[] A list of Users. + * @throws Exception + */ + public function getAllActiveUsers() + { + $client = $this->getHttpClient(); + + $api_sig = hash_hmac('sha256', time().$this->apiKey, $this->apiSecret); + + $response = $client->get($this->apiUrl, [ + 'connect_timeout' => $this->timeout, + 'headers' => [ + 'Accept' => 'application/json', + 'x-api-key' => $this->apiKey, + 'x-auth-hmac-sha256' => $api_sig, + ], + 'http_errors' => false, + ]); + + $statusCode = (int)$response->getStatusCode(); + if (($statusCode >= 200) && ($statusCode <= 299)) { + $allUsersInfo = Json::decode($response->getBody()); + } else { + throw new Exception(sprintf( + 'Unexpected response (%s %s): %s', + $response->getStatusCode(), + $response->getReasonPhrase(), + $response->getBody() + ), 1642083102); + } + + if (! is_array($allUsersInfo)) { + throw new Exception(sprintf( + 'Unexpected result when getting all active users: %s', + var_export($allUsersInfo, true) + ), 1642083103); + } + + $allActiveUsersInfo = array_filter( + $allUsersInfo, + function ($user) { + return ($user[User::ACTIVE] === true); + } + ); + + return array_map( + function ($entry) { + // Unset 'active', since ID Stores only return active users. + unset($entry[User::ACTIVE]); + + // Convert the resulting user info to a User. + return self::getAsUser($entry); + }, + $allActiveUsersInfo + ); + + } + + /** + * Get the HTTP client to use. + * + * @return Client + */ + protected function getHttpClient() + { + if ($this->httpClient === null) { + $this->httpClient = new Client(); + } + return $this->httpClient; + } + + public function getIdStoreName(): string + { + return 'SecureUser'; + } + +} diff --git a/application/features/behat.yml b/application/features/behat.yml index 49f42ce..93dfe98 100644 --- a/application/features/behat.yml +++ b/application/features/behat.yml @@ -32,6 +32,13 @@ default: tags: '~@usersChangedSince&&~@canUpdateLastSynced' paths: [ "%paths.base%/../features/id-store-integration.feature" ] contexts: [ Sil\Idp\IdSync\Behat\Context\SagePeopleIntegrationContext ] + secure_user_integration_features: + filters: + # removing whitespace to silence faulty deprecation warning + # see https://github.com/Behat/Gherkin/pull/215#issuecomment-944115733 + tags: '~@usersChangedSince&&~@canUpdateLastSynced' + paths: [ "%paths.base%/../features/id-store-integration.feature" ] + contexts: [ Sil\Idp\IdSync\Behat\Context\SecureUserIntegrationContext ] notification_features: paths: [ "%paths.base%/../features/notification.feature" ] contexts: [ Sil\Idp\IdSync\Behat\Context\NotificationContext ] diff --git a/application/features/bootstrap/SecureUserIntegrationContext.php b/application/features/bootstrap/SecureUserIntegrationContext.php new file mode 100644 index 0000000..832056d --- /dev/null +++ b/application/features/bootstrap/SecureUserIntegrationContext.php @@ -0,0 +1,45 @@ +idStore = new SecureUserIdStore([ + 'apiUrl' => $secureUserApiUrl, + 'apiKey' => $secureUserApiKey, + 'apiSecret' => $secureUserApiSecret, + ]); + } + + /** + * @When I ask the ID Store for a specific active user + */ + public function iAskTheIdStoreForASpecificActiveUser() + { + $this->activeEmployeeId = Env::requireEnv('TEST_SECURE_USER_EMPLOYEE_ID'); + $this->result = $this->idStore->getActiveUser($this->activeEmployeeId); + } +} diff --git a/local.env.dist b/local.env.dist index f37146e..2f39824 100644 --- a/local.env.dist +++ b/local.env.dist @@ -110,6 +110,10 @@ ID_SYNC_ACCESS_TOKENS= #TEST_SAGE_PEOPLE_CONFIG_password=test_password #TEST_SAGE_PEOPLE_CONFIG_queryConditions=ID != null #TEST_SAGE_PEOPLE_EMPLOYEE_ID=12345 +#TEST_SECURE_USER_CONFIG_apiUrl=https://example.com +#TEST_SECURE_USER_CONFIG_apiKey=abc123 +#TEST_SECURE_USER_CONFIG_apiSecret=abc123 +#TEST_SECURE_USER_EMPLOYEE_ID=123456 # (optional) IP Address of development machine. Used for Xdebug connection. From 0f13120acb60b3daf6d810524b584679eebf6cec Mon Sep 17 00:00:00 2001 From: Steve Bagwell Date: Fri, 14 Jan 2022 09:50:38 -0500 Subject: [PATCH 2/6] Ran composer update --- application/composer.lock | 58 ++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/application/composer.lock b/application/composer.lock index 6f4e29e..e95aac1 100644 --- a/application/composer.lock +++ b/application/composer.lock @@ -4,14 +4,14 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2f314e9b12d2d829b3bbe67037031cf8", + "content-hash": "41c19dbefe62cba2d7fa32d486fa2152", "packages": [ { "name": "bower-asset/inputmask", "version": "3.3.11", "source": { "type": "git", - "url": "https://github.com/RobinHerbots/Inputmask.git", + "url": "git@github.com:RobinHerbots/Inputmask.git", "reference": "5e670ad62f50c738388d4dcec78d2888505ad77b" }, "dist": { @@ -136,32 +136,28 @@ }, { "name": "doctrine/lexer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "reference": "9c50f840f257bbb941e6f4a0e94ccf5db5c3f76c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/9c50f840f257bbb941e6f4a0e94ccf5db5c3f76c", + "reference": "9c50f840f257bbb941e6f4a0e94ccf5db5c3f76c", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" @@ -196,7 +192,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" + "source": "https://github.com/doctrine/lexer/tree/1.2.2" }, "funding": [ { @@ -212,7 +208,7 @@ "type": "tidelift" } ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2022-01-12T08:27:12+00:00" }, { "name": "egulias/email-validator", @@ -1027,16 +1023,16 @@ }, { "name": "mlocati/ip-lib", - "version": "1.17.1", + "version": "1.18.0", "source": { "type": "git", "url": "https://github.com/mlocati/ip-lib.git", - "reference": "28763c87f9a3e24ff4df9258ec4e8375d8fa6523" + "reference": "c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/28763c87f9a3e24ff4df9258ec4e8375d8fa6523", - "reference": "28763c87f9a3e24ff4df9258ec4e8375d8fa6523", + "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2", + "reference": "c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2", "shasum": "" }, "require": { @@ -1082,7 +1078,7 @@ ], "support": { "issues": "https://github.com/mlocati/ip-lib/issues", - "source": "https://github.com/mlocati/ip-lib/tree/1.17.1" + "source": "https://github.com/mlocati/ip-lib/tree/1.18.0" }, "funding": [ { @@ -1094,7 +1090,7 @@ "type": "other" } ], - "time": "2021-11-10T15:24:32+00:00" + "time": "2022-01-13T18:05:33+00:00" }, { "name": "monolog/monolog", @@ -1666,12 +1662,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "11696ae4d8970456cbd1919c17e6bb2ddb63553a" + "reference": "a1ce0a793ff447c9085f350d2fa8744efbdb5c1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/11696ae4d8970456cbd1919c17e6bb2ddb63553a", - "reference": "11696ae4d8970456cbd1919c17e6bb2ddb63553a", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/a1ce0a793ff447c9085f350d2fa8744efbdb5c1e", + "reference": "a1ce0a793ff447c9085f350d2fa8744efbdb5c1e", "shasum": "" }, "conflict": { @@ -1734,7 +1730,7 @@ "doctrine/mongodb-odm": ">=1,<1.0.2", "doctrine/mongodb-odm-bundle": ">=2,<3.0.1", "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", - "dolibarr/dolibarr": "<14|>= 3.3.beta1, < 13.0.2", + "dolibarr/dolibarr": "<=14.0.4|>= 3.3.beta1, < 13.0.2", "dompdf/dompdf": ">=0.6,<0.6.2", "drupal/core": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4", "drupal/drupal": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4", @@ -1868,7 +1864,7 @@ "openmage/magento-lts": "<19.4.15|>=20,<20.0.13", "orchid/platform": ">=9,<9.4.4", "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7", - "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.21|>=4.1,<4.1.14|>=4.2,<4.2.8", + "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<4.2.8", "padraic/humbug_get_contents": "<1.1.2", "pagarme/pagarme-php": ">=0,<3", "pagekit/pagekit": "<=1.0.18", @@ -1939,7 +1935,7 @@ "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", "simplito/elliptic-php": "<1.0.6", "slim/slim": "<2.6", - "smarty/smarty": "<3.1.39", + "smarty/smarty": "<3.1.43|>=4,<4.0.3", "snipe/snipe-it": "<5.3.5", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", @@ -2099,7 +2095,7 @@ "type": "tidelift" } ], - "time": "2022-01-08T00:53:14+00:00" + "time": "2022-01-12T23:15:39+00:00" }, { "name": "silinternational/email-service-php-client", @@ -7420,7 +7416,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.0", + "php": ">=7.1", "ext-json": "*" }, "platform-dev": [], From e388ba9e853b01490ea944131de1814b521d391c Mon Sep 17 00:00:00 2001 From: Steve Bagwell Date: Fri, 14 Jan 2022 10:00:32 -0500 Subject: [PATCH 3/6] psr2 formatting --- application/common/components/adapters/SecureUserIdStore.php | 2 -- .../features/bootstrap/SecureUserIntegrationContext.php | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/application/common/components/adapters/SecureUserIdStore.php b/application/common/components/adapters/SecureUserIdStore.php index 2378d6e..df3d2e7 100644 --- a/application/common/components/adapters/SecureUserIdStore.php +++ b/application/common/components/adapters/SecureUserIdStore.php @@ -143,7 +143,6 @@ function ($entry) { }, $allActiveUsersInfo ); - } /** @@ -163,5 +162,4 @@ public function getIdStoreName(): string { return 'SecureUser'; } - } diff --git a/application/features/bootstrap/SecureUserIntegrationContext.php b/application/features/bootstrap/SecureUserIntegrationContext.php index 832056d..a63d39f 100644 --- a/application/features/bootstrap/SecureUserIntegrationContext.php +++ b/application/features/bootstrap/SecureUserIntegrationContext.php @@ -11,7 +11,6 @@ */ class SecureUserIntegrationContext extends IdStoreIntegrationContextBase { - public function __construct() { echo 'Testing integration with Secure User.' . PHP_EOL; @@ -23,7 +22,7 @@ public function __construct() */ public function iCanMakeAuthenticatedCallsToTheIdStore() { - $secureUserApiUrl = Env::requireEnv( 'TEST_SECURE_USER_CONFIG_apiUrl'); + $secureUserApiUrl = Env::requireEnv('TEST_SECURE_USER_CONFIG_apiUrl'); $secureUserApiKey = Env::requireEnv('TEST_SECURE_USER_CONFIG_apiKey'); $secureUserApiSecret = Env::requireEnv('TEST_SECURE_USER_CONFIG_apiSecret'); From 52a9d5a500664d4a3d9cbad66deb19f37890bc16 Mon Sep 17 00:00:00 2001 From: Steve Bagwell Date: Fri, 14 Jan 2022 10:34:02 -0500 Subject: [PATCH 4/6] remove unneeded comment --skip-ci --- application/common/components/adapters/SecureUserIdStore.php | 1 - 1 file changed, 1 deletion(-) diff --git a/application/common/components/adapters/SecureUserIdStore.php b/application/common/components/adapters/SecureUserIdStore.php index df3d2e7..a77d4b3 100644 --- a/application/common/components/adapters/SecureUserIdStore.php +++ b/application/common/components/adapters/SecureUserIdStore.php @@ -40,7 +40,6 @@ public function init() public static function getFieldNameMap(): array { return [ - // 'active' field isn't needed, since all Workday records returned are active. 'employee_number' => User::EMPLOYEE_ID, 'first_name' => User::FIRST_NAME, 'last_name' => User::LAST_NAME, From d211ef72b9718b79e52c124c2fe3117a531cb9a3 Mon Sep 17 00:00:00 2001 From: Steve Bagwell Date: Fri, 14 Jan 2022 10:54:24 -0500 Subject: [PATCH 5/6] More example env variables for secureuser --- local.env.dist | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/local.env.dist b/local.env.dist index 2f39824..0fd4aad 100644 --- a/local.env.dist +++ b/local.env.dist @@ -53,6 +53,11 @@ ID_STORE_ADAPTER= #ID_STORE_CONFIG_password=your_password #ID_STORE_CONFIG_queryConditions=ID != null +#### Required values for SecureUser ID Store (when ID_STORE_ADAPTER=secureuser): +#ID_STORE_CONFIG_apiUrl= +#ID_STORE_CONFIG_apiKey= +#ID_STORE_CONFIG_apiSecret= + # Comma-delimited list of authorization tokens ID Sync will accept. # Example: abc123,def456 # NOTE: To run tests, make sure "abc123" is in that list. From 1fa099bd4c582f5e2a31722a0a1a695fcc5af490 Mon Sep 17 00:00:00 2001 From: Steve Bagwell Date: Fri, 14 Jan 2022 13:05:57 -0500 Subject: [PATCH 6/6] PR suggestion --- application/common/components/adapters/SecureUserIdStore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/common/components/adapters/SecureUserIdStore.php b/application/common/components/adapters/SecureUserIdStore.php index a77d4b3..529e977 100644 --- a/application/common/components/adapters/SecureUserIdStore.php +++ b/application/common/components/adapters/SecureUserIdStore.php @@ -94,7 +94,7 @@ public function getAllActiveUsers() { $client = $this->getHttpClient(); - $api_sig = hash_hmac('sha256', time().$this->apiKey, $this->apiSecret); + $api_sig = hash_hmac('sha256', time() . $this->apiKey, $this->apiSecret); $response = $client->get($this->apiUrl, [ 'connect_timeout' => $this->timeout,