From a2cf6d2827e77e057e387504d0d3ba2ba26ab81a Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Fri, 4 Sep 2020 10:13:08 +0000 Subject: [PATCH 01/74] Adds the option to login to GOCDB via IRIS IAM login --- htdocs/landing/{index.html => index.php} | 18 ++- .../AuthTokens/IAMAuthToken.php | 130 ++++++++++++++++++ .../AuthTokens/ShibAuthToken.php | 6 +- .../AuthTokens/UnauthenticatedToken.php | 114 +++++++++++++++ lib/Authentication/MyConfig1.php | 2 + 5 files changed, 265 insertions(+), 5 deletions(-) rename htdocs/landing/{index.html => index.php} (75%) create mode 100644 lib/Authentication/AuthTokens/IAMAuthToken.php create mode 100644 lib/Authentication/AuthTokens/UnauthenticatedToken.php diff --git a/htdocs/landing/index.html b/htdocs/landing/index.php similarity index 75% rename from htdocs/landing/index.html rename to htdocs/landing/index.php index 4185dad7e..b361c7289 100644 --- a/htdocs/landing/index.html +++ b/htdocs/landing/index.php @@ -21,7 +21,21 @@

Welcome to GOCDB

Use of GOCDB is governed by the EGI Acceptable Use Policy which places restrictions on your use of the service.

The GOCDB Privacy Notice describes what personal data is collected and why, and your rights regarding this data.

Please read these documents before accessing GOCDB.

- Access GOCDB + +
+ Access GOCDB via X.509 Certificate +

or

+

Access GOCDB using one of the following:

+
+ + EGI Check-In + IRIS IAM +

Browse the GOCDB documentation index on the EGI wiki.

@@ -57,5 +71,7 @@

Welcome to GOCDB

+ + diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php new file mode 100644 index 000000000..4413e43fb --- /dev/null +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -0,0 +1,130 @@ + + * Requires installation/config of ShibSP before use. + * You will almost certainly need to modify this class to request the necessary + * SAML attribute from the IdP that is used as the principle string. + *

+ * The token is stateless because it relies on the ShibSP session and simply + * reads the attributes stored in the ShibSP session. + * + * @see IAuthentication + * @author David Meredith + */ +class IAMAuthToken implements IAuthentication { + + private $userDetails = null; + private $authorities = array(); + private $principal; + + public function __construct() { + $this->getAttributesInitToken(); + } + + /** + * {@see IAuthentication::eraseCredentials()} + */ + public function eraseCredentials() { + + } + + /** + * {@see IAuthentication::getAuthorities()} + */ + public function getAuthorities() { + return $this->authorities; + } + + /** + * {@see IAuthentication::getCredentials()} + * @return string An empty string as passwords are not used by this token. + */ + public function getCredentials() { + return ""; // none used in this token, handled by SSO/SAML + } + + /** + * A custom object used to store additional user details. + * Allows non-security related user information (such as email addresses, + * telephone numbers etc) to be stored in a convenient location. + * {@see IAuthentication::getDetails()} + * + * @return Object or null if not used + */ + public function getDetails() { + return $this->userDetails; + } + + /** + * {@see IAuthentication::getPrinciple()} + * @return string unique principle string of user + */ + public function getPrinciple() { + return $this->principal; + } + + + + private function getAttributesInitToken(){ + $hostname = $_SERVER['HTTP_HOST']; // don't use $_SERVER['SERVER_NAME'] as this don't support DNS + $this->principal = $_SERVER["REMOTE_USER"]; + $this->userDetails = array('AuthenticationRealm' => array('IRIS IAM - OIDC')); + //print_r($_SERVER); + } + + /** + * {@see IAuthentication::setAuthorities($authorities)} + */ + public function setAuthorities($authorities) { + $this->authorities = $authorities; + } + + /** + * {@see IAuthentication::setDetails($userDetails)} + * @param Object $userDetails + */ + public function setDetails($userDetails) { + $this->userDetails = $userDetails; + } + + /** + * {@see IAuthentication::validate()} + */ + public function validate() { + + } + + /** + * {@see IAuthentication::isPreAuthenticating()} + */ + public static function isPreAuthenticating() { + return true; + } + + /** + * Returns true, this token reads the ShibSP session attributes and so + * does not need to be stateful itself. + * {@see IAuthentication::isStateless()} + */ + public static function isStateless() { + return true; + } + +} diff --git a/lib/Authentication/AuthTokens/ShibAuthToken.php b/lib/Authentication/AuthTokens/ShibAuthToken.php index db9d364b4..064c15d38 100644 --- a/lib/Authentication/AuthTokens/ShibAuthToken.php +++ b/lib/Authentication/AuthTokens/ShibAuthToken.php @@ -171,10 +171,8 @@ private function getAttributesInitToken(){ // else { // die('Now go configure this AuthToken file ['.__FILE__.']'); // } - // if we have not set the principle/userDetails, re-direct to our Discovery Service - $target = urlencode("https://" . $hostname . "/portal/"); - header("Location: https://" . $hostname . "/Shibboleth.sso/Login?target=" . $target); - die(); + // if we have not set the principle/userDetails, re-direct to landing page + //header("Location: https://www.google.com"); } /** diff --git a/lib/Authentication/AuthTokens/UnauthenticatedToken.php b/lib/Authentication/AuthTokens/UnauthenticatedToken.php new file mode 100644 index 000000000..2496a2d90 --- /dev/null +++ b/lib/Authentication/AuthTokens/UnauthenticatedToken.php @@ -0,0 +1,114 @@ +IAuthentication to return to landing page to choose access path. + * + * @see IAuthentication + * @author Sarah Byrne + */ +class UnauthenticatedToken implements IAuthentication { + + private $userDetails = null; + private $authorities = array(); + + public function __construct() { + $this->chooseAuth(); + } + + /** + * {@see IAuthentication::isStateless()} + */ + public static function isStateless() { + return true; + } + + /** + * {@see IAuthentication::isPreAuthenticating()} + */ + public static function isPreAuthenticating() { + return true; + } + + /** + * {@see IAuthentication::getCredentials()} + * @return string An empty string as no password required. + */ + public function getCredentials() { + return ""; + } + + /** + * {@see IAuthentication::eraseCredentials()} + * Does nothing, no password required. + */ + public function eraseCredentials() { + // do nothing, no password required. + } + + /** + * {@see IAuthentication::getPrinciple()) + * Returns empty string - nothing to authenticate. + */ + public function getPrinciple() { + return ""; + } + + /** + * {@see IAuthentication::validate()} + */ + public function validate() { + //do nothing, nothing to validate + } + + private function chooseAuth() { + //when no X509 authentication, re-direct user to landing page + $hostname = $_SERVER['HTTP_HOST']; + header("Location:https://" . $hostname); + } + + /** + * A custom object used to store additional user details. + * Allows non-security related user information (such as email addresses, + * telephone numbers etc) to be stored in a convenient location. + * {@see IAuthentication::getDetails()} + * + * @return Object or null if not used + */ + public function getDetails() { + return $this->userDetails; + } + + /** + * {@see IAuthentication::getDetails()} + * @param Object $userDetails + */ + public function setDetails($userDetails) { + $this->userDetails = $userDetails; + } + + /** + * {@see IAuthentication::getAuthorities()} + * @return array + */ + public function getAuthorities() { + return $this->authorities; + } + + /** + * {@see IAuthentication::setAuthorities($authorities)} + * @param array $authorities + */ + public function setAuthorities($authorities) { + $this->authorities = $authorities; + } + + + +} + diff --git a/lib/Authentication/MyConfig1.php b/lib/Authentication/MyConfig1.php index 7a1f2ecf3..346e043a0 100644 --- a/lib/Authentication/MyConfig1.php +++ b/lib/Authentication/MyConfig1.php @@ -36,6 +36,8 @@ function __construct() { $this->tokenClassList = array(); $this->tokenClassList[] = 'org\gocdb\security\authentication\X509AuthenticationToken'; $this->tokenClassList[] = 'org\gocdb\security\authentication\ShibAuthToken'; + $this->tokenClassList[] = 'org\gocdb\security\authentication\IAMAuthToken'; + $this->tokenClassList[] = 'org\gocdb\security\authentication\UnauthenticatedToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\SimpleSamlPhpAuthToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\UsernamePasswordAuthenticationToken'; } From 02ff8758a34315c7f3f6126bf8f597b4966dc619 Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Fri, 4 Sep 2020 14:46:43 +0000 Subject: [PATCH 02/74] Makes the target link uri on the landing page dynamic --- htdocs/landing/index.php | 12 +++++++++--- lib/Authentication/AuthTokens/IAMAuthToken.php | 5 +---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/htdocs/landing/index.php b/htdocs/landing/index.php index b361c7289..329f75686 100644 --- a/htdocs/landing/index.php +++ b/htdocs/landing/index.php @@ -29,9 +29,15 @@

EGI Check-In IRIS IAM diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php index 4413e43fb..d48f51be9 100644 --- a/lib/Authentication/AuthTokens/IAMAuthToken.php +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -16,14 +16,11 @@ */ /** - * AuthToken for use with ShibSP. + * AuthToken for use with IRIS IAM. *

- * Requires installation/config of ShibSP before use. * You will almost certainly need to modify this class to request the necessary * SAML attribute from the IdP that is used as the principle string. *

- * The token is stateless because it relies on the ShibSP session and simply - * reads the attributes stored in the ShibSP session. * * @see IAuthentication * @author David Meredith From 06a183180f94338453214a9d25c423d188a0f838 Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Thu, 15 Oct 2020 09:20:52 +0000 Subject: [PATCH 03/74] Adds requirement to be part of gocdb IAM group when logging in via IAM --- lib/Authentication/AuthTokens/IAMAuthToken.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php index d48f51be9..77e0329f9 100644 --- a/lib/Authentication/AuthTokens/IAMAuthToken.php +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -84,6 +84,10 @@ private function getAttributesInitToken(){ $this->principal = $_SERVER["REMOTE_USER"]; $this->userDetails = array('AuthenticationRealm' => array('IRIS IAM - OIDC')); //print_r($_SERVER); + //echo(gettype($_SERVER['OIDC_CLAIM_groups'])); + if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ + die('You do not belog to the correct group(s) to gain access to this site.'); + } } /** From d56cd7fd2940fa39bd5f5c3d5a53a1eb51222006 Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Fri, 16 Oct 2020 11:27:50 +0000 Subject: [PATCH 04/74] Automatically fills in user details from IAM token when registering as new user --- htdocs/web_portal/controllers/user/register.php | 11 ++++++++++- htdocs/web_portal/views/user/register.php | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/htdocs/web_portal/controllers/user/register.php b/htdocs/web_portal/controllers/user/register.php index 97e2902c4..06f99167b 100644 --- a/htdocs/web_portal/controllers/user/register.php +++ b/htdocs/web_portal/controllers/user/register.php @@ -56,10 +56,19 @@ function draw() { die(); } + $authDetails = $_SERVER['OIDC_CLAIM_external_authn']; + $startPos = 3+strpos($authDetails, ":", (strpos($authDetails, "MAIL"))); + $endPos = strpos($authDetails, "\"", 3+$startPos); + $length = $endPos-$startPos; + $userEmail = substr($authDetails, $startPos, $length); + /* @var $authToken \org\gocdb\security\authentication\IAuthentication */ $authToken = Get_User_AuthToken(); $params['authAttributes'] = $authToken->getDetails(); + $params['given_name'] = $_SERVER['OIDC_CLAIM_given_name']; + $params['family_name'] = $_SERVER['OIDC_CLAIM_family_name']; + $params['email'] = $userEmail; $params['dn'] = $dn; show_view('user/register.php', $params); } @@ -91,4 +100,4 @@ function submit() { } } -?> \ No newline at end of file +?> diff --git a/htdocs/web_portal/views/user/register.php b/htdocs/web_portal/views/user/register.php index e6d063267..4a730b47b 100644 --- a/htdocs/web_portal/views/user/register.php +++ b/htdocs/web_portal/views/user/register.php @@ -56,19 +56,19 @@ Forename * (unaccentuated letters, spaces, dashes and quotes) - + Surname * (unaccentuated letters, spaces, dashes and quotes) - + E-Mail * (valid e-mail format) - + Telephone Number From b891caabb0b7ad1a297e0be0c1e2946eed025006 Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Tue, 20 Oct 2020 09:57:52 +0000 Subject: [PATCH 05/74] Stops users in localAccounts group accessing site --- lib/Authentication/AuthTokens/IAMAuthToken.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php index 77e0329f9..4c72beb5f 100644 --- a/lib/Authentication/AuthTokens/IAMAuthToken.php +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -85,6 +85,10 @@ private function getAttributesInitToken(){ $this->userDetails = array('AuthenticationRealm' => array('IRIS IAM - OIDC')); //print_r($_SERVER); //echo(gettype($_SERVER['OIDC_CLAIM_groups'])); + if(strpos($_SERVER['OIDC_CLAIM_groups'], "localAccounts")===false){ + }else{ + die('You must login via your organisation on IAM to gain access to this site.'); + } if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ die('You do not belog to the correct group(s) to gain access to this site.'); } From 09b89ce2ed00809198dec262e6427522aaf2909c Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Tue, 3 Nov 2020 16:21:32 +0000 Subject: [PATCH 06/74] Adds oidc authentication to the API --- .../web_portal/components/Get_User_Principle.php | 14 +++++++++++--- .../AuthProviders/GOCDBAuthProvider.php | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/htdocs/web_portal/components/Get_User_Principle.php b/htdocs/web_portal/components/Get_User_Principle.php index 15af58eca..e343661df 100644 --- a/htdocs/web_portal/components/Get_User_Principle.php +++ b/htdocs/web_portal/components/Get_User_Principle.php @@ -162,21 +162,29 @@ function Get_User_Principle(){ } /** - * Get the DN from an x509 cert or null if a user certificate can't be loaded. - * Called from the PI to authenticate requests using certificates only. + * Get the DN from an x509 cert, principle from oidc auth, or null if a user certificate can't be loaded. + * Called from the PI to authenticate requests using certificates or oidc. * @return string or null if can't authenticate request */ function Get_User_Principle_PI() { $fwMan = \org\gocdb\security\authentication\FirewallComponentManager::getInstance(); $firewallArray = $fwMan->getFirewallArray(); - try { + try{ $x509Token = new org\gocdb\security\authentication\X509AuthenticationToken(); $auth = $firewallArray['fwC1']->authenticate($x509Token); return $auth->getPrinciple(); } catch(org\gocdb\security\authentication\AuthenticationException $ex){ + // failed auth, so attempt OIDC auth + try{ + $token = new org\gocdb\security\authentication\IAMAuthToken(); + $auth = $firewallArray['fwC1']->authenticate($token); + return $auth->getPrinciple(); + } catch(org\gocdb\security\authentication\AuthenticationException $ex){ // failed auth, so return null and let calling page decide to allow // access or not (some PI methods don't need to be authenticated with a cert) + } } + return null; } diff --git a/lib/Authentication/AuthProviders/GOCDBAuthProvider.php b/lib/Authentication/AuthProviders/GOCDBAuthProvider.php index 19a953f53..d67cb7c73 100644 --- a/lib/Authentication/AuthProviders/GOCDBAuthProvider.php +++ b/lib/Authentication/AuthProviders/GOCDBAuthProvider.php @@ -43,6 +43,9 @@ public function authenticate(IAuthentication $auth){ else if($auth instanceof SimpleSamlPhpAuthToken){ $roles[] = 'ROLE_SAMLUSER'; } + else if($auth instanceof IAMAuthToken){ + $roles[] = 'ROLE_IRISUSER'; + } $auth->setAuthorities($roles); return $auth; } From b393dcc33aa8eb98825e5a416c101ef795ff993c Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Wed, 4 Nov 2020 10:10:05 +0000 Subject: [PATCH 07/74] Removes the need for an unauthenticated token in the token class list --- .../AuthTokens/ShibAuthToken.php | 2 +- .../AuthTokens/UnauthenticatedToken.php | 114 ------------------ lib/Authentication/MyConfig1.php | 3 +- 3 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 lib/Authentication/AuthTokens/UnauthenticatedToken.php diff --git a/lib/Authentication/AuthTokens/ShibAuthToken.php b/lib/Authentication/AuthTokens/ShibAuthToken.php index 064c15d38..0c621baaf 100644 --- a/lib/Authentication/AuthTokens/ShibAuthToken.php +++ b/lib/Authentication/AuthTokens/ShibAuthToken.php @@ -172,7 +172,7 @@ private function getAttributesInitToken(){ // die('Now go configure this AuthToken file ['.__FILE__.']'); // } // if we have not set the principle/userDetails, re-direct to landing page - //header("Location: https://www.google.com"); + header("Location: https://" . $hostname); } /** diff --git a/lib/Authentication/AuthTokens/UnauthenticatedToken.php b/lib/Authentication/AuthTokens/UnauthenticatedToken.php deleted file mode 100644 index 2496a2d90..000000000 --- a/lib/Authentication/AuthTokens/UnauthenticatedToken.php +++ /dev/null @@ -1,114 +0,0 @@ -IAuthentication to return to landing page to choose access path. - * - * @see IAuthentication - * @author Sarah Byrne - */ -class UnauthenticatedToken implements IAuthentication { - - private $userDetails = null; - private $authorities = array(); - - public function __construct() { - $this->chooseAuth(); - } - - /** - * {@see IAuthentication::isStateless()} - */ - public static function isStateless() { - return true; - } - - /** - * {@see IAuthentication::isPreAuthenticating()} - */ - public static function isPreAuthenticating() { - return true; - } - - /** - * {@see IAuthentication::getCredentials()} - * @return string An empty string as no password required. - */ - public function getCredentials() { - return ""; - } - - /** - * {@see IAuthentication::eraseCredentials()} - * Does nothing, no password required. - */ - public function eraseCredentials() { - // do nothing, no password required. - } - - /** - * {@see IAuthentication::getPrinciple()) - * Returns empty string - nothing to authenticate. - */ - public function getPrinciple() { - return ""; - } - - /** - * {@see IAuthentication::validate()} - */ - public function validate() { - //do nothing, nothing to validate - } - - private function chooseAuth() { - //when no X509 authentication, re-direct user to landing page - $hostname = $_SERVER['HTTP_HOST']; - header("Location:https://" . $hostname); - } - - /** - * A custom object used to store additional user details. - * Allows non-security related user information (such as email addresses, - * telephone numbers etc) to be stored in a convenient location. - * {@see IAuthentication::getDetails()} - * - * @return Object or null if not used - */ - public function getDetails() { - return $this->userDetails; - } - - /** - * {@see IAuthentication::getDetails()} - * @param Object $userDetails - */ - public function setDetails($userDetails) { - $this->userDetails = $userDetails; - } - - /** - * {@see IAuthentication::getAuthorities()} - * @return array - */ - public function getAuthorities() { - return $this->authorities; - } - - /** - * {@see IAuthentication::setAuthorities($authorities)} - * @param array $authorities - */ - public function setAuthorities($authorities) { - $this->authorities = $authorities; - } - - - -} - diff --git a/lib/Authentication/MyConfig1.php b/lib/Authentication/MyConfig1.php index 346e043a0..b9fb402d8 100644 --- a/lib/Authentication/MyConfig1.php +++ b/lib/Authentication/MyConfig1.php @@ -35,9 +35,8 @@ function __construct() { $this->tokenClassList = array(); $this->tokenClassList[] = 'org\gocdb\security\authentication\X509AuthenticationToken'; - $this->tokenClassList[] = 'org\gocdb\security\authentication\ShibAuthToken'; $this->tokenClassList[] = 'org\gocdb\security\authentication\IAMAuthToken'; - $this->tokenClassList[] = 'org\gocdb\security\authentication\UnauthenticatedToken'; + $this->tokenClassList[] = 'org\gocdb\security\authentication\ShibAuthToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\SimpleSamlPhpAuthToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\UsernamePasswordAuthenticationToken'; } From 59b08cee67b3229e13e092c240fd0479c3316526 Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Tue, 1 Dec 2020 15:34:49 +0000 Subject: [PATCH 08/74] Checks access tokens are set being checking groups --- htdocs/PI/index.php | 4 ++-- .../AuthTokens/IAMAuthToken.php | 21 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/htdocs/PI/index.php b/htdocs/PI/index.php index 34e5471d0..4027de40c 100644 --- a/htdocs/PI/index.php +++ b/htdocs/PI/index.php @@ -369,8 +369,8 @@ function getXml() { function authAnyCert() { if (empty($this->dn)) - die("principal = $_SERVER["REMOTE_USER"]; - $this->userDetails = array('AuthenticationRealm' => array('IRIS IAM - OIDC')); - //print_r($_SERVER); - //echo(gettype($_SERVER['OIDC_CLAIM_groups'])); - if(strpos($_SERVER['OIDC_CLAIM_groups'], "localAccounts")===false){ - }else{ - die('You must login via your organisation on IAM to gain access to this site.'); - } - if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ - die('You do not belog to the correct group(s) to gain access to this site.'); + if(isset($_SERVER['OIDC_access_token'])){ + $this->principal = $_SERVER["REMOTE_USER"]; + $this->userDetails = array('AuthenticationRealm' => array('IRIS IAM - OIDC')); + if(strpos($_SERVER['OIDC_CLAIM_groups'], "localAccounts")===false){ + }else{ + die('You must login via your organisation on IAM to gain access to this site.'); + } + if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ + die('You do not belog to the correct group(s) to gain access to this site.'); + } } } From 6dfc91686903dfd02808a405a7509447f63feb28 Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Tue, 8 Dec 2020 17:01:14 +0000 Subject: [PATCH 09/74] Integrates the write API with oidc authentication and allows oidc credentials to be added to a site --- config/gocdb_schema.xml | 2 +- htdocs/PI/write/utils.php | 7 ++++--- htdocs/web_portal/controllers/site/add_api_auth.php | 1 + htdocs/web_portal/controllers/site/edit_api_auth.php | 1 + htdocs/web_portal/views/site/add_api_auth.php | 2 +- htdocs/web_portal/views/site/edit_api_auth.php | 2 +- lib/Gocdb_Services/Site.php | 10 ++++++++++ 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/config/gocdb_schema.xml b/config/gocdb_schema.xml index 089ae632b..9f087392d 100644 --- a/config/gocdb_schema.xml +++ b/config/gocdb_schema.xml @@ -583,7 +583,7 @@ TYPE 255 - /^X509$/ + /^(X509|OIDC Subject)$/ diff --git a/htdocs/PI/write/utils.php b/htdocs/PI/write/utils.php index e452dfcce..3e44ce85e 100644 --- a/htdocs/PI/write/utils.php +++ b/htdocs/PI/write/utils.php @@ -62,9 +62,10 @@ function returnJsonWriteAPIResult ($httpResponseCode, $object) { */ function getAuthenticationInfo () { require_once __DIR__ . '/../../web_portal/components/Get_User_Principle.php'; - #Only x509 authentication is currently supported. If in the future we support - #API keys then I suggest we only look for a x509 DN if an API key isn't presented - $identifierType = 'X509'; + #Check if associated cert/token is set to define identifier type + if(isset($_SERVER['SSL_CLIENT_CERT'])){$identifierType = 'X509';} + if(isset($_SERVER['OIDC_access_token'])){$identifierType = 'OIDC Subject';} + #This will return null if no cert is presented $identifier = Get_User_Principle_PI(); diff --git a/htdocs/web_portal/controllers/site/add_api_auth.php b/htdocs/web_portal/controllers/site/add_api_auth.php index bbf7cadaa..1dd9b51b9 100644 --- a/htdocs/web_portal/controllers/site/add_api_auth.php +++ b/htdocs/web_portal/controllers/site/add_api_auth.php @@ -59,6 +59,7 @@ function draw(\User $user = null, \Site $site = null) { $params['site'] = $site; $params['authTypes'] = array(); $params['authTypes'][]='X509'; + $params['authTypes'][]='OIDC Subject'; show_view("site/add_api_auth.php", $params); die(); diff --git a/htdocs/web_portal/controllers/site/edit_api_auth.php b/htdocs/web_portal/controllers/site/edit_api_auth.php index 772c70120..3120cca85 100644 --- a/htdocs/web_portal/controllers/site/edit_api_auth.php +++ b/htdocs/web_portal/controllers/site/edit_api_auth.php @@ -61,6 +61,7 @@ function draw(\User $user = null, \APIAuthentication $authEnt = null, \Site $sit $params['authEnt'] = $authEnt; $params['authTypes'] = array(); $params['authTypes'][]='X509'; + $params['authTypes'][]='OIDC Subject'; show_view("site/edit_api_auth.php", $params); die(); diff --git a/htdocs/web_portal/views/site/add_api_auth.php b/htdocs/web_portal/views/site/add_api_auth.php index 063d8e344..005e710ac 100644 --- a/htdocs/web_portal/views/site/add_api_auth.php +++ b/htdocs/web_portal/views/site/add_api_auth.php @@ -4,7 +4,7 @@ Caution: it is possible to delete information using the write functionality of the API.

- Identifier (e.g. Certificate DN)* + Identifier (e.g. Certificate DN or OIDC Subject)*
Credential type* diff --git a/htdocs/web_portal/views/site/edit_api_auth.php b/htdocs/web_portal/views/site/edit_api_auth.php index 284bae1fe..f30848efb 100644 --- a/htdocs/web_portal/views/site/edit_api_auth.php +++ b/htdocs/web_portal/views/site/edit_api_auth.php @@ -2,7 +2,7 @@

Edit API credential for getName());?>


