Files
namaste/classes/gacUsers.class.inc
gramps 373ebc8c93 Archive: Namaste PHP AMQP framework v1.0 (2017-2020)
952 days continuous production uptime, 40k+ tp/s single node.
Original corpo Bitbucket history not included — clean archive commit.
2026-04-05 09:49:30 -07:00

963 lines
41 KiB
PHP

<?php
/**
* Class gacUsers -- public GA class
*
* This is a Namaste::Admin data class for all things User.
*
* The user data lives on Namaste's Admin service. This class encompasses all the code for managing the user entity
* including CRUD requests, and general user-type events such as login, logout, etc. The sister-table to this
* collection is also a mongo collection living on Namaste::Admin and that's the session class.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
*
* HISTORY:
* ========
* 08-17-20 mks DB-168: original coding
*
*/
class gacUsers extends gacMongoDB
{
public string $res = 'cUSR: ';
public ?array $securityConfig = null;
public ?array $emailConfig = null;
public ?array $sessionConfig = null;
public string $myToken;
public string $myAPIKey;
public string $myUserType;
/**
* gacUsers constructor.
* @param array|null $_meta
* @param string $_id
*/
public function __construct(?array $_meta = null, string $_id = '')
{
if (empty($_meta) or is_null($_meta)) {
$_meta = [
META_TEMPLATE => TEMPLATE_CLASS_USERS,
META_SESSION_DAEMON => 1 // todo -- hook this up to something!
];
} elseif (!isset($_meta[META_TEMPLATE])) {
// client didn't submit a template; let's fix that for them
$_meta[META_TEMPLATE] = TEMPLATE_CLASS_USERS;
} elseif ($_meta[META_TEMPLATE] != TEMPLATE_CLASS_USERS) {
// there's a difference between unset and being set to the wrong class
// todo -- system event for a hack attempt
$_meta[META_TEMPLATE] = TEMPLATE_CLASS_USERS;
}
try {
parent::__construct($_meta, $_id);
if (!$this->isServiceLocal(ENV_TERCERO)) return;
$this->myToken = '';
$this->myAPIKey = '';
$this->myUserType = '';
$this->securityConfig = gasConfig::$settings[CONFIG_SECURITY];
$this->emailConfig = gasConfig::$settings[CONFIG_EMAIL];
$this->sessionConfig = gasConfig::$settings[CONFIG_SESSIONS];
} catch (Throwable $t) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$this->eventMessages[] = ERROR_EXCEPTION;
$msg = ERROR_FAILED_TO_INSTANTIATE . TEMPLATE_CLASS_USERS;
$this->logger->fatal($hdr . $msg);
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
}
/**
* registerNewUser() -- public method
*
* This method is called when we're adding a new user to the system. The array of request data, as processed by
* the broker, is the only input parameter.
*
* The function is of type void -- processing results are reflected in the class member settings.
*
* The method process the input data, ensuring that the minimally-required fields are present. The method then
* performs the following validation checks and storage actions:
*
* 1. Validate the user's email address and domain
* 2. Create the user record
* 3. Create the user's session, linking the user record
* 4. Create a System Event for the timer (expiry) event on Admin
* 5. Publish an Admin request to register the session with AT(1)
*
* In the event of a system error, where we fail to instantiate a class or save a record, since we're working
* with three different class objects in this method, and if the error is encountered in the session or system-
* event classes, then we'll copy the eventMessages stack on that object over to the user object before
* returning control to the calling client.
*
* Again, as there are no explicit or implicit returns via the parameters, the calling client has to check the
* user-class data members for the results.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
*
*
* HISTORY:
* ========
* 09-14-20 mks DB-168: Original coding
*
*/
public function registerNewUser(array $_data):void
{
$error = false;
$this->state = STATE_VALIDATION_ERROR;
$this->status = false;
// we minimally need the user email and hashed password to proceed
$minFields = [ USER_PII_EMAIL . $this->ext, USER_PASSWORD . $this->ext ]; // add fields as necessary
// first, we're only going to allow the creation of one record per request...
// ...so make sure $data is an array...
if (!is_array($_data[BROKER_DATA])) {
$this->eventMessages[] = ERROR_DATA_ARRAY_NOT_ARRAY;
return;
}
// ...and that the array only has one record
if (count($_data[BROKER_DATA]) != 1) {
$this->eventMessages[] = sprintf(ERROR_DATA_ARRAY_COUNT, 1, STRING_DATA, count($_data[BROKER_DATA]));
return;
}
$data = $_data[BROKER_DATA][0];
// make sure the minimally-required fields are present:
foreach ($minFields as $field) {
if (!array_key_exists($field, $data)) {
$this->eventMessages[] = ERROR_DATA_KEY_404 . $field;
$error = true;
}
}
if ($error) return;
// email validation: proper email, wblist, email does not already exist
$this->validateUserEmail($data[USER_PII_EMAIL . $this->ext]);
if (!$this->status) return;
// grab the partnerID, if it exists
// (if no partner ID, the assumption is that we're creating an internal user)
if (array_key_exists(CLIENT_AUTH_TOKEN, $_data[BROKER_META_DATA]))
$data[USER_PARTNER_API_KEY] = $_data[BROKER_META_DATA][CLIENT_AUTH_TOKEN];
// create the new-user record and start gathering session data
$this->_createRecord([$data]);
if (!$this->status) return;
// calculate the session duration based on the XML configuration
$duration = (gasConfig::$settings[CONFIG_SESSIONS][CONFIG_SESSIONS_DURATION_DAYS])
? gasConfig::$settings[CONFIG_SESSIONS][CONFIG_SESSIONS_DURATION_DAYS] * NUMBER_ONE_DAY
: gasConfig::$settings[CONFIG_SESSIONS][CONFIG_SESSIONS_DURATION_HOURS] * NUMBER_ONE_HOUR_SEC;
// build the session-record payload
$sessionData = [
SESSION_EXPIRES => time() + $duration,
SESSION_DURATION => $duration,
SESSION_LEVEL => SESSION_LEVEL_USER,
SESSION_FK_USER => $this->getColumn(DB_TOKEN)
// todo: other fields are legacy and we need to learn what to fill them with...
];
// instantiate a new session object
$metaCopy = $this->metaPayload;
$metaCopy[META_TEMPLATE] = TEMPLATE_CLASS_SESSIONS;
$errors = [];
if (is_null($objSession = grabWidget($metaCopy, '', $errors))) {
$this->eventMessages = [...$this->eventMessages, $errors];
$this->state = STATE_FRAMEWORK_WARNING;
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_TEMPLATE_INSTANTIATE . $metaCopy[META_TEMPLATE];
$this->logger->warn($hdr . $msg);
consoleLog($this->res, CON_ERROR, $msg);
return;
}
// create the session record
$objSession->_createRecord([$sessionData], DATA_USER);
if (!$objSession->status) {
$this->eventMessages = [ ...$this->eventMessages, ...$objSession->eventMessages ];
if (is_object($objSession)) $objSession->__destruct();
unset($objSession);
$this->state = STATE_FRAMEWORK_FAIL;
return;
}
// theses two values will be harvested back up at the broker level and used to create the return payload
$this->sessionGUID = $objSession->getColumn(DB_TOKEN);
$this->userGUID = $this->getColumn(DB_TOKEN);
// create the system-event data and publish the event to admin (or save locally if admin is local)
$eventData = [
SYSTEM_EVENT_NAME => EVENT_NAME_SESSION_EXPIRY,
SYSTEM_EVENT_STATUS => STATUS_ACTIVE,
SYSTEM_EVENT_TYPE => EVENT_TYPE_SESSION,
SYSTEM_EVENT_FK_SESSION_GUID => $this->sessionGUID,
SYSTEM_EVENT_FK_USER_GUID => $this->userGUID,
SYSTEM_EVENT_CLASS => get_class($this),
SYSTEM_EVENT_DURATION => $duration,
SYSTEM_EVENT_BROKER_EVENT => $_data[BROKER_REQUEST],
SYSTEM_EVENT_OGUID => $metaCopy[META_EVENT_GUID],
SYSTEM_EVENT_CODE_LOC => basename(__FILE__) . AT . __LINE__,
SYSTEM_EVENT_META_DATA => $metaCopy,
SYSTEM_EVENT_NOTES => basename(__METHOD__)
];
// invoke the function to publish the system event request and register the session with AT(1) on Admin:
if (!postSystemEvent($eventData, $metaCopy[META_EVENT_GUID], $this->logger)) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
@handleExceptionMessaging($hdr, ERROR_MDB_SYS_EVENT_SAVE, $this->eventMessages, true);
}
// call admin to fetch the token for the system event record we just created
// next, register the event with the admin broker AT(1) service
$request = [
BROKER_REQUEST => BROKER_REQUEST_NEW_SESSION,
BROKER_DATA => [ SYSTEM_EVENT_DURATION => $duration ],
BROKER_META_DATA => [
META_TEMPLATE => TEMPLATE_CLASS_SYS_EVENTS,
META_SESSION_GUID => $this->sessionGUID,
META_CLIENT => CLIENT_SYSTEM
]
];
// get a copy of the record we just created
// $dataSystemEvent = $objSession->getData();
// $dataSystemEvent = $dataSystemEvent[0];
// create the broker client and publish the BROKER_REQUEST_NEW_SESSION event to AdminInBroker to register
// the session expiry with AT(1)
/** @var gacWorkQueueClient $tmpObj */
$tmpObj = new gacWorkQueueClient(basename(__METHOD__) . AT . __LINE__);
if (is_null($tmpObj) or !$tmpObj->status) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_TEMPLATE_INSTANTIATE . sprintf(ERROR_BROKER_CLIENT_INSTANTIATION, STRING_WORK_QUEUE_CLIENT);
@handleExceptionMessaging($hdr, $msg, $this->eventMessages, true);
return;
}
// fire-n-forget queue
$tmpObj->call(gzcompress(json_encode($request)));
// todo: validation email
// clean-up and return a success condition
if (is_object($objSession)) $objSession->__destruct();
if (is_object($tmpObj)) $tmpObj->__destruct();
unset($objSession, $tmpObj);
$this->state = STATE_SUCCESS;
$this->status = true;
}
/**
* validateUserEmail() -- public method
*
* this method is the single entry-point for all email checks.
*
* this method checks a submitted email address:
*
* 1. sanitize the email removing invalid characters
* 2. validate that the email is in the correct format
* 3. validate the email domain
* 4. validate the the email is unique
* 5. validate against the WBL list
*
* If case 1 or case 2 fails, then a STATE_VALIDATION_ERROR is returned -- the email address itself is invalid
*
* Next, check to see if the email address exists in the database. We're going to check both the primary
* and alternate email addresses.
*
* if the query comes back success and the data count is true, that's implicit indication that the
* email is in-use. What we want to do is make an additional check on the status of the email.
*
* If the status is false, (from the query), then we want to check for a 404-state -- indicating that no records
* were found for that email and it's ok to use.
*
* any other state/status combination defaults to a framework warning and a check-logs diagnostic is generated.
*
* Return States:
* --------------
* STATE_SUCCESS -- email address is valid, not already in-use, and is white-listed
* STATE_NOT_WHITE_LIST -- email does not appear on the white list and white-list checks are enabled
* STATE_BLACK_LIST -- email has been black-listed
* STATE_VALIDATION_ERROR -- email address as submitted was empty or malformed
* STATE_ALREADY_EXISTS -- email address is in-use and cannot be reused
* STATE_MAIL_FAIL -- processing error within the framework
* STATE_FRAMEWORK_WARNING -- some random bad thing happened and needs investigation
*
* Note that the $_email parameter is a call-by-reference who's contents may be altered by this method.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_email -- the email to validate/check
* @param Boolean $_skipCheck -- if set to true, skip email-already-exists check (for logins)
*
* HISTORY:
* ========
* 08-18-20 mks DB-168: original coding
*
*/
public function validateUserEmail(string &$_email, bool $_skipCheck = false):void
{
$oldData = null;
$whiteList = true;
$blackList = true;
$oldCount = 0;
$this->state = STATE_VALIDATION_ERROR;
$this->status = false;
$backup = false;
if (empty($this->metaPayload)) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$this->logger->warn($hdr . ERROR_DATA_META_404);
$this->eventMessages[] = ERROR_DATA_META_404;
$this->status = false;
$this->state = STATE_META_ERROR;
return;
}
$metaCopy = $this->metaPayload;
$metaCopy[META_TEMPLATE] = TEMPLATE_CLASS_WBL;
if ($this->count and !empty($this->data)) {
$oldData = $this->getData();
$oldCount = $this->count;
$this->data = [];
$this->count = 0;
$backup = true;
}
// ensure we have input parameters
if (empty($_email)) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_DATA_KEY_404 . USER_PII_EMAIL;
$this->eventMessages[] = $msg;
$this->logger->data($hdr . $msg);
if ($backup) {
$this->count = $oldCount;
$this->data = $oldData;
}
return;
}
if (empty($this->emailConfig) or !is_array($this->emailConfig)) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_CONFIG_RESOURCE_404 . RESOURCE_EMAIL;
$this->eventMessages[] = $msg;
$this->logger->data($hdr . $msg);
if ($backup) {
$this->count = $oldCount;
$this->data = $oldData;
}
return;
}
// force the user email to lowercase
$_email = trim(mb_strtolower($_email));
// validate the email and email domain
if (!checkEmailAndDomain($_email)) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_DIAG_EMAIL_MALFORMED . COLON . $_email;
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
$this->state = STATE_MAIL_FAIL;
return;
}
// check for duplicate emails
if (!$_skipCheck) {
$state = $this->emailSearch($_email);
switch ($state) {
case STATE_FRAMEWORK_WARNING :
case STATE_ALREADY_EXISTS :
$this->state = $state;
$this->status = false;
if ($backup) {
$this->count = $oldCount;
$this->data = $oldData;
}
return;
break;
case STATE_DOES_NOT_EXIST :
// do nothing: optimal return
break;
default :
$this->status = false;
$msg = ERROR_UNKNOWN_STATE . $state;
$this->eventMessages[] = $msg;
$this->logger->warn($msg);
if ($backup) {
$this->count = $oldCount;
$this->data = $oldData;
}
return;
break;
}
}
// if either whitelisting or blacklisting are enabled for the client, then check the wbl
if ($whiteList or $blackList) {
try {
$this->checkWBL($_email);
if ($this->debug) {
$this->logger->debug('email checked: ' . $_email);
$this->logger->debug('objWBL state: ' . $this->state);
$this->logger->debug('objWBL status: ' . (($this->status) ? STRING_TRUE : STRING_FALSE));
}
} catch (Throwable $t) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = $t->getMessage();
$this->eventMessages[] = ERROR_EXCEPTION;
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
$this->eventMessages = array_pop($this->eventMessages);
return;
}
} else {
$this->state = STATE_SUCCESS;
$this->status = true;
}
// reset the current class data
if ($backup) {
$this->count = $oldCount;
$this->data = $oldData;
}
}
/**
* emailSearch() -- private method
*
* this method accepts an email address as it's only input parameter and generates a query to check to
* see if the email already exists in the user collection.
*
* the method returns a state (string) determined by the following conditions:
*
* STATE_FRAMEWORK_WARNING - processing or db error
* STATE_ALREADY_EXISTS - email is already in-use in the db
* STATE_DOES_NOT_EXIST - email is not in-use (desired return)
*
* The calling client should evaluate the return state accordingly as this method does not change the class
* state/status params.
*
* The calling client should also reset the data payload as, if a record is found, then the found record will
* be added to the current data member.
*
* This method was written to reduce the code footprint of the validateUserEmail method.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_email
* @return string
*
* HISTORY:
* ========
* 08-18-20 mks DB-169: original coding
*
*/
private function emailSearch(string $_email): string
{
$return = STATE_FRAMEWORK_WARNING;
// check if tercero is not a "local" service
if (!gasConfig::$settings[ENV_TERCERO][CONFIG_IS_LOCAL]) return $return;
// set-up the email query
$query = [
STRING_QUERY_DATA => [
USER_PII_EMAIL => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_email ]]],
USER_PII_SECONDARY_EMAIL => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_email]]],
OPERAND_OR => null
],
STRING_RETURN_DATA => [ CM_TOKEN ]
];
// query the db
$this->_fetchRecords($query);
switch ($this->status) {
case true :
if ($this->count) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = sprintf(ERROR_EMAIL_DUPLICATE, $_email);
$this->eventMessages[] = $msg;
$this->logger->data($hdr . $msg);
$return = STATE_ALREADY_EXISTS;
} elseif ($this->state == STATE_NOT_FOUND) {
return STATE_DOES_NOT_EXIST;
}
break;
case false :
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$this->logger->warn($hdr . $this->strQuery);
$this->logger->warn($hdr . ERROR_CHECK_LOGS);
$this->eventMessages[] = ERROR_CHECK_LOGS;
break;
}
return($return);
}
/**
* checkWBL() -- public method
*
* this method checks the submitted email and domain against the White/Black list data.
*
* there is but one input parameter required for the method:
*
* $_email -- a string containing the email to be evaluated
*
* the method begins by looping through the email - the address is processed by first evaluating the
* entire email (for a match in the WBL table) and, if not found, then we continue with the domain-part of
* the email (right-side of the '@') and continue to remove sub-domains until we get to the TLD. If we
* get to the TLD, then the user is neither black-listed or white-listed.
*
* if we get a domain match, or if we match the entire email, then we look at the WBL record "type" (a boolean)
* to determine if the WBL record is a black (false) or white (true) listed email.
*
* The following states are assigned to the class under the following:
*
* STATE_DB_ERROR -- the WBL record was found, but no value was stored in the "type_wbl" column
* -- more than one WBL record was found for the email/domain
* -- the search query failed to execute successfully
* STATE_SUCCESS -- the email/domain is white-listed
* STATE_BLACK_LIST -- the email/domain is black-listed
* STATE_NOT_FOUND -- the email/domain is neither white-listed or black-listed, or wbl is disabled
*
* the method return is the class STATE variable which should be evaluated by the calling client upon return.
*
* Programmer's Notes:
* -------------------
* This method is a member of the user class, as opposed to the WBList class, for efficiency - we can check a user's
* email in this class without resorting to instantiating another data class (WBList) for the check.
*
* PENDING WORK:
* -------------
* todo: code the security event when a black-listed return is encountered
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_email
*
* HISTORY:
* ========
* 08-19-20 mks DB-168: original coding
*
*/
public function checkWBL(string $_email):void
{
// lvar init
$wblState = null;
$emailBits = explode(AT, $_email);
$domain = $emailBits[1];
$diminishingDomain = $domain;
$method = basename(__METHOD__);
$domainBits = explode(DOT, $domain);
$counter = 0;
$errorList = [];
$this->status = false;
$this->state = STATE_VALIDATION_ERROR;
$blEnabled = boolval(gasConfig::$settings[CONFIG_SECURITY][CONFIG_SECURITY_BANNED_LIST]);
$wlEnabled = boolval(gasConfig::$settings[CONFIG_SECURITY][CONFIG_SECURITY_RESTRICTED_LIST]);
// return immediately if wbl is disabled system-wide
if (!$blEnabled and !$wlEnabled) {
$this->state = STATE_NOT_SUPPORTED;
$this->status = true;
return;
}
$tmpMeta = $this->metaPayload;
$tmpMeta[META_TEMPLATE] = TEMPLATE_CLASS_WBL;
/** @var gacMongoDB $widget */
if (is_null($widget = grabWidget($tmpMeta, '', $errorList))) {
foreach ($errorList as $error)
$this->logger->error($error);
$this->eventMessages = [...$this->eventMessages, ...$errorList];
return;
}
/*
* we start with all of the domain. If there are sub-domains embedded in the domain,
* start by searching with the left-most sub-domain and each iteration, remove a sub-domain
* until we either find an entry in the collection, or we run out of domain.
*
* This technique allows us to validate an email submitted such as: mike@backend.engineering.givva.com
* to one of the following:
*
* 1. mike@backend.engineering.givva.com
* 2. mike@engineering.givva.com
* 3. mike@givva.com
*
*/
// first, check to see if the email USER is explicitly listed in the WBL collection:
$query = [ USER_PII_EMAIL => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_email ]]]];
$widget->_fetchRecords([STRING_QUERY_DATA => $query]);
if ($widget->status and $widget->count == 1) {
// email is either white or black-listed
if (is_null($wbl = $widget->getColumn(MONGO_WBL_TYPE))) {
// edge case: there is no WBL Type setting so ... this is a system event! Why would a user be listed
// in the table but not have a setting?!? Database error...
// todo: system event for incomplete data record found in the database
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$this->state = STATE_DATA_ERROR;
$this->eventMessages[] = ERROR_GENERIC_CUSTOMER;
$this->logger->warn($hdr . ERROR_BAD_DATA_RECORD . 'wbl-type is not populated');
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
}
// ... otherwise, if there we found a record for the email address, check to see if it's a white or black-listed entry
$this->state = (boolval($wbl)) ? STATE_SUCCESS : STATE_BLACK_LIST;
$this->status = ($this->state == STATE_SUCCESS) ? true : false;
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
} elseif ($widget->status and $widget->state == STATE_NOT_FOUND) {
// record was not found - which is not a problem unless whitelisting is enabled
$this->status = true;
$this->state = ($wlEnabled) ? STATE_NOT_WHITE_LIST : STATE_SUCCESS;
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
} elseif ($widget->status and $widget->count > 1 and $widget->state != STATE_NOT_FOUND) {
// more than one record was found
$msg = sprintf(MONGO_FAILED_TOO_MANY_RECS, 1) . $widget->count;
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
@handleExceptionMessaging($hdr, $msg, $this->eventMessages, true);
$this->state = STATE_DB_ERROR;
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
} elseif (!$widget->status) {
// query fail
$this->eventMessages[] = ERROR_CHECK_LOGS;
$this->state = STATE_FAIL;
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
}
// process the email DOMAIN until we find an entry in the wbl table or we run out of domain
for ($index = count($domainBits); $index > 1; $index--) {
$query = [USER_PII_EMAIL => [OPERAND_NULL => [OPERATOR_EQ => [(AT . $diminishingDomain)]]]];
$widget->_fetchRecords([STRING_QUERY_DATA => $query]);
$wblState = $widget->getColumn(MONGO_WBL_TYPE);
if ($widget->status and $widget->count == 1) {
// a wbl record exists for the domain
if (false === boolval($wblState)) {
// domain has been explicitly black listed
$this->state = STATE_BLACK_LIST;
$this->status = true;
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
// todo -- system event
} elseif (true === boolval($wblState)) {
// domain has been explicitly white listed
$this->state = STATE_SUCCESS;
$this->status = true;
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
} elseif (is_null($wblState)) {
// record exists but type is not defined
$this->state = STATE_DATA_ERROR;
if (is_object($widget)) $widget->__destruct();
unset($widget);
return;
}
} elseif ($widget->status and $widget->state == STATE_NOT_FOUND) {
// no records returned -- shrink the domain
$diminishingDomain = ltrim($diminishingDomain, ($domainBits[$counter++] . DOT));
}
}
// if we're done processing the domain and we land here, then the check the $wblType for null
if (is_null($wblState) and $wlEnabled) {
// none of the domain was found and white-listing is enabled
$this->state = STATE_NOT_WHITE_LIST;
$this->status = false;
} else {
// wl is not enabled and no wbl record was found for any of the domain
$this->state = STATE_SUCCESS;
$this->status = true;
}
if (is_object($widget)) $widget->__destruct();
unset($widget);
}
/**
* hashText() -- protected method
*
* This method requires a single input parameter, the text to be hashed:
*
* $_text -- string containing the text to be hashed
*
* The method will check the XML configuration for the hashing algorithm to use. If the security section was
* not properly loaded during instantiation, or if the calling client did not provide input text, then an error
* message will be generated and a null value returned.
*
* Otherwise, we'll use the password_hash() function to generate a hash of the request text. If the function
* returns a Boolean(false), then we'll generate an error message and return a null to the requesting client.
*
* Otherwise, return the hashed string.
*
* Programmer's Notes:
* -------------------
* I've marked this as protected so that it cannot be invoked (generate a hash) outside of the user instantiation
* stack. This is to limit access to the Namaste hashing algorithm to only the user class.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_text
* @return string|null
*
*
* HISTORY:
* ========
* 08-31-20 mks DB-168: original coding
*
*/
protected function hashText(string $_text):?string
{
$method = basename(__METHOD__);
if (empty($this->securityConfig)) {
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$msg = ERROR_CONFIG_RESOURCE_404 . CONFIG_SECURITY;
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
return null;
}
if (empty($_text)) {
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$msg = ERROR_DATA_404;
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
return null;
}
try {
// if set, pull the hash algorithm from namaste config, o/w set to default and call hash function
$hash = password_hash($_text, (!isset($this->securityConfig[CONFIG_SECURITY_HASH_ALGO])) ? PASSWORD_ARGON2I : $this->securityConfig[CONFIG_SECURITY_HASH_ALGO]);
if (false === $hash) {
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$msg = ERROR_PASSWORD_HASH_GENERATION_FAILED;
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
return null;
}
return $hash;
} catch (TypeError | Throwable $t) {
$hdr = sprintf(INFO_LOC, $method, __LINE__);
@handleExceptionMessaging($hdr, $t->getMessage(), $this->eventMessages);
return null;
}
}
/**
* hashFetch() -- private method
*
* This method has two input parameters:
*
* $_searchValue - this should be either a 36-character GUID value, or an email address
* $_retData -- call-by-reference parameter that, if submitted, will contain the user's type and api-key in
* addition to the password hash and account token
*
* We'll test to see if the input value is either a GUID or an email address and will structure the query
* to match. If the searchValue is not either, then generate error messages, return a null, and exit.
*
* If the query generated an error, or returns a not found, we'll generate appropriate messaging and return
* a null value to the calling client preserving class state/status and query results data.
*
* Otherwise, we'll return the password hash to the calling client.
*
* Programmer's Notes:
* -------------------
* This function is private so as to limit access to the user table for the purposes of accessing the hash key
* to this class only.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string|null $_searchValue -- the email or guid value of the target record
* @param array|null $_retData -- call by reference param to return additional record data
* @return string|null
*
*
* HISTORY:
* ========
* 08-31-20 mks DB-168: original coding
*
*/
private function hashFetch(?string $_searchValue = null, ?array &$_retData = null):?string
{
$method = basename(__METHOD__);
// search key can be either a token or an email address - figure out which
if (empty($_searchValue)) {
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$msg = ERROR_PARAM_404 . STRING_TOKEN;
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
return null;
} elseif (validateGUID($_searchValue))
$searchKey = STRING_TOKEN;
elseif (false !== filter_var($_searchValue, FILTER_VALIDATE_EMAIL))
$searchKey = USER_PII_EMAIL;
else {
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$msg = ERROR_DATA_INVALID_KEY . $_searchValue;
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
return null;
}
// build the query to fetch the user's hash based on the record token
$query = [
$searchKey => [ STRING_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_searchValue]]]],
STRING_RETURN_DATA => [ USER_PASSWORD, USER_TYPE, USER_PARTNER_API_KEY, DB_STATUS ]
];
$this->_fetchRecords($query);
if (!$this->status) {
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$msg = sprintf(ERROR_MDB_QUERY_FAIL, STRING_SEARCH);
@handleExceptionMessaging($hdr, $msg, $this->eventMessages);
return null;
} elseif ($this->state == STATE_NOT_FOUND) {
$this->eventMessages[] = ERROR_DATA_404;
return null;
}
// class data gets reset on successful search and return
$_retData = $this->getData();
$_retData = $_retData[0];
if (isset($_retData[STRING_PASSWORD . $this->ext])) unset($_retData[STRING_PASSWORD . $this->ext]);
$hash = $this->getColumn(USER_PASSWORD);
$this->removeData();
return $hash;
}
/**
* hashCheck() -- public function
*
* This is the public function, access point, for validating a user's password hash. The method has the
* following input parameters to the method:
*
* $_searchValue -- this can be either a GUID or an email address and will be used as search key
* $_hashText -- this is the hash text as generated by the client that we'll compare to the stored hash
*
* There are no explicit parameters returned. However, we make use of the class state/status members to pass-back
* the state and status of the request which should be processed by the calling client.
* If either parameter is passed empty, then generate messaging and return.
*
* The method invokes the hashFetch() method, passing in the search-value to fetch the password hash from the
* database record and, in the same line, invokes the password_verify() function to validate the user
* submitted pre-hash value against the stored hash.
*
* Programmer's Notes:
* -------------------
* Anytime you want to add layered-validation, like ensuring that partner account belongs to a partner, you'll want
* to add those checks to this method.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_searchValue
* @param $_hashText
*
*
* HISTORY:
* ========
* 08-31-20 mks DB-168: Original coding
*
*/
public function hashCheck(string $_searchValue, $_hashText):void
{
$this->status = false;
$this->state = STATE_AUTH_ERROR;
$userData = null;
$method = basename(__METHOD__);
if (empty($_searchValue) or empty($_hashText)) {
$this->eventMessages[] = ERROR_DATA_404;
return;
}
if (password_verify($_hashText, $this->hashFetch($_searchValue, $userData))) {
// if the hash verification was successful...
// check the account status for non-active states:
if ($userData[DB_STATUS . $this->ext] != STATUS_ACTIVE) {
switch ($userData[DB_STATUS . $this->ext]) {
case STATUS_LOCKED :
case STATUS_CLOSED :
case STATUS_SUSPENDED :
case STATUS_REVOKED :
case STATUS_INACTIVE :
// account needs CSR intervention
break;
case STATUS_ABANDONED :
// account needs to have status updated and password change forced
break;
case STATUS_PENDING :
// account needs to validate their email
break;
}
}
if (isset($userData[USER_TYPE . $this->ext]) and ($userData[USER_TYPE . $this->ext] == USER_TYPE_PARTNER)) {
// ...and we have a userType and that type is equal to "partner"...
if (isset($userData[USER_PARTNER_API_KEY . $this->ext]) and validateGUID($userData[USER_PARTNER_API_KEY . $this->ext])) {
// ...and we have an partner API key in the user record...
if (isset($this->metaPayload[CLIENT_AUTH_TOKEN])) {
// ...and we have the XPI-Key set in the meta payload...
if ($this->metaPayload[CLIENT_AUTH_TOKEN] == $userData[USER_PARTNER_API_KEY . $this->ext]) {
// ...and the meta-payload X-API-Key matches the API-Key stored in the user record...
// then we have a successful partner-user login!
$this->myAPIKey = $userData[USER_PARTNER_API_KEY . $this->ext];
$this->myUserType = $userData[USER_TYPE . $this->ext];
if (isset($userData[STRING_TOKEN . $this->ext]))
$this->myToken = $userData[STRING_TOKEN . $this->ext];
$this->state = STATE_SUCCESS;
$this->status = true;
return;
} else {
// client auth token in meta payload does not match xpi-key in user record
$this->eventMessages[] = ERROR_PARTNER_API_KEY_MISMATCH;
return;
// todo security system event
}
} else {
// Event request was from a partner, but user is not associated with a partner account
$this->eventMessages[] = ERROR_PARTNER_USER_NOT_MEMBER;
// todo security system event
return;
}
} else {
// either the api-key is not set or is an invalid guid (and the user is partner'd)
if (!isset($userData[USER_PARTNER_API_KEY . $this->ext])) {
// partner key was not set in the user record
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$this->logger->warn($hdr . ERROR_PARTNER_USER_NOT_REGISTERED);
$this->eventMessages[] = ERROR_PARTNER_USER_NOT_MEMBER;
$this->state = STATE_DB_ERROR;
return;
} else {
// the partner key guid stored in the user record is bad
$hdr = sprintf(INFO_LOC, $method, __LINE__);
$this->eventMessages[] = ERROR_PARTNER_USER_DATA;
$msg = sprintf(ERROR_PARTNER_USER_HAS_BAD_GUID, $userData[USER_PARTNER_API_KEY . $this->ext], USER_PARTNER_API_KEY);
$this->logger->warn($hdr . $msg);
$this->logger->warn(ERROR_INVALID_GUID . $userData[USER_PARTNER_API_KEY . $this->ext]);
$this->state = STATE_DATA_ERROR;
return;
}
}
} // END - check to see if user belongs to a partner
} else {
$this->eventMessages[] = ERROR_PASSWORD_MISMATCH;
}
}
}