Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SecureUser ID Store #126

Merged
merged 6 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions application/common/components/IdStoreBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down
164 changes: 164 additions & 0 deletions application/common/components/adapters/SecureUserIdStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php
namespace Sil\Idp\IdSync\common\components\adapters;

use Exception;
use GuzzleHttp\Client;
use InvalidArgumentException;
use Sil\Idp\IdSync\common\components\IdStoreBase;
use Sil\Idp\IdSync\common\models\User;
use yii\helpers\Json;

class SecureUserIdStore extends IdStoreBase
{
public $apiUrl = null;
public $apiKey = null;
public $apiSecret = null;

public $timeout = 45; // Timeout in seconds (per call to ID Store API).

protected $httpClient = null;

public function init()
{
$requiredProperties = [
'apiUrl',
'apiKey',
'apiSecret',
];
foreach ($requiredProperties as $requiredProperty) {
if (empty($this->$requiredProperty)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the PHP Magic which will use the new ID_STORE_CONFIG variables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

application/common/config/main.php line 20

$idStoreOptionalConfig = Env::getArrayFromPrefix('ID_STORE_CONFIG_');

and then lines 73-77

       'idStore' => ArrayHelper::merge([
            'class' => IdStoreBase::getAdapterClassFor(
                Env::get('ID_STORE_ADAPTER')
            ),
        ], $idStoreOptionalConfig),

throw new InvalidArgumentException(sprintf(
'No %s was provided.',
$requiredProperty
), 1642083101);
}
}

parent::init();
}

public static function getFieldNameMap(): array
{
return [
'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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this complete? I thought Schram added 2 manager fields. Just checking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Schram's addition was just to the workday store for adding the HR fields.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Not all fields must be listed here.

];
}

/**
* 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');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be pushing code with "not yet implemented" things?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok because configuration dictates which features are needed for a given ID Store. This method is only used for incremental sync. Presumably, secureuser is fine with running full sync each time.

}

/**
* 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';
}
}
58 changes: 27 additions & 31 deletions application/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions application/features/behat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Loading