- Identifier (e.g. Certificate DN)* + Identifier (e.g. Certificate DN or OIDC Subject)*
Credential type* diff --git a/lib/Gocdb_Services/Site.php b/lib/Gocdb_Services/Site.php index 85efe8e8f..a6018af6a 100644 --- a/lib/Gocdb_Services/Site.php +++ b/lib/Gocdb_Services/Site.php @@ -1408,6 +1408,11 @@ public function addAPIAuthEntity(\Site $site, \User $user, $newValues) { if ($type == 'X509' && !preg_match("/^(\/[A-Za-z]+=[a-zA-Z0-9\/\-\_\s\.,'@:\/]+)*$/", $identifier)) { throw new \Exception("Invalid x509 DN"); } + + //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"); + } //Check there isn't already a identifier of that type with that identifier for that Site $this->uniqueAPIAuthEnt($site, $identifier, $type); @@ -1488,6 +1493,11 @@ public function editAPIAuthEntity(\APIAuthentication $authEntity, \User $user, $ throw new \Exception("Invalid x509 DN"); } + //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"); + } + /** * As long as something has changed, check there isn't already a * identifier of that type with that identifier for that Site From 75c0a89d1d0e13fbedf2aed5d66c811ebd1a42cf Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Tue, 8 Dec 2020 17:21:05 +0000 Subject: [PATCH 10/74] Adds some comments and removes some unused code --- htdocs/landing/index.php | 2 -- htdocs/web_portal/controllers/user/register.php | 1 + lib/Authentication/AuthTokens/IAMAuthToken.php | 8 +++----- lib/Authentication/AuthTokens/ShibAuthToken.php | 7 ------- lib/Authentication/MyConfig1.php | 2 +- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/htdocs/landing/index.php b/htdocs/landing/index.php index 329f75686..5ba1e6d9e 100644 --- a/htdocs/landing/index.php +++ b/htdocs/landing/index.php @@ -77,7 +77,5 @@
- - diff --git a/htdocs/web_portal/controllers/user/register.php b/htdocs/web_portal/controllers/user/register.php index 06f99167b..6c76ba73d 100644 --- a/htdocs/web_portal/controllers/user/register.php +++ b/htdocs/web_portal/controllers/user/register.php @@ -56,6 +56,7 @@ function draw() { die(); } + //Extract users email from oidc claims $authDetails = $_SERVER['OIDC_CLAIM_external_authn']; $startPos = 3+strpos($authDetails, ":", (strpos($authDetails, "MAIL"))); $endPos = strpos($authDetails, "\"", 3+$startPos); diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php index b3caf9981..986efc279 100644 --- a/lib/Authentication/AuthTokens/IAMAuthToken.php +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -17,13 +17,9 @@ /** * AuthToken for use with IRIS IAM. - *

- * You will almost certainly need to modify this class to request the necessary - * SAML attribute from the IdP that is used as the principle string. - *

* * @see IAuthentication - * @author David Meredith + * @author Sarah Byrne */ class IAMAuthToken implements IAuthentication { @@ -83,10 +79,12 @@ private function getAttributesInitToken(){ if(isset($_SERVER['OIDC_access_token'])){ $this->principal = $_SERVER["REMOTE_USER"]; $this->userDetails = array('AuthenticationRealm' => array('IRIS IAM - OIDC')); + //Don't allow access if user only has a local account on IRIS if(strpos($_SERVER['OIDC_CLAIM_groups'], "localAccounts")===false){ }else{ die('You must login via your organisation on IAM to gain access to this site.'); } + //Don't allow access unless user is a member of the IRIS gocdb group if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ die('You do not belog to the correct group(s) to gain access to this site.'); } diff --git a/lib/Authentication/AuthTokens/ShibAuthToken.php b/lib/Authentication/AuthTokens/ShibAuthToken.php index 0c621baaf..af6f9a9a2 100644 --- a/lib/Authentication/AuthTokens/ShibAuthToken.php +++ b/lib/Authentication/AuthTokens/ShibAuthToken.php @@ -166,13 +166,6 @@ private function getAttributesInitToken(){ $this->userDetails = array('AuthenticationRealm' => array('EGI Proxy IdP')); return; } - - -// else { -// die('Now go configure this AuthToken file ['.__FILE__.']'); -// } - // if we have not set the principle/userDetails, re-direct to landing page - header("Location: https://" . $hostname); } /** diff --git a/lib/Authentication/MyConfig1.php b/lib/Authentication/MyConfig1.php index b9fb402d8..f60f0c23a 100644 --- a/lib/Authentication/MyConfig1.php +++ b/lib/Authentication/MyConfig1.php @@ -36,7 +36,7 @@ function __construct() { $this->tokenClassList = array(); $this->tokenClassList[] = 'org\gocdb\security\authentication\X509AuthenticationToken'; $this->tokenClassList[] = 'org\gocdb\security\authentication\IAMAuthToken'; - $this->tokenClassList[] = 'org\gocdb\security\authentication\ShibAuthToken'; + //$this->tokenClassList[] = 'org\gocdb\security\authentication\ShibAuthToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\SimpleSamlPhpAuthToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\UsernamePasswordAuthenticationToken'; } From af99f55ccefd70983d95c76dcf390f5b184f6b7f Mon Sep 17 00:00:00 2001 From: Sarah <69518420+sarahbyrnie@users.noreply.github.com> Date: Wed, 9 Dec 2020 13:51:15 +0000 Subject: [PATCH 11/74] Update lib/Gocdb_Services/Site.php --- lib/Gocdb_Services/Site.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Gocdb_Services/Site.php b/lib/Gocdb_Services/Site.php index a6018af6a..9086eb6ec 100644 --- a/lib/Gocdb_Services/Site.php +++ b/lib/Gocdb_Services/Site.php @@ -1410,7 +1410,7 @@ public function addAPIAuthEntity(\Site $site, \User $user, $newValues) { } //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 ($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)) { throw new \Exception("Invalid OIDC Subject"); } From 2a3fc5fe4088497aba3d8358b42f679232cae173 Mon Sep 17 00:00:00 2001 From: Sarah <69518420+sarahbyrnie@users.noreply.github.com> Date: Wed, 9 Dec 2020 14:44:02 +0000 Subject: [PATCH 12/74] Update lib/Authentication/AuthTokens/IAMAuthToken.php Co-authored-by: ineilson --- lib/Authentication/AuthTokens/IAMAuthToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php index 986efc279..f55cbd18a 100644 --- a/lib/Authentication/AuthTokens/IAMAuthToken.php +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -82,7 +82,7 @@ private function getAttributesInitToken(){ //Don't allow access if user only has a local account on IRIS if(strpos($_SERVER['OIDC_CLAIM_groups'], "localAccounts")===false){ }else{ - die('You must login via your organisation on IAM to gain access to this site.'); + die('You must login via your organisation on IRIS IAM to gain access to this site.'); } //Don't allow access unless user is a member of the IRIS gocdb group if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ From ada39241eebaa609e626bfae90b2cb7838a6a0c5 Mon Sep 17 00:00:00 2001 From: Sarah <69518420+sarahbyrnie@users.noreply.github.com> Date: Wed, 9 Dec 2020 16:17:17 +0000 Subject: [PATCH 13/74] Update htdocs/landing/index.php --- htdocs/landing/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/landing/index.php b/htdocs/landing/index.php index 5ba1e6d9e..f9e377bcd 100644 --- a/htdocs/landing/index.php +++ b/htdocs/landing/index.php @@ -23,7 +23,7 @@

Please read these documents before accessing GOCDB.

- Access GOCDB via X.509 Certificate + Access GOCDB using your IGTF X.509 Certificate

or

Access GOCDB using one of the following:

From b11ee9053d563533d3d4b7aafa1340b99896a07e Mon Sep 17 00:00:00 2001 From: Sarah <69518420+sarahbyrnie@users.noreply.github.com> Date: Thu, 10 Dec 2020 11:38:30 +0000 Subject: [PATCH 14/74] Update htdocs/web_portal/components/Get_User_Principle.php --- htdocs/web_portal/components/Get_User_Principle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/web_portal/components/Get_User_Principle.php b/htdocs/web_portal/components/Get_User_Principle.php index e343661df..efbc20fe2 100644 --- a/htdocs/web_portal/components/Get_User_Principle.php +++ b/htdocs/web_portal/components/Get_User_Principle.php @@ -162,7 +162,7 @@ function Get_User_Principle(){ } /** - * Get the DN from an x509 cert, principle from oidc auth, or null if a user certificate can't be loaded. + * Get the DN from an x509 cert, Principle from oidc token, or null if neither can be loaded. * Called from the PI to authenticate requests using certificates or oidc. * @return string or null if can't authenticate request */ From c0e14ee00a8015f3d0e30bf75b3c55f7d7f41903 Mon Sep 17 00:00:00 2001 From: Sarah <69518420+sarahbyrnie@users.noreply.github.com> Date: Thu, 10 Dec 2020 11:38:51 +0000 Subject: [PATCH 15/74] Update lib/Authentication/AuthTokens/IAMAuthToken.php --- lib/Authentication/AuthTokens/IAMAuthToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php index f55cbd18a..936385519 100644 --- a/lib/Authentication/AuthTokens/IAMAuthToken.php +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -86,7 +86,7 @@ private function getAttributesInitToken(){ } //Don't allow access unless user is a member of the IRIS gocdb group if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ - die('You do not belog to the correct group(s) to gain access to this site.'); + die('You do not belong to the correct group to gain access to this site. Please visit iris-iam.stfc.ac.uk and submit a request to join the GOCDB group. This shall be reviewed by a GOCDB admin.'); } } } From fe1a66c752a5d4b2f7eebda409107d0283adcfa1 Mon Sep 17 00:00:00 2001 From: Sarah <69518420+sarahbyrnie@users.noreply.github.com> Date: Thu, 21 Jan 2021 10:03:34 +0000 Subject: [PATCH 16/74] Update htdocs/landing/index.php --- htdocs/landing/index.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/landing/index.php b/htdocs/landing/index.php index f9e377bcd..778500241 100644 --- a/htdocs/landing/index.php +++ b/htdocs/landing/index.php @@ -37,7 +37,8 @@ else{ $iam_target=ltrim($_SERVER['REQUEST_URI'], '/?'); } - $iam_redirect = "https://" . $hostname . "/portal/redirect_uri?iss=https%3A%2F%2Firis-iam.stfc.ac.uk%2F&" . $iam_target; + $iris_url = urlencode("https://iris-iam.stfc.ac.uk/"); + $iam_redirect = "https://" . $hostname . "/portal/redirect_uri?iss=" . $iris_url . "&" . $iam_target; ?> EGI Check-In IRIS IAM From e79dda7583b1523ca2280b8be65d9b28ff811a0b Mon Sep 17 00:00:00 2001 From: sarahbyrnie Date: Mon, 1 Feb 2021 16:59:31 +0000 Subject: [PATCH 17/74] Allows for groups claim to be a string or array depending on auth type --- lib/Authentication/AuthTokens/IAMAuthToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Authentication/AuthTokens/IAMAuthToken.php b/lib/Authentication/AuthTokens/IAMAuthToken.php index 936385519..4bdceb572 100644 --- a/lib/Authentication/AuthTokens/IAMAuthToken.php +++ b/lib/Authentication/AuthTokens/IAMAuthToken.php @@ -85,7 +85,7 @@ private function getAttributesInitToken(){ die('You must login via your organisation on IRIS IAM to gain access to this site.'); } //Don't allow access unless user is a member of the IRIS gocdb group - if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false){ + if(strpos($_SERVER['OIDC_CLAIM_groups'], "gocdb")===false and in_array('gocdb', $_SERVER['OIDC_CLAIM_groups'])===false){ die('You do not belong to the correct group to gain access to this site. Please visit iris-iam.stfc.ac.uk and submit a request to join the GOCDB group. This shall be reviewed by a GOCDB admin.'); } } From 2d7ec7abc35ae5cfb10fd2ea1f0d6b9449f866e7 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Tue, 29 Jun 2021 10:38:42 +0000 Subject: [PATCH 18/74] Fix QueryPopulateSSO_UsernameRunner This script does not appear to have been working, as the EGI URL has changed, and the proxy host no longer seems to exist. The URL has therefore been updated and the proxy settings have been removed. --- resources/QueryPopulateSSO_UsernameRunner.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/QueryPopulateSSO_UsernameRunner.php b/resources/QueryPopulateSSO_UsernameRunner.php index 430e4c7e6..50ffebe82 100644 --- a/resources/QueryPopulateSSO_UsernameRunner.php +++ b/resources/QueryPopulateSSO_UsernameRunner.php @@ -39,12 +39,10 @@ ++$count; $cleanDN = cleanDN($user->getCertificateDn()); if (!empty($cleanDN)) { - $url = "https://www.egi.eu/sso/api/user?dn=" . $cleanDN; + $url = "https://sso.egi.eu/admin/api/user?dn=" . $cleanDN; $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_PROXY, 'http://wwwcache.rl.ac.uk'); - curl_setopt($ch, CURLOPT_PROXYPORT, 8080); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //return result instead of outputting it curl_setopt($ch, CURLOPT_TIMEOUT, 3); //3secs From fd9e830039b8f554c0d269b0cca5756caeba2739 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Tue, 27 Jul 2021 10:20:24 +0000 Subject: [PATCH 19/74] Delete unused controller --- htdocs/web_portal/controllers/cert_change.php | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 htdocs/web_portal/controllers/cert_change.php diff --git a/htdocs/web_portal/controllers/cert_change.php b/htdocs/web_portal/controllers/cert_change.php deleted file mode 100644 index 2a9001871..000000000 --- a/htdocs/web_portal/controllers/cert_change.php +++ /dev/null @@ -1,57 +0,0 @@ -

Error


Missing validation code
"; - } - - // get the request corresponding to the given code - $change_request = \Factory::getCertChangeService()->getChangeRequest($code); - - // verify the change request is valid - if($change_request == null) { - return "

Error


There is no active request with supplied confirmation code
"; - } - - // get account corresponding to old DN and update the certificate on that account - $account_to_update = \Factory::getCertChangeService()->getAccount($change_request["OLD_DN"]); - $account_to_update["CERTIFICATE_DN"]=$change_request["NEW_DN"]; - $xml_to_insert = \Factory::getCertChangeService()->createAccountXml($account_to_update); - $silent = 1; - test_then_insert_xml($xml_to_insert); - // delete change request - \Factory::getCertChangeService()->deleteRequest($change_request["COBJECTID"], $change_request["CGRIDID"]); - - $HTML.="

Success


Your certificate change is now complete. "; - $HTML.="You can access your GOCDB account with your new certificate.
"; - - return $HTML; -} - -?> \ No newline at end of file From d0699aa9fe6a9a8219c0dcbf392a6850b08bcc43 Mon Sep 17 00:00:00 2001 From: gregcorbett Date: Thu, 3 Feb 2022 16:44:40 +0000 Subject: [PATCH 20/74] Re-enable shib by default, disable IAM by default - shib will still be the main SSO usecase for 5.9.0, so I want to have that activiated by default. --- lib/Authentication/MyConfig1.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Authentication/MyConfig1.php b/lib/Authentication/MyConfig1.php index f60f0c23a..5054c41dd 100644 --- a/lib/Authentication/MyConfig1.php +++ b/lib/Authentication/MyConfig1.php @@ -35,8 +35,8 @@ function __construct() { $this->tokenClassList = array(); $this->tokenClassList[] = 'org\gocdb\security\authentication\X509AuthenticationToken'; - $this->tokenClassList[] = 'org\gocdb\security\authentication\IAMAuthToken'; - //$this->tokenClassList[] = 'org\gocdb\security\authentication\ShibAuthToken'; + //$this->tokenClassList[] = 'org\gocdb\security\authentication\IAMAuthToken'; + $this->tokenClassList[] = 'org\gocdb\security\authentication\ShibAuthToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\SimpleSamlPhpAuthToken'; //$this->tokenClassList[] = 'org\gocdb\security\authentication\UsernamePasswordAuthenticationToken'; } From 04954dc3ada48c0fb7ba851ce56b3716ec96f6d9 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Wed, 4 Aug 2021 10:52:47 +0000 Subject: [PATCH 21/74] Create user identifier entity, storing information for user authentication The user's unique ID string, associated with a single IdP, is currently stored in the User entity as certificateDn. Instead, a UserIdentifier entity can allow each user to be associated with one or more IdPs. This will allow account linking across multiple IdPs, as well as flexibility to add IdPs without further changes to the database. --- lib/Doctrine/bootstrap.php | 1 + lib/Doctrine/entities/User.php | 26 ++++++ lib/Doctrine/entities/UserIdentifier.php | 107 +++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 lib/Doctrine/entities/UserIdentifier.php diff --git a/lib/Doctrine/bootstrap.php b/lib/Doctrine/bootstrap.php index 71d62eec1..153a7c283 100644 --- a/lib/Doctrine/bootstrap.php +++ b/lib/Doctrine/bootstrap.php @@ -39,6 +39,7 @@ require_once $entitiesPath."/EndpointProperty.php"; require_once $entitiesPath."/RoleActionRecord.php"; require_once $entitiesPath."/APIAuthentication.php"; +require_once $entitiesPath."/UserIdentifier.php"; //if (!class_exists("Doctrine\Common\Version", false)) { // require_once __DIR__."/bootstrap_doctrine.php"; diff --git a/lib/Doctrine/entities/User.php b/lib/Doctrine/entities/User.php index e966beb24..77a749d6c 100644 --- a/lib/Doctrine/entities/User.php +++ b/lib/Doctrine/entities/User.php @@ -71,6 +71,12 @@ class User { /** @Column(type="datetime", nullable=true) */ protected $lastLoginDate; + /** + * Bidirectional - A User (INVERSE ORM SIDE) can have many identifiers + * @OneToMany(targetEntity="UserIdentifier", mappedBy="parentUser", cascade={"remove"}) + */ + protected $userIdentifiers = null; + /* * TODO: * This entity will need to own a property bag (akin to custom props) @@ -87,6 +93,7 @@ public function __construct() { $this->creationDate = new \DateTime("now"); //$this->sites = new ArrayCollection(); $this->roles = new ArrayCollection(); + $this->userIdentifiers = new ArrayCollection(); } /** @@ -193,6 +200,15 @@ public function getLastLoginDate(){ return $this->lastLoginDate; } + /** + * The User's list of {@see UserIdentifier} extension objects. When the + * User is deleted, the userIdentifiers are also cascade deleted. + * @return ArrayCollection + */ + public function getUserIdentifiers() { + return $this->userIdentifiers; + } + /** * Is the user a GOCDB admin user. Defaults to false. * @return boolean @@ -343,4 +359,14 @@ public function getRoles() { return $this->roles; } + /** + * Add a UserIdentifier entity to this User's collection of identifiers. + * This method also sets the UserIdentifier's parentUser. + * @param \UserIdentifier $userIdentifier + */ + public function addUserIdentifierDoJoin($userIdentifier) { + $this->userIdentifiers[] = $userIdentifier; + $userIdentifier->_setParentUser($this); + } + } diff --git a/lib/Doctrine/entities/UserIdentifier.php b/lib/Doctrine/entities/UserIdentifier.php new file mode 100644 index 000000000..38452167b --- /dev/null +++ b/lib/Doctrine/entities/UserIdentifier.php @@ -0,0 +1,107 @@ + + * A unique constraint is defined on the DB preventing duplicate keys for a given user. + * This allows the pairs to be upadated based enitrely on the key name and entity + * unique identifier, rather than needing the custom ID. + *

+ * When the owning parent User is deleted, its UserIdentifiers + * are also cascade-deleted. + * + * @Entity @Table(name="User_Identifiers", uniqueConstraints={@UniqueConstraint(name="user_keypairs", columns={"parentUser_id", "keyName"})}) + */ +class UserIdentifier { + + /** @Id @Column(type="integer") @GeneratedValue */ + protected $id; + + /** + * Bidirectional - Many UserIdentifiers (SIDE THAT OWNS FK) + * can be linked to one User (OWNING ORM SIDE). + * + * @ManyToOne(targetEntity="User", inversedBy="userIdentifiers") + * @JoinColumn(name="parentUser_id", referencedColumnName="id", onDelete="CASCADE") + */ + protected $parentUser = null; + + /** @Column(type="string", nullable=false) */ + protected $keyName = null; + + /** @Column(type="string", nullable=true, unique=true) */ + protected $keyValue = null; + + public function __construct() { + } + + /** + * Get the owning parent {@see User}. When the User is deleted, + * these identifiers are also cascade deleted. + * @return \User + */ + public function getParentUser() { + return $this->parentUser; + } + + /** + * Get the key name, usually a simple alphanumeric name, but this is not + * enforced by the entity. + * @return string + */ + public function getKeyName() { + return $this->keyName; + } + + /** + * Get the key value, can contain any char. + * @return String + */ + public function getKeyValue() { + return $this->keyValue; + } + + /** + * @return int The PK of this entity or null if not persisted + */ + public function getId() { + return $this->id; + } + + /** + * Do not call in client code, always use the opposite + * $user->addUserIdentifierDoJoin($userIdentifier) + * instead which internally calls this method to keep the bidirectional + * relationship consistent. + *

+ * This is the OWNING side of the ORM relationship so this method WILL + * establish the relationship in the database. + * + * @param \User $user + */ + public function _setParentUser(\User $user) { + $this->parentUser = $user; + } + + /** + * The custom keyname of this key=value pair. + * This value should be a simple alphanumeric name without special chars, but + * this is not enforced here by the entity. + * @param string $keyName + */ + public function setKeyName($keyName) { + $this->keyName = $keyName; + } + + /** + * The custom value of this key=value pair. + * This value can contain any chars. + * @param string $keyValue + */ + public function setKeyValue($keyValue) { + $this->keyValue = $keyValue; + } + +} From 7a44029de2c48f39125fafd970a0260fbeb1ab96 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Wed, 4 Aug 2021 11:12:03 +0000 Subject: [PATCH 22/74] Add functions to add identifiers to users These functions allow identifiers to be added to users as key/value pairs, containing the authentication type and associated ID string. The functions are based on similar functions used to add extension properties to sites and services, and will require revisiting (e.g. validation). When adding the first identifier, certificateDn is currently overwritten by user ID, so the value is non-null and unique. An alternative placeholder may be used later. --- lib/Gocdb_Services/User.php | 138 ++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index 81f673f60..74a3b5a73 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -450,6 +450,144 @@ public function getUsers($surname=null, $forename=null, $dn=null, $isAdmin=null) return $users; } + /** + * Adds an identifier to a user. + * @param \User $user user having identifier added + * @param array $identifierArr identifier name and value + * @param \User $currentUser user adding the identifier + * @throws \Exception + */ + public function addUserIdentifier(\User $user, array $identifierArr, \User $currentUser) { + // Check the portal is not in read only mode, throws exception if it is + $this->checkPortalIsNotReadOnlyOrUserIsAdmin($user); + + // Check to see whether the current user can edit this user + $this->editUserAuthorization($user, $currentUser); + + // Add the identifier + $this->em->getConnection()->beginTransaction(); + try { + $this->addUserIdentifierLogic($user, $identifierArr); + $this->em->flush(); + $this->em->getConnection()->commit(); + } catch (\Exception $e) { + $this->em->getConnection()->rollback(); + $this->em->close(); + throw $e; + } + } + + /** + * Logic to add an identifier to a user. + * @param \User $user user having identifier added + * @param array $identifierArr identifier name and value + * @throws \Exception + */ + protected function addUserIdentifierLogic(\User $user, array $identifierArr) { + + // We will use this variable to track the keys as we go along, this will be used check they are all unique later + $keys = array(); + + $existingIdentifiers = $user->getUserIdentifiers(); + + // We will use this variable to track the final number of identifiers and ensure we do not exceede the specified limit + $identifierCount = count($existingIdentifiers); + + // Trim off any trailing and leading whitespace + $keyName = trim($identifierArr[0]); + $keyValue = trim($identifierArr[1]); + + // $this->addUserIdentifierValidation(); + + /* Find out if an identifier with the provided key already exists for this user + * If it does, we will throw an exception + */ + $identifier = null; + foreach ($existingIdentifiers as $existIdentifier) { + if ($existIdentifier->getKeyName() === $keyName) { + $identifier = $existIdentifier; + } + } + + /* If the identifier does not already exist, we add it + * If it already exists, we throw an exception + */ + if (is_null($identifier)) { + $identifier = new \UserIdentifier(); + $identifier->setKeyName($keyName); + $identifier->setKeyValue($keyValue); + $user->addUserIdentifierDoJoin($identifier); + $this->em->persist($identifier); + + // Increment the identifier counter to enable check against extension limit + $identifierCount++; + } else { + throw new \Exception("An identifier with name \"$keyName\" already exists for this object, no identifiers were added."); + } + + // Add the key to the keys array, to enable unique check + $keys[] = $keyName; + + // Keys should be unique, create an exception if they are not + if (count(array_unique($keys)) !== count($keys)) { + throw new \Exception( + "Identifier names should be unique. The requested new identifiers include multiple identifiers with the same name." + ); + } + + // Check to see if adding the new identifiers will exceed the max limit defined in local_info.xml, and throw an exception if so + $extensionLimit = \Factory::getConfigService()->getExtensionsLimit(); + if ($identifierCount > $extensionLimit) { + throw new \Exception("Identifier could not be added due to the extension limit of $extensionLimit"); + } + } + + /** + * Migrates a user's identifier from certificateDn to UserIdentifiers. + * certificateDn is overwritten with a placeholder, before the user's + * ID string and its auth type are added as an identifier + * @param \User $user user having first identifier added + * @param array $identifierArr identifier name and value + * @param \User $currentUser user adding the identifier + * @throws \Exception + */ + public function migrateUserCredentials(\User $user, array $identifierArr, \User $currentUser) { + // Check the portal is not in read only mode, throws exception if it is + $this->checkPortalIsNotReadOnlyOrUserIsAdmin($user); + + // Check to see whether the current user can edit this user + $this->editUserAuthorization($user, $currentUser); + + // Check the user identifier being added corresponds to the current certificateDn + $idString = trim($identifierArr[1]); + if ($idString !== $user->getCertificateDn()) { + throw new \Exception("ID string must match the current certificateDn"); + } + + // Overwrite certificateDn and add the identifier + $this->em->getConnection()->beginTransaction(); + try { + $this->setDefaultCertDn($user); + $this->addUserIdentifierLogic($user, $identifierArr); + $this->em->flush(); + $this->em->getConnection()->commit(); + } catch (\Exception $e) { + $this->em->getConnection()->rollback(); + $this->em->close(); + throw $e; + } + } + + /** + * Overwrites a user's certificateDn to a default value + * Currently set to the user's ID + * @param \User $user user having certificate DN overwritten + * @throws \Exception + */ + private function setDefaultCertDn(\User $user) { + $user->setCertificateDn($user->getId()); + } + /** * Changes the isAdmin user property. * @param \User $user The user who's admin status is to change From 533defa2021e44e99fdea3650a853280fe8fc53a Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Wed, 4 Aug 2021 11:36:11 +0000 Subject: [PATCH 23/74] Look up users using user identifiers getUserByPrinciple has been repurposed to query user identifiers rather than certificateDns when searching for a user via their ID string. This ensures identifiers are correctly used throughout the code, as in almost all cases where the ID string is known, it should be stored as an identifier. The original function has been renamed so it can be used in cases such as login where an identifier may not yet exist. --- .../components/Get_User_Principle.php | 14 ++++++-- lib/Gocdb_Services/User.php | 36 ++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/htdocs/web_portal/components/Get_User_Principle.php b/htdocs/web_portal/components/Get_User_Principle.php index efbc20fe2..f2f323f4f 100644 --- a/htdocs/web_portal/components/Get_User_Principle.php +++ b/htdocs/web_portal/components/Get_User_Principle.php @@ -150,11 +150,19 @@ function Get_User_Principle(){ MyStaticPrincipleHolder::getInstance()->setPrincipleString($principleString); MyStaticAuthTokenHolder::getInstance()->setAuthToken($auth); + $serv = \Factory::getUserService(); + + // Get user by searching user identifiers + $user = $serv->getUserByPrinciple($principleString); + + // If cannot find user, search certificate DNs instead + if ($user === null) { + $user = $serv->getUserByCertificateDn($principleString); + // Is user registered/known in the DB? if true, update their last login time // once for the current request. - $user = \Factory::getUserService()->getUserByPrinciple($principleString); - if($user != null){ - \Factory::getUserService()->updateLastLoginTime($user); + if ($user !== null) { + $serv->updateLastLoginTime($user); } return $principleString; } diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index 74a3b5a73..c3dbd10c0 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -49,19 +49,37 @@ public function getUser($id) { } /** - * Lookup a User object by user's principle id string. - * @param string $userPrinciple the user's principle id string, e.g. DN. + * Lookup a User object by user's ID string, stored in certificateDn. + * @param string $userPrinciple the user's principle ID string, e.g. DN. * @return User object or null if no user can be found with the specified principle */ - public function getUserByPrinciple($userPrinciple){ - if(empty($userPrinciple)){ - return null; - } - $dql = "SELECT u from User u WHERE u.certificateDn = :certDn"; - $user = $this->em->createQuery($dql) + public function getUserByCertificateDn($userPrinciple) { + if (empty($userPrinciple)) { + return null; + } + $dql = "SELECT u from User u WHERE u.certificateDn = :certDn"; + $user = $this->em->createQuery($dql) ->setParameter(":certDn", $userPrinciple) ->getOneOrNullResult(); - return $user; + return $user; + } + + /** + * Lookup a User object by user's principle ID string from UserIdentifier. + * @param string $userPrinciple the user's principle ID string, e.g. DN. + * @return User object or null if no user can be found with the specified principle + */ + public function getUserByPrinciple($userPrinciple) { + if (empty($userPrinciple)) { + return null; + } + + $dql = "SELECT u FROM User u JOIN u.userIdentifiers up WHERE up.keyValue = :keyValue"; + $user = $this->em->createQuery($dql) + ->setParameters(array('keyValue' => $userPrinciple)) + ->getOneOrNullResult(); + + return $user; } /** From 21d8e524c7de4dd4ee969bb753cda5b5084c6a3f Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 26 Aug 2021 10:57:56 +0000 Subject: [PATCH 24/74] Add user identifier when logging in Check if user identifiers exist when attempting to log in. If not, but the user is registered, add their ID string and auth type as a new identifier and overwrite certificateDn. --- htdocs/web_portal/components/Get_User_Principle.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/htdocs/web_portal/components/Get_User_Principle.php b/htdocs/web_portal/components/Get_User_Principle.php index f2f323f4f..859a24cad 100644 --- a/htdocs/web_portal/components/Get_User_Principle.php +++ b/htdocs/web_portal/components/Get_User_Principle.php @@ -158,11 +158,23 @@ function Get_User_Principle(){ // If cannot find user, search certificate DNs instead if ($user === null) { $user = $serv->getUserByCertificateDn($principleString); + $authExists = False; + } else { + $authExists = True; + } // Is user registered/known in the DB? if true, update their last login time // once for the current request. if ($user !== null) { $serv->updateLastLoginTime($user); + + // If identifier for current auth does not exist, add to user + if (!$authExists) { + // Get type of auth logged in with e.g. IGTF) + $authType = $auth->getDetails()['AuthenticationRealm'][0]; + $identifierArr = array($authType, $principleString); + $serv->migrateUserCredentials($user, $identifierArr, $user); + } } return $principleString; } From 982d731d6952037078ad7e0346e9c3a986c61716 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 26 Aug 2021 10:59:18 +0000 Subject: [PATCH 25/74] Add user identifiers on registration When a new user registers, their details are now added as a user identifier rather than being stored in certificateDn. However, it is currently necessary to temporarily populate certificateDn, as the user does not have an ID before they are created, and the field is required. --- .../web_portal/controllers/user/register.php | 27 +++++++++-------- htdocs/web_portal/views/user/register.php | 2 +- lib/Gocdb_Services/User.php | 30 ++++++++++++------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/htdocs/web_portal/controllers/user/register.php b/htdocs/web_portal/controllers/user/register.php index 6c76ba73d..dd87a5920 100644 --- a/htdocs/web_portal/controllers/user/register.php +++ b/htdocs/web_portal/controllers/user/register.php @@ -44,12 +44,12 @@ function register() { */ function draw() { $serv = \Factory::getUserService(); - $dn = Get_User_Principle(); - if(empty($dn)){ + $idString = Get_User_Principle(); + if (empty($idString)) { show_view('error.php', "Could not authenticate user - null user principle"); die(); } - $user = $serv->getUserByPrinciple($dn); + $user = $serv->getUserByPrinciple($idString); if(!is_null($user)) { show_view('error.php', "Only unregistered users can register"); @@ -70,29 +70,30 @@ function draw() { $params['given_name'] = $_SERVER['OIDC_CLAIM_given_name']; $params['family_name'] = $_SERVER['OIDC_CLAIM_family_name']; $params['email'] = $userEmail; - $params['dn'] = $dn; + $params['idString'] = $idString; show_view('user/register.php', $params); } function submit() { - $values = getUserDataFromWeb(); + $userValues = getUserDataFromWeb(); - $dn = Get_User_Principle(); - if(empty($dn)){ + $idString = Get_User_Principle(); + if (empty($idString)) { show_view('error.php', "Could not authenticate user - null user principle"); die(); } - $values['CERTIFICATE_DN'] = $dn; + $userIdentifierValues['VALUE'] = $idString; - // todo: on registering, we also want to persist the authAttributes, this - // will require new UserProperty records owned by the User.php entity. /* @var $authToken \org\gocdb\security\authentication\IAuthentication */ - //$authToken = Get_User_AuthToken(); - //$params['authAttributes'] = $authToken->getDetails(); + $authToken = Get_User_AuthToken(); + $params['authAttributes'] = $authToken->getDetails(); + $authType = $params['authAttributes']['AuthenticationRealm'][0]; + + $userIdentifierValues['NAME'] = $authType; $serv = \Factory::getUserService(); try { - $user = $serv->register($values); + $user = $serv->register($userValues, $userIdentifierValues); $params = array('user' => $user); show_view('user/registered.php', $params); } catch(Exception $e) { diff --git a/htdocs/web_portal/views/user/register.php b/htdocs/web_portal/views/user/register.php index d1fc8c319..fef035762 100644 --- a/htdocs/web_portal/views/user/register.php +++ b/htdocs/web_portal/views/user/register.php @@ -3,7 +3,7 @@ onsubmit="return confirm('Click OK to confirm your agreement to the terms and conditions of GOCDB account registration.');">

Register


- Register Unique Identity: + Register Unique Identity:

diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index c3dbd10c0..d0d0c2255 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -340,11 +340,16 @@ private function validateUser($userData) { * [SURNAME] => TestFace * [EMAIL] => JCasson@gmail.com * [TELEPHONE] => 01235 44 5010 - * [CERTIFICATE_DN] => /C=UK/O=eScience/OU=CLRC/L=RAL/CN=claire devereuxxxx * ) - * @param array $values User details, defined above + * Array + * ( + * [NAME] => IGTF + * [VALUE] => /C=UK/O=eScience/OU=CLRC/L=RAL/CN=a person + * ) + * @param array $userValues User details, defined above + * @param array $userIdentifierValues User Identifier details, defined above */ - public function register($values) { + public function register($userValues, $userIdentifierValues) { // validate the input fields for the user $this->validateUser($values); @@ -357,21 +362,24 @@ public function register($values) { //Explicity demarcate our tx boundary $this->em->getConnection()->beginTransaction(); $user = new \User(); + $identifierArr = array($userIdentifierValues['NAME'], $userIdentifierValues['VALUE']); try { - $user->setTitle($values['TITLE']); - $user->setForename($values['FORENAME']); - $user->setSurname($values['SURNAME']); - $user->setEmail($values['EMAIL']); - $user->setTelephone($values['TELEPHONE']); - $user->setCertificateDn($values['CERTIFICATE_DN']); + $user->setTitle($userValues['TITLE']); + $user->setForename($userValues['FORENAME']); + $user->setSurname($userValues['SURNAME']); + $user->setEmail($userValues['EMAIL']); + $user->setTelephone($userValues['TELEPHONE']); + $user->setCertificateDn($userIdentifierValues['VALUE']); $user->setAdmin(false); $this->em->persist($user); $this->em->flush(); + $this->migrateUserCredentials($user, $identifierArr, $user); + $this->em->flush(); $this->em->getConnection()->commit(); - } catch (\Exception $ex) { + } catch (\Exception $e) { $this->em->getConnection()->rollback(); $this->em->close(); - throw $ex; + throw $e; } return $user; } From d9ffda22dbaa7332b52717ad11ccf3f3e876dae4 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 26 Aug 2021 17:01:03 +0000 Subject: [PATCH 26/74] Create entity to store identity linking requests The LinkIdentityRequest entity has a similar role to the RetrieveAccountRequest entity, storing the user that new log-in details are to be added to, as well as the required confirmation code and the user's ID string. Unlike RetrieveAccountRequest, this entity may also store the user that created the request (as they may or may not be registered), as well as the authentication types of the identifiers, and the time the request was created. --- lib/Doctrine/bootstrap.php | 2 +- lib/Doctrine/entities/LinkIdentityRequest.php | 198 ++++++++++++++++++ .../entities/RetrieveAccountRequest.php | 109 ---------- 3 files changed, 199 insertions(+), 110 deletions(-) create mode 100644 lib/Doctrine/entities/LinkIdentityRequest.php delete mode 100644 lib/Doctrine/entities/RetrieveAccountRequest.php diff --git a/lib/Doctrine/bootstrap.php b/lib/Doctrine/bootstrap.php index 153a7c283..f2e307944 100644 --- a/lib/Doctrine/bootstrap.php +++ b/lib/Doctrine/bootstrap.php @@ -30,7 +30,6 @@ require_once $entitiesPath."/Project.php"; require_once $entitiesPath."/ServiceGroup.php"; require_once $entitiesPath."/ServiceGroupProperty.php"; -require_once $entitiesPath."/RetrieveAccountRequest.php"; require_once $entitiesPath."/PrimaryKey.php"; require_once $entitiesPath."/ArchivedNGI.php"; require_once $entitiesPath."/ArchivedService.php"; @@ -40,6 +39,7 @@ require_once $entitiesPath."/RoleActionRecord.php"; require_once $entitiesPath."/APIAuthentication.php"; require_once $entitiesPath."/UserIdentifier.php"; +require_once $entitiesPath."/LinkIdentityRequest.php"; //if (!class_exists("Doctrine\Common\Version", false)) { // require_once __DIR__."/bootstrap_doctrine.php"; diff --git a/lib/Doctrine/entities/LinkIdentityRequest.php b/lib/Doctrine/entities/LinkIdentityRequest.php new file mode 100644 index 000000000..0e2748f5f --- /dev/null +++ b/lib/Doctrine/entities/LinkIdentityRequest.php @@ -0,0 +1,198 @@ + + * Users may want to link two or more auth mechanisms to a single account. + * This record stores the relevant data needed to do this, including + * a confirmation code that is sent to the user's existing email + * address - they need to provide the code to complete the identity linking transaction. + * + * @Entity @Table(name="LinkIdentityRequests") + */ +class LinkIdentityRequest { + + /** @Id @Column(type="integer") @GeneratedValue */ + protected $id; + + /** + * @OneToOne(targetEntity="User") + * @JoinColumn(name="primaryUserId", referencedColumnName="id", onDelete="CASCADE", nullable=false) + */ + protected $primaryUser; + + /** @Column(type="string") */ + protected $primaryIdString; + + /** + * @OneToOne(targetEntity="User") + * @JoinColumn(name="currentUserId", referencedColumnName="id", onDelete="CASCADE", nullable=true) + */ + protected $currentUser; + + /** @Column(type="string") */ + protected $currentIdString; + + /** @Column(type="string") */ + protected $confirmCode; + + /** @Column(type="string") */ + protected $primaryAuthType; + + /** @Column(type="string") */ + protected $currentAuthType; + + /** @Column(type="datetime", nullable=false) */ + protected $creationDate; + + public function __construct(\User $primaryUser, $currentUser, $code, $primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType) { + $this->creationDate = new \DateTime("now"); + $this->setPrimaryUser($primaryUser); + $this->setCurrentUser($currentUser); + $this->setConfirmCode($code); + $this->setPrimaryIdString($primaryIdString); + $this->setCurrentIdString($currentIdString); + $this->setPrimaryAuthType($primaryAuthType); + $this->setCurrentAuthType($currentAuthType); + } + + /** + * @return int The PK of this entity or null if not persisted + */ + public function getId() { + return $this->id; + } + + /** + * Get the User which is having an identity added to it. + * @return \User + */ + public function getPrimaryUser() { + return $this->primaryUser; + } + + /** + * Get the User whose log-in is being added to the primary User. + * Can be null if user not yet registered. + * @return \User + */ + public function getCurrentUser() { + return $this->currentUser; + } + + /** + * Return the confirmation code that is used to authenticate the user. + * This code is sent to the primary user's email address - they need to + * provide the code to complete the identity linking transaction. + * @return string + */ + public function getConfirmCode() { + return $this->confirmCode; + } + + /** + * Get the ID string of the user who is having a new ID string added. + * @return string + */ + public function getPrimaryIdString() { + return $this->primaryIdString; + } + + /** + * Get the ID string to be added to the primary user. + * @return string + */ + public function getCurrentIdString() { + return $this->currentIdString; + } + + /** + * Get the auth type of the primary user. + * @return string + */ + public function getPrimaryAuthType() { + return $this->primaryAuthType; + } + + /** + * Get the auth type of the current user. + * @return string + */ + public function getCurrentAuthType() { + return $this->currentAuthType; + } + + /** + * Get the DateTime when the request was created. + * @return \DateTime + */ + public function getCreationDate() { + return $this->creationDate; + } + + /** + * Set the primary user. + * @param \User $primaryUser + */ + public function setPrimaryUser($primaryUser) { + $this->primaryUser = $primaryUser; + } + + /** + * Set the current user. + * @param \User $currentUser + */ + public function setCurrentUser($currentUser) { + $this->currentUser = $currentUser; + } + + /** + * Set the confirmation code that is used to authenticate the user. + * This code is sent to the primary user's email address - they need to + * provide the code to complete the identity linking transaction. + * @param string $code + */ + public function setConfirmCode($code) { + $this->confirmCode = $code; + } + + /** + * Set the ID string of the primary user account. + * @param string $primaryIdString + */ + public function setPrimaryIdString($primaryIdString) { + $this->primaryIdString = $primaryIdString; + } + + /** + * Set the ID string of the current user account. + * @param string $currentIdString + */ + public function setCurrentIdString($currentIdString) { + $this->currentIdString = $currentIdString; + } + + /** + * Set the auth type of the primary user account. + * @param string $primaryAuthType + */ + public function setPrimaryAuthType($primaryAuthType) { + $this->primaryAuthType = $primaryAuthType; + } + + /** + * Set the auth type of the current user account. + * @param string $currentAuthType + */ + public function setCurrentAuthType($currentAuthType) { + $this->currentAuthType = $currentAuthType; + } + + /** + * Set the DateTime when the request was created. + * @param \DateTime $creationDate + */ + public function setCreationDate($creationDate) { + $this->creationDate = $creationDate; + } +} \ No newline at end of file diff --git a/lib/Doctrine/entities/RetrieveAccountRequest.php b/lib/Doctrine/entities/RetrieveAccountRequest.php deleted file mode 100644 index ddfae290f..000000000 --- a/lib/Doctrine/entities/RetrieveAccountRequest.php +++ /dev/null @@ -1,109 +0,0 @@ - - * Users may need to retrieve their old account when their ID/DN has changed. - * This record stores the relevant data needed to retrieve their old account, - * including a confirmation code that is sent to the user's existing email - * address - they need to provide the code to complete the account retrieval transaction. - * - * @author John Casson - * @author David Meredith - * @Entity @Table(name="RetrieveAccountRequests", options={"collate"="utf8mb4_bin", "charset"="utf8mb4"}) - */ -class RetrieveAccountRequest { - - /** @Id @Column(type="integer") @GeneratedValue */ - protected $id; - - /** - * @OneToOne(targetEntity="User") - * @JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") - */ - protected $user; - - /** @Column(type="string") */ - protected $newDn; - - /** @Column(type="string") */ - protected $confirmCode; - - public function __construct(\User $user, $code, $newDn) { - $this->setUser($user); - $this->setConfirmCode($code); - $this->setNewDn($newDn); - } - - /** - * @return int The PK of this entity or null if not persisted - */ - public function getId() { - return $this->id; - } - - /** - * Get the User who made this request. - * @return \User - */ - public function getUser() { - return $this->user; - } - - /** - * Return the confirmation code that is used to authenticate the user. - * This code is sent to the user's existing email address - they need to - * provide the code to complete the account retrieval transaction. - * @return string - */ - public function getConfirmCode() { - return $this->confirmCode; - } - - /** - * Get the updated user DN/ID that newly identifies the user account. - * @return string - */ - public function getNewDn() { - return $this->newDn; - } - - /** - * Set the user. - * @param \User $user - */ - public function setUser($user) { - $this->user = $user; - } - - /** - * Set the confirmation code that is used to authenticate the user. - * This code is sent to the user's existing email address - they need to - * provide the code to complete the account retrieval transaction. - * @param string $code - */ - public function setConfirmCode($code) { - $this->confirmCode = $code; - } - - /** - * Set the new DN/ID string of the user account. - * @param string $dn - */ - public function setNewDn($dn) { - $this->newDn = $dn; - } - -} From 9f9f3d0dc34639e8265a28aaa09bb3d1ec8e3e8a Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Wed, 4 Aug 2021 15:12:17 +0000 Subject: [PATCH 27/74] Add function to return the user's current authentication type Functionality is similar to accessing the user's principle string. --- htdocs/web_portal/components/Get_User_Principle.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/htdocs/web_portal/components/Get_User_Principle.php b/htdocs/web_portal/components/Get_User_Principle.php index 859a24cad..d4239d02e 100644 --- a/htdocs/web_portal/components/Get_User_Principle.php +++ b/htdocs/web_portal/components/Get_User_Principle.php @@ -119,6 +119,18 @@ function Get_User_AuthToken(){ return null; } +/** + * Get the auth type for the user. + * @return string or null if can't authenticate request */ +function Get_User_AuthType() { + $authType = null; + $auth = Get_User_AuthToken(); + if ($auth !== null) { + $authType = $auth->getDetails()['AuthenticationRealm'][0]; + } + return $authType; +} + /** * Get the user's principle string (x509 DN from certificate or from SAML attribute). *

From 037e16cad456d2df2e334e55b576c6137665e52d Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 26 Aug 2021 17:02:23 +0000 Subject: [PATCH 28/74] Add functions to delete, edit and validate user identifiers Added functions which allow the identifiers of a user to be edited and deleted. This functionality will be used for account recovery, as well as potentially admin user editing. Also added general validation for identifier names and values, as well as specific requirements when adding or editing an identifier, including enforcing unique ID strings. --- config/gocdb_schema.xml | 20 ++++ lib/Gocdb_Services/User.php | 209 +++++++++++++++++++++++++++++++++--- 2 files changed, 217 insertions(+), 12 deletions(-) diff --git a/config/gocdb_schema.xml b/config/gocdb_schema.xml index f1681b8cf..d35de6d48 100644 --- a/config/gocdb_schema.xml +++ b/config/gocdb_schema.xml @@ -482,6 +482,26 @@ + + useridentifier + + + NAME + 255 + + /^[a-zA-Z0-9@_\-\[\]\+\.]+(\s*[a-zA-Z0-9@_\-\[\]\+\.]+)*$/ + + + + VALUE + 255 + + + + /^[^`'\"<>\s]+(\s*[^`'\"<>\s]+)*$/ + + + serviceproperty diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index d0d0c2255..58c58f298 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -262,7 +262,7 @@ public function editUser(\User $user, $newValues, \User $currentUser = null) { $this->editUserAuthorization($user, $currentUser); // validate the input fields for the user - $this->validateUser($newValues); + $this->validate($newValues, 'user'); //Explicity demarcate our tx boundary $this->em->getConnection()->beginTransaction(); @@ -320,12 +320,12 @@ public function editUserAuthorization(\User $user, \User $currentUser = null) { * validated. The \Exception message will contain a human * readable description of which field failed validation. * @return null */ - private function validateUser($userData) { + private function validate($userData, $type) { require_once __DIR__ .'/Validate.php'; $serv = new \org\gocdb\services\Validate(); foreach($userData as $field => $value) { - $valid = $serv->validate('user', $field, $value); - if(!$valid) { + $valid = $serv->validate($type, $field, $value); + if (!$valid) { $error = "$field contains an invalid value: $value"; throw new \Exception($error); } @@ -351,13 +351,7 @@ private function validateUser($userData) { */ public function register($userValues, $userIdentifierValues) { // validate the input fields for the user - $this->validateUser($values); - - // Check the DN isn't already registered - $user = $this->getUserByPrinciple($values['CERTIFICATE_DN']); - if(!is_null($user)) { - throw new \Exception("DN is already registered in GOCDB"); - } + $this->validate($userValues, 'user'); //Explicity demarcate our tx boundary $this->em->getConnection()->beginTransaction(); @@ -523,7 +517,7 @@ protected function addUserIdentifierLogic(\User $user, array $identifierArr) { $keyName = trim($identifierArr[0]); $keyValue = trim($identifierArr[1]); - // $this->addUserIdentifierValidation(); + $this->addUserIdentifierValidation($keyName, $keyValue); /* Find out if an identifier with the provided key already exists for this user * If it does, we will throw an exception @@ -614,6 +608,197 @@ private function setDefaultCertDn(\User $user) { $user->setCertificateDn($user->getId()); } + /** + * Validation when adding a user identifier + * @param string $keyName + * @param string $keyValue + * @throws \Exception + */ + protected function addUserIdentifierValidation($keyName, $keyValue) { + // Validate against schema + $validateArray['NAME'] = $keyName; + $validateArray['VALUE'] = $keyValue; + $this->validate($validateArray, 'useridentifier'); + + // Check the ID string does not already exist + $this->valdidateUniqueIdString($keyValue); + + // Check auth type is valid + $this->valdidateAuthType($keyName); + } + + /** + * Edit a user's identifier. + * @param \User $user user that owns the identifier + * @param \UserIdentifier $identifier identifier being edited + * @param array $newIdentifierArr new key and/or value for the identifier + * @param \User $currentUser user editing the identifier + * @throws \Exception + */ + public function editUserIdentifier(\User $user, \UserIdentifier $identifier, array $newIdentifierArr, \User $currentUser) { + // Check the portal is not in read only mode, throws exception if it is + $this->checkPortalIsNotReadOnlyOrUserIsAdmin($currentUser); + + // Check to see whether the current user can edit this user + $this->editUserAuthorization($user, $currentUser); + + // Make the change + $this->em->getConnection()->beginTransaction(); + try { + $this->editUserIdentifierLogic($user, $identifier, $newIdentifierArr); + $this->em->flush (); + $this->em->getConnection ()->commit(); + } catch (\Exception $e) { + $this->em->getConnection ()->rollback(); + $this->em->close (); + throw $e; + } + } + + /** + * Logic to edit a user's identifier, without the user validation. + * Validation of the edited identifier values is performed by a seperate function. + * @param \User $user user that owns the identifier + * @param \UserIdentifier $identifier identifier being edited + * @param array $newIdentifierArr new key and/or value for the identifier + * @throws \Exception + */ + protected function editUserIdentifierLogic(\User $user, \UserIdentifier $identifier, array $newIdentifierArr) { + + // Trim off trailing and leading whitespace + $keyName = trim($newIdentifierArr[0]); + $keyValue = trim($newIdentifierArr[1]); + + // Validate new identifier + $this->editUserIdentifierValidation($user, $identifier, $keyName, $keyValue); + + // Set the user identifier values + $identifier->setKeyName($keyName); + $identifier->setKeyValue($keyValue); + $this->em->merge($identifier); + } + + /** + * Validation when editing a user's identifier + * @param \User $user + * @param \UserIdentifier $identifier + * @param string $keyName + * @param string $keyValue + * @throws \Exception + */ + protected function editUserIdentifierValidation(\User $user, \UserIdentifier $identifier, $keyName, $keyValue) { + + // Validate new values against schema + $validateArray['NAME'] = $keyName; + $validateArray['VALUE'] = $keyValue; + $this->validate($validateArray, 'useridentifier'); + + // Check that the identifier is owned by the user + if ($identifier->getParentUser() !== $user) { + $id = $identifier->getId(); + throw new \Exception("Identifier {$id} does not belong to the specified user"); + } + + // Check the identifier has changed + if ($keyName === $identifier->getKeyName() && $keyValue === $identifier->getKeyValue()) { + throw new \Exception("The specified user identifier is the same as the current user identifier"); + } + + // Check the ID string is unique if it is being changed + if ($keyValue !== $identifier->getKeyValue()) { + $this->valdidateUniqueIdString($keyValue); + } + + // Check auth type is valid + $this->valdidateAuthType($keyName); + + // If the identifiers key has changed, check there isn't an existing identifier with that key + if ($keyName !== $identifier->getKeyName()) { + $existingIdentifiers = $user->getUserIdentifiers(); + foreach ($existingIdentifiers as $existingIdentifier) { + if ($existingIdentifier->getKeyName() === $keyName) { + throw new \Exception("An identifier with that name already exists for this object"); + } + } + } + } + + /** + * Validate authentication type based on known list. + * @param string $authType + * @throws \Exception + */ + protected function valdidateAuthType($authType) { + if (!in_array($authType, $this->getAuthTypes(false))) { + throw new \Exception("The authentication type entered is invalid"); + } + } + + /** + * Validate ID string is unique. + * Checks both user identifiers and certificateDns + * @param string $idString + * @throws \Exception + */ + protected function valdidateUniqueIdString($idString) { + $oldUser = $this->getUserByCertificateDn($idString); + $newUser = $this->getUserByPrinciple($idString); + if (!is_null($oldUser) || !is_null($newUser)) { + throw new \Exception("ID string is already registered in GOCDB"); + } + } + + /** + * Delete a user identifier + * Validates the user has permission, then calls the required logic + * @param \User $user user having the identifier deleted + * @param \UserIdentifier $identifier identifier being deleted + * @param \User $currentUser user deleting the identifier + */ + public function deleteUserIdentifier(\User $user, \UserIdentifier $identifier, \User $currentUser) { + //Check the portal is not in read only mode, throws exception if it is + $this->checkPortalIsNotReadOnlyOrUserIsAdmin($user); + + // Check to see whether the current user can edit this user + $this->editUserAuthorization($user, $currentUser); + + // Make the change + $this->em->getConnection()->beginTransaction(); + try { + $this->deleteUserIdentifierLogic($user, $identifier); + $this->em->flush(); + $this->em->getConnection()->commit(); + } catch (\Exception $e) { + $this->em->getConnection()->rollback(); + $this->em->close(); + throw $e; + } + } + + /** + * Logic to delete a user's identifier + * Before deletion a check is done to confirm the identifier is from the parent user + * specified by the request, and an exception is thrown if this is not the case + * @param \User $user user having the identifier deleted + * @param \UserIdentifier $identifier identifier being deleted + */ + protected function deleteUserIdentifierLogic(\User $user, \UserIdentifier $identifier) { + // Check that the identifier's parent user is the same as the one given + if ($identifier->getParentUser() !== $user) { + $id = $identifier->getId(); + throw new \Exception("Identifier {$id} does not belong to the specified user"); + } + // Check the user has more than one identifier + if (count($user->getUserIdentifiers()) < 2) { + throw new \Exception("Users must have at least one identity string."); + } + // User is the owning side so remove elements from the user + $user->getUserIdentifiers()->removeElement($identifier); + + // Once relationship is removed, delete the actual element + $this->em->remove($identifier); + } + /** * Changes the isAdmin user property. * @param \User $user The user who's admin status is to change From f07a03513e1f46855cbff7334010640ff8753d87 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 5 Aug 2021 09:22:49 +0000 Subject: [PATCH 29/74] Change authentication realm name for certificates The authentication realms are becoming more visible to users, so this is intended to make the name more user friendly. --- htdocs/web_portal/components/Get_User_Principle.php | 2 +- lib/Authentication/AuthTokens/X509AuthenticationToken.php | 2 +- lib/Gocdb_Services/User.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/web_portal/components/Get_User_Principle.php b/htdocs/web_portal/components/Get_User_Principle.php index d4239d02e..98c64a004 100644 --- a/htdocs/web_portal/components/Get_User_Principle.php +++ b/htdocs/web_portal/components/Get_User_Principle.php @@ -182,7 +182,7 @@ function Get_User_Principle(){ // If identifier for current auth does not exist, add to user if (!$authExists) { - // Get type of auth logged in with e.g. IGTF) + // Get type of auth logged in with e.g. X.509) $authType = $auth->getDetails()['AuthenticationRealm'][0]; $identifierArr = array($authType, $principleString); $serv->migrateUserCredentials($user, $identifierArr, $user); diff --git a/lib/Authentication/AuthTokens/X509AuthenticationToken.php b/lib/Authentication/AuthTokens/X509AuthenticationToken.php index ce973f4a1..7329647c2 100644 --- a/lib/Authentication/AuthTokens/X509AuthenticationToken.php +++ b/lib/Authentication/AuthTokens/X509AuthenticationToken.php @@ -26,7 +26,7 @@ public function __construct() { //$this->logger->pushHandler(new StreamHandler(__DIR__.'/../../../gocdb.log', Logger::DEBUG)); $this->initialDN = $this->getDN(); - $this->userDetails = array('AuthenticationRealm' => array('IGTF')); + $this->userDetails = array('AuthenticationRealm' => array('X.509')); } /** diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index 58c58f298..ee36784c6 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -343,7 +343,7 @@ private function validate($userData, $type) { * ) * Array * ( - * [NAME] => IGTF + * [NAME] => X.509 * [VALUE] => /C=UK/O=eScience/OU=CLRC/L=RAL/CN=a person * ) * @param array $userValues User details, defined above From 675991637b2f533561398b9bffbda01c85bff43a Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 26 Aug 2021 17:14:00 +0000 Subject: [PATCH 30/74] Add function to get auth types, users and identifiers Rather than relisting possible auth types in multiple places, getAuthTypes offers a consistently ordered list. The order is updated based on the token order in MyConfig1, but for tokens with multiple realms, their order is hardcoded based on the current order within the tokens. getUserByPrincipleAndType behaves similarly to getUserByPrinciple, but ensures the auth type specified matches a user identifier. This is not typically required, but can be useful to confirm. getIdentifierByIdString allows identifiers to be accessed via their ID strings. --- lib/Gocdb_Services/User.php | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index ee36784c6..39fe707d7 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -82,6 +82,27 @@ public function getUserByPrinciple($userPrinciple) { return $user; } + /** + * Lookup a User object by user's principle ID string and auth type from UserIdentifier. + * @param string $userPrinciple the user's principle ID string, e.g. DN. + * @param string $authType the authorisation type e.g. X.509. + * @return User object or null if no user can be found with the specified principle + */ + public function getUserByPrincipleAndType($userPrinciple, $authType) { + if (empty($userPrinciple) || empty($authType)) { + return null; + } + + $dql = "SELECT u FROM User u JOIN u.userIdentifiers up + WHERE up.keyName = :keyName + AND up.keyValue = :keyValue"; + $user = $this->em->createQuery($dql) + ->setParameters(array('keyName' => $authType, 'keyValue' => $userPrinciple)) + ->getOneOrNullResult(); + + return $user; + } + /** * Updates the users last login time to the current time in UTC. * @param \User $user @@ -470,6 +491,68 @@ public function getUsers($surname=null, $forename=null, $dn=null, $isAdmin=null) return $users; } + /** + * Returns a single user identifier from its ID + * @param $id ID of user identifier + * @return \UserIdentifier + */ + public function getIdentifierById($id) { + $dql = "SELECT p FROM UserIdentifier p WHERE p.id = :ID"; + $identifier = $this->em->createQuery($dql)->setParameter('ID', $id)->getOneOrNullResult(); + return $identifier; + } + + /** + * Returns a single user identifier from its ID string + * @param $idString ID string of user identifier + * @return \UserIdentifier + */ + public function getIdentifierByIdString($idString) { + $dql = "SELECT p FROM UserIdentifier p WHERE p.keyValue = :IDSTRING"; + $identifier = $this->em->createQuery($dql)->setParameter('IDSTRING', $idString)->getOneOrNullResult(); + return $identifier; + } + + /** + * Returns list of authentication types + * List composed of AuthenticationRealms defined within tokens + * Order of tokens determined by order listed in MyConfig1 + * Order of realms hardcoded based on order within tokens + * @param bool $reducedRealms if true only return the "main" authentication types + * @return array of authentication types + */ + public function getAuthTypes($reducedRealms=true) { + + require_once __DIR__ . '/../Authentication/_autoload.php'; + // Get list of tokens in order they are currently used + $myConfig1 = new \org\gocdb\security\authentication\MyConfig1(); + $authTokenNames = $myConfig1->getAuthTokenClassList(); + + // Hardcoded authentication realms in same order as in token definitions + $x509Realms = ['X.509']; + if ($reducedRealms) { + $shibRealms = ['EGI Proxy IdP']; + } else { + $shibRealms = ['EUDAT_SSO_IDP', 'UK_ACCESS_FED', 'EGI Proxy IdP']; + } + $irisRealms = ['IRIS IAM - OIDC']; + + // Add auth types to a list in the correct order + $authTypes = array(); + foreach ($authTokenNames as $authTokenName) { + if (strpos($authTokenName, 'Shib') !== false) { + $authTypes = array_merge($authTypes, $shibRealms); + } + if (strpos($authTokenName, 'X509') !== false) { + $authTypes = array_merge($authTypes, $x509Realms); + } + if (strpos($authTokenName, 'IAM') !== false) { + $authTypes = array_merge($authTypes, $irisRealms); + } + } + return $authTypes; + } + /** * Adds an identifier to a user. * @param \User $user user having identifier added From ab8370800202ce3bb6a3f6ea538b69f4d3f9ee87 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Wed, 4 Aug 2021 16:22:37 +0000 Subject: [PATCH 31/74] Add views for identity linking process Created views for identity linking, similar to those used in the account retrieval process, which have been deleted. A javascript file is also used in a similar manner to the add_downtime view, for interactive elements. --- htdocs/web_portal/javascript/linking.js | 273 ++++++++++++++++++ .../web_portal/views/user/link_identity.php | 192 ++++++++++++ .../views/user/link_identity_accepted.php | 17 ++ .../views/user/link_identity_rejected.php | 20 ++ .../web_portal/views/user/linked_identity.php | 5 + .../views/user/retrieve_account.php | 29 -- .../views/user/retrieve_account_accepted.php | 6 - .../views/user/retrieved_account.php | 4 - 8 files changed, 507 insertions(+), 39 deletions(-) create mode 100644 htdocs/web_portal/javascript/linking.js create mode 100644 htdocs/web_portal/views/user/link_identity.php create mode 100644 htdocs/web_portal/views/user/link_identity_accepted.php create mode 100644 htdocs/web_portal/views/user/link_identity_rejected.php create mode 100644 htdocs/web_portal/views/user/linked_identity.php delete mode 100644 htdocs/web_portal/views/user/retrieve_account.php delete mode 100644 htdocs/web_portal/views/user/retrieve_account_accepted.php delete mode 100644 htdocs/web_portal/views/user/retrieved_account.php diff --git a/htdocs/web_portal/javascript/linking.js b/htdocs/web_portal/javascript/linking.js new file mode 100644 index 000000000..535634736 --- /dev/null +++ b/htdocs/web_portal/javascript/linking.js @@ -0,0 +1,273 @@ +$(document).ready(function() { + // Add the jQuery form change event handlers + $('#linkIdentityForm').find(":input").change(function() { + validate(); + }); +}); + +/** +* Updates the authentication type message +* Message depends on whether the selected auth type is the same as the auth type currently in use +* If auth types are the same, different severity of warnings depending on which type +* +* @returns {null} +*/ +function updateWarningMessage() { + var selectedAuthType = $('#authType').val(); + var currentAuthType = $('#currentAuthType').text(); + var authTypeText1 = ""; + var authTypeText2 = ""; + var authTypeText3 = ""; + + if (selectedAuthType !== null && selectedAuthType !== "") { + $('.authTypeShared').removeClass("hidden"); + } else { + $('.authTypeShared').addClass("hidden"); + } + + $('.authTextPlaceholder').addClass("hidden"); + $('.authTypeSelected').text(selectedAuthType); + + // Different warnings if selected auth type is same as method currently in use + if (selectedAuthType === currentAuthType) { + + $('#linkingDetails').addClass("hidden"); + $('#recoveryDetails').removeClass("hidden"); + $('#requestPlaceholder').addClass("hidden"); + $('#authTypeRecoverPlaceholder').addClass("hidden"); + + authTypeText1 = " is the same as your current authentication type."; + authTypeText3 = "account recovery"; + + // Stronger warning for certain types. Certificates will be less severe? + if (selectedAuthType === "X.509") { + authTypeText2 = "Are you sure you wish to continue?"; + $('#authTypeRecover').removeClass("hidden"); + $('#authTypeRecover').removeClass("auth-warning"); + $('#authTypeSelected').addClass("hidden"); + } else { + authTypeText2 = "These identifiers rarely expire. Are you sure you wish to continue?"; + $('#authTypeRecover').removeClass("hidden"); + $('#authTypeRecover').addClass("auth-warning"); + $('#authTypeSelected').removeClass("hidden"); + } + + } else { + + $('#linkingDetails').removeClass("hidden"); + $('#recoveryDetails').addClass("hidden"); + $('#requestPlaceholder').removeClass("hidden"); + + authTypeText1 = " is different to your current authentication type."; + authTypeText3 = "identity linking"; + $('#authTypeRecover').addClass("hidden"); + $('#authTypeRecoverPlaceholder').removeClass("hidden"); + } + + $('#authTypeMsg1').text(authTypeText1); + $('#authTypeMsg2').text(authTypeText2); + $('.requestType').text(authTypeText3); +} + +function getRegExAuthType() { + return regExAuthType = /^[^`'\";<>]{0,4000}$/; +} + +function getRegExIdString() { + var inputAuthType = '#authType'; + var authType = $(inputAuthType).val(); + + // Start with slash only + if (authType === "X.509") { + // var regExIdString = /^(\/[a-zA-Z]+=[a-zA-Z0-9\-\_\s\.@,'\/]+)+$/; + var regExIdString = /^\/.+$/; + + // End with @iris.iam.ac.uk + } else if (authType === "IRIS IAM - OIDC") { + // var regExIdString = /^([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12})@iris\-iam\.ac\.uk$/; + var regExIdString = /^.+@iris\-iam\.ac\.uk$/; + + // End with @egi.eu + } else if (authType === "EGI Proxy IdP") { + var regExIdString = /^.+@egi\.eu$/; + + } else { + var regExIdString = /^[^`'\";<>]{0,4000}$/; + } + return regExIdString; +} + +function getRegExEmail() { + return regExEmail = /^(([0-9a-zA-Z]+[-._])*[0-9a-zA-Z]+@([-0-9a-zA-Z]+[.])+[a-zA-Z]{2,6}){1}$/; +} + +// Validate all inputs on any change +// Enable/disabled ID string input based on selection of auth type +// Enable/disable and format submit button based on all other inputs +function validate() { + var idStringValid = false; + var emailValid = false; + var authTypeValid = false; + + // Validate auth type + var regExAuthType = getRegExAuthType(); + var inputAuthType = '#authType'; + authTypeValid = isInputValid(regExAuthType, inputAuthType); + authTypeEmpty = isInputEmpty(inputAuthType); + + // Validate ID string + var regExIdString = getRegExIdString(); + var inputIdString = '#primaryIdString'; + idStringValid = isInputValid(regExIdString, inputIdString); + idStringEmpty = isInputEmpty(inputIdString); + + // Validate email + var regExEmail = getRegExEmail(); + var inputEmail = '#email'; + emailValid = isInputValid(regExEmail, inputEmail); + emailEmpty = isInputEmpty(inputEmail); + + // Set the button based on validate status + if (authTypeValid && idStringValid && emailValid && !authTypeEmpty && !idStringEmpty && !emailEmpty) { + $('#submitRequest_btn').addClass("btn btn-success"); + $('#submitRequest_btn').prop("disabled", false); + } else { + $('#submitRequest_btn').removeClass("btn btn-success"); + $('#submitRequest_btn').addClass("btn btn-default"); + $('#submitRequest_btn').prop("disabled", true); + } +} + +// Check if user input is valid based on regex +// Input is regex and a selector e.g. '#id' +// Returns boolean flag (true if valid) +function isInputValid(regEx, input) { + var inputValue = $(input).val(); + var inputValid = false; + if (regEx.test(inputValue) !== false) { + inputValid=true; + } + return inputValid; +} + +// Check if user input is empty +// Input is selector e.g. '#id' +// Returns boolean flag (true if empty) +function isInputEmpty(input) { + var inputValue = $(input).val(); + var inputEmpty = true; + if (inputValue) { + inputEmpty=false; + } + return inputEmpty; +} + +// Enable ID string input if auth type is valid +function enableIdString(valid, empty) { + // Disable/enable ID string based on auth type validity + if (valid && !empty) { + $('#primaryIdString').prop("disabled", false); + } else { + $('#primaryIdString').prop("disabled", true); + } +} + +// Format authentication type input on selecting a value based on validation +// Selections should be successful, but invalid/empty formating retained +function formatAuthType() { + var regEx = getRegExAuthType(); + var input = '#authType'; + var valid = isInputValid(regEx, input); + var empty = isInputEmpty(input); + + if (valid && !empty) { + $('#authTypeGroup').addClass("has-success"); + $('#authTypeGroup').removeClass("has-error"); + } else { + $('#authTypeGroup').removeClass("has-success"); + $('#authTypeGroup').addClass("has-error"); + } + + // Enable ID string input if auth type is valid + enableIdString(valid, empty); +} + +// Format ID string input on selection of auth type based on validation +// Only apply if value has been entered (valid/invalid based on regex) +function formatIdStringFromAuth() { + var regEx = getRegExIdString(); + var input = '#primaryIdString'; + var valid = isInputValid(regEx, input); + var empty = isInputEmpty(input) + + if (!empty) { + if (valid) { + $('#primaryIdStringGroup').addClass("has-success"); + $('#primaryIdStringGroup').removeClass("has-error"); + $('#idStringError').addClass("hidden"); + $('#idStringPlaceholder').removeClass("hidden"); + } else { + $('#primaryIdStringGroup').removeClass("has-success"); + $('#primaryIdStringGroup').addClass("has-error"); + $('#idStringError').removeClass("hidden"); + $('#idStringPlaceholder').addClass("hidden"); + $('#idStringError').text("You have entered an invalid ID string for the selected authentication method"); + } + } else { + $('#primaryIdStringGroup').removeClass("has-error"); + $('#idStringError').addClass("hidden"); + $('#idStringPlaceholder').removeClass("hidden"); + } +} + +// Format ID string input on entering value based on validation +// Error if invalid (regex) format or if nothing entered +function formatIdString() { + var regEx = getRegExIdString(); + var input = '#primaryIdString'; + var valid = isInputValid(regEx, input); + var empty = isInputEmpty(input); + + if (valid && !empty) { + $('#primaryIdStringGroup').addClass("has-success"); + $('#primaryIdStringGroup').removeClass("has-error"); + $('#idStringError').addClass("hidden"); + $('#idStringPlaceholder').removeClass("hidden"); + } else { + $('#primaryIdStringGroup').removeClass("has-success"); + $('#primaryIdStringGroup').addClass("has-error"); + $('#idStringError').removeClass("hidden"); + $('#idStringPlaceholder').addClass("hidden"); + } + if (!valid && !empty) { + $('#idStringError').text("You have entered an invalid ID string for the selected authentication method"); + } else if (empty) { + $('#idStringError').text("Please enter the account's ID string"); + } +} + +// Format email input on entering a value based on validation +// Error if invalid (regex) format or if nothing entered +function formatEmail() { + var regEx = getRegExEmail(); + var input = '#email'; + var valid = isInputValid(regEx, input); + var empty = isInputEmpty(input); + + if (valid && !empty) { + $('#emailGroup').addClass("has-success"); + $('#emailGroup').removeClass("has-error"); + $('#emailError').addClass("hidden"); + $('#emailPlaceholder').removeClass("hidden"); + } else { + $('#emailGroup').removeClass("has-success"); + $('#emailGroup').addClass("has-error"); + $('#emailError').removeClass("hidden"); + $('#emailPlaceholder').addClass("hidden"); + } + if (!valid && !empty) { + $('#emailError').text("Please enter a valid email"); + } else if (empty) { + $('#emailError').text("Please enter the account's email"); + } +} \ No newline at end of file diff --git a/htdocs/web_portal/views/user/link_identity.php b/htdocs/web_portal/views/user/link_identity.php new file mode 100644 index 000000000..aae0a97c4 --- /dev/null +++ b/htdocs/web_portal/views/user/link_identity.php @@ -0,0 +1,192 @@ +

+

Link Identity or Recover an Account

+ +
+ +
+

What is identity linking?

+
    +
  • + You can use this process to add your current authentication method as a way to log in to an existing account. +
  • +
  • + This allows access to a single account through two or more identifiers. +
  • +
  • + You must have access to the email address associated with the account being linked. +
  • +
  • + Your current authentication type must be different to any authentication types already associated + with the account being linked. +
  • +
+ +

What is account recovery?

+
    +
  • + If your identifier has changed, you can use this process to update it and regain control of your old account. +
  • +
  • + You must have access to the email address associated with your old account. +
  • +
  • + Your current authentication type must be the same as the authentication type you enter for your old account. +
  • +
+ +
+ +
+
+ + + Your current ID string (e.g. certificate DN) is: + +
+ + Your current authentication type is: + + +
+
+ +

Details of account to be linked or recovered

+ +
+ +
+ +
+ +
+ +
+
+ + + + +
+
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+ + +
+ +
+ +
+
+ +

What happens next?

+
+
  • + Once you have submitted this form, you will receive a confirmation + e-mail containing instructions on how to validate the request. +
  • +
  • + Any existing linking or recovery requests you have made will expire. +
  • + + + +
  • If you successfully validate your recovery request: +
      +
    • + The ID string of your old account that matches your current authentication type will be updated to your current ID string. +
    • +
    • + You will no longer be able to log in with your old ID string. +
    • +
    • > + Any roles you have with the account you are currently using will be requested for your old account. +
    • +
    • > + These roles will be approved automatically if either account has permission to do so. +
    • + +
    • > + The account you are currently using will be deleted. +
    • +
    +
  • + + +
    + +
    + + + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/htdocs/web_portal/views/user/link_identity_accepted.php b/htdocs/web_portal/views/user/link_identity_accepted.php new file mode 100644 index 000000000..2d144c51e --- /dev/null +++ b/htdocs/web_portal/views/user/link_identity_accepted.php @@ -0,0 +1,17 @@ +
    +

    Success

    +

    Your request has been submitted with the following details:

    + +
      +
    • Authentication type:
    • +
    • ID String:
    • +
    • Email:
    • +
    + +

    If these details are correct, an email will have been sent to the address + registered to your account. Please follow the instructions contained within it to + complete your .

    + +

    If you did not receive an email, please check the above details are correct, and the + guidance on identity linking and account recovery.

    +
    \ No newline at end of file diff --git a/htdocs/web_portal/views/user/link_identity_rejected.php b/htdocs/web_portal/views/user/link_identity_rejected.php new file mode 100644 index 000000000..471632323 --- /dev/null +++ b/htdocs/web_portal/views/user/link_identity_rejected.php @@ -0,0 +1,20 @@ +
    +

    Error

    +

    You cannot recover or link your identifier to another account while registered with an account + associated with multiple identifiers.

    +

    Your current identifier is:

    + +

    + +

    The other identifiers associated with this account are:

    +
      + {$identifier->getKeyName()}: {$identifier->getKeyValue()} "; + } ?> +
    + +

    If you wish to associate your current identifier with another account, + please unlink all other identifiers first.

    +

    If you wish to add new identifiers to this account, please + access GOCDB while authenticated with the new identifier.

    +
    \ No newline at end of file diff --git a/htdocs/web_portal/views/user/linked_identity.php b/htdocs/web_portal/views/user/linked_identity.php new file mode 100644 index 000000000..34a0ce3e7 --- /dev/null +++ b/htdocs/web_portal/views/user/linked_identity.php @@ -0,0 +1,5 @@ +
    +

    Success

    + Your identifier has been successfully +
    +
    \ No newline at end of file diff --git a/htdocs/web_portal/views/user/retrieve_account.php b/htdocs/web_portal/views/user/retrieve_account.php deleted file mode 100644 index 440ada1e8..000000000 --- a/htdocs/web_portal/views/user/retrieve_account.php +++ /dev/null @@ -1,29 +0,0 @@ -
    -
    -
    -
    -

    Retrieve An Account

    - Your current Account ID (e.g. certificate DN) is: -
    -
    - Old Account ID (as registered within your old account) * - (e.g. if DN: /C=.../OU=.../...) - - - - E-mail address (as registered within your account) * - (valid e-mail format) - - - - - Once you have submitted this form, you will receive a confirmation - e-mail containing instructions on how to validate the request. - - - -
    -
    -
    -
    \ No newline at end of file diff --git a/htdocs/web_portal/views/user/retrieve_account_accepted.php b/htdocs/web_portal/views/user/retrieve_account_accepted.php deleted file mode 100644 index f542dae61..000000000 --- a/htdocs/web_portal/views/user/retrieve_account_accepted.php +++ /dev/null @@ -1,6 +0,0 @@ -
    -

    Success

    - Your request to retrieve an old account has been accepted. An e-mail has - been sent to the address registered with your account. Please follow the - instructions contained within it to complete your account retrieval.
    -
    \ No newline at end of file diff --git a/htdocs/web_portal/views/user/retrieved_account.php b/htdocs/web_portal/views/user/retrieved_account.php deleted file mode 100644 index 8c7696a62..000000000 --- a/htdocs/web_portal/views/user/retrieved_account.php +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    Success

    - Your certificate DN has been successfully updated
    -
    \ No newline at end of file From 17b28a0fcea0bbff2d22b57d9cfc4c427edbcd03 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Wed, 4 Aug 2021 16:42:55 +0000 Subject: [PATCH 32/74] Add controllers and links for identity linking Created controllers for identity linking views, similar to those used for the account retrieval process, which have been deleted. Links have been added to allow users to navigate to the identity linking pages, whether registered or not. --- .../Draw_Components/draw_user_status.php | 7 +- .../controllers/user/link_identity.php | 110 ++++++++++++++++++ .../user/link_identity_user_validate.php | 41 +++++++ .../controllers/user/retrieve_account.php | 80 ------------- .../user/retrieve_account_user_validate.php | 54 --------- htdocs/web_portal/index.php | 20 ++-- 6 files changed, 165 insertions(+), 147 deletions(-) create mode 100644 htdocs/web_portal/controllers/user/link_identity.php create mode 100644 htdocs/web_portal/controllers/user/link_identity_user_validate.php delete mode 100644 htdocs/web_portal/controllers/user/retrieve_account.php delete mode 100644 htdocs/web_portal/controllers/user/retrieve_account_user_validate.php diff --git a/htdocs/web_portal/components/Draw_Components/draw_user_status.php b/htdocs/web_portal/components/Draw_Components/draw_user_status.php index 515b8ba10..51fd27515 100644 --- a/htdocs/web_portal/components/Draw_Components/draw_user_status.php +++ b/htdocs/web_portal/components/Draw_Components/draw_user_status.php @@ -31,16 +31,17 @@ function Get_User_Status_HTML() $user = \Factory::getUserService()->getUserByPrinciple($dn); if($user == null) { $HTML .= "Unregistered user
    "; - $HTML .= "
    ". + $HTML .= "
    ". "Register
    ". - "". - "Retrieve Old Account
    "; + "". + "Link Identity/Recover Account"; $HTML .="
    "; return $HTML; } $HTML .= "Registered as:
    ".$user->getForename() . " " . $user->getSurname() . "

    "; $HTML .= Get_User_Info_HTML($user); + $HTML .= "
    " . "Link Identity/Recover Account
    "; $HTML .= ""; return $HTML; } diff --git a/htdocs/web_portal/controllers/user/link_identity.php b/htdocs/web_portal/controllers/user/link_identity.php new file mode 100644 index 000000000..cfa9ec64a --- /dev/null +++ b/htdocs/web_portal/controllers/user/link_identity.php @@ -0,0 +1,110 @@ +getUserByPrinciple($idString); + $authTypes = $serv->getAuthTypes(); + + if (is_null($user)) { + $params['registered'] = false; + } else { + $params['registered'] = true; + } + + $params['idString'] = $idString; + $params['currentAuthType'] = $authType; + $params['authTypes'] = $authTypes; + + // Prevent users with multiple identifiers from continuing + if ($user !== null) { + if (count($user->getUserIdentifiers()) > 1) { + // Store identifiers that aren't the one currently in use + foreach ($user->getUserIdentifiers() as $identifier) { + if ($identifier->getKeyValue() !== $params['idString']) { + $params['otherIdentifiers'][] = $identifier; + } + } + show_view('user/link_identity_rejected.php', $params); + die(); + } + } + + show_view('user/link_identity.php', $params, 'Link Identity'); +} + +function submit() { + + // "Primary" account info entered by the user, corresponding to a registered account + // This account will have its ID string updated, or an identifier added to it + $primaryIdString = $_REQUEST['primaryIdString']; + $givenEmail = $_REQUEST['email']; + $primaryAuthType = $_REQUEST['authType']; + + // Current account info, inferred from the in-use authentication + // There may or may not be a corresponding registered account + $currentIdString = Get_User_Principle(); + $currentAuthType = Get_User_AuthType(); + + if (empty($currentIdString)) { + show_view('error.php', "Could not authenticate user - null user principle"); + die(); + } + + // Check ID string to be linked is different to current ID string + if ($currentIdString === $primaryIdString) { + show_view('error.php', "The ID string entered must differ to your current ID string"); + die(); + } + + try { + \Factory::getLinkIdentityService()->newLinkIdentityRequest($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $givenEmail); + } catch(\Exception $e) { + show_view('error.php', $e->getMessage()); + die(); + } + + $params['idString'] = $primaryIdString; + $params['authType'] = $primaryAuthType; + $params['email'] = $givenEmail; + + // Recovery or identity linking + if ($primaryAuthType === $currentAuthType) { + $params['requestText'] = 'account recovery'; + } else { + $params['requestText'] = 'identity linking'; + } + + show_view('user/link_identity_accepted.php', $params); +} \ No newline at end of file diff --git a/htdocs/web_portal/controllers/user/link_identity_user_validate.php b/htdocs/web_portal/controllers/user/link_identity_user_validate.php new file mode 100644 index 000000000..bc3fe2c7b --- /dev/null +++ b/htdocs/web_portal/controllers/user/link_identity_user_validate.php @@ -0,0 +1,41 @@ +confirmIdentityLinking($code, $currentIdString); + } catch(\Exception $e) { + show_view('error.php', $e->getMessage()); + die(); + } + + // Recovery or identity linking + if ($request->getPrimaryAuthType() === $request->getCurrentAuthType()) { + $params['requestText'] = 'recovered'; + } else { + $params['requestText'] = 'linked'; + } + + show_view('user/linked_identity.php', $params); +} \ No newline at end of file diff --git a/htdocs/web_portal/controllers/user/retrieve_account.php b/htdocs/web_portal/controllers/user/retrieve_account.php deleted file mode 100644 index b53e4a891..000000000 --- a/htdocs/web_portal/controllers/user/retrieve_account.php +++ /dev/null @@ -1,80 +0,0 @@ -getUserByPrinciple($dn); - - if(!is_null($user)) { - show_view('error.php', "Only unregistered users can retrieve an account."); - die(); - } - - $params['DN'] = $dn; - - show_view('user/retrieve_account.php', $params, 'Retrieve Account'); -} - -function submit() { - $oldDn = $_REQUEST['OLDDN']; - $givenEmail =$_REQUEST['EMAIL']; - $currentDn = Get_User_Principle(); - if(empty($currentDn)){ - show_view('error.php', "Could not authenticate user - null user principle"); - die(); - } - - try { - $changeReq = \Factory::getRetrieveAccountService()->newRetrieveAccountRequest($currentDn, $givenEmail, $oldDn); - } catch(\Exception $e) { - show_view('error.php', $e->getMessage()); - die(); - } - - show_view('user/retrieve_account_accepted.php'); -} \ No newline at end of file diff --git a/htdocs/web_portal/controllers/user/retrieve_account_user_validate.php b/htdocs/web_portal/controllers/user/retrieve_account_user_validate.php deleted file mode 100644 index f4d99acda..000000000 --- a/htdocs/web_portal/controllers/user/retrieve_account_user_validate.php +++ /dev/null @@ -1,54 +0,0 @@ -confirmAccountRetrieval($confirmationCode, $currentDn); - } catch(\Exception $e) { - show_view('error.php', $e->getMessage()); - die(); - } - show_view('user/retrieved_account.php'); -} \ No newline at end of file diff --git a/htdocs/web_portal/index.php b/htdocs/web_portal/index.php index 082e0d22c..8eb2de063 100644 --- a/htdocs/web_portal/index.php +++ b/htdocs/web_portal/index.php @@ -412,11 +412,6 @@ function Draw_Page($Page_Type) { require_once __DIR__.'/controllers/site/edit_cert_status.php'; edit(); break; - case "Retrieve_Account": - rejectIfNotAuthenticated(); - require_once __DIR__.'/controllers/user/retrieve_account.php'; - retrieve(); - break; case "Remove_Project_NGIs": rejectIfNotAuthenticated(); require_once __DIR__.'/controllers/project/remove_ngis.php'; @@ -532,11 +527,6 @@ function Draw_Page($Page_Type) { require_once __DIR__.'/controllers/admin/delete_ngi.php'; delete_ngi(); break; - case "User_Validate_DN_Change" : - rejectIfNotAuthenticated(); - require_once __DIR__ . '/controllers/user/retrieve_account_user_validate.php'; - validate_dn_change (); - break; case "Add_Site_Properties" : rejectIfNotAuthenticated(); require_once __DIR__ . '/controllers/site/add_site_properties.php'; @@ -647,6 +637,16 @@ function Draw_Page($Page_Type) { require_once __DIR__ . '/controllers/site/delete_api_auth.php'; delete_entity(); break; + case "Link_Identity" : + rejectIfNotAuthenticated(); + require_once __DIR__ . '/controllers/user/link_identity.php'; + link_identity(); + break; + case "User_Validate_Identity_Link" : + rejectIfNotAuthenticated(); + require_once __DIR__ . '/controllers/user/link_identity_user_validate.php'; + validate_identity_link(); + break; default: // require auth by default rejectIfNotAuthenticated(); From 02dba7dafb65bf50c4967d8f70b736cfd8a32381 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 5 Aug 2021 09:48:06 +0000 Subject: [PATCH 33/74] Create identity linking service Similar to the deleted account retrieval service, this provides functions called by controllers during the identity linking/recovery process, including creating, looking up and validating requests. Validation of request inputs will be added later, as well as functionality for merging roles. --- lib/Gocdb_Services/Factory.php | 18 +- lib/Gocdb_Services/LinkIdentity.php | 371 +++++++++++++++++++++++++ lib/Gocdb_Services/RetrieveAccount.php | 221 --------------- 3 files changed, 380 insertions(+), 230 deletions(-) create mode 100644 lib/Gocdb_Services/LinkIdentity.php delete mode 100644 lib/Gocdb_Services/RetrieveAccount.php diff --git a/lib/Gocdb_Services/Factory.php b/lib/Gocdb_Services/Factory.php index 356ee835f..339b9727d 100644 --- a/lib/Gocdb_Services/Factory.php +++ b/lib/Gocdb_Services/Factory.php @@ -42,13 +42,13 @@ class Factory { private static $configService = null; private static $validateService = null; private static $certStatusService = null; - private static $retrieveAccountService = null; private static $serviceTypeService = null; private static $projectService = null; private static $OwnedEntityService = null; private static $exService = null; private static $notificationService = null; private static $emailService = null; + private static $linkIdentityService = null; public static $properties = array(); //private static $properties = null; @@ -336,16 +336,16 @@ public static function getProjectService() { } /** - * Singleton Retrieve Account service - * @return org\gocdb\services\RetrieveAccount + * Singleton Link Identity service + * @return org\gocdb\services\LinkIdentity */ - public static function getRetrieveAccountService() { - if (self::$retrieveAccountService == null) { - require_once __DIR__ . '/RetrieveAccount.php'; - self::$retrieveAccountService = new org\gocdb\services\RetrieveAccount(); - self::$retrieveAccountService->setEntityManager(self::getEntityManager()); + public static function getLinkIdentityService() { + if (self::$linkIdentityService == null) { + require_once __DIR__ . '/LinkIdentity.php'; + self::$linkIdentityService = new org\gocdb\services\LinkIdentity(); + self::$linkIdentityService->setEntityManager(self::getEntityManager()); } - return self::$retrieveAccountService; + return self::$linkIdentityService; } /** diff --git a/lib/Gocdb_Services/LinkIdentity.php b/lib/Gocdb_Services/LinkIdentity.php new file mode 100644 index 000000000..4e954d336 --- /dev/null +++ b/lib/Gocdb_Services/LinkIdentity.php @@ -0,0 +1,371 @@ +getUserByPrincipleAndType($primaryIdString, $primaryAuthType); + if ($primaryUser === null) { + // If no valid user identifiers, check certificateDNs + $primaryUser = $serv->getUserByCertificateDn($primaryIdString); + } + + // $currentUser is user making request + // May not be registered so can be null + $currentUser = $serv->getUserByPrinciple($currentIdString); + + // Recovery or identity linking + if ($primaryAuthType === $currentAuthType) { + $isLinking = false; + } else { + $isLinking = true; + } + + // $this->validate() + + // Remove any existing requests involving either user + $this->removeRelatedRequests($primaryUser, $currentUser, $primaryIdString, $currentIdString); + + // Generate confirmation code + $code = $this->generateConfirmationCode($primaryIdString); + + // Create link identity request + $linkIdentityReq = new \LinkIdentityRequest($primaryUser, $currentUser, $code, $primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType); + + // Recovery or identity linking + if ($currentUser === null) { + $isRegistered = false; + } else { + $isRegistered = true; + } + + // Apply change + try { + $this->em->getConnection()->beginTransaction(); + $this->em->persist($linkIdentityReq); + $this->em->flush(); + + // Send confirmation email(s) to primary user, and current user if registered with a different email + // (before commit - if it fails we'll need a rollback) + if (\Factory::getConfigService()->getSendEmails()) { + $this->sendConfirmationEmails($primaryUser, $currentUser, $code, $primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered); + } + + $this->em->getConnection()->commit(); + } catch(\Exception $e) { + $this->em->getConnection()->rollback(); + $this->em->close(); + throw $e; + } + } + + /** + * Removes any existing requests which involve either user + * @param \User $primaryUser user who will have identifier added/updated + * @param \User $currentUser user creating the request + * @param string $primaryIdString ID string of primary user + * @param string $currentIdString ID string of current user + */ + private function removeRelatedRequests($primaryUser, $currentUser, $primaryIdString, $currentIdString) { + + // Set up list for previous requests matching various criteria + $previousRequests = []; + + // Matching the primary user + $previousRequests[] = $this->getRequestByUserId($primaryUser->getId()); + + // Matching the primary user's ID string - unlikely to exist but not impossible + $previousRequests[] = $this->getRequestByIdString($primaryIdString); + + // Matching the current user, if registered + if ($currentUser !== null) { + $previousRequests[] = $this->getRequestByUserId($currentUser->getId()); + } + + // Matching the current ID string + $previousRequests[] = $this->getRequestByIdString($currentIdString); + + // Remove any requests found + foreach ($previousRequests as $previousRequest) { + if (!is_null($previousRequest)) { + try{ + $this->em->getConnection()->beginTransaction(); + $this->em->remove($previousRequest); + $this->em->flush(); + $this->em->getConnection()->commit(); + } catch(\Exception $e) { + $this->em->getConnection()->rollback(); + $this->em->close(); + throw $e; + } + } + } + } + + /** + * Generates a confirmation code + * @param string $idString ID string used to generated code + */ + private function generateConfirmationCode($idString) { + $confirmCode = rand(1, 10000000); + $confirmHash = sha1($idString.$confirmCode); + return $confirmHash; + } + + /** + * Gets a link identity request from the database based on user ID + * @param integer $userId userid of the request to be linked + * @return arraycollection + */ + private function getRequestByUserId($userId) { + $dql = "SELECT l + FROM LinkIdentityRequest l + JOIN l.primaryUser pu + JOIN l.currentUser cu + WHERE pu.id = :id OR cu.id = :id"; + + $request = $this->em + ->createQuery($dql) + ->setParameter('id', $userId) + ->getOneOrNullResult(); + + return $request; + } + + /** + * Gets a link identity request from the database based on current ID string + * ID string may be present as primary or current user + * @param string $idString ID string of user to be linked in primary account + * @return arraycollection + */ + private function getRequestByIdString($idString) { + $dql = "SELECT l + FROM LinkIdentityRequest l + WHERE l.primaryIdString = :idString + OR l.currentIdString = :idString"; + + $request = $this->em + ->createQuery($dql) + ->setParameter('idString', $idString) + ->getOneOrNullResult(); + + return $request; + } + + /** + * Gets an identity link request from the database based on the confirmation code + * @param string $code confirmation code of the request being retrieved + * @return arraycollection + */ + public function getRequestByConfirmationCode($code) { + $dql = "SELECT l + FROM LinkIdentityRequest l + WHERE l.confirmCode = :code"; + + $request = $this->em + ->createQuery($dql) + ->setParameter('code', $code) + ->getOneOrNullResult(); + + return $request; + } + + /** + * Composes confimation email to be sent to the user + * @param string $primaryIdString ID string of primary user + * @param string $currentIdString ID string of current user + * @param string $primaryAuthType auth type of primary ID string + * @param string $currentAuthType auth type of current ID string + * @param bool $isLinking true if linking, false if recovering + * @param bool $isRegistered true if current user is registered + * @param bool $isPrimary true if composing for primary user + * @param string $link to be clicked (only sent to primary user email) + * @return arraycollection + */ + private function composeEmail($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered, $isPrimary, $link=null) { + + $subject = "Validation of " . ($isLinking ? "linking" : "recovering") . " your GOCDB account"; + + $body = "Dear GOCDB User," + . "\n\nA request to " . ($isLinking ? "associate a new identifier with" : "update an identifier associated with") + . ($isRegistered ? " one of your accounts" : " your account") . " has just been made on GOCDB." + . " The details of this request are:" + . "\n\n" . ($isLinking ? "Identifier" : "Current identifier") . " of GOCDB account being " . ($isLinking ? "linked" : "recovered") . ":" + . "\n\n - Authentication type: $primaryAuthType" + . "\n - ID string: $primaryIdString" + . "\n\nRequested new identifier " . ($isLinking ? "to be added to your account" : "with updated ID string") . ":" + . "\n\n - Authentication type: $currentAuthType" + . "\n - ID string: $currentIdString"; + + if ($isRegistered) { + $body .= "\n\nThis new identifier is currently associated with a second registered account." + . " If " . ($isLinking ? "identity linking" : "account recovery") . " is successful, any roles currently associated with this second account ($currentIdString)" + . " will be requested for your primary GOCDB account ($primaryIdString)." + . " These roles will be approved automatically if either account has permission to do so." + . "\n\nYour second account will then be deleted."; + } + + if (!$isLinking) { + $body .= "\n\nPlease note that you will no longer be able to access your account using your old ID string ($primaryIdString)."; + } + + if ($isPrimary) { + $body .= "\n\nIf you wish to associate your GOCDB account with this new identifier, please validate your request by clicking on the link below" + . " while authenticated with the new identifier:" + . "\n\n$link"; + } + + $body .= "\n\nIf you did not create this request, please immediately contact gocdb-admins@mailman.egi.eu"; + + return array('subject'=>$subject, 'body'=>$body); + } + + /** + * Send confirmation email(s) to primary user, and current user if registered with a different email + * @param \User $primaryUser user who will have identifier added/updated + * @param \User $currentUser user creating the request + * @param string $code confirmation code of the request being retrieved + * @param string $primaryIdString ID string of primary user + * @param string $currentIdString ID string of current user + * @param string $primaryAuthType auth type of primary ID string + * @param string $currentAuthType auth type of current ID string + * @param bool $isLinking true if linking, false if recovering + * @param bool $isRegistered true if current user is registered + */ + private function sendConfirmationEmails($primaryUser, $currentUser, $code, $primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered) { + + // Create link to be clicked in email + $portalUrl = \Factory::getConfigService()->GetPortalURL(); + $link = $portalUrl."/index.php?Page_Type=User_Validate_Identity_Link&c=" . $code; + + // Compose email to send to primary user + $isPrimary = true; + $composedPrimaryEmail = $this->composeEmail($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered, $isPrimary, $link); + $primarySubject = $composedPrimaryEmail['subject']; + $primaryBody = $composedPrimaryEmail['body']; + + // If "sendmail_from" is set in php.ini, use second line ($headers = '';): + $headers = "From: no-reply@goc.egi.eu"; + + // Mail command returns boolean. False if message not accepted for delivery. + if (!mail($primaryUser->getEmail(), $primarySubject, $primaryBody, $headers)) { + throw new \Exception("Unable to send email message"); + } + + // Send confirmation email to current user, if registered with different email to primary user + if ($isRegistered) { + if ($currentUser->getEmail() !== $primaryUser->getEmail()) { + + // Compose email to send to current user + $isPrimary = false; + $composedCurrentEmail = $this->composeEmail($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered, $isPrimary); + $currentSubject = $composedCurrentEmail['subject']; + $currentBody = $composedCurrentEmail['body']; + + // Mail command returns boolean. False if message not accepted for delivery. + if (!mail($currentUser->getEmail(), $currentSubject, $currentBody, $headers)) { + throw new \Exception("Unable to send email message"); + } + } + } + } + + /** + * Confirm and execute linking or recovery request + * @param string $code confirmation code of the request being retrieved + * @param string $currentIdString ID string of current user + */ + public function confirmIdentityLinking($code, $currentIdString) { + + $serv = \Factory::getUserService(); + + // Get the request + $request = $this->getRequestByConfirmationCode($code); + + $invalidURL = "Confirmation URL invalid." + . " If you have submitted multiple requests for the same account, please ensure you have used the link in the most recent email." + . " Please also ensure you are authenticated in the same way as when you made the request."; + + // Check there is a result + if (is_null($request)) { + throw new \Exception($invalidURL); + } + + $primaryUser = $request->getPrimaryUser(); + $currentUser = $request->getCurrentUser(); + + // Check the portal is not in read only mode, throws exception if it is. + // If portal is read only, but the primary user is an admin, we will still be able to proceed. + $this->checkPortalIsNotReadOnlyOrUserIsAdmin($primaryUser); + + // Check the ID string currently being used by the user is same as in the request + if ($currentIdString !== $request->getCurrentIdString()) { + throw new \Exception($invalidURL); + } + + // Create identifier array from the current user's credentials + $identifierArr = array($request->getCurrentAuthType(), $request->getCurrentIdString()); + + // Are we recovering or linking an identity? True if linking + $isLinking = ($request->getPrimaryAuthType() !== $request->getCurrentAuthType()); + + // If primary user does not have user identifiers, add using the request info + $isOldUser = ($primaryUser->getCertificateDn() === $request->getPrimaryIdString()); + if ($isOldUser) { + $identifierArrOld = array($request->getPrimaryAuthType(), $request->getPrimaryIdString()); + } + + // Update primary user, remove request (and current user) + try { + $this->em->getConnection()->beginTransaction(); + + // Add certificateDn as identifier if necessary + if ($isOldUser) { + $serv->migrateUserCredentials($primaryUser, $identifierArrOld, $primaryUser); + } + + // Delete request before user so references still exist + $this->em->remove($request); + + // Remove current user so their identifier is free to be added + if ($currentUser !== null) { + $serv->deleteUser($currentUser, $currentUser); + } + + $this->em->flush(); + + // Add or update to current identifier + if ($isLinking) { + $serv->addUserIdentifier($primaryUser, $identifierArr, $primaryUser); + } else { + $identifier = $serv->getIdentifierByIdString($request->getPrimaryIdString()); + $serv->editUserIdentifier($primaryUser, $identifier, $identifierArr, $primaryUser); + } + + $this->em->remove($request); + + $this->em->flush(); + $this->em->getConnection()->commit(); + } catch(\Exception $e) { + $this->em->getConnection()->rollback(); + $this->em->close(); + throw $e; + } + + return $request; + } +} \ No newline at end of file diff --git a/lib/Gocdb_Services/RetrieveAccount.php b/lib/Gocdb_Services/RetrieveAccount.php deleted file mode 100644 index 766fe13b3..000000000 --- a/lib/Gocdb_Services/RetrieveAccount.php +++ /dev/null @@ -1,221 +0,0 @@ -setEntityManager($this->em); - $user = $userService->getUserByPrinciple($oldDn); - if($user == null) { - throw new \Exception("Can't find user with DN $oldDn"); - } - - if(strcasecmp($user->getEmail(), $email)) { - throw new \Exception("E-mail address doesn't match DN"); - } - } - - /** - * Processes an account retrieval request for the passed user - * @param \User $user - */ - public function newRetrieveAccountRequest($currentDn, $givenEmail, $oldDn) { - - - //get user from old dn and throw exception if they don't exist - require_once __DIR__ . '/User.php'; - $userService = new \org\gocdb\services\User(); - $userService->setEntityManager($this->em); - $user = $userService->getUserByPrinciple($oldDn); - if($user == null) { - throw new \Exception("Can't find user with DN $oldDn"); - } - - //check the given email address matches the one given - if(strcasecmp($user->getEmail(), $givenEmail)) { - throw new \Exception("E-mail address doesn't match DN"); - } - - //Check the portal is not in read only mode, throws exception if it is. If portal is read only, but the user whos DN is being changed is an admin, we will still be able to proceed. - $this->checkPortalIsNotReadOnlyOrUserIsAdmin($user); - - //check if there has already been a request for this dn, if there has, - //remove it. This must be in a seperate try catch block to the new one, - //to prevent constraint violations - $previousRequest = $this->getRetrieveAccountRequestByUserId($user->getId()); - if(!is_null($previousRequest)){ - try{ - $this->em->getConnection()->beginTransaction(); - $this->em->remove($previousRequest); - $this->em->flush(); - $this->em->getConnection()->commit(); - } catch(\Exception $e){ - $this->em->getConnection()->rollback(); - $this->em->close(); - throw $e; - } - } - - //Generate confirmation code - $code = $this->generateConfirmationCode($user->getCertificateDn()); - - $retrieveAccountReq = new \RetrieveAccountRequest($user, $code, $currentDn); - //die('code ['.$code.']'); - - //apply change - try { - $this->em->getConnection()->beginTransaction(); - $this->em->persist($retrieveAccountReq); - $this->em->flush(); - - //send email (before commit - if it fails we'll need a rollback) - $this->sendConfirmationEmail($user, $code, $currentDn); - - $this->em->getConnection()->commit(); - } catch(\Exception $ex){ - $this->em->getConnection()->rollback(); - $this->em->close(); - throw $ex; - } - - } - - private function generateConfirmationCode($userdn){ - $confirm_code = rand(1, 10000000); - $confirm_hash = sha1($userdn.$confirm_code); - return $confirm_hash; - } - - /** - * Gets a retrieve account request from the database based on userid - * @param integer $userId userid of the request to be retrieved - * @return arraycollection - */ - public function getRetrieveAccountRequestByUserId($userId){ - $dql = "SELECT r - FROM RetrieveAccountRequest r - JOIN r.user u - WHERE u.id = :id"; - - $request = $this->em - ->createQuery($dql) - ->setParameter('id', $userId) - ->getOneOrNullResult(); - - return $request; - } - - /** - * Gets a retrieve account request from the database based on the conformation code - * @param string $code confirmation code of the request being retrieved - * @return arraycollection - */ - public function getRetrieveAccountRequestByConfirmationCode($code){ - $dql = "SELECT r - FROM RetrieveAccountRequest r - WHERE r.confirmCode = :code"; - - $request = $this->em - ->createQuery($dql) - ->setParameter('code', $code) - ->getOneOrNullResult(); - - return $request; - } - - /** - * sends a confimation email to the user - * - * @param \User $user user who's dn is being changed - * @param type $confirmationCode genersted confirmation code - * @param type $newDn new dn for $user - * @throws \Exception - */ - private function sendConfirmationEmail(\User $user, $confirmationCode, $newDn){ - $portal_url = \Factory::getConfigService()->GetPortalURL(); - //echo $portal_url; - //die(); - - $link = $portal_url."/index.php?Page_Type=User_Validate_DN_Change&c=".$confirmationCode; - $subject = "Validation of changes on your GOCDB account"; - $body = "Dear GOCDB User,\n\n" - ."A request to retrieve and associate your GOCDB account and privileges with a " - . "new account ID has just been made on GOCDB (e.g. you have a new certificate with a different DN)." - ."\n\nThe new account ID is: $newDn" - ."\n\nIf you wish to associate your GOCDB account with this account ID, please validate your request by clicking on the link below:\n" - ."$link". - "\n\nIf you did not create this request in GOCDB, please immediately contact gocdb-admins@mailman.egi.eu" ; - ; - //If "sendmail_from" is set in php.ini, use second line ($headers = '';): - $headers = "From: no-reply@goc.egi.eu"; - //$headers = ""; - - //mail command returns boolean. False if message not accepted for delivery. - if (!mail($user->getEmail(), $subject, $body, $headers)){ - throw new \Exception("Unable to send email message"); - } - - //echo $body; - } - - public function confirmAccountRetrieval ($code, $currentDn){ - //get the request - $request = $this->getRetrieveAccountRequestByConfirmationCode($code); - - //check there is a result - if(is_null($request)){ - throw new \Exception("Confirmation URL invalid. If you have submitted multiple requests for the same account, please ensure you have used the link in the most recent email"); - } - - $user = $request->getUser(); - - //Check the portal is not in read only mode, throws exception if it is. If portal is read only, but the user whos DN is being changed is an admin, we will still be able to proceed. - $this->checkPortalIsNotReadOnlyOrUserIsAdmin($user); - - //check the DN currently being used by the user is the one they want their account changed to - if($currentDn != $request->getNewDn()){ - throw new Exception("Your current certificate DN does not match the one to which it was requested to link the user account. The link will only work once, if you have refreshed the page or clicked the link a second time you will see this messaage"); //TODO: reword - } - - //update user, remove request from table - try{ - $user->setCertificateDn($request->getNewDn()); - $this->em->getConnection()->beginTransaction(); - $this->em->merge($user); - $this->em->remove($request); - $this->em->flush(); - $this->em->getConnection()->commit(); - } catch(\Exception $e){ - $this->em->getConnection()->rollback(); - $this->em->close(); - throw $e; - } - - } -} \ No newline at end of file From 374bcade747998595baceaf667efcfe5a2cb4d3c Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 5 Aug 2021 09:52:12 +0000 Subject: [PATCH 34/74] Validate identity linking requests Validate the current user, as well as the input ID string, auth types and email. For most errors we do not throw exceptions to limit the information being shared about other users. --- lib/Gocdb_Services/LinkIdentity.php | 47 ++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/Gocdb_Services/LinkIdentity.php b/lib/Gocdb_Services/LinkIdentity.php index 4e954d336..ad6327560 100644 --- a/lib/Gocdb_Services/LinkIdentity.php +++ b/lib/Gocdb_Services/LinkIdentity.php @@ -35,7 +35,10 @@ public function newLinkIdentityRequest($primaryIdString, $currentIdString, $prim $isLinking = true; } - // $this->validate() + // Validate details. For most errors, return without throwing an error to avoid sharing info + if ($this->validate($primaryUser, $currentUser, $currentAuthType, $isLinking, $givenEmail) === 1) { + return; + } // Remove any existing requests involving either user $this->removeRelatedRequests($primaryUser, $currentUser, $primaryIdString, $currentIdString); @@ -73,6 +76,48 @@ public function newLinkIdentityRequest($primaryIdString, $currentIdString, $prim } } + /** + * Performs validation on request + * @param \User $primaryUser user who will have identifier added/updated + * @param \User $currentUser user creating the request + * @param string $currentAuthType auth type of current ID string + * @param bool $isLinking true if linking, false if recovering + * @param string $givenEmail email of primary user + */ + private function validate($primaryUser, $currentUser, $currentAuthType, $isLinking, $givenEmail) { + + if ($primaryUser === null) { + // Don't throw exception to limit info shared + return 1; + } + + if ($primaryUser === $currentUser) { + // Can throw exception as it's their own ID string + throw new \Exception("The details entered are already associated with this account"); + } + + // Check the portal is not in read only mode, throws exception if it is + // If portal is read only, but the current user is an admin, we will still be able to proceed + $this->checkPortalIsNotReadOnlyOrUserIsAdmin($currentUser); + + // Check the given email address matches the one given + if (strcasecmp($primaryUser->getEmail(), $givenEmail)) { + // Don't throw exception to limit info shared + return 1; + } + + // Prevent attempt to add duplicate auth type when linking + if ($isLinking) { + foreach ($primaryUser->getUserIdentifiers() as $identifier) { + if ($identifier->getKeyName() === $currentAuthType) { + return 1; + } + } + } + + return 0; + } + /** * Removes any existing requests which involve either user * @param \User $primaryUser user who will have identifier added/updated From 72898dbd9705c60b386624adaf1afe5a6ad15f13 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 26 Aug 2021 11:29:13 +0000 Subject: [PATCH 35/74] Merge roles during linking and recovery If both users are registered, request any roles the current user has for the primary user. Both users attempt to approve these requests, and the current user then self-revokes their roles, ensuring a clear log of the events exists. This change includes moving role request processing to services, so it can accessed more easily, and adding a new function to look up named roles by user and entity. --- .../political_role/request_role.php | 4 +- lib/Gocdb_Services/LinkIdentity.php | 3 +- lib/Gocdb_Services/Role.php | 142 +++++++++++++++++- 3 files changed, 144 insertions(+), 5 deletions(-) diff --git a/htdocs/web_portal/controllers/political_role/request_role.php b/htdocs/web_portal/controllers/political_role/request_role.php index 4061d9e38..84bf2405d 100644 --- a/htdocs/web_portal/controllers/political_role/request_role.php +++ b/htdocs/web_portal/controllers/political_role/request_role.php @@ -93,9 +93,7 @@ function submitRoleRequest($roleName, $entityId, \User $user =null) { // Create a new Role linking user, entity and roletype. The addRole // perfoms role validation and throws exceptios accordingly. - $newRole = \Factory::getRoleService()->addRole($roleName, $user, $entity); - - \Factory::getNotificationService()->roleRequest($newRole, $user, $entity); + $newRole = \Factory::getRoleService()->requestRole($roleName, $user, $entity); show_view('political_role/new_request.php'); } diff --git a/lib/Gocdb_Services/LinkIdentity.php b/lib/Gocdb_Services/LinkIdentity.php index ad6327560..7f883774c 100644 --- a/lib/Gocdb_Services/LinkIdentity.php +++ b/lib/Gocdb_Services/LinkIdentity.php @@ -386,8 +386,9 @@ public function confirmIdentityLinking($code, $currentIdString) { // Delete request before user so references still exist $this->em->remove($request); - // Remove current user so their identifier is free to be added + // Merge roles and remove current user so their identifier is free to be added if ($currentUser !== null) { + \Factory::getRoleService()->mergeRoles($primaryUser, $currentUser); $serv->deleteUser($currentUser, $currentUser); } diff --git a/lib/Gocdb_Services/Role.php b/lib/Gocdb_Services/Role.php index 688f8298c..b528f66d4 100644 --- a/lib/Gocdb_Services/Role.php +++ b/lib/Gocdb_Services/Role.php @@ -471,6 +471,30 @@ public function getRoleById($id){ return $role; } + /** + * Get the named role associated with a user and entity + * @param \User $user user + * @param \OwnedEntity entity + * @param string $roleTypeName + * @return \Role + */ + public function getRoleByUserEntityType(\User $user, \OwnedEntity $entity, $roleTypeName) { + $dql = "SELECT r FROM Role r + INNER JOIN r.user u + INNER JOIN r.ownedEntity o + INNER JOIN r.roleType rt + WHERE u.id = :userId + AND o.id = :entityId + AND rt.name= :roleTypeName"; + + $role = $this->em->createQuery($dql) + ->setParameter(":userId", $user->getId()) + ->setParameter(":entityId", $entity->getId()) + ->setParameter(":roleTypeName", $roleTypeName) + ->getSingleResult(); + + return $role; + } /** * Create and return a new \Role instance linking the given user and entity. @@ -520,7 +544,7 @@ public function addRole($roleTypeName, \User $user, \OwnedEntity $entity){ $roleType = $this->getRoleTypeByName($roleTypeName); $r = new \Role($roleType, $user, $entity, $roleStatus); $this->em->persist($r); - + // Ensure roleId has been generated $this->em->flush(); @@ -703,6 +727,122 @@ public function rejectRoleRequest(\Role $role, \User $callingUser) { } } + /** + * Processes a role request for user + * + * @param string $roleTypeName name of role being requested + * @param \User $user user requesting the role + * @param \OwnedEntity $entity entity role is over + * @return \Role pending role + */ + public function requestRole($roleTypeName, \User $user, \OwnedEntity $entity) { + + // Create a new Role linking user, entity and roletype + // addRole perfoms role validation and throws exceptions accordingly + $newRole = $this->addRole($roleTypeName, $user, $entity); + + \Factory::getNotificationService()->roleRequest($newRole, $user, $entity); + + return $newRole; + } + + /** + * Calling (current) user attempts to 'merge' roles with another (primary) user + * All roles the current user has are requested for the primary user + * Both users attempt to grant these requests, and current user self-revokes their roles + * Logic is handled by a seperate function + * + * @param \User $primaryUser user to request and be granted roles + * @param \User $currentUser user currently holding the roles + */ + public function mergeRoles(\User $primaryUser, \User $currentUser) { + + // Check the portal is not in read only mode or user is an admin + $this->checkPortalIsNotReadOnlyOrUserIsAdmin($currentUser); + + $this->em->getConnection()->beginTransaction(); + try { + $this->mergeRolesLogic($primaryUser, $currentUser); + $this->em->flush(); + $this->em->getConnection()->commit(); + } catch (\Exception $e) { + $this->em->getConnection()->rollback(); + $this->em->close(); + throw $e; + } + } + + /** + * Logic to 'merge' current user's roles with another user + * All roles the current user has are requested for the primary user + * Both users attempt to grant these requests, and the current user self-revokes their roles + * + * @param \User $primaryUser user to be granted the roles + * @param \User $currentUser user currently holding the roles + */ + private function mergeRolesLogic(\User $primaryUser, \User $currentUser) { + + $currentRoles = $currentUser->getRoles(); + foreach ($currentRoles as $currentRole) { + + $roleTypeName = $currentRole->getRoleType()->getName(); + $entity = $currentRole->getOwnedEntity(); + + // If primary user already has the same role GRANTED over entity, no need to grant again + if (in_array($roleTypeName, $this->getUserRoleNamesOverEntity($entity, $primaryUser, \RoleStatus::GRANTED))) { + continue; + } + + // If primary user already has the same role PENDING over entity, will attempt to grant + if (in_array($roleTypeName, $this->getUserRoleNamesOverEntity($entity, $primaryUser, \RoleStatus::PENDING))) { + $rolesToGrant[] = $this->getRoleByUserEntityType($primaryUser, $entity, $roleTypeName); + } else { + // Request role on behalf of primary user + $rolesToGrant[] = $this->requestRole($roleTypeName, $primaryUser, $entity); + } + } + + // Attempt to 'self-grant' roles for primary user + foreach ($rolesToGrant as $role) { + $this->selfGrantRole($primaryUser, $currentUser, $role); + } + + // Revoke roles from current user after granting + foreach ($currentRoles as $role) { + $this->revokeRole($role, $currentUser); + } + } + + /** + * Attempt to 'self-grant' a role based on two user permissions + * + * @param \User $primaryUser user to be granted the role + * @param \User $currentUser user currently holding the role + * @param \Role $role role to be granted + */ + private function selfGrantRole(\User $primaryUser, \User $currentUser, \Role $role) { + + // Allow this exception as users may not have permission to grant + $grantMessage = 'You do not have permission to grant this role'; + + // Try approving based on primary user permissions + try { + $this->grantRole($role, $primaryUser); + } catch (\Exception $e) { + if ($e->getMessage() !== $grantMessage) { + throw $e; + } + // Try approving based on current user permissions + try { + $this->grantRole($role, $currentUser); + } catch (\Exception $e) { + if ($e->getMessage() !== $grantMessage) { + throw $e; + } + } + } + } + /** * Get an array of {@link \RoleActionRecord}s for the {@link \OwnedEntity} * that has the given id and type. From bcb95912ff952671fdeacfa07fd91b98a5836b93 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Fri, 27 Aug 2021 09:25:23 +0000 Subject: [PATCH 36/74] Set certificateDn to null by default Change the schema to allow certificateDn to be null, rather than enforcing unique values, as unique ID strings are now handled by user identifiers. When adding an identifier to an old user with certificateDn, the ID string of the identifier must match, and certificateDn is set to null. --- lib/Doctrine/entities/User.php | 9 +++++---- lib/Gocdb_Services/User.php | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/entities/User.php b/lib/Doctrine/entities/User.php index 77a749d6c..fa5c14763 100644 --- a/lib/Doctrine/entities/User.php +++ b/lib/Doctrine/entities/User.php @@ -47,7 +47,7 @@ class User { /** @Column(type="string", nullable=true) */ protected $workingHoursEnd = null; - /** @Column(type="string", unique=true) */ + /** @Column(type="string", nullable=true) */ protected $certificateDn = null; /** @Column(type="string", nullable=true) */ @@ -158,7 +158,7 @@ public function getWorkingHoursEnd() { /** * Get the user's unique ID string, typically an x509 DN string. - * @todo This needs to be renamed to getAccountID + * This should return null once the user has user identifiers. * @return string */ public function getCertificateDn() { @@ -274,8 +274,9 @@ public function setWorkingHoursEnd($workingHoursEnd) { } /** - * Set the user's unique account ID, typcially and x509 DN string. Required. - * @todo Needs renaming to setAccountID + * Set the user's unique ID string, typically an x509 DN string. + * This should only be used to set the value to null + * when user identifiers are first added to an old user. * @param string $certificateDn */ public function setCertificateDn($certificateDn) { diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index 39fe707d7..3745cd54a 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -384,11 +384,10 @@ public function register($userValues, $userIdentifierValues) { $user->setSurname($userValues['SURNAME']); $user->setEmail($userValues['EMAIL']); $user->setTelephone($userValues['TELEPHONE']); - $user->setCertificateDn($userIdentifierValues['VALUE']); $user->setAdmin(false); $this->em->persist($user); $this->em->flush(); - $this->migrateUserCredentials($user, $identifierArr, $user); + $this->addUserIdentifier($user, $identifierArr, $user); $this->em->flush(); $this->em->getConnection()->commit(); } catch (\Exception $e) { @@ -683,12 +682,14 @@ public function migrateUserCredentials(\User $user, array $identifierArr, \User /** * Overwrites a user's certificateDn to a default value - * Currently set to the user's ID + * Currently set to null * @param \User $user user having certificate DN overwritten * @throws \Exception */ private function setDefaultCertDn(\User $user) { - $user->setCertificateDn($user->getId()); + $user->setCertificateDn(null); + $this->em->persist($user); + $this->em->flush(); } /** From bbde1f426db66e1cbf005edcbd2c370481198f8b Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 26 Aug 2021 17:21:02 +0000 Subject: [PATCH 37/74] Add function to get user's default ID string Function returns one of a user's ID strings, with priority based on the order from getAuthTypes, which itself uses the order of tokens in MyConfig1. --- lib/Gocdb_Services/User.php | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index 3745cd54a..f13650c46 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -552,6 +552,61 @@ public function getAuthTypes($reducedRealms=true) { return $authTypes; } + /** + * Get one of the user's unique ID strings, favouring certain types + * If user does not have user identifiers, returns certificateDn + * @param \User $user User whose ID string we want + * @return string + */ + public function getDefaultIdString($user) { + + $authTypes = $this->getAuthTypes(); + $idString = null; + + // For each ordered auth type, check if an identifier matches + // Gets certifcateDn if no user identifiers and X.509 listed + foreach ($authTypes as $authType) { + $idString = $this->getIdStringByAuthType($user, $authType); + if ($idString !== null) { + break; + } + } + + // If user only has unlisted identifiers, return first identifier + if ($idString === null) { + $idString = $user->getUserIdentifiers()[0]->getKeyValue(); + } + + return $idString; + } + + /** + * Get a user's ID string of specified authentication type + * If user does not have user identifiers, returns certificateDn for X.509 + * @param \User $user User whose ID string we want + * @param $authType authentication type of ID string we want + * @return string + */ + public function getIdStringByAuthType($user, $authType) { + + $identifiers = $user->getUserIdentifiers(); + $idString = null; + + // For each auth type, check if an identifier matches + foreach ($identifiers as $identifier) { + if ($identifier->getKeyName() === $authType) { + $idString = $identifier->getKeyValue(); + } + } + + // If no user identifiers and want X.509, return certificateDn + if (count($identifiers) === 0 && $authType === 'X.509') { + $idString = $user->getCertificateDn(); + } + + return $idString; + } + /** * Adds an identifier to a user. * @param \User $user user having identifier added From 988bb4dc77c608ef69bf1f9a7fb6bac8fbcf7e83 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 5 Aug 2021 15:05:49 +0000 Subject: [PATCH 38/74] Add controller and view for user identifier removal Controller ensures users have permission to remove the identifier and do not remove their own in-use identifier. Limited information is given to non-admin users when validating the user/identifier IDs, although this could be restricted further. --- .../user/delete_user_identifier.php | 81 +++++++++++++++++++ htdocs/web_portal/index.php | 5 ++ .../views/user/deleted_user_identifier.php | 7 ++ 3 files changed, 93 insertions(+) create mode 100644 htdocs/web_portal/controllers/user/delete_user_identifier.php create mode 100644 htdocs/web_portal/views/user/deleted_user_identifier.php diff --git a/htdocs/web_portal/controllers/user/delete_user_identifier.php b/htdocs/web_portal/controllers/user/delete_user_identifier.php new file mode 100644 index 000000000..2ca89bae8 --- /dev/null +++ b/htdocs/web_portal/controllers/user/delete_user_identifier.php @@ -0,0 +1,81 @@ +getUserByPrinciple($currentIdString); + + if ($currentUser === null) { + throw new \Exception("Unregistered users cannot edit users"); + } + + // Check the portal is not in read only mode, returns exception if it is and user is not an admin + checkPortalIsNotReadOnlyOrUserIsAdmin($currentUser); + + // Get the posted data + $userId = $_REQUEST['id']; + $identifierId = $_REQUEST['identifierId']; + + $user = $serv->getUser($userId); + $identifier = $serv->getIdentifierById($identifierId); + + // Throw exception if not a valid user ID + if (is_null($user)) { + throw new \Exception("A user with ID '" . $userId . "' cannot be found"); + } + + $serv->editUserAuthorization($user, $currentUser); + + // Throw exception if not a valid identifier ID + // Non-admins can't tell if a given identifier matches a specific user + // but they can currently still tell how many identifiers exist + // This could be changed to only give info about if the identifier matches one of theirs + if (is_null($identifier)) { + throw new \Exception("An identifier with ID '" . $identifierId . "' cannot be found"); + } + + // Throw exception if trying to remove identifier that current user is authenticated with + if ($identifier->getKeyValue() === $currentIdString) { + throw new \Exception("You cannot unlink your current ID string. Please log in using a different authentication mechanism and try again."); + } + + $params = array('ID' => $user->getId()); + + try { + // Function will throw error if user does not have the correct permissions + $serv->deleteUserIdentifier($user, $identifier, $currentUser); + show_view("user/deleted_user_identifier.php", $params); + } catch (Exception $e) { + show_view('error.php', $e->getMessage()); + die(); + } +} \ No newline at end of file diff --git a/htdocs/web_portal/index.php b/htdocs/web_portal/index.php index 8eb2de063..2e28711dc 100644 --- a/htdocs/web_portal/index.php +++ b/htdocs/web_portal/index.php @@ -284,6 +284,11 @@ function Draw_Page($Page_Type) { require_once __DIR__.'/controllers/user/view_user.php'; view_user(); break; + case "Remove_User_Identifier": + rejectIfNotAuthenticated(); + require_once __DIR__.'/controllers/user/delete_user_identifier.php'; + delete_identifier(); + break; case "Downtime": rejectIfNotAuthenticated(); require_once __DIR__.'/controllers/downtime/view_downtime.php'; diff --git a/htdocs/web_portal/views/user/deleted_user_identifier.php b/htdocs/web_portal/views/user/deleted_user_identifier.php new file mode 100644 index 000000000..34f79f761 --- /dev/null +++ b/htdocs/web_portal/views/user/deleted_user_identifier.php @@ -0,0 +1,7 @@ +
    +

    Success

    +

    Identifier removed successfully.

    + + View user + +
    \ No newline at end of file From 769a05c2f6f0713907f944fecf2e6a0bc6ec0da7 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 5 Aug 2021 15:42:35 +0000 Subject: [PATCH 39/74] Update controller and view for admin editing user identifiers Updated functionality that allowed admins to edit user DNs to update user identifiers, including updating the ID string and/or auth type for an identifier, except the one they are currently using. If a user does not have identifiers, this also allows one to be added, if it matches their current certificateDn. --- .../controllers/admin/edit_user_dn.php | 103 ----------- .../admin/edit_user_identifier.php | 166 ++++++++++++++++++ htdocs/web_portal/index.php | 6 +- .../web_portal/views/admin/edit_user_dn.php | 19 -- .../views/admin/edit_user_identifier.php | 44 +++++ ...user_dn.php => edited_user_identifier.php} | 2 +- lib/Gocdb_Services/User.php | 40 ----- 7 files changed, 214 insertions(+), 166 deletions(-) delete mode 100644 htdocs/web_portal/controllers/admin/edit_user_dn.php create mode 100644 htdocs/web_portal/controllers/admin/edit_user_identifier.php delete mode 100644 htdocs/web_portal/views/admin/edit_user_dn.php create mode 100644 htdocs/web_portal/views/admin/edit_user_identifier.php rename htdocs/web_portal/views/admin/{edited_user_dn.php => edited_user_identifier.php} (89%) diff --git a/htdocs/web_portal/controllers/admin/edit_user_dn.php b/htdocs/web_portal/controllers/admin/edit_user_dn.php deleted file mode 100644 index b8726cfe8..000000000 --- a/htdocs/web_portal/controllers/admin/edit_user_dn.php +++ /dev/null @@ -1,103 +0,0 @@ -getUser($_REQUEST['id']); - - //Throw exception if not a valid user id - if(is_null($user)) { - throw new \Exception("A user with ID '".$_REQUEST['id']."' Can not be found"); - } - - $params["ID"]=$user->getId(); - $params["Title"]=$user->getTitle(); - $params["Forename"]=$user->getForename(); - $params["Surname"]=$user->getSurname(); - $params["CertDN"]=$user->getCertificateDn(); - - //show the edit user dn view - show_view("admin/edit_user_dn.php", $params, "Edit Certificate DN"); -} - -/** - * Retrieves the new dn from a portal request and submit it to the - * services layer's user functions. - * @return null -*/ -function submit() { - require_once __DIR__ . '/../../../../htdocs/web_portal/components/Get_User_Principle.php'; - - //Get a user service - $serv = \Factory::getUserService(); - - //Get the posted service type data - $userID =$_REQUEST['ID']; - $newDN = $_REQUEST['DN']; - $user = $serv->getUser($userID); - - //get the user data for the edit user DN function (so it can check permissions) - $currentUserDN = Get_User_Principle(); - $currentUser = $serv->getUserByPrinciple($currentUserDN); - - try { - //function will through error if user does not have the correct permissions - $serv->editUserDN($user, $newDN, $currentUser); - - $params = array('Name' => $user->getForename()." ".$user->getSurname(), - 'ID'=> $user->getId()); - show_view("admin/edited_user_dn.php", $params, "Success"); - } catch (Exception $e) { - show_view('error.php', $e->getMessage()); - die(); - } -} - -?> \ No newline at end of file diff --git a/htdocs/web_portal/controllers/admin/edit_user_identifier.php b/htdocs/web_portal/controllers/admin/edit_user_identifier.php new file mode 100644 index 000000000..bfb6fedf7 --- /dev/null +++ b/htdocs/web_portal/controllers/admin/edit_user_identifier.php @@ -0,0 +1,166 @@ +getUser($_REQUEST['id']); + + // Check user ID is valid + if (is_null($user)) { + throw new \Exception("A user with ID '" . $_REQUEST['id'] . "' cannot be found"); + } + + // Can only use identifier ID if user has identifiers + if (count($user->getUserIdentifiers()) !== 0) { + + // Throw exception if identifier ID not set or invalid + if (!isset($_REQUEST['identifierId']) || !is_numeric($_REQUEST['identifierId'])) { + throw new \Exception("An identifier ID must be specified for this user"); + } + + $identifier = $serv->getIdentifierbyId($_REQUEST['identifierId']); + + // Throw exception if not identifier doesn't exist + if (is_null($identifier)) { + throw new \Exception("An identifier with ID '" . $_REQUEST['identifierId'] . "' cannot be found"); + } + + $params["identifierId"] = $identifier->getId(); + $params["idString"] = $identifier->getKeyValue(); + $params["authType"] = $identifier->getKeyName(); + + // Prevent user editing the identifier they are currently using + if ($params["idString"] === Get_User_Principle()) { + throw new \Exception("You cannot edit the identifier you are using"); + } + + // Check identifier belongs to user + if ($user !== $serv->getUserByPrinciple($params["idString"])) { + throw new \Exception("The ID string must belong to the user"); + } + + } else { + // Use certificate DN + $params["identifierId"] = null; + $params["idString"] = $user->getCertificateDn(); + $params["authType"] = null; + } + + // Will show warning if no user identifiers + $dnWarning = false; + if ($params['identifierId'] === null) { + $dnWarning = true; + } + + // Get all valid auth types + $authTypes = $serv->getAuthTypes(false); + + $params["ID"] = $user->getId(); + $params["Title"] = $user->getTitle(); + $params["Forename"] = $user->getForename(); + $params["Surname"] = $user->getSurname(); + $params["dnWarning"] = $dnWarning; + $params["authTypes"] = $authTypes; + + // Show the edit user identifier view + show_view("admin/edit_user_identifier.php", $params, "Edit ID string"); +} + +/** + * Retrieves the new identifier from a portal request and submit it to the + * services layer's user functions. + * @return null +*/ +function submit() { + require_once __DIR__ . '/../../../../htdocs/web_portal/components/Get_User_Principle.php'; + + $serv = \Factory::getUserService(); + + // Get the posted data + $userID = $_REQUEST['ID']; + $newIdString = $_REQUEST['idString']; + $identifierId = $_REQUEST['identifierId']; + $newAuthType = $_REQUEST['authType']; + + $user = $serv->getUser($userID); + + // If identifier exists, fetch and prepare updated values for edit + $identifier = null; + if ($identifierId !== "") { + $identifier = $serv->getIdentifierById($identifierId); + } + $identifierArr = array($newAuthType, $newIdString); + + // Get the user data for the edit user identifier function (so it can check permissions) + $currentIdString = Get_User_Principle(); + $currentUser = $serv->getUserByPrinciple($currentIdString); + + try { + // Function will throw error if user does not have the correct permissions + if ($identifier !== null) { + $serv->editUserIdentifier($user, $identifier, $identifierArr, $currentUser); + } else { + $serv->migrateUserCredentials($user, $identifierArr, $currentUser); + } + + $params = array('Name' => $user->getForename() . " " . $user->getSurname(), + 'ID' => $user->getId()); + show_view("admin/edited_user_identifier.php", $params, "Success"); + } catch (\Exception $e) { + show_view('error.php', $e->getMessage()); + die(); + } +} + +?> \ No newline at end of file diff --git a/htdocs/web_portal/index.php b/htdocs/web_portal/index.php index 2e28711dc..b0bc2f2b5 100644 --- a/htdocs/web_portal/index.php +++ b/htdocs/web_portal/index.php @@ -487,10 +487,10 @@ function Draw_Page($Page_Type) { require_once __DIR__.'/controllers/admin/users.php'; show_users(); break; - case "Admin_Edit_User_DN": + case "Admin_Edit_User_Identifier": rejectIfNotAuthenticated(); - require_once __DIR__.'/controllers/admin/edit_user_dn.php'; - edit_dn(); + require_once __DIR__.'/controllers/admin/edit_user_identifier.php'; + edit_identifier(); break; // case "Admin_Change_User_Admin_Status": // rejectIfNotAuthenticated(); diff --git a/htdocs/web_portal/views/admin/edit_user_dn.php b/htdocs/web_portal/views/admin/edit_user_dn.php deleted file mode 100644 index cd0d3690e..000000000 --- a/htdocs/web_portal/views/admin/edit_user_dn.php +++ /dev/null @@ -1,19 +0,0 @@ -
    -

    Update Certificate DN

    -
    -
    - The current certificate DN for - - is: -
    - -
    -
    -
    - New Certificate DN - - -
    - -
    -
    \ No newline at end of file diff --git a/htdocs/web_portal/views/admin/edit_user_identifier.php b/htdocs/web_portal/views/admin/edit_user_identifier.php new file mode 100644 index 000000000..5bf878b65 --- /dev/null +++ b/htdocs/web_portal/views/admin/edit_user_identifier.php @@ -0,0 +1,44 @@ +
    +

    Update User Identifier

    + +
    + +
    + The ID string for + + is: +
    + +
    + +
    + +
    > + Warning: This user does not have any UserIdentifiers. Submitting will create an identifier, which must share the current ID string. +
    +
    +
    + +
    + New ID String + +
    + +
    + New Authentication Type: + +
    + + + + +
    +
    \ No newline at end of file diff --git a/htdocs/web_portal/views/admin/edited_user_dn.php b/htdocs/web_portal/views/admin/edited_user_identifier.php similarity index 89% rename from htdocs/web_portal/views/admin/edited_user_dn.php rename to htdocs/web_portal/views/admin/edited_user_identifier.php index 4e3050f9e..87b259864 100644 --- a/htdocs/web_portal/views/admin/edited_user_dn.php +++ b/htdocs/web_portal/views/admin/edited_user_identifier.php @@ -1,7 +1,7 @@

    Success


    - The certificate DN for + The identifier for has been successfully updated. diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index f13650c46..a89120898 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -398,46 +398,6 @@ public function register($userValues, $userIdentifierValues) { return $user; } - /** - * Update a user's DN - * @param \User $user user to have DN updated - * @param string $dn new DN - * @param \User $currentUser User doing the updating - * @throws \Exception - * @throws \org\gocdb\services\Exception - */ - public function editUserDN(\User $user, $dn, \User $currentUser = null){ - //Authorisation - only GOCDB Admins shoud be able to change DNs (Throws exception if not) - $this->checkUserIsAdmin($currentUser); - - //Check the DN is changed - if($dn == $user->getCertificateDn()) { - throw new \Exception("The specified certificate DN is the same as the current DN"); - } - - //Check the DN is unique (if not null) - if(!is_null($this->getUserByPrinciple($dn))) { - throw new \Exception("DN is already registered in GOCDB"); - } - - //Validate the DN - $dnInAnArray['CERTIFICATE_DN']= $dn; - $this->validateUser($dnInAnArray); - - //Explicity demarcate our tx boundary - $this->em->getConnection()->beginTransaction(); - try { - $user->setCertificateDn($dn); - $this->em->merge($user); - $this->em->flush(); - $this->em->getConnection()->commit(); - } catch (\Exception $e) { - $this->em->getConnection()->rollback(); - $this->em->close(); - throw $e; - } - } - /** * Deletes a user * @param \User $user To be deleted From 62e0badcb25afd8f5a876b7e4a85fc3e6504a917 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 5 Aug 2021 15:49:53 +0000 Subject: [PATCH 40/74] Update user search function for user identifiers Search both user identifiers and certificateDns for an ID string when searching for users. --- lib/Gocdb_Services/User.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Gocdb_Services/User.php b/lib/Gocdb_Services/User.php index a89120898..c926e8e7f 100644 --- a/lib/Gocdb_Services/User.php +++ b/lib/Gocdb_Services/User.php @@ -425,17 +425,17 @@ public function deleteUser(\User $user, \User $currentUser = null) { * forename and surname are handled case insensitivly * @param string $surname surname of users to be returned (matched case insensitivly) * @param string $forename forename of users to be returned (matched case insensitivly) - * @param string $dn dn of user to be returned. If specified only one user will be returned. Matched case sensitivly + * @param string $idString ID string of user to be returned. If specified only one user will be returned. Matched case sensitivly * @param mixed $isAdmin if null then admin status is ignored, if true only admin users are returned and if false only non admins * @return array An array of site objects */ - public function getUsers($surname=null, $forename=null, $dn=null, $isAdmin=null) { + public function getUsers($surname=null, $forename=null, $idString=null, $isAdmin=null) { $dql = - "SELECT u FROM User u + "SELECT u FROM User u LEFT JOIN u.userIdentifiers up WHERE (UPPER(u.surname) LIKE UPPER(:surname) OR :surname is null) AND (UPPER(u.forename) LIKE UPPER(:forename) OR :forename is null) - AND (u.certificateDn LIKE :dn OR :dn is null) + AND (u.certificateDn LIKE :idString OR up.keyValue LIKE :idString OR :idString is null) AND (u.isAdmin = :isAdmin OR :isAdmin is null) ORDER BY u.surname"; @@ -443,7 +443,7 @@ public function getUsers($surname=null, $forename=null, $dn=null, $isAdmin=null) ->createQuery($dql) ->setParameter(":surname", $surname) ->setParameter(":forename", $forename) - ->setParameter(":dn", $dn) + ->setParameter(":idString", $idString) ->setParameter(":isAdmin", $isAdmin) ->getResult(); From be1bf599798e766c7d82b68d93c1ef13e5d112b8 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Thu, 5 Aug 2021 15:21:05 +0000 Subject: [PATCH 41/74] Update (admin) user views and conrollers for user identifiers The user view now shows the default ID string, as well as a table for each ID string and auth type, with buttons to remove each identifier if the user has permission. The admin view includes all identifiers associated with each user, each of which can be edited. --- htdocs/web_portal/controllers/admin/users.php | 23 +++--- .../web_portal/controllers/user/view_user.php | 17 +++-- htdocs/web_portal/views/admin/users.php | 47 +++++++----- htdocs/web_portal/views/user/view_user.php | 76 ++++++++++++++----- 4 files changed, 109 insertions(+), 54 deletions(-) diff --git a/htdocs/web_portal/controllers/admin/users.php b/htdocs/web_portal/controllers/admin/users.php index 8bbab4116..c309db037 100644 --- a/htdocs/web_portal/controllers/admin/users.php +++ b/htdocs/web_portal/controllers/admin/users.php @@ -20,7 +20,7 @@ * /*====================================================== */ -function show_users(){ +function show_users() { require_once __DIR__.'/../../../../lib/Gocdb_Services/Factory.php'; require_once __DIR__ . '/../utils.php'; require_once __DIR__ . '/../../../web_portal/components/Get_User_Principle.php'; @@ -42,31 +42,30 @@ function show_users(){ } $params["Forename"] = $forename; - $dn = null; - if(!empty($_REQUEST['DN'])) { - $dn = $_REQUEST['DN']; + $idString = null; + if (!empty($_REQUEST['IdString'])) { + $idString = $_REQUEST['IdString']; } - $params["DN"] = $dn; + $params["IdString"] = $idString; //Note that the true/false specified must be converted into boolean true/false. $isAdmin = null; - if(!empty($_REQUEST['IsAdmin'])) { - if($_REQUEST['IsAdmin']=="true"){ + if (!empty($_REQUEST['IsAdmin'])) { + if ($_REQUEST['IsAdmin'] === "true") { $isAdmin = true; } - elseif ($_REQUEST['IsAdmin']=="false"){ + elseif ($_REQUEST['IsAdmin'] === "false") { $isAdmin = false; } } $params["IsAdmin"] = $isAdmin; - $currentUserDN = Get_User_Principle(); - $user = \Factory::getUserService()->getUserByPrinciple($currentUserDN); + $currentIdString = Get_User_Principle(); + $user = \Factory::getUserService()->getUserByPrinciple($currentIdString); $params['portalIsReadOnly'] = portalIsReadOnlyAndUserIsNotAdmin($user); //get users - $params["Users"] = \Factory::getUserService()->getUsers($surname, $forename, $dn, $isAdmin); - + $params["Users"] = \Factory::getUserService()->getUsers($surname, $forename, $idString, $isAdmin); show_view("admin/users.php", $params, "Users"); } \ No newline at end of file diff --git a/htdocs/web_portal/controllers/user/view_user.php b/htdocs/web_portal/controllers/user/view_user.php index 656210724..458bef6a2 100644 --- a/htdocs/web_portal/controllers/user/view_user.php +++ b/htdocs/web_portal/controllers/user/view_user.php @@ -28,12 +28,15 @@ function view_user() { } $userId = $_GET['id']; - $user = \Factory::getUserService()->getUser($userId); - if($user === null){ + $serv = \Factory::getUserService(); + $user = $serv->getUser($userId); + if ($user === null) { throw new Exception("No user with that ID"); } $params['user'] = $user; + // Check if user only has one identifier to disable unlinking + $params['lastIdentifier'] = (count($user->getUserIdentifiers()) === 1); // 2D array, each element stores role and a child array holding project Ids $role_ProjIds = array(); @@ -41,7 +44,9 @@ function view_user() { // get the targetUser's roles $roles = \Factory::getRoleService()->getUserRoles($user, \RoleStatus::GRANTED); //$user->getRoles(); - $callingUser = \Factory::getUserService()->getUserByPrinciple(Get_User_Principle()); + $currentIdString = Get_User_Principle(); + $params['currentIdString'] = $currentIdString; + $callingUser = $serv->getUserByPrinciple($currentIdString); // can the calling user revoke the targetUser's roles? /* @var $r \Role */ @@ -107,15 +112,13 @@ function view_user() { // Check to see if the current calling user has permission to edit the target user try { - \Factory::getUserService()->editUserAuthorization($user, $callingUser); + $serv->editUserAuthorization($user, $callingUser); $params['ShowEdit'] = true; } catch (Exception $e) { $params['ShowEdit'] = false; } - /* @var $authToken \org\gocdb\security\authentication\IAuthentication */ - $authToken = Get_User_AuthToken(); - $params['authAttributes'] = $authToken->getDetails(); + $params['idString'] = $serv->getDefaultIdString($user); $params['projectNamesIds'] = $projectNamesIds; $params['role_ProjIds'] = $role_ProjIds; diff --git a/htdocs/web_portal/views/admin/users.php b/htdocs/web_portal/views/admin/users.php index 426c22393..bb886277b 100644 --- a/htdocs/web_portal/views/admin/users.php +++ b/htdocs/web_portal/views/admin/users.php @@ -1,5 +1,5 @@ - +
    @@ -52,10 +52,10 @@
    - Certificate DN: - ID String: + - + - + 0): ?> - + 0) { - foreach($users as $user) { + if ($numUsers > 0) { + foreach ($users as $user) { ?> - isAdmin()) { $style = " style=\"background-color: #A3D7A3;\""; } else { $style = ""; } ?> + isAdmin()) { $style = " style=\"background-color: #A3D7A3;\""; } else { $style = ""; } ?> > +
    NameCertificate DNID String GOCDB
    Admin.?
    @@ -102,13 +102,24 @@
    + getUserIdentifiers()) > 0) { + foreach ($user->getUserIdentifiers() as $i => $identifier) { + ?> - - getCertificateDn()); ?> + + getKeyName() . ": " . $identifier->getKeyValue(); ?> +
    + + + + getCertificateDn(); ?> + +
    @@ -138,9 +149,9 @@
    -
    +
      Click on a user's name to view more details, or to edit or - delete them. Click on their DN to update it. + delete them. Click on their ID string to update it.
    diff --git a/htdocs/web_portal/views/user/view_user.php b/htdocs/web_portal/views/user/view_user.php index 26b965306..596e1a92d 100644 --- a/htdocs/web_portal/views/user/view_user.php +++ b/htdocs/web_portal/views/user/view_user.php @@ -16,7 +16,7 @@ if(!$params['portalIsReadOnly']) { ?> + +
    + + Identifiers + + + + + + + + + + getUserIdentifiers() as $identifier): ?> -
    - Authentication Attributes: -
    - $val) { - $attributeValStr = ''; - foreach ($val as $v) { - $attributeValStr .= ', '.$v; - } - if(strlen($attributeValStr) > 2){$attributeValStr = substr($attributeValStr, 2);} - xecho('[' . $key . '] [' . $attributeValStr . ']'); - echo '
    '; - } - ?> +
    + + + + + + + +
    ID StringAuthentication typeRemove Identifier
    +
    + getKeyValue())?> +
    +
    +
    + getKeyName())?> +
    +
    +
    +
    + getKeyValue()) echo "title='Cannot remove the identifier you are using'";?> + > + getKeyValue()) echo "disabled";?> + > +
    +
    +
    - - + +
    diff --git a/htdocs/web_portal/views/admin/scopes.php b/htdocs/web_portal/views/scopes.php similarity index 82% rename from htdocs/web_portal/views/admin/scopes.php rename to htdocs/web_portal/views/scopes.php index aec09ce10..6aaf7f48e 100644 --- a/htdocs/web_portal/views/admin/scopes.php +++ b/htdocs/web_portal/views/scopes.php @@ -5,11 +5,11 @@ Scopes - Click on the name of a scope to edit it and view objects with that scope tag. + Click on the name of a scope to view objects with that scope tag.
    - - + +
    @@ -29,7 +29,7 @@ - + @@ -42,13 +42,13 @@ - + - +
    Name Remove
    From c0810f950db8de7f72a3a882a87f550d7ebd9676 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Tue, 18 May 2021 15:07:22 +0000 Subject: [PATCH 60/74] Display all scopes information on Scopes page Add information from Scope Help page to Scopes, including help information, scope descriptions and whether scopes are reserved. --- htdocs/web_portal/controllers/scopes.php | 8 ++ htdocs/web_portal/views/scopes.php | 103 ++++++++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/htdocs/web_portal/controllers/scopes.php b/htdocs/web_portal/controllers/scopes.php index 557e8b55e..188a37173 100644 --- a/htdocs/web_portal/controllers/scopes.php +++ b/htdocs/web_portal/controllers/scopes.php @@ -36,6 +36,14 @@ function show_scopes() { $params['UserIsAdmin'] = $user->isAdmin(); } + $optionalScopes = \Factory::getScopeService()->getScopesFilterByParams( + array('excludeReserved' => true), null); + $reservedScopes = \Factory::getScopeService()->getScopesFilterByParams( + array('excludeNonReserved' => true), null); + + $params['optionalScopes'] = $optionalScopes; + $params['reservedScopes'] = $reservedScopes; + show_view('scopes.php', $params, 'Scopes'); die(); } diff --git a/htdocs/web_portal/views/scopes.php b/htdocs/web_portal/views/scopes.php index 6aaf7f48e..f4af57b09 100644 --- a/htdocs/web_portal/views/scopes.php +++ b/htdocs/web_portal/views/scopes.php @@ -29,8 +29,10 @@ + + - + - + + -
    NameDescriptionReserved? RemoveRemove
    + getDescription()); ?> + @@ -64,4 +68,97 @@ ?>
    + + + +
    +

    What are scope tags?

    +
      +
    • + Scope tags are used to selectively tag Services, ServiceGroups, + Sites and NGIs so that API queries and users of the UI can filter for + objects that define the required set of tags. +
    • +
    • + The available tags are controlled by the GOCDB admins, allowing + users to select relevant tags from the list. New tags can + be requested if required. +
    • +
    • + In EGI, scope tags are used to categorise resources, + e.g. a Site could be tagged with the 'EGI' and 'ProjX' tags + while a single 'Local' tag can be used to declare that + this site does not provide any resources to EGI or ProjX. +
    • +
    • + Scope tags should not be confused with projects. Projects + provide a means to cascade roles/permissions over + child resources (NGIs, Sites, Services) grouped under the project. + Scope tags have no effect on permissions. +
    • + +
    +

    What are Reserved tags?

    +
      +
    • Some tags may be 'Reserved' which means they are protected - they are used to restrict tag usage + and prevent non-authorised sites/services from using tags not intended for them.
    • +
    • Reserved tags are initially assigned to resources by the gocdb-admins, and can then be optionially + inherited by child resources (tags can be initially assigned to NGIs, Sites, Services and ServiceGroups).
    • +
    • When creating a new child resource (e.g. a child Site or child Service), + the scopes that are assigned to the parent are automatically inherited and assigned to the child.
    • +
    • Reserved tags assigned to a resource are optional and can be de-selected if required.
    • +
    • Users can reapply Reserved tags to a resource ONLY if the tag can be + inherited from the parent Scoped Entity (parents include NGIs/Sites).
    • +
    • For Sites: If a Reserved tag is removed from a Site, then the same tag is also removed + from all the child Services - a Service can't have a reserved tag that + is not supported by its parent Site.
    • +
    • For NGIs: If a Reserved tag is removed from an NGI, then the same tag is NOT + removed from all the child Sites - this is intentionally different from the Site→Service relationship.
    • +
    +

    How are scope tags used in the API?

    + The following are some examples of scope tags in use in PI + queries: +
      +
    • +
      ?method=get_site&scope=EGI
      + (Fetch all sites tagged as 'EGI') +
    • +
    • +
      ?method=get_site&scope=EGI,ProjX&scope_match=all
      + (Fetch all sites tagged with both 'EGI' and ProjX) +
    • +
    • +
      ?method=get_site&scope=EGI,ProjX&scope_match=any
      + (Fetch all sites tagged with either 'EGI' or ProjX) +
    • +
    • +
      ?method=get_site&scope=
      + (Fetch all sites regardless of scope tags) +
    • +
    + +

    What does having the EGI scope applied mean?

    +
      +
    • + If a site, service, service group, or NGI is scoped as ‘EGI’ then it + will be exposed to the central operational tools for monitoring and + will appear in the operations portal. +
    • +
    • + The 'EGI' scope not being selected for a given object makes the + object invisible to EGI and the central operation tools (it will not + show in the central dashboard and it will not be monitored + centrally). This can be useful if you wish to hide certain parts of + your infrastructure from EGI but still have the information stored + and accessed from the same GOCDB instance. In this case you should + use the 'Local' scope tag. +
    • +
    • + Note that scoping a site / service endpoint as EGI does not override + the production status or certification status fields. For example if + a site is not marked as production it won't be monitored centrally + even if it's marked as visible to EGI. +
    • +
    +
    From 15b02119871eb0296d3432f3905d7ce39188e558 Mon Sep 17 00:00:00 2001 From: ElliottKasoar Date: Tue, 18 May 2021 14:08:11 +0000 Subject: [PATCH 61/74] Delete Scope Help scripts and update references Deleted Scope Help view, controller and menu link, as all information is now in Scopes page. References to the page have been updated to instead reference Scopes. --- config/local_info.xml | 1 - config/web_portal/menu.xml | 6 - htdocs/web_portal/controllers/scope_help.php | 35 ----- htdocs/web_portal/index.php | 5 - .../views/admin/view_service_type.php | 2 +- .../views/downtime/downtimes_calendar.php | 2 +- .../views/downtime/view_downtime.php | 2 +- htdocs/web_portal/views/ngi/view_ngi.php | 2 +- htdocs/web_portal/views/ngi/view_ngis.php | 2 +- htdocs/web_portal/views/scope_help.php | 131 ------------------ htdocs/web_portal/views/service/view_all.php | 2 +- .../web_portal/views/service/view_service.php | 2 +- .../add_new_se_to_service_group.php | 2 +- .../views/service_group/view_all.php | 2 +- .../views/service_group/view_sgroup.php | 4 +- htdocs/web_portal/views/site/view_all.php | 2 +- htdocs/web_portal/views/site/view_site.php | 2 +- 17 files changed, 13 insertions(+), 191 deletions(-) delete mode 100644 htdocs/web_portal/controllers/scope_help.php delete mode 100644 htdocs/web_portal/views/scope_help.php diff --git a/config/local_info.xml b/config/local_info.xml index 6287a7a75..7f1c952cb 100755 --- a/config/local_info.xml +++ b/config/local_info.xml @@ -131,7 +131,6 @@ show show show - show diff --git a/htdocs/web_portal/views/downtime/view_downtime.php b/htdocs/web_portal/views/downtime/view_downtime.php index f79c5b433..8e4f284e1 100644 --- a/htdocs/web_portal/views/downtime/view_downtime.php +++ b/htdocs/web_portal/views/downtime/view_downtime.php @@ -137,7 +137,7 @@ function submitform()
    Service Hostname (service type) Description ProductionScope(s)Scope(s)
    diff --git a/htdocs/web_portal/views/ngi/view_ngis.php b/htdocs/web_portal/views/ngi/view_ngis.php index bf90e2f69..a12878f91 100644 --- a/htdocs/web_portal/views/ngi/view_ngis.php +++ b/htdocs/web_portal/views/ngi/view_ngis.php @@ -25,7 +25,7 @@
    - Scopes: + Scopes:
    - - - - - - - - - - - - - - - - - - - - - - - -
    Tag nameDescriptionReserved?
    getName());?>getDescription()); ?>
    getName());?>getDescription()); ?>
    -
    -
    - - - -
    -

    What does having the EGI scope applied mean?

    -
      -
    • - If a site, service, service group, or NGI is scoped as ‘EGI’ then it - will be exposed to the central operational tools for monitoring and - will appear in the operations portal. -
    • -
    • - The 'EGI' scope not being selected for a given object makes the - object invisible to EGI and the central operation tools (it will not - show in the central dashboard and it will not be monitored - centrally). This can be useful if you wish to hide certain parts of - your infrastructure from EGI but still have the information stored - and accessed from the same GOCDB instance. In this case you should - use the 'Local' scope tag. -
    • -
    • - Note that scoping a site / service endpoint as EGI does not override - the production status or certification status fields. For example if - a site is not marked as production it won't be monitored centrally - even if it's marked as visible to EGI. -
    • -
    -
    - -
    diff --git a/htdocs/web_portal/views/service/view_all.php b/htdocs/web_portal/views/service/view_all.php index d3725bdfe..311c947cc 100644 --- a/htdocs/web_portal/views/service/view_all.php +++ b/htdocs/web_portal/views/service/view_all.php @@ -91,7 +91,7 @@
    - Service Scopes: + Service Scopes: