public function tearDown() { parent::tearDown(); BasicAuth::protect_entire_site(false); Member::config()->unique_identifier_field = self::$original_unique_identifier_field; Security::$force_database_is_ready = null; }
/** * If the REMOTE_USER is set and is in the Member table, log that member in. If * not, and Config::inst()->get('AuthRemoteUserExtension', 'auto_create_user') is set, add that * Member to the configured group, and log the new user in. Otherwise, do nothing. */ public function onAfterInit() { if (isset($_SERVER['REMOTE_USER'])) { $unique_identifier = $_SERVER['REMOTE_USER']; } elseif (isset($_SERVER['REDIRECT_REMOTE_USER'])) { $unique_identifier = $_SERVER['REDIRECT_REMOTE_USER']; } if (isset($unique_identifier)) { $unique_identifier_field = Member::config()->unique_identifier_field; $member = Member::get()->filter($unique_identifier_field, $unique_identifier)->first(); if ($member) { $member->logIn(); $this->owner->redirectBack(); } elseif (Config::inst()->get('AuthRemoteUserExtension', 'auto_create_user') && strlen(Config::inst()->get('AuthRemoteUserExtension', 'auto_user_group'))) { $group = Group::get()->filter('Title', Config::inst()->get('AuthRemoteUserExtension', 'auto_user_group'))->first(); if ($group) { $member = new Member(); $member->{$unique_identifier_field} = $unique_identifier; $member->write(); $member->Groups()->add($group); $member->logIn(); } } } }
/** * EmailVerificationLoginForm is the same as MemberLoginForm with the following changes: * - The code has been cleaned up. * - A form action for users who have lost their verification email has been added. * * We add fields in the constructor so the form is generated when instantiated. * * @param Controller $controller The parent controller, necessary to create the appropriate form action tag. * @param string $name The method on the controller that will return this form object. * @param FieldList|FormField $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects. * @param FieldList|FormAction $actions All of the action buttons in the form - a {@link FieldList} of {@link FormAction} objects * @param bool $checkCurrentUser If set to TRUE, it will be checked if a the user is currently logged in, and if so, only a logout button will be rendered */ function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true) { $email_field_label = singleton('Member')->fieldLabel(Member::config()->unique_identifier_field); $email_field = TextField::create('Email', $email_field_label, null, null, $this)->setAttribute('autofocus', 'autofocus'); $password_field = PasswordField::create('Password', _t('Member.PASSWORD', 'Password')); $authentication_method_field = HiddenField::create('AuthenticationMethod', null, $this->authenticator_class, $this); $remember_me_field = CheckboxField::create('Remember', 'Remember me next time?', true); if ($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) { $fields = FieldList::create($authentication_method_field); $actions = FieldList::create(FormAction::create('logout', _t('Member.BUTTONLOGINOTHER', "Log in as someone else"))); } else { if (!$fields) { $fields = FieldList::create($authentication_method_field, $email_field, $password_field); if (Security::config()->remember_username) { $email_field->setValue(Session::get('SessionForms.MemberLoginForm.Email')); } else { // Some browsers won't respect this attribute unless it's added to the form $this->setAttribute('autocomplete', 'off'); $email_field->setAttribute('autocomplete', 'off'); } } if (!$actions) { $actions = FieldList::create(FormAction::create('doLogin', _t('Member.BUTTONLOGIN', "Log in")), new LiteralField('forgotPassword', '<p id="ForgotPassword"><a href="Security/lostpassword">' . _t('Member.BUTTONLOSTPASSWORD', "I've lost my password") . '</a></p>'), new LiteralField('resendEmail', '<p id="ResendEmail"><a href="Security/verify-email">' . _t('MemberEmailVerification.BUTTONLOSTVERIFICATIONEMAIL', "I've lost my verification email") . '</a></p>')); } } if (isset($_REQUEST['BackURL'])) { $fields->push(HiddenField::create('BackURL', 'BackURL', $_REQUEST['BackURL'])); } // Reduce attack surface by enforcing POST requests $this->setFormMethod('POST', true); parent::__construct($controller, $name, $fields, $actions); $this->setValidator(RequiredFields::create('Email', 'Password')); }
public function setUp() { parent::setUp(); // Fixtures assume Email is the field used to identify the log in identity Member::config()->unique_identifier_field = 'Email'; Member::config()->lock_out_after_incorrect_logins = 10; }
/** * Clear the cart, and session variables on member logout */ public function memberLoggedOut() { if (Member::config()->login_joins_cart) { ShoppingCart::singleton()->clear(); OrderManipulation::clear_session_order_ids(); } }
/** * Attempt to find and authenticate member if possible from the given data. * * @param array $data * @param Form $form * @param bool &$success Success flag * @return Member Found member, regardless of successful login * @see MemberAuthenticator::authenticate_member() */ protected static function authenticate_member($data, $form, &$success) { // Default success to false $success = false; // Attempt to identify by temporary ID $member = null; $email = null; if (!empty($data['tempid'])) { // Find user by tempid, in case they are re-validating an existing session $member = Member::member_from_tempid($data['tempid']); if ($member) { $email = $member->Email; } } // Otherwise, get email from posted value instead if (!$member && !empty($data['Email'])) { $email = $data['Email']; } // Check default login (see Security::setDefaultAdmin()) the standard way and the "extension"-way :-) $asDefaultAdmin = $email === Security::default_admin_username(); if ($asDefaultAdmin || isset($GLOBALS['_DEFAULT_ADMINS']) && array_key_exists($email, $GLOBALS['_DEFAULT_ADMINS'])) { // If logging is as default admin, ensure record is setup correctly $member = Member::default_admin(); $success = Security::check_default_admin($email, $data['Password']); // If not already true check if one of the extra admins match if (!$success) { $success = $GLOBALS['_DEFAULT_ADMINS'][$email] == $data['Password']; } if ($success) { return $member; } } // Attempt to identify user by email if (!$member && $email) { // Find user by email $member = Member::get()->filter(Member::config()->unique_identifier_field, $email)->first(); } // Validate against member if possible if ($member && !$asDefaultAdmin) { $result = $member->checkPassword($data['Password']); $success = $result->valid(); } else { $result = new ValidationResult(false, _t('Member.ERRORWRONGCRED')); } // Emit failure to member and form (if available) if (!$success) { if ($member) { $member->registerFailedLogin(); } if ($form) { $form->sessionMessage($result->message(), 'bad'); } } else { if ($member) { $member->registerSuccessfulLogin(); } } return $member; }
public function testCustomIdentifierField() { $origField = Member::config()->unique_identifier_field; Member::config()->unique_identifier_field = 'Username'; $label = singleton('Member')->fieldLabel(Member::config()->unique_identifier_field); $this->assertEquals($label, 'Username'); Member::config()->unique_identifier_field = $origField; }
public function tearDown() { parent::tearDown(); // set old Member::config()->unique_identifier_field value if ($this->member_unique_identifier_field) { Member::config()->unique_identifier_field = $this->member_unique_identifier_field; } }
public function setUp() { parent::setUp(); // Fixtures assume Email is the field used to identify the log in identity Member::config()->unique_identifier_field = 'Email'; Security::$force_database_is_ready = true; // Prevents Member test subclasses breaking ready test Member::config()->lock_out_after_incorrect_logins = 10; }
public function getData(Order $order) { $data = array(); if ($member = Member::currentUser()) { $idf = Member::config()->unique_identifier_field; $data[$idf] = $member->{$idf}; } return $data; }
/** * Factory method for the Verify Email form. * * @return Form */ public function VerifyEmailForm() { $email_field_label = singleton('Member')->fieldLabel(Member::config()->unique_identifier_field); $email_field = TextField::create('Email', $email_field_label, null, null, $this)->setAttribute('autofocus', 'autofocus'); $fields = FieldList::create($email_field); $actions = FieldList::create(FormAction::create('submitVerifyEmailForm', _t('MemberEmailVerification.BUTTONRESENDEMAIL', "Send me the email verification link again"))); $form = new EmailVerificationLoginForm($this->owner, 'submitVerifyEmailForm', $fields, $actions, false); return $form; }
/** * Assertion Consumer Service * * The user gets sent back here after authenticating with the IdP, off-site. * The earlier redirection to the IdP can be found in the SAMLAuthenticator::authenticate. * * After this handler completes, we end up with a rudimentary Member record (which will be created on-the-fly * if not existent), with the user already logged in. Login triggers memberLoggedIn hooks, which allows * LDAP side of this module to finish off loading Member data. * * @throws OneLogin_Saml2_Error */ public function acs() { $auth = Injector::inst()->get('SAMLHelper')->getSAMLAuth(); $auth->processResponse(); $error = $auth->getLastErrorReason(); if (!empty($error)) { SS_Log::log($error, SS_Log::ERR); Form::messageForForm("SAMLLoginForm_LoginForm", "Authentication error: '{$error}'", 'bad'); Session::save(); return $this->getRedirect(); } if (!$auth->isAuthenticated()) { Form::messageForForm("SAMLLoginForm_LoginForm", _t('Member.ERRORWRONGCRED'), 'bad'); Session::save(); return $this->getRedirect(); } $decodedNameId = base64_decode($auth->getNameId()); // check that the NameID is a binary string (which signals that it is a guid if (ctype_print($decodedNameId)) { Form::messageForForm("SAMLLoginForm_LoginForm", "Name ID provided by IdP is not a binary GUID.", 'bad'); Session::save(); return $this->getRedirect(); } // transform the NameId to guid $guid = LDAPUtil::bin_to_str_guid($decodedNameId); if (!LDAPUtil::validGuid($guid)) { $errorMessage = "Not a valid GUID '{$guid}' recieved from server."; SS_Log::log($errorMessage, SS_Log::ERR); Form::messageForForm("SAMLLoginForm_LoginForm", $errorMessage, 'bad'); Session::save(); return $this->getRedirect(); } // Write a rudimentary member with basic fields on every login, so that we at least have something // if LDAP synchronisation fails. $member = Member::get()->filter('GUID', $guid)->limit(1)->first(); if (!($member && $member->exists())) { $member = new Member(); $member->GUID = $guid; } $attributes = $auth->getAttributes(); foreach ($member->config()->claims_field_mappings as $claim => $field) { if (!isset($attributes[$claim][0])) { SS_Log::log(sprintf('Claim rule \'%s\' configured in LDAPMember.claims_field_mappings, but wasn\'t passed through. Please check IdP claim rules.', $claim), SS_Log::WARN); continue; } $member->{$field} = $attributes[$claim][0]; } $member->SAMLSessionIndex = $auth->getSessionIndex(); // This will throw an exception if there are two distinct GUIDs with the same email address. // We are happy with a raw 500 here at this stage. $member->write(); // This will trigger LDAP update through LDAPMemberExtension::memberLoggedIn. // Both SAML and LDAP identify Members by the GUID field. $member->logIn(); return $this->getRedirect(); }
public function updatedetails($data, $form) { $form->saveInto($this->member); if (Member::config()->send_frontend_update_notifications) { $this->sendUpdateNotification(); } $this->member->write(); $form->sessionMessage("Your member details have been updated.", "good"); return $this->controller->redirectBack(); }
public function getEditForm($id = null, $fields = null) { $form = parent::getEditForm(); if ($this->modelClass == "Member") { if ($columns = Member::config()->export_fields) { $form->Fields()->fieldByName("Member")->getConfig()->getComponentByType("GridFieldExportButton")->setExportColumns($columns); } } return $form; }
public function testLoginDoesntJoinCart() { Member::config()->login_joins_cart = false; $order = $this->objFromFixture("Order", "cart"); ShoppingCart::singleton()->setCurrent($order); $member = $this->objFromFixture("Member", "jeremyperemy"); $member->logIn(); $this->assertEquals(0, $order->MemberID); $member->logOut(); $this->assertTrue((bool) ShoppingCart::curr()); }
public function getMembershipFields() { $fields = $this->getContactFields(); $idfield = Member::config()->unique_identifier_field; if (!$fields->fieldByName($idfield)) { $fields->push(TextField::create($idfield, $idfield)); //TODO: scaffold the correct id field } $fields->push($this->getPasswordField()); return $fields; }
/** * Constructor * * @param Controller $controller The parent controller, necessary to * create the appropriate form action tag. * @param string $name The method on the controller that will return this * form object. * @param FieldList|FormField $fields All of the fields in the form - a * {@link FieldList} of {@link FormField} * objects. * @param FieldList|FormAction $actions All of the action buttons in the * form - a {@link FieldList} of * {@link FormAction} objects * @param bool $checkCurrentUser If set to TRUE, it will be checked if a * the user is currently logged in, and if * so, only a logout button will be rendered * @param string $authenticatorClassName Name of the authenticator class that this form uses. */ public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true) { // This is now set on the class directly to make it easier to create subclasses // $this->authenticator_class = $authenticatorClassName; $customCSS = project() . '/css/member_login.css'; if (Director::fileExists($customCSS)) { Requirements::css($customCSS); } if (isset($_REQUEST['BackURL'])) { $backURL = $_REQUEST['BackURL']; } else { $backURL = Session::get('BackURL'); } if ($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) { $fields = new FieldList(new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this)); $actions = new FieldList(new FormAction("logout", _t('Member.BUTTONLOGINOTHER', "Log in as someone else"))); } else { if (!$fields) { $label = singleton('Member')->fieldLabel(Member::config()->unique_identifier_field); $fields = new FieldList(new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this), $emailField = new TextField("Email", $label, null, null, $this), new PasswordField("Password", _t('Member.PASSWORD', 'Password'))); if (Security::config()->remember_username) { $emailField->setValue(Session::get('SessionForms.MemberLoginForm.Email')); } else { // Some browsers won't respect this attribute unless it's added to the form $this->setAttribute('autocomplete', 'off'); $emailField->setAttribute('autocomplete', 'off'); } if (Security::config()->autologin_enabled) { $fields->push(CheckboxField::create("Remember", _t('Member.KEEPMESIGNEDIN', "Keep me signed in"))->setAttribute('title', sprintf(_t('Member.REMEMBERME', "Remember me next time? (for %d days on this device)"), Config::inst()->get('RememberLoginHash', 'token_expiry_days')))); } } if (!$actions) { $actions = new FieldList(new FormAction('dologin', _t('Member.BUTTONLOGIN', "Log in")), new LiteralField('forgotPassword', '<p id="ForgotPassword"><a href="Security/lostpassword">' . _t('Member.BUTTONLOSTPASSWORD', "I've lost my password") . '</a></p>')); } } if (isset($backURL)) { $fields->push(new HiddenField('BackURL', 'BackURL', $backURL)); } // Reduce attack surface by enforcing POST requests $this->setFormMethod('POST', true); parent::__construct($controller, $name, $fields, $actions); $this->setValidator(new RequiredFields('Email', 'Password')); // Focus on the email input when the page is loaded $js = <<<JS \t\t\t(function() { \t\t\t\tvar el = document.getElementById("MemberLoginForm_LoginForm_Email"); \t\t\t\tif(el && el.focus && (typeof jQuery == 'undefined' || jQuery(el).is(':visible'))) el.focus(); \t\t\t})(); JS; Requirements::customScript($js, 'MemberLoginFormFieldFocus'); }
/** * Helper that only allows orders to be started internally. * * @return Order */ protected function findOrMake() { if ($this->current()) { return $this->current(); } $this->order = Order::create(); if (Member::config()->login_joins_cart && Member::currentUserID()) { $this->order->MemberID = Member::currentUserID(); } $this->order->write(); $this->order->extend('onStartOrder'); Session::set(self::config()->cartid_session_name, $this->order->ID); return $this->order; }
/** * Initialisation function that is run before any action on the controller is called. * * @uses BasicAuth::requireLogin() */ public function init() { if ($this->basicAuthEnabled) { BasicAuth::protect_site_if_necessary(); } // Directly access the session variable just in case the Group or Member tables don't yet exist if (Member::config()->log_last_visited) { Deprecation::notice('4.0', 'Member::$LastVisited is deprecated. From 4.0 onwards you should implement this as a custom extension'); if (Session::get('loggedInAs') && Security::database_is_ready() && ($member = Member::currentUser())) { DB::prepared_query(sprintf('UPDATE "Member" SET "LastVisited" = %s WHERE "ID" = ?', DB::get_conn()->now()), array($member->ID)); } } // This is used to test that subordinate controllers are actually calling parent::init() - a common bug $this->baseInitCalled = true; }
/** * Provides a front-end utility menu with administrative functions and developer tools * Relies on SilverStripeNavigator * * @return string */ public function BetterNavigator() { // Make sure this is a page if (!($this->owner && $this->owner->dataRecord && $this->owner->dataRecord instanceof SiteTree && $this->owner->dataRecord->ID > 0)) { return false; } // Only show navigator to appropriate users $isDev = Director::isDev(); $canViewDraft = Permission::check('VIEW_DRAFT_CONTENT') || Permission::check('CMS_ACCESS_CMSMain'); if ($isDev || $canViewDraft) { // Get SilverStripeNavigator links & stage info (CMS/Stage/Live/Archive) $nav = array(); $viewing = ''; $navigator = new SilverStripeNavigator($this->owner->dataRecord); $items = $navigator->getItems(); foreach ($items as $item) { $name = $item->getName(); $active = $item->isActive(); $nav[$name] = array('Link' => $item->getLink(), 'Active' => $active); if ($active) { if ($name == 'LiveLink') { $viewing = 'Live'; } if ($name == 'StageLink') { $viewing = 'Draft'; } if ($name == 'ArchiveLink') { $viewing = 'Archived'; } } } // Only show edit link if user has permission to edit this page $editLink = $this->owner->dataRecord->canEdit() && Permission::check('CMS_ACCESS_CMSMain') || $isDev ? $nav['CMSLink']['Link'] : false; // Is the logged in member nominated as a developer? $member = Member::currentUser(); $devs = Config::inst()->get('BetterNavigator', 'developers'); $identifierField = Member::config()->unique_identifier_field; $isDeveloper = $member && is_array($devs) ? in_array($member->{$identifierField}, $devs) : false; // Add other data for template $backURL = '?BackURL=' . urlencode($this->owner->Link()); $bNData = array_merge($nav, array('Member' => $member, 'Stage' => Versioned::current_stage(), 'Viewing' => $viewing, 'LoginLink' => Config::inst()->get('Security', 'login_url') . $backURL, 'LogoutLink' => 'Security/logout' . $backURL, 'EditLink' => $editLink, 'Mode' => Director::get_environment_type(), 'IsDeveloper' => $isDeveloper)); // Merge with page data, send to template and render $bNData = new ArrayData($bNData); $page = $this->owner->customise(array('BetterNavigator' => $bNData)); return $page->renderWith('BetterNavigator'); } return false; }
public function sendUpdateNotification($data) { $name = $data['FirstName'] . " " . $data['Surname']; $body = "{$name} has updated their details via the website. Here is the new information:<br/>"; $notifyOnFields = Member::config()->frontend_update_notification_fields ?: DataObject::database_fields('Member'); $changedFields = $this->member->getChangedFields(true, 2); $send = false; foreach ($changedFields as $key => $field) { if (in_array($key, $notifyOnFields)) { $body .= "<br/><strong>{$key}:</strong><br/>" . "<strike style='color:red;'>" . $field['before'] . "</strike><br/>" . "<span style='color:green;'>" . $field['after'] . "</span><br/>"; $send = true; } } if ($send) { $email = new Email(Email::config()->admin_email, Email::config()->admin_email, "Member details update: {$name}", $body); $email->send(); } }
/** * Constructor * * @param Controller $controller The parent controller, necessary to * create the appropriate form action tag. * @param string $name The method on the controller that will return this * form object. * @param FieldList|FormField $fields All of the fields in the form - a * {@link FieldList} of {@link FormField} * objects. * @param FieldList|FormAction $actions All of the action buttons in the * form - a {@link FieldList} of * {@link FormAction} objects * @param bool $checkCurrentUser If set to TRUE, it will be checked if a * the user is currently logged in, and if * so, only a logout button will be rendered * @param string $authenticatorClassName Name of the authenticator class that this form uses. */ public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true) { // This is now set on the class directly to make it easier to create subclasses // $this->authenticator_class = $authenticatorClassName; $customCSS = project() . '/css/member_login.css'; if (Director::fileExists($customCSS)) { Requirements::css($customCSS); } if (isset($_REQUEST['BackURL'])) { $backURL = $_REQUEST['BackURL']; } else { $backURL = Session::get('BackURL'); } if ($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) { $fields = new FieldList(new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this)); $actions = new FieldList(new FormAction("logout", _t('Member.BUTTONLOGINOTHER', "Log in as someone else"))); } else { if (!$fields) { $label = singleton('Member')->fieldLabel(Member::config()->unique_identifier_field); $fields = new FieldList(new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this), new TextField("Email", $label, Session::get('SessionForms.MemberLoginForm.Email'), null, $this), new PasswordField("Password", _t('Member.PASSWORD', 'Password'))); if (Security::config()->autologin_enabled) { $fields->push(new CheckboxField("Remember", _t('Member.REMEMBERME', "Remember me next time?"))); } } if (!$actions) { $actions = new FieldList(new FormAction('dologin', _t('Member.BUTTONLOGIN', "Log in")), new LiteralField('forgotPassword', '<p id="ForgotPassword"><a href="Security/lostpassword">' . _t('Member.BUTTONLOSTPASSWORD', "I've lost my password") . '</a></p>')); } } if (isset($backURL)) { $fields->push(new HiddenField('BackURL', 'BackURL', $backURL)); } // Reduce attack surface by enforcing POST requests $this->setFormMethod('POST', true); parent::__construct($controller, $name, $fields, $actions); $this->setValidator(new RequiredFields('Email', 'Password')); // Focus on the email input when the page is loaded Requirements::customScript(<<<JS \t\t\t(function() { \t\t\t\tvar el = document.getElementById("MemberLoginForm_LoginForm_Email"); \t\t\t\tif(el && el.focus) el.focus(); \t\t\t})(); JS ); }
public function php($data) { $valid = parent::php($data); $identifierField = Member::config()->unique_identifier_field; $member = Member::get()->filter($identifierField, $data[$identifierField])->first(); if (is_object($member) && $member->isInDB()) { $uniqueField = $this->form->Fields()->dataFieldByName($identifierField); $this->validationError($uniqueField->id(), sprintf(_t('Member.VALIDATIONMEMBEREXISTS', 'A member already exists with the same %s'), strtolower($identifierField)), 'required'); $valid = false; } // Execute the validators on the extensions if ($this->extension_instances) { foreach ($this->extension_instances as $extension) { if (method_exists($extension, 'hasMethod') && $extension->hasMethod('updatePHP')) { $valid &= $extension->updatePHP($data, $this->form); } } } return $valid; }
/** * Ensures member unique id stays unique. */ public function php($data) { $valid = parent::php($data); $field = (string) Member::config()->unique_identifier_field; if (isset($data[$field])) { $uid = $data[$field]; $currentMember = Member::currentUser(); //can't be taken if (Member::get()->filter($field, $uid)->exclude('ID', $currentMember->ID)->count() > 0) { // get localized field labels $fieldLabels = $currentMember->fieldLabels(false); // if a localized value exists, use this for our error-message $fieldLabel = isset($fieldLabels[$field]) ? $fieldLabels[$field] : $field; $this->validationError($field, _t('Checkout.MemberExists', 'A member already exists with the {Field} {Identifier}', '', array('Field' => $fieldLabel, 'Identifier' => $uid)), "required"); $valid = false; } } return $valid; }
/** * Test that the PasswordExpiry date is set when passwords are changed */ public function testPasswordExpirySetting() { Member::config()->password_expiry_days = 90; $member = $this->objFromFixture('Member', 'test'); $this->assertNotNull($member); $valid = $member->changePassword("Xx?1234234"); $this->assertTrue($valid->valid()); $expiryDate = date('Y-m-d', time() + 90 * 86400); $this->assertEquals($expiryDate, $member->PasswordExpiry); Member::config()->password_expiry_days = null; $valid = $member->changePassword("Xx?1234235"); $this->assertTrue($valid->valid()); $this->assertNull($member->PasswordExpiry); }
/** * Method to authenticate an user * * @param array $RAW_data Raw data to authenticate the user * @param Form $form Optional: If passed, better error messages can be * produced by using * {@link Form::sessionMessage()} * @return bool|Member Returns FALSE if authentication fails, otherwise * the member object * @see Security::setDefaultAdmin() */ public static function authenticate($RAW_data, Form $form = null) { if (array_key_exists('Email', $RAW_data) && $RAW_data['Email']) { $SQL_user = Convert::raw2sql($RAW_data['Email']); } else { return false; } $isLockedOut = false; $result = null; // Default login (see Security::setDefaultAdmin()) if (Security::check_default_admin($RAW_data['Email'], $RAW_data['Password'])) { $member = Security::findAnAdministrator(); } else { $member = DataObject::get_one("Member", "\"" . Member::config()->unique_identifier_field . "\" = '{$SQL_user}' AND \"Password\" IS NOT NULL"); if ($member) { $result = $member->checkPassword($RAW_data['Password']); } else { $result = new ValidationResult(false, _t('Member.ERRORWRONGCRED')); } if ($member && !$result->valid()) { $member->registerFailedLogin(); $member = false; } } // Optionally record every login attempt as a {@link LoginAttempt} object /** * TODO We could handle this with an extension */ if (Security::config()->login_recording) { $attempt = new LoginAttempt(); if ($member) { // successful login (member is existing with matching password) $attempt->MemberID = $member->ID; $attempt->Status = 'Success'; // Audit logging hook $member->extend('authenticated'); } else { // failed login - we're trying to see if a user exists with this email (disregarding wrong passwords) $existingMember = DataObject::get_one("Member", "\"" . Member::config()->unique_identifier_field . "\" = '{$SQL_user}'"); if ($existingMember) { $attempt->MemberID = $existingMember->ID; // Audit logging hook $existingMember->extend('authenticationFailed'); } else { // Audit logging hook singleton('Member')->extend('authenticationFailedUnknownUser', $RAW_data); } $attempt->Status = 'Failure'; } if (is_array($RAW_data['Email'])) { user_error("Bad email passed to MemberAuthenticator::authenticate(): {$RAW_data['Email']}", E_USER_WARNING); return false; } $attempt->Email = $RAW_data['Email']; $attempt->IP = Controller::curr()->getRequest()->getIP(); $attempt->write(); } // Legacy migration to precision-safe password hashes. // A login-event with cleartext passwords is the only time // when we can rehash passwords to a different hashing algorithm, // bulk-migration doesn't work due to the nature of hashing. // See PasswordEncryptor_LegacyPHPHash class. if ($member && self::$migrate_legacy_hashes && array_key_exists($member->PasswordEncryption, self::$migrate_legacy_hashes)) { $member->Password = $RAW_data['Password']; $member->PasswordEncryption = self::$migrate_legacy_hashes[$member->PasswordEncryption]; $member->write(); } if ($member) { Session::clear('BackURL'); } else { if ($form && $result) { $form->sessionMessage($result->message(), 'bad'); } } return $member; }
/** * Check if the submitted member data is valid (server-side) * * Check if a member with that email doesn't already exist, or if it does * that it is this member. * * @param array $data Submitted data * @return bool Returns TRUE if the submitted data is valid, otherwise * FALSE. */ public function php($data) { $valid = parent::php($data); $identifierField = Member::config()->unique_identifier_field; $SQL_identifierField = Convert::raw2sql($data[$identifierField]); $member = DataObject::get_one('Member', "\"{$identifierField}\" = '{$SQL_identifierField}'"); // if we are in a complex table field popup, use ctf[childID], else use ID if (isset($_REQUEST['ctf']['childID'])) { $id = $_REQUEST['ctf']['childID']; } elseif (isset($_REQUEST['ID'])) { $id = $_REQUEST['ID']; } else { $id = null; } if ($id && is_object($member) && $member->ID != $id) { $uniqueField = $this->form->Fields()->dataFieldByName($identifierField); $this->validationError($uniqueField->id(), _t('Member.VALIDATIONMEMBEREXISTS', 'A member already exists with the same %s', array('identifier' => strtolower($identifierField))), 'required'); $valid = false; } // Execute the validators on the extensions if ($this->extension_instances) { foreach ($this->extension_instances as $extension) { if (method_exists($extension, 'hasMethod') && $extension->hasMethod('updatePHP')) { $valid &= $extension->updatePHP($data, $this->form); } } } return $valid; }
/** * Ensures member unique id stays unique. */ public function php($data) { $valid = parent::php($data); $field = (string) Member::config()->unique_identifier_field; if (isset($data[$field])) { $uid = $data[(string) Member::config()->unique_identifier_field]; $currentmember = Member::currentUser(); //can't be taken if (DataObject::get_one('Member', "{$field} = '{$uid}' AND ID != " . $currentmember->ID)) { // get localized field labels $fieldLabels = $currentmember->fieldLabels(false); // if a localized value exists, use this for our error-message $fieldLabel = isset($fieldLabels[$field]) ? $fieldLabels[$field] : $field; $this->validationError($field, sprintf(_t("Checkout.MEMBEREXISTS", "A member already exists with the %s %s"), $fieldLabel, $uid), "required"); $valid = false; } } return $valid; }
public function testAlternatingRepeatedLoginAttempts() { Member::config()->lock_out_after_incorrect_logins = 3; // ATTEMPTING LOG-IN TWICE WITH ONE ACCOUNT AND TWICE WITH ANOTHER SHOULDN'T LOCK ANYBODY OUT $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $member1 = DataObject::get_by_id("Member", $this->idFromFixture('Member', 'test')); $member2 = DataObject::get_by_id("Member", $this->idFromFixture('Member', 'noexpiry')); $this->assertNull($member1->LockedOutUntil); $this->assertNull($member2->LockedOutUntil); // BUT, DOING AN ADDITIONAL LOG-IN WITH EITHER OF THEM WILL LOCK OUT, SINCE THAT IS THE 3RD FAILURE IN // THIS SESSION $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $member1 = DataObject::get_by_id("Member", $this->idFromFixture('Member', 'test')); $this->assertNotNull($member1->LockedOutUntil); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $member2 = DataObject::get_by_id("Member", $this->idFromFixture('Member', 'noexpiry')); $this->assertNotNull($member2->LockedOutUntil); }
/** * Update the current Member record with data from LDAP. * * Constraints: * - Member *must* be in the database before calling this as it will need the ID to be mapped to a {@link Group}. * - GUID of the member must have already been set, for integrity reasons we don't allow it to change here. * * @param Member * @param array|null $data If passed, this is pre-existing AD attribute data to update the Member with. * If not given, the data will be looked up by the user's GUID. * @return bool */ public function updateMemberFromLDAP(Member $member, $data = null) { if (!$this->enabled()) { return false; } if (!$member->GUID) { SS_Log::log(sprintf('Cannot update Member ID %s, GUID not set', $member->ID), SS_Log::WARN); return false; } if (!$data) { $data = $this->getUserByGUID($member->GUID); if (!$data) { SS_Log::log(sprintf('Could not retrieve data for user. GUID: %s', $member->GUID), SS_Log::WARN); return false; } } $member->IsExpired = ($data['useraccountcontrol'] & 2) == 2; $member->LastSynced = (string) SS_Datetime::now(); $member->IsImportedFromLDAP = true; foreach ($member->config()->ldap_field_mappings as $attribute => $field) { if (!isset($data[$attribute])) { SS_Log::log(sprintf('Attribute %s configured in Member.ldap_field_mappings, but no available attribute in AD data (GUID: %s, Member ID: %s)', $attribute, $data['objectguid'], $member->ID), SS_Log::NOTICE); continue; } if ($attribute == 'thumbnailphoto') { $imageClass = $member->getRelationClass($field); if ($imageClass !== 'Image' && !is_subclass_of($imageClass, 'Image')) { SS_Log::log(sprintf('Member field %s configured for thumbnailphoto AD attribute, but it isn\'t a valid relation to an Image class', $field), SS_Log::WARN); continue; } $filename = sprintf('thumbnailphoto-%s.jpg', $data['samaccountname']); $path = ASSETS_DIR . '/' . $member->config()->ldap_thumbnail_path; $absPath = BASE_PATH . '/' . $path; if (!file_exists($absPath)) { Filesystem::makeFolder($absPath); } // remove existing record if it exists $existingObj = $member->getComponent($field); if ($existingObj && $existingObj->exists()) { $existingObj->delete(); } // The image data is provided in raw binary. file_put_contents($absPath . '/' . $filename, $data[$attribute]); $record = new $imageClass(); $record->Name = $filename; $record->Filename = $path . '/' . $filename; $record->write(); $relationField = $field . 'ID'; $member->{$relationField} = $record->ID; } else { $member->{$field} = $data[$attribute]; } } // if a default group was configured, ensure the user is in that group if ($this->config()->default_group) { $group = Group::get()->filter('Code', $this->config()->default_group)->limit(1)->first(); if (!($group && $group->exists())) { SS_Log::log(sprintf('LDAPService.default_group misconfiguration! There is no such group with Code = \'%s\'', $this->config()->default_group), SS_Log::WARN); } else { $group->Members()->add($member, array('IsImportedFromLDAP' => '1')); } } // this is to keep track of which groups the user gets mapped to // and we'll use that later to remove them from any groups that they're no longer mapped to $mappedGroupIDs = array(); // ensure the user is in any mapped groups if (isset($data['memberof'])) { $ldapGroups = is_array($data['memberof']) ? $data['memberof'] : array($data['memberof']); foreach ($ldapGroups as $groupDN) { foreach (LDAPGroupMapping::get() as $mapping) { if (!$mapping->DN) { SS_Log::log(sprintf('LDAPGroupMapping ID %s is missing DN field. Skipping', $mapping->ID), SS_Log::WARN); continue; } // the user is a direct member of group with a mapping, add them to the SS group. if ($mapping->DN == $groupDN) { $mapping->Group()->Members()->add($member, array('IsImportedFromLDAP' => '1')); $mappedGroupIDs[] = $mapping->GroupID; } // the user *might* be a member of a nested group provided the scope of the mapping // is to include the entire subtree. Check all those mappings and find the LDAP child groups // to see if they are a member of one of those. If they are, add them to the SS group if ($mapping->Scope == 'Subtree') { $childGroups = $this->getNestedGroups($mapping->DN, array('dn')); if (!$childGroups) { continue; } foreach ($childGroups as $childGroupDN => $childGroupRecord) { if ($childGroupDN == $groupDN) { $mapping->Group()->Members()->add($member, array('IsImportedFromLDAP' => '1')); $mappedGroupIDs[] = $mapping->GroupID; } } } } } } // remove the user from any previously mapped groups, where the mapping has since been removed $groupRecords = DB::query(sprintf('SELECT "GroupID" FROM "Group_Members" WHERE "IsImportedFromLDAP" = 1 AND "MemberID" = %s', $member->ID)); foreach ($groupRecords as $groupRecord) { if (!in_array($groupRecord['GroupID'], $mappedGroupIDs)) { $group = Group::get()->byId($groupRecord['GroupID']); // Some groups may no longer exist. SilverStripe does not clean up join tables. if ($group) { $group->Members()->remove($member); } } } // This will throw an exception if there are two distinct GUIDs with the same email address. // We are happy with a raw 500 here at this stage. $member->write(); }