Skip to content

Commit

Permalink
external identity sync...
Browse files Browse the repository at this point in the history
  • Loading branch information
Ioannis committed May 3, 2025
1 parent 57183cf commit 85c835b
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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}"
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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: [
Expand All @@ -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;
}

/**
Expand Down
179 changes: 146 additions & 33 deletions app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
*
Expand Down Expand Up @@ -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');
}


Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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)
];
}

/**
Expand All @@ -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);

Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down

0 comments on commit 85c835b

Please sign in to comment.