From 6259cc5b0dbecfde9f13304d341b5b6b7ed91359 Mon Sep 17 00:00:00 2001 From: Sae126V Date: Fri, 4 Aug 2023 10:12:01 +0000 Subject: [PATCH] [GT-184] Add support for renewing API credentials --- .../controllers/site/edit_api_auth.php | 114 ++++++--- .../web_portal/views/site/edit_api_auth.php | 74 +++++- .../web_portal/views/site/edited_api_auth.php | 12 +- htdocs/web_portal/views/site/view_site.php | 28 +++ .../APIAuthenticationService.php | 180 +++++++++++--- lib/Gocdb_Services/Site.php | 78 +++++-- .../ManageAPICredentialsActions.php | 221 ++++++++++++++---- .../ManageUnusedAPICredentials.php | 17 +- .../ManageUnusedAPICredentialsOptions.php | 99 +++++++- .../ManageUnrenewedAPICredentialsTest.php | 2 +- .../ManageUnusedAPICredentialsTest.php | 2 +- 11 files changed, 690 insertions(+), 137 deletions(-) diff --git a/htdocs/web_portal/controllers/site/edit_api_auth.php b/htdocs/web_portal/controllers/site/edit_api_auth.php index e84c1b451..40d32b69f 100644 --- a/htdocs/web_portal/controllers/site/edit_api_auth.php +++ b/htdocs/web_portal/controllers/site/edit_api_auth.php @@ -21,24 +21,47 @@ * See the License for the specific language governing permissions and * limitations under the License. /*======================================================*/ -require_once __DIR__.'/../../../web_portal/components/Get_User_Principle.php'; -require_once __DIR__.'/../utils.php'; -require_once __DIR__.'/../../../../lib/Gocdb_Services/Factory.php'; +require_once __DIR__ . '/../../../web_portal/components/Get_User_Principle.php'; +require_once __DIR__ . '/../utils.php'; +require_once __DIR__ . '/../../../../lib/Gocdb_Services/Factory.php'; + +use Exception; /** - * Controller to edit authentication entity request + * Controller to either edit authentication entity request or renewal request. + * * @global array $_POST only set if the browser has POSTed data * @return null */ -function edit_entity() { - $dn = Get_User_Principle(); - $user = \Factory::getUserService()->getUserByPrinciple($dn); +function edit_entity() +{ + list($user, $authEnt, $site, $serv) = initialize(); + + if ($_POST) { + submit($user, $authEnt, $site, $serv); + } else { + draw($user, $authEnt, $site); + } +} - //Check the portal is not in read only mode, returns exception if it is and user is not an admin +function initialize() +{ + $identifier = Get_User_Principle(); + $user = \Factory::getUserService()->getUserByPrinciple($identifier); + + /** + * Check the portal is not in read only mode, + * returns exception if it is and user is not an admin. + */ checkPortalIsNotReadOnlyOrUserIsAdmin($user); - if (!isset($_REQUEST['authentityid']) || !is_numeric($_REQUEST['authentityid']) ){ - throw new Exception("A authentication entity id must be specified in the url"); + if ( + !isset($_REQUEST['authentityid']) || + !is_numeric($_REQUEST['authentityid']) + ) { + throw new Exception( + "An authentication entity ID must be specified in the URL." + ); } $serv = \Factory::getSiteService(); @@ -47,48 +70,85 @@ function edit_entity() { // Validate the user has permission to edit properties if (!$serv->userCanEditSite($user, $site)) { - throw new \Exception("Permission denied: a site role is required to edit authentication entities at " . $site->getShortName()); + throw new \Exception( + "Permission denied: A site role is required " . + "to edit authentication entities at " . + $site->getShortName() + ); } - if($_POST) { // If we receive a POST request it's to edit an authentication entity - submit($user, $authEnt, $site, $serv); - } else { // If there is no post data, draw the edit authentication entity form - draw($user, $authEnt, $site); - } + return [$user, $authEnt, $site, $serv]; } -function draw(\User $user = null, \APIAuthentication $authEnt = null, \Site $site = null) { - if(is_null($user)){ - throw new Exception("Unregistered users can't edit authentication credentials"); +/** + * Helper to draw either the edit or renewal authentication entity form. + * + * @param \User|null $user + * @param \APIAuthentication|null $authEntity + * @param \Site|null $site + * @throws \Exception + */ +function draw( + \User $user = null, + \APIAuthentication $authEnt = null, + \Site $site = null +) { + if (is_null($user)) { + throw new Exception( + "Unregistered users can't edit authentication credentials." + ); } $params = array(); $params['site'] = $site; $params['authEnt'] = $authEnt; $params['authTypes'] = array(); - $params['authTypes'][]='X.509'; - $params['authTypes'][]='OIDC Subject'; + $params['authTypes'][] = 'X.509'; + $params['authTypes'][] = 'OIDC Subject'; $params['user'] = $user; + if ($_REQUEST['isRenewalRequest']) { + $params['isRenewalRequest'] = true; + } + show_view("site/edit_api_auth.php", $params); die(); } -function submit(\User $user, \APIAuthentication $authEnt, \Site $site, org\gocdb\services\Site $serv) { - $newValues = getAPIAuthenticationFromWeb(); +/** + * If this receives a POST request, + * it can be either to edit an API authentication entity or + * to update the `$lastRenewTime` in `APIAuthentication`. + * + * @param \User $user + * @param \APIAuthentication $authEntity + * @param \Site $site + * @param org\gocdb\services\Site $service + */ +function submit( + \User $user, + \APIAuthentication $authEnt, + \Site $site, + org\gocdb\services\Site $serv +) { + $params = array(); + + if ($_REQUEST['isRenewalRequest']) { + $newValues['isRenewalRequest'] = $params['isRenewalRequest'] = true; + } else { + $newValues = getAPIAuthenticationFromWeb(); + } try { $authEnt = $serv->editAPIAuthEntity($authEnt, $user, $newValues); - } catch(Exception $e) { + } catch (Exception $e) { show_view('error.php', $e->getMessage()); die(); } - $params = array(); $params['apiAuthenticationEntity'] = $authEnt; $params['site'] = $site; + show_view("site/edited_api_auth.php", $params); die(); - - } diff --git a/htdocs/web_portal/views/site/edit_api_auth.php b/htdocs/web_portal/views/site/edit_api_auth.php index 9663b898b..23849b845 100644 --- a/htdocs/web_portal/views/site/edit_api_auth.php +++ b/htdocs/web_portal/views/site/edit_api_auth.php @@ -4,12 +4,16 @@ $user = $params['user']; $entUser = $params['authEnt']->getUser(); - echo('

Edit API credential for '); + echo('

'); + if ($params['isRenewalRequest']) { + echo('Renew API credential for '); + } else { + echo('Edit API credential for '); + } xecho($params['site']->getName()); echo('

'); if (!is_null($entUser)) { - echo('

This credential is linked to GOCDB user '); echo(''); - echo("WARNING: editing will change the linked user from '"); + if ($params['isRenewalRequest']) { + echo( + "WARNING: Renewing this will change the linked user from '" + ); + } else { + echo("WARNING: Editing will change the linked user from '"); + } xecho($entUser->getFullname()); echo("' to '"); xecho($user->getFullname()); - echo("'. Click the browser Back button to cancel the edit."); + echo("'. Click the browser Back button to cancel the"); + if ($params['isRenewalRequest']) { + echo(' renewal.'); + } else { + echo(' edit.'); } - - } else { + } + } else { // This clause should be deleted or replaced with exception after all // authentication entities are assigned a user. echo('
'); @@ -41,11 +55,22 @@
Identifier (e.g. Certificate DN or OIDC Subject)* - + + >
+
Credential type* - + >
+
- WARNING: it is possible to delete information using the write functionality of the API. Leave Allow API write unchecked if - you do not need to write data. +

+ WARNING: It is possible to delete information using + the write functionality of the API. Leave Allow API write + unchecked if you do not need to write data. +

+
Allow API write
- + + +

Are you sure you want to continue?

+ +
+ + + + +
diff --git a/htdocs/web_portal/views/site/edited_api_auth.php b/htdocs/web_portal/views/site/edited_api_auth.php index 7e84ef18d..ca6c8cdc3 100644 --- a/htdocs/web_portal/views/site/edited_api_auth.php +++ b/htdocs/web_portal/views/site/edited_api_auth.php @@ -1,6 +1,16 @@

Success


- The API authenication credential has now been updated. Type:getType()) ?>, identifier: getIdentifier()) ?>. + The API authenication credential has now been + getType()); + echo ','; + } + ?> + identifier: + getIdentifier()) ?>.
View site diff --git a/htdocs/web_portal/views/site/view_site.php b/htdocs/web_portal/views/site/view_site.php index 6c0a5b24c..cf5d9649d 100644 --- a/htdocs/web_portal/views/site/view_site.php +++ b/htdocs/web_portal/views/site/view_site.php @@ -607,8 +607,12 @@ class="header" Type Identifier User + +

