From 85c835bf8a3b53e3a55be7aea3ef0a2cb9273efe Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Sat, 3 May 2025 21:32:16 +0300 Subject: [PATCH] external identity sync... --- .../resources/locales/en_US/orcid_source.po | 7 +- .../Table/OrcidSourceCollectorsTable.php | 28 ++- .../src/Model/Table/OrcidSourcesTable.php | 179 ++++++++++++++---- 3 files changed, 174 insertions(+), 40 deletions(-) diff --git a/app/plugins/OrcidSource/resources/locales/en_US/orcid_source.po b/app/plugins/OrcidSource/resources/locales/en_US/orcid_source.po index 5d0340228..bc59d655e 100644 --- a/app/plugins/OrcidSource/resources/locales/en_US/orcid_source.po +++ b/app/plugins/OrcidSource/resources/locales/en_US/orcid_source.po @@ -41,10 +41,10 @@ msgid "enumeration.OrcidSourceApiEnum.AUT" msgstr "Authorize" msgid "enumeration.OrcidSourceApiEnum.MEM" -msgstr "Public" +msgstr "Members" msgid "enumeration.OrcidSourceApiEnum.PUB" -msgstr "Members" +msgstr "Public" msgid "error.search" msgstr "Search request returned {0}" @@ -87,3 +87,6 @@ msgstr "Obtained ORCID Identifier {0}" msgid "result.orcid.saved" msgstr "ORCID Token recorded" + +msgid "result.pipeline.status" +msgstr "Pipeline completed with status {0}" diff --git a/app/plugins/OrcidSource/src/Model/Table/OrcidSourceCollectorsTable.php b/app/plugins/OrcidSource/src/Model/Table/OrcidSourceCollectorsTable.php index 3919feb02..3ea6700c8 100644 --- a/app/plugins/OrcidSource/src/Model/Table/OrcidSourceCollectorsTable.php +++ b/app/plugins/OrcidSource/src/Model/Table/OrcidSourceCollectorsTable.php @@ -129,6 +129,10 @@ public function hydrate(int $id, \App\Model\Entity\Petition $petition) { $PetitionHistoryRecords = TableRegistry::getTableLocator()->get('PetitionHistoryRecords'); $PetitionOrcids = TableRegistry::getTableLocator()->get('OrcidSource.PetitionOrcids'); $OrcidSources = TableRegistry::getTableLocator()->get('OrcidSource.OrcidSources'); + $OrcidTokens = TableRegistry::getTableLocator()->get('OrcidSource.OrcidTokens'); + $ExtIdentitySources = TableRegistry::getTableLocator()->get('ExternalIdentitySources'); + + $orcid_source = $OrcidSources->find() ->where(['external_identity_source_id' => $orcidSourceCollectorsEntity->external_identity_source_id]) ->first(); @@ -150,9 +154,6 @@ public function hydrate(int $id, \App\Model\Entity\Petition $petition) { 'orcid_source_id' => $orcid_source->id ]; - - $OrcidTokens = TableRegistry::getTableLocator()->get('OrcidSource.OrcidTokens'); - $OrcidTokens->upsertOrFail( data: $data, whereClause: [ @@ -161,16 +162,33 @@ public function hydrate(int $id, \App\Model\Entity\Petition $petition) { ], ); - // Record Petition History + // Continue on to process the sync + // Trigger the ExternalIdentitySource sync and push the data to the pipeline + $status = $ExtIdentitySources->sync( + id: $orcidSourceCollectorsEntity->external_identity_source_id, + sourceKey: $token->orcid, + personId: $petition->enrollee_person_id, + syncOnly: true + ); + + // Record Petition History - Token Save $PetitionHistoryRecords->record( petitionId: $petition->id, enrollmentFlowStepId: $orcidSourceCollectorsEntity->enrollment_flow_step_id, action: PetitionActionEnum::AttributesUpdated, comment: __d('orcid_source', 'result.orcid.saved') ); + + // Record Petition History - Pipeline save + $PetitionHistoryRecords->record( + petitionId: $petition->id, + enrollmentFlowStepId: $orcidSourceCollectorsEntity->enrollment_flow_step_id, + action: PetitionActionEnum::Finalized, + comment: __d('orcid_source', 'result.pipeline.status', [$status]) + ); } - return 'provision'; + return true; } /** diff --git a/app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php b/app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php index cab870687..f72cacb91 100644 --- a/app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php +++ b/app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php @@ -31,9 +31,12 @@ use App\Model\Entity\ExternalIdentitySource; use Cake\ORM\Table; +use Cake\ORM\TableRegistry; use Cake\Routing\Router; use Cake\Validation\Validator; +use CoreServer\Model\Table\Oauth2ServersTable; use OrcidSource\Lib\Enum\{OrcidSourceApiEnum, OrcidSourceTierEnum}; +use \App\Lib\Enum\HttpStatusCodesEnum; use \OrcidSource\Model\Entity\OrcidSource; class OrcidSourcesTable extends Table { @@ -54,6 +57,12 @@ class OrcidSourcesTable extends Table { // Cache of the type map, for flat mode protected $typeCache = []; + private $orcidSource; + private $orcidToken; + private $httpClient; + private $orcidTokensTable; + private $oauth2ServersTable; + /** * Perform Cake Model initialization. * @@ -138,6 +147,9 @@ public function initialize(array $config): void { 'index' => ['platformAdmin', 'coAdmin'] ] ]); + + $this->orcidTokensTable = TableRegistry::getTableLocator()->get('OrcidSource.OrcidTokens'); + $this->oauth2ServersTable = TableRegistry::getTableLocator()->get('CoreServer.Oauth2Servers'); } @@ -209,7 +221,53 @@ protected function resultToEntityData( OrcidSource $OrcidSource, array $result ): array { - return []; + $typesTable = TableRegistry::getTableLocator()->get('Types'); + // Build the External Identity as an array + $eidata = []; + + // We don't currently have a field to record DoB, so we need to null it + $eidata['date_of_birth'] = null; + + // Single value fields that map to the External Identity Role + $role = [ + // We only support one role per record + 'role_key' => '1', + 'affiliation' => 'member' + ]; + + $eidata['external_identity_roles'][] = $role; + + $name = [ + 'type' => 'official', + 'given' => $result["name"]["given-names"]["value"], + 'family' => $result["name"]["family-names"]["value"] + ]; + + $eidata['names'][] = $name; + + foreach($result['emails']["email"] as $m) { + $eidata['email_addresses'][] = [ + 'mail' => $m['email'], + 'type' => 'official', + 'verified' => $m['verified'] + ]; + } + + if (!empty($result['addresses']['address'])) { + $address = []; + $address['type'] = 'office'; + foreach($result['addresses']["address"] as $ad) { + $address['country'][] = $ad['country']['value']; + } + $eidata['addresses'][] = $address; + } + + $eidata['identifiers'][] = [ + 'identifier' => $result['name']["path"], + 'type' => 'orcid' + ]; + + return $eidata; } /** @@ -227,18 +285,20 @@ public function retrieve( string $source_key ): array { try { - $this->orcidConnect($this->pluginCfg['server_id'], $id); + $this->httpClient = $this->orcidConnect($source, $source_key); - $orcidbio = $this->orcidRequest('/v3.0/' . $id . '/person'); + $orcidbio = $this->orcidRequest('/v3.0/' . $source_key . '/person'); +// $orcidActivities = $this->orcidRequest('/v3.0/' . $source_key . '/activities'); } catch(InvalidArgumentException $e) { - throw new InvalidArgumentException(_txt('er.id.unk-a', array($id))); + throw new \InvalidArgumentException(__d('error', 'unknown.identifier', [$source_key])); } - return array( - 'raw' => json_encode($orcidbio), - 'orgidentity' => $this->resultToOrgIdentity($id, $orcidbio) - ); + return [ + 'source_key' => $source_key, + 'source_record' => json_encode($orcidbio), + 'entity_data' => $this->resultToEntityData($source->orcid_source, $orcidbio) + ]; } /** @@ -265,7 +325,7 @@ public function search( // We just let search exceptions pop up the stack - $this->orcidConnect($this->pluginCfg['server_id']); + $this->orcidConnect($source); $records = $this->orcidRequest('/v3.0/search/', $searchAttrs); @@ -306,50 +366,50 @@ public function searchableAttributes(): array { ]; } + /** - * Make a request to the ORCID API. + * Make an HTTP request to the ORCID API * - * @since COmanage Registry v3.2.0 - * @param String $urlPath URL Path to request from API - * @param Array $data Array of query paramaters - * @param String $action HTTP action - * @return Array Decoded json message body - * @throws InvalidArgumentException - * @throws RuntimeException + * @param string $urlPath The API endpoint path to request + * @param array $data Request parameters or body data + * @param string $action HTTP method to use (get, post, etc) + * @return array Response data decoded from JSON + * @throws InvalidArgumentException If the ORCID identifier is invalid + * @throws RuntimeException If the API request fails + * @since COmanage Registry v5.2.0 */ - public function orcidRequest(string $urlPath, array $data=[], string $action="get"): array { - $OrcidToken = new OrcidToken(); - $options = [ - 'header' => [ + 'headers' => [ 'Accept' => 'application/json', - 'Authorization' => 'Bearer ' . ($OrcidToken->getUnencrypted($this->orcidToken['OrcidToken']['access_token']) - ?? $this->server['Oauth2Server']['access_token']), + 'Authorization' => 'Bearer ' . ($this->orcidTokensTable->getUnencrypted($this->orcidToken->access_token) + ?? $this->orcidSource->server->oauth2_server->access_token), 'Content-Type' => 'application/orcid+json' ] ]; - $orcidUrlBase = $this->orcidUrl($this->orcidSource['OrcidSource']['api_type'], - $this->orcidSource['OrcidSource']['api_tier']); - $results = $this->Http->$action($orcidUrlBase . $urlPath, - ($action == 'get' ? $data : json_encode($data)), - $options); + $orcidUrlBase = $this->orcidUrl($this->orcidSource->api_type, $this->orcidSource->api_tier); + $fullUrl = $orcidUrlBase . $urlPath; + $response = $this->httpClient->$action( + url: $fullUrl, + data: ($action == 'get' ? $data : json_encode($data)), + options: $options + ); - if($results->code == 404) { + if($response->getStatusCode() == HttpStatusCodesEnum::HTTP_BAD_REQUEST) { // Most likely retrieving an invalid ORCID - throw new InvalidArgumentException(_txt('er.orcidsource.search', [$results->code])); + throw new \InvalidArgumentException(__d('orcid_source', 'error.search', [$response->getStatusCode()])); } - if($results->code != 200) { + if($response->getStatusCode() != HttpStatusCodesEnum::HTTP_OK) { // This is probably an RDF blob, which is slightly annoying to parse. // Rather than do it properly since we don't parse RDF anywhere else, // we return a generic error. - throw new RuntimeException(_txt('er.orcidsource.search', [$results->code])); + throw new \RuntimeException(__d('orcid_source', 'error.search', [$response->getStatusCode()])); } - return json_decode($results->body); + return $response->getJson(); } /** @@ -381,6 +441,59 @@ public function orcidUrl(string $api=OrcidSourceApiEnum::PUBLIC, string $tier=Or return $orcidUrls[$api][$tier]; } + + /** + * Establish connection to ORCID API by configuring the HTTP client with appropriate credentials. + * + * @param int $exterrnalIdentitySourceId The external identity source ID to connect to + * @param string $orcidIdentifier The ORCID identifier to use for authentication + * @return \Cake\Http\Client Configured HTTP client for ORCID API requests + * @throws \InvalidArgumentException If OAuth2 server configuration or access token not found + * @since COmanage Registry v5.2.0 + */ + protected function orcidConnect( + \App\Model\Entity\ExternalIdentitySource $exterrnalIdentitySource, + string $orcidIdentifier + ): \Cake\Http\Client { + $this->orcidSource = $this->find() + ->contain([ + 'Servers.Oauth2Servers' => function ($q) { + return $q->where(["LOWER(Oauth2Servers.url) LIKE" => '%orcid%']); + }, + 'ExternalIdentitySources', + ]) + ->innerJoinWith('Servers.Oauth2Servers', function ($q) { + return $q->where([ + "LOWER(Oauth2Servers.url) LIKE" => '%orcid%' + ]); + }) + ->innerJoinWith('ExternalIdentitySources') + ->where([ + 'Servers.plugin' => 'CoreServer.Oauth2Servers', + 'ExternalIdentitySources.id' => $exterrnalIdentitySource->id, + 'ExternalIdentitySources.plugin' => 'OrcidSource.OrcidSources' + ]) + ->first(); + + if ( empty($this->orcidSource->id)) { + throw new \InvalidArgumentException(__d('error', 'notfound', [__d('core_server', 'controller.Oauth2Servers')])); + } + + $this->orcidToken = $this->orcidTokensTable + ->find() + ->where([ + 'orcid_source_id' => $this->orcidSource->id, + 'orcid_identifier' => $orcidIdentifier, + ]) + ->first(); + + if (empty($this->orcidToken->access_token)) { + throw new \InvalidArgumentException(__d('orcid_source', 'error.token.none')); + } + + return $this->oauth2ServersTable->createHttpClient($this->orcidSource->server->oauth2_server->id); + } + /** * Set validation rules. *