Last Renewed

+ Last Used Write + Renew Edit Delete @@ -649,6 +653,17 @@ class="header" } ?> + + getLastRenewTime(); + $titleStr = 'Last renewed ' . + $useTime->format('d-m-Y H:iTP'); + + echo '

'; + echo $useTime->format('d-m-y'); + echo '
'; + ?> + getLastUseTime(); @@ -672,6 +687,19 @@ class="header" } ?> /> + + +
+ +
+ +
validate($newValues, $identifier, $type); + public function editAPIAuthentication( + \APIAuthentication $authEntity, + \User $user, + $newValues + ) { + $isRenewalRequest = $newValues['isRenewalRequest'] || false; - //Edit the property - $this->em->getConnection()->beginTransaction(); try { - // This would probably be the place hook for any future policy acceptance tracking - if ($user->getId() != $authEntity->getUser()) { - $authEntity->setLastRenewTime(); - } - $authEntity->setIdentifier($identifier); - $authEntity->setType($type); - $authEntity->setAllowAPIWrite($allowWrite); - $user->addAPIAuthenticationEntitiesDoJoin($authEntity); + $this->em->getConnection()->beginTransaction(); - $this->em->persist($authEntity); - $this->em->persist($user); + if ($isRenewalRequest) { + $this->handleRenewalRequest( + $authEntity, + $user, + $isRenewalRequest + ); + } else { + $this->handleEditRequest( + $authEntity, + $user, + $newValues, + $isRenewalRequest + ); + } - $this->em->flush(); $this->em->getConnection()->commit(); } catch (\Exception $e) { $this->em->getConnection()->rollback(); @@ -194,6 +189,7 @@ public function editAPIAuthentication(\APIAuthentication $authEntity, \User $use throw $e; } } + /** * Set the last use time field to the current UTC time * @@ -271,12 +267,132 @@ private function validate($data, $identifier, $type) throw new \Exception("Invalid X.509 DN"); } - //If the entity is of type OIDC subject, do a more thorough check again + /** + * If the entity is of type OIDC subject, + * do a more thorough check again. + */ if ( $type == 'OIDC Subject' && - !preg_match("/^([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12})$/", $identifier) + !preg_match( + "/^([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12})$/", + $identifier + ) ) { throw new \Exception("Invalid OIDC Subject"); } } + + /** + * Helper to handle the renewal request for API authentication code flow. + * + * @param \APIAuthentication $authEntity Entity to update. + * @param \User $user Owning user. + * @param bool $isRenewalRequest A boolean indicating + * if it's a renewal request. + */ + private function handleRenewalRequest( + \APIAuthentication $authEntity, + \User $user, + $isRenewalRequest + ) { + $this->updateLastRenewTime($authEntity, $user, $isRenewalRequest); + + $user->addAPIAuthenticationEntitiesDoJoin($authEntity); + + $this->em->persist($authEntity); + $this->em->persist($user); + $this->em->flush(); + } + + /** + * Helper to handles the edit request for API authentication code flow. + * + * @param \APIAuthentication $authEntity Entity to update. + * @param \User $user Owning user. + * @param array $newValues An array containing data for + * updating the APIAuthentication entity. + * @param bool $isRenewalRequest A boolean indicating + * if it's a renewal request. + * + * @throws \Exception Throws an exception if the identifier is empty. + */ + private function handleEditRequest( + \APIAuthentication $authEntity, + \User $user, + $newValues, + $isRenewalRequest + ) { + $identifier = $newValues['IDENTIFIER']; + $type = $newValues['TYPE']; + $allowWrite = $newValues['ALLOW_WRITE']; + + // Check that an identifier has been provided + if (empty($identifier)) { + throw new \Exception( + "A value must be provided for the identifier" + ); + } + + $this->validate($newValues, $identifier, $type); + + $this->updateLastRenewTime($authEntity, $user, $isRenewalRequest); + $this->updateAuthenticationEntity( + $authEntity, + $identifier, + $type, + $allowWrite + ); + $user->addAPIAuthenticationEntitiesDoJoin($authEntity); + + $this->em->persist($authEntity); + $this->em->persist($user); + $this->em->flush(); + } + + /** + * Validates whether to update the `LastRenewTime` + * of the APIAuthentication entity or NOT. + * + * @param \APIAuthentication $authEntity Entity to update. + * @param \User $user Owning user. + * @param bool $isRenewalRequest A boolean indicating + * if it's a renewal request. + */ + private function updateLastRenewTime( + \APIAuthentication $authEntity, + \User $user, + $isRenewalRequest + ) { + /** + * This would probably be the place hook for any + * future policy acceptance tracking. + */ + if ( + ($user->getId() != $authEntity->getUser()) || + $isRenewalRequest + ) { + $authEntity->setLastRenewTime(); + } + } + + /** + * Helper to update the APIAuthentication entity with edited values. + * + * @param \APIAuthentication $authEntity Entity to update. + * @param string $identifier Unique identifier for the + * API authentication entity. + * @param string $type Type for the API authentication entity. + * @param bool $allowWrite Helps to identify write functionality + * of the API is enabled or NOT. + */ + private function updateAuthenticationEntity( + \APIAuthentication $authEntity, + $identifier, + $type, + $allowWrite + ) { + $authEntity->setIdentifier($identifier); + $authEntity->setType($type); + $authEntity->setAllowAPIWrite($allowWrite); + } } diff --git a/lib/Gocdb_Services/Site.php b/lib/Gocdb_Services/Site.php index ecced35d4..f874f2ca9 100644 --- a/lib/Gocdb_Services/Site.php +++ b/lib/Gocdb_Services/Site.php @@ -1429,39 +1429,89 @@ public function deleteAPIAuthEntity(\APIAuthentication $authEntity, \User $user) $authEntServ->deleteAPIAuthentication($authEntity); } - public function editAPIAuthEntity(\APIAuthentication $authEntity, \User $user, $newValues) { - + /** + * Helper to edit an `APIAuthentication` entity. + * + * @param \APIAuthentication $authEntity `APIAuthentication` entity. + * @param \User $user User doing the edit. + * @param mixed $newValues Holds the new data for + * updating the `APIAuthentication` entity. + */ + public function editAPIAuthEntity( + \APIAuthentication $authEntity, + \User $user, + $newValues + ) { $parentSite = $authEntity->getParentSite(); // Check the user can do this. Thows exception if not. $this->checkUserAuthz($user, $parentSite); - $identifier = $newValues['IDENTIFIER']; - $type = $newValues['TYPE']; - /** @var org\gocdb\services\APIAuthenticationService */ $authEntServ = \Factory::getAPIAuthenticationService(); $authEntServ->setEntityManager($this->em); - //If the entity is of type OIDC subject, do a more thorough check again - if ($type == 'OIDC Subject' && !preg_match("/^([a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12})$/", $identifier)) { + if (!($newValues['isRenewalRequest'])) { + $this->validIdentifier( + $authEntity, + $newValues, + $parentSite, + $authEntServ + ); + } + + $authEntServ->editAPIAuthentication($authEntity, $user, $newValues); + + return $authEntity; + } + + /** + * Helper to check if the given identifier is valid. + * + * @param \APIAuthentication $authEntity `APIAuthentication` entity. + * @param mixed $newValues Holds the new data for updating + * the `APIAuthentication` entities. + * @param \Site $parentSite Site details to validate whether + * the given identifier is unique. + * @param \APIAuthenticationService $authEntServ `APIAuthenticationService` + * Singleton service. + * + * @throws \Exception If the given identifier is invalid OIDC Subject. + */ + private function validIdentifier( + \APIAuthentication $authEntity, + $newValues, + $parentSite, + $authEntServ + ) { + $identifier = $newValues['IDENTIFIER']; + $type = $newValues['TYPE']; + + /** + * If the entity is of type OIDC subject, + * do a more thorough check again. + */ + if ( + $type == 'OIDC Subject' && + !preg_match( + "/^([a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12})$/", + $identifier + ) + ) { throw new \Exception("Invalid OIDC Subject"); } /** - * If identifier or type has changed, check the credential is not - * already registered. - */ + * If identifier or type has changed, + * check the credential is NOT already registered. + */ if ($authEntity->getIdentifier() !== $identifier) { $authEntServ->uniqueAPIAuthEnt($parentSite, $identifier); } - - $authEntServ->editAPIAuthentication($authEntity, $user, $newValues); - - return $authEntity; } + /** * Helper combines admin check and authz check to make sure a user * can modify properties of the site. diff --git a/resources/ManageAPICredentials/ManageAPICredentialsActions.php b/resources/ManageAPICredentials/ManageAPICredentialsActions.php index 51e2b895b..692dd80ab 100644 --- a/resources/ManageAPICredentials/ManageAPICredentialsActions.php +++ b/resources/ManageAPICredentials/ManageAPICredentialsActions.php @@ -33,6 +33,7 @@ public function __construct($dryRun, $entityManager, $baseTime) $this->entityManager = $entityManager; $this->baseTime = $baseTime; } + /** * Find API credentials unused for a number of months. * @@ -62,6 +63,7 @@ public function getCreds($threshold, $propertyName) return $creds; } + /** * Select API credentials for deletion. * @@ -73,9 +75,12 @@ public function getCreds($threshold, $propertyName) * @param \Doctrine\Orm\EntityManager $entitymanager A valid Doctrine Entity Manager * @param \DateTime $baseTime Time from which interval of no-use is measured * @param int $deleteThreshold The number of months of no-use which will trigger deletion + * @param bool $isRenewalRequest Flag indicating the presence + * or absence of the `r` + * command-line argument. * @return array Credentials which were not deleted. */ - public function deleteCreds($creds, $deleteThreshold) + public function deleteCreds($creds, $deleteThreshold, $isRenewalRequest) { $deletedCreds = []; @@ -84,8 +89,16 @@ public function deleteCreds($creds, $deleteThreshold) /* @var $apiCred APIAuthentication */ foreach ($creds as $apiCred) { - if ($this->isOverThreshold($apiCred, $this->baseTime, $deleteThreshold)) { + if ( + $this->isOverThreshold( + $apiCred, + $this->baseTime, + $deleteThreshold, + $isRenewalRequest + ) + ) { $deletedCreds[] = $apiCred; + if (!$this->dryRun) { $serv->deleteAPIAuthentication($apiCred); } @@ -97,6 +110,7 @@ public function deleteCreds($creds, $deleteThreshold) return array_udiff($creds, $deletedCreds, array($this, 'compareCredIds')); } + /** * Send of warning emails where credentials have not been used for a given number of months * @@ -111,14 +125,19 @@ public function deleteCreds($creds, $deleteThreshold) * @param int $deleteThreshold The number of months of no-use which will trigger deletion * @param string $fromEmail Email address to use as sender's (From:) address * @param string $replyToEmail Email address for replies (Reply-To:) - * @return array Array of credentials identifed for sending warning emails + * @param bool $isRenewalRequest Flag indicating the presence + * or absence of the `r` + * command-line argument. + * @return array [] An Array of credentials identifed + * for sending warning emails. */ public function warnUsers( $creds, $warningThreshold, $deletionThreshold, $fromEmail, - $replyToEmail + $replyToEmail, + $isRenewalRequest ) { $warnedCreds = []; @@ -126,17 +145,28 @@ public function warnUsers( foreach ($creds as $apiCred) { // The credentials list is pre-selected based on the given threshold in the query // so this check is probably redundant. - if ($this->isOverThreshold($apiCred, $this->baseTime, $warningThreshold)) { - $lastUsed = $apiCred->getLastUseTime(); - $lastUseMonths = $this->baseTime->diff($lastUsed)->format('%m'); + if ( + $this->isOverThreshold( + $apiCred, + $this->baseTime, + $warningThreshold, + $isRenewalRequest + ) + ) { + $lastUseOrRenewTime = $isRenewalRequest + ? $apiCred->getLastRenewTime() + : $apiCred->getLastUseTime(); + $elapsedMonths = $this->baseTime->diff($lastUseOrRenewTime) + ->format('%m'); if (!$this->dryRun) { $this->sendWarningEmail( $fromEmail, $replyToEmail, $apiCred, - intval($lastUseMonths), - $deletionThreshold + intval($elapsedMonths), + $deletionThreshold, + $isRenewalRequest ); } @@ -150,18 +180,26 @@ public function warnUsers( return array_udiff($creds, $warnedCreds, array($this, 'compareCredIds')); } + /** * @return boolean true if the credential has not been used within $threshold months, else false */ - private function isOverThreshold(APIAuthentication $cred, DateTime $baseTime, $threshold) - { - $lastUsed = $cred->getLastUseTime(); + private function isOverThreshold( + APIAuthentication $cred, + DateTime $baseTime, + $threshold, + $isRenewalRequest + ) { + $lastUseOrRenewTime = $isRenewalRequest + ? $cred->getLastRenewTime() + : $cred->getLastUseTime(); - $diffTime = $baseTime->diff($lastUsed); - $lastUseMonths = ($diffTime->y * 12) + $diffTime->m; + $diffTime = $baseTime->diff($lastUseOrRenewTime); + $lastUseOrRenewMonths = ($diffTime->y * 12) + $diffTime->m; - return $lastUseMonths >= $threshold; + return $lastUseOrRenewMonths >= $threshold; } + /** * Helper function to check if two API credentials have the same id. * @@ -191,6 +229,9 @@ private function compareCredIds(APIAuthentication $cred1, APIAuthentication $cre * @param \APIAuthentication $api Credential to warn about * @param int $elapsedMonths The number of months of non-use so far. * @param int $deleteionThreshold The number of months of no-use which will trigger deletion if reached. + * @param bool $isRenewalRequest Flag indicating the presence + * or absence of the `r + * command-line argument. * @return void */ private function sendWarningEmail( @@ -198,38 +239,37 @@ private function sendWarningEmail( $replyToEmail, \APIAuthentication $api, $elapsedMonths, - $deletionThreshold + $deletionThreshold, + $isRenewalRequest ) { - $user = $api->getUser(); - $userEmail = $user->getEmail(); - $siteName = $api->getParentSite()->getShortName(); - $siteEmail = $siteName . ' <' . $api->getParentSite()->getEmail() . '>'; - - $headersArray = array ("From: $fromEmail", - "Cc: $siteEmail"); - if (strlen($replyToEmail) > 0 && $fromEmail !== $replyToEmail) { - $headersArray[] = "Reply-To: $replyToEmail"; - } - $headers = join("\r\n", $headersArray); - $subject = "GOCDB: Site API credential deletion notice"; - $body = "Dear " . $user->getForename() . ",\n\n" . - "The API credential associated with the following identifier registered\n" . - "at site $siteName has not been used during\n" . - "the last $elapsedMonths months and will be deleted if this period of inactivity\n" . - "reaches $deletionThreshold months.\n\n"; - - $body .= "Identifier: " . $api->getIdentifier() . "\n"; - $body .= "Owner email: " . $userEmail . "\n"; - - $body .= "\n"; - $body .= "Use of the credential will prevent its deletion.\n"; - $body .= "\nRegards,\nGOCDB Administrators\n"; + list($headers, $siteName) = $this->getHeaderContent( + $api, + $fromEmail, + $replyToEmail + ); + + if ($isRenewalRequest) { + list($userEmail, $body) = $this->getRenewalsBodyContent( + $api, + $siteName, + $elapsedMonths, + $deletionThreshold + ); + } else { + list($userEmail, $body) = $this->getInactiveBodyContent( + $api, + $siteName, + $elapsedMonths, + $deletionThreshold + ); + } // Send the email (or not, according to local configuration) Factory::getEmailService()->send($userEmail, $subject, $body, $headers); } + /** * Generate a summary report. * @@ -260,4 +300,105 @@ private function reportDryRun(array $creds, $text) ); } } + + /** + * Helper to generate header content. + * + * @param \APIAuthentication $api Credential to warn about. + * @param string $fromEmail Email address to use + * as sender's (From:) address. + * @param string $replyToEmail Email address for replies + * (Reply-To:) + * + * @return array An array containing $headers and $siteName. + */ + private function getHeaderContent( + \APIAuthentication $api, + $fromEmail, + $replyToEmail + ) { + $siteName = $api->getParentSite()->getShortName(); + $siteEmail = $siteName . ' <' . $api->getParentSite()->getEmail() . '>'; + $headersArray = array ("From: $fromEmail", "Cc: $siteEmail"); + + if (strlen($replyToEmail) > 0 && $fromEmail !== $replyToEmail) { + $headersArray[] = "Reply-To: $replyToEmail"; + } + + $headers = join("\r\n", $headersArray); + + return [$headers, $siteName]; + } + + /** + * Helper to generate body content for `renewals` option request. + * + * @param \APIAuthentication $api Credential to warn about. + * @param string $siteName Site Name. + * @param int $elapsedMonths The number of months of non-use so far. + * @param int $deleteionThreshold The number of months of no-use which + * will trigger deletion if reached. + * + * @return array An array containing $userEmail and $body content. + */ + private function getRenewalsBodyContent( + \APIAuthentication $api, + $siteName, + $elapsedMonths, + $deletionThreshold + ) { + $user = $api->getUser(); + $userEmail = $user->getEmail(); + + $body = "Dear " . $user->getForename() . ",\n\n" . + "The API credential associated with the following identifier\n" . + "registered at site $siteName has not been renewed for\n" . + "the last $elapsedMonths months and will be deleted if it " . + "reaches $deletionThreshold months.\n\n"; + + $body .= "Identifier: " . $api->getIdentifier() . "\n"; + $body .= "Owner email: " . $userEmail . "\n"; + + $body .= "\n"; + $body .= "Renewal of the credential will prevent its deletion.\n"; + $body .= "\nRegards,\nGOCDB Administrators\n"; + + return [$userEmail, $body]; + } + + /** + * Helper to generate body content for `inactive` option request. + * + * @param \APIAuthentication $api Credential to warn about. + * @param string $siteName Site Name. + * @param int $elapsedMonths The number of months of non-use so far. + * @param int $deleteionThreshold The number of months of no-use which + * will trigger deletion if reached. + * + * @return array An array containing $userEmail and $body content. + */ + private function getInactiveBodyContent( + \APIAuthentication $api, + $siteName, + $elapsedMonths, + $deletionThreshold + ) { + $user = $api->getUser(); + $userEmail = $user->getEmail(); + + $body = "Dear " . $user->getForename() . ",\n\n" . + "The API credential associated with the following identifier " . + "registered\nat site $siteName has not been used during the last " . + "$elapsedMonths months\nand will be deleted if this period of " . + "inactivity reaches $deletionThreshold months.\n\n"; + + $body .= "Identifier: " . $api->getIdentifier() . "\n"; + $body .= "Owner email: " . $userEmail . "\n"; + + $body .= "\n"; + $body .= "Use of the credential will prevent its deletion.\n"; + $body .= "\nRegards,\nGOCDB Administrators\n"; + + return [$userEmail, $body]; + } } diff --git a/resources/ManageAPICredentials/ManageUnusedAPICredentials.php b/resources/ManageAPICredentials/ManageUnusedAPICredentials.php index 88009d04f..dc02e65d5 100644 --- a/resources/ManageAPICredentials/ManageUnusedAPICredentials.php +++ b/resources/ManageAPICredentials/ManageUnusedAPICredentials.php @@ -49,14 +49,22 @@ $baseTime = new DateTime("now", new DateTimeZone('UTC')); - $actions = new ManageAPICredentialsActions($options->isDryRun(), $entityManager, $baseTime); + $actions = new ManageAPICredentialsActions( + $options->isDryRun(), + $entityManager, + $baseTime + ); - $creds = $actions->getCreds($options->getThreshold(), 'lastUseTime'); + $creds = $actions->getCreds( + $options->getThreshold(), + $options->getPropertyName() + ); if ($options->isDeleteEnabled()) { $creds = $actions->deleteCreds( $creds, - $options->getDelete() + $options->getDelete(), + $options->hasRenewalsOptionProvided() ); } @@ -66,7 +74,8 @@ $options->getWarn(), $options->getDelete(), $fromEmail, - $replyToEmail + $replyToEmail, + $options->hasRenewalsOptionProvided() ); } } catch (InvalidArgumentException $except) { diff --git a/resources/ManageAPICredentials/ManageUnusedAPICredentialsOptions.php b/resources/ManageAPICredentials/ManageUnusedAPICredentialsOptions.php index 8463c6e8b..9399da9f3 100644 --- a/resources/ManageAPICredentials/ManageUnusedAPICredentialsOptions.php +++ b/resources/ManageAPICredentials/ManageUnusedAPICredentialsOptions.php @@ -16,21 +16,27 @@ class ManageUnusedAPICredentialsOptions protected $warn; protected $delete; protected $dryRun; + protected $propertyName; + protected $isRenewalRequest; + protected $isInactiveRequest; public function __construct() { $this->getOptions(); } + /** - * @throws \InvInvalidArgumentException If errors found in argument processing + * @throws \InvalidArgumentException If errors found in argument processing */ public function getOptions() { - $shortOptions = 'hw:d:'; + $shortOptions = 'hriw:d:'; $longOptions = [ 'help', 'dry-run', + 'renewals', + 'inactive', 'warning_threshold:', 'deletion_threshold:' ]; @@ -38,8 +44,10 @@ public function getOptions() // Beware that getopt is not clever at spotting invalid/misspelled arguments $given = getopt($shortOptions, $longOptions); - if ($given === false) { - throw new InvalidArgumentException('failed to parse command line arguments'); + if ($given === false || (is_array($given) && count($given) <= 0)) { + throw new InvalidArgumentException( + 'failed to parse command line arguments' + ); } if ($this->getBoolOption($given, 'help', 'h')) { @@ -49,7 +57,8 @@ public function getOptions() } $this->dryRun = isset($given['dry-run']); - + $this->tryToObtainDatetime($given); + $this->setPropertyName(); $this->delete = $this->getValOption($given, 'deletion_threshold', 'd'); $this->warn = $this->getValOption($given, 'warning_threshold', 'w'); @@ -60,8 +69,10 @@ public function getOptions() ); } } + return; } + private function getValOption($given, $long, $short) { if (isset($given[$long]) || isset($given[$short])) { @@ -70,10 +81,12 @@ private function getValOption($given, $long, $short) } return; } + private function getBoolOption($given, $long, $short) { return isset($given[$long]) || isset($given[$short]); } + private function positiveInteger($val, $txt) { if ((string)abs((int)$val) != $val) { @@ -83,6 +96,7 @@ private function positiveInteger($val, $txt) } return (int)$val; } + public static function usage($message = '') { if ($message != '') { @@ -94,41 +108,84 @@ public static function usage($message = '') print ( "Usage: php ManageAPICredentials.php [--help | -h] [--dry-run] \\\ \n" . + " [--renewals | -r] \\\ \n" . + " [--inactive | -i] \n" . " [[--warning_threshold | -w] MONTHS ] \\\ \n" . - " [[--deletion_threshold | -d ] MONTHS ] \n" . + " [[--deletion_threshold | -d ] MONTHS ] \\\ \n" . "Options: \n" . " -h, --help Print this message.\n" . " --dry-run Report but do nothing.\n" . + " -r, --renewals Email the owning user " . + "about credentials\n" . + " which have not been " . + "renewed.\n" . + " -i, --inactive Email the owning user " . + "about credentials\n" . + " which have not been " . + "used.\n" . " -w, --warning_threshold MONTHS Email the owning user about credentials \n" . " which have not been used for MONTHS months.\n" . " -d, --deletion_threshold MONTHS Delete credentials which have not been used\n" . " for MONTHS months.\n" ); } + public function isShowHelp() { return $this->showHelp; } + public function isDryRun() { return $this->dryRun; } + public function isDeleteEnabled() { return !is_null($this->getDelete()); } + public function isWarnEnabled() { return !is_null($this->getWarn()); } + public function getWarn() { return $this->warn; } + public function getDelete() { return $this->delete; } + + public function getPropertyName() + { + return $this->propertyName; + } + + private function setPropertyName() + { + if ($this->hasRenewalsOptionProvided()) { + $this->propertyName = 'lastRenewTime'; + } else { + if ($this->hasInactiveOptionProvided()) { + $this->propertyName = 'lastUseTime'; + } + } + } + + public function hasRenewalsOptionProvided() + { + return $this->isRenewalRequest; + } + + public function hasInactiveOptionProvided() + { + return $this->isInactiveRequest; + } + /** * The delete threshold may not be given in which case the warning threshold should be used. * Note that it is an error is delete is greater than warning. @@ -137,4 +194,34 @@ public function getThreshold() { return $this->isWarnEnabled() ? $this->getWarn() : $this->getDelete(); } + + /** + * Validates whether the user has passed the argument required + * for obtaining the datetime. + * + * @param mixed $given Gets options from the command line argument list. + * + * @throws InvalidArgumentException When the user is NOT specifying + * which datetime to obtain. + */ + private function tryToObtainDatetime($given) + { + $this->isRenewalRequest = $this->getBoolOption($given, 'renewals', 'r'); + + if (!$this->isRenewalRequest) { + $this->isInactiveRequest = $this->getBoolOption( + $given, + 'inactive', + 'i' + ); + } + + $optionProvided = ($this->isRenewalRequest || $this->isInactiveRequest); + + if (!$optionProvided) { + throw new InvalidArgumentException( + 'failed to parse command line arguments' + ); + } + } } diff --git a/tests/resourcesTests/ManageUnrenewedAPICredentialsTest.php b/tests/resourcesTests/ManageUnrenewedAPICredentialsTest.php index 6065dea04..3942a8085 100644 --- a/tests/resourcesTests/ManageUnrenewedAPICredentialsTest.php +++ b/tests/resourcesTests/ManageUnrenewedAPICredentialsTest.php @@ -146,7 +146,7 @@ public function testLastRenewTime() // remove credentials last renewed more than 9 months ago // there should be 2 left after this operation (as 2 of // the 6 fetched above have been renewed with 9 months). - $creds = $actions->deleteCreds($creds, 9); + $creds = $actions->deleteCreds($creds, 9, true); $this->assertCount( 2, diff --git a/tests/resourcesTests/ManageUnusedAPICredentialsTest.php b/tests/resourcesTests/ManageUnusedAPICredentialsTest.php index ca5dd4a33..c0cc2c3f4 100644 --- a/tests/resourcesTests/ManageUnusedAPICredentialsTest.php +++ b/tests/resourcesTests/ManageUnusedAPICredentialsTest.php @@ -143,7 +143,7 @@ public function testLastUseTime() // remove credentials last used more than 13 months ago // there should be one left after this operation - $creds = $actions->deleteCreds($creds, 13); + $creds = $actions->deleteCreds($creds, 13, false); $this->assertCount( 1,