diff --git a/docs/en/00_Getting_Started/01_Installation/How_To/MySQL_SSL_Support.md b/docs/en/00_Getting_Started/01_Installation/How_To/MySQL_SSL_Support.md
new file mode 100644
index 000000000..c47e358a6
--- /dev/null
+++ b/docs/en/00_Getting_Started/01_Installation/How_To/MySQL_SSL_Support.md
@@ -0,0 +1,169 @@
+title: MySQL SSL Support
+summary: Setting up MySQL SSL certificates to work with Silverstripe
+
+# MySQL SSL Support: Why do I need it?
+
+In a typical Silverstripe set up, you will only need to use a single host to function as the web server, email server, database server, among others.
+
+In some cases, however, you may be required to connect to a database on a remote host. Connecting to a remote host without SSL encryption exposes your data to [packet sniffing](http://www.linuxjournal.com/content/packet-sniffing-basics) and may compromise the security of your Silverstripe instance.
+
+This article demonstrates how to generate SSL certificates using MySQL and implementing them in Silverstripe.
+
+
+This article assumes that you have `MySQL` and `OpenSSL` installed.
+
+
+
+## Generating Certificates
+
+There are three components to an SSL certificate implementation. The first two components are the ***private key***, and the ***public certificate***, which are mathematically-generated, symetrical pieces of the puzzle that allow [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) to work. The third component is the [Certificate Authority (CA) certificate](https://en.wikipedia.org/wiki/Certificate_authority) that signs the pubic key to prove its validity.
+
+In the case of MySQL, we will need to generate three sets of certificates, namely:
+
+- the CA key and certificate
+- the server key and certificate
+- the client key and certificate
+
+We also need to sign the certificates with our generated CA.
+
+The commands below illustrate how to do so on your MySQL host.
+
+
+The following commands will work on Linux/Unix based servers. For other servers such as windows, refer to the [MySQL documentation](https://dev.mysql.com/doc/refman/5.7/en/creating-ssl-files-using-openssl.html)
+
+
+
+ :::bash
+
+ # Create directory
+ sudo mkdir ssl
+ cd ssl
+
+ # Generate CA key and CA cert
+ sudo openssl genrsa 2048 | sudo tee -a ca-key.pem
+ sudo openssl req -new -x509 -nodes -days 365000 -key ca-key.pem -out ca-cert.pem
+
+ # Generate SERVER key and server certificate signing request
+ # IMPORTANT: the common name of the certificate should match the domain name of your host!
+ sudo openssl rsa -in server-key.pem -out server-key.pem
+ sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout server-key.pem -out server-req.pem
+
+ # Generate and sign SERVER certificate
+ sudo openssl x509 -req -in server-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem
+
+ # Generate CLIENT key and certificate signing request
+ sudo openssl rsa -in client-key.pem -out client-key.pem
+ sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout client-key.pem -out client-req.pem
+
+ # Generate and sign CLIENT certificate
+ sudo openssl x509 -req -in client-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem
+
+ # Verify validity of generated certificates
+ sudo openssl verify -CAfile ca-cert.pem server-cert.pem client-cert.pem
+
+
+After generating the certificates, make sure to set the correct permissions to prevent unauthorized access to your keys!
+
+It is critical that the key files (files ending in *key.pem) are kept secret. Once these files are exposed, you will need to regenerate the certificates to prevent exposing your data traffic.
+
+
+ :::bash
+ # Set permissions readonly permissions and change owner to root
+ sudo chown root:root *.pem
+ sudo chmod 440 *.pem
+
+ # Server certificates need to be readable by mysql
+ sudo chgrp mysql server*.pem
+ sudo mv *.pem /etc/mysql/ssl
+
+
+## Setting up MySQL to use SSL certificates
+
+
+For Debian/Ubuntu instances, the configuration file is usually in `/etc/mysql/my.cnf`. Refer to your MySQL manual for more information
+
+
+We must edit the MySQL configuration to use the newly generated certificates.
+
+Edit your MySQL configuration file as follows.
+
+
+ [mysqld]
+ ...
+ ssl-ca=/etc/mysql/ca-cert.pem
+ ssl-cert=/etc/mysql/server-cert.pem
+ ssl-key=/etc/mysql/server-key.pem
+
+ # IMPORTANT! When enabling MySQL remote connections, make sure to take adequate steps to secure your machine from unathorized access!
+ bind-address=0.0.0.0
+
+
+Enabling remote connections to your MySQL instance introduces various security risks. Make sure to take appropriate steps to secure your instance by using a strong password, disabling MySQL root access, and using a firewall to only accept qualified hosts, for example.
+
+
+Make sure to restart your MySQL instance to reflect the changes.
+
+ :::bash
+ sudo service mysql restart
+
+
+## Setting up Silverstripe to connect to MySQL
+
+Now that we have successfully setup the SSL your MySQL host, we now need to configure Silverstripe to use the certificates.
+
+### Copying SSL Certificates
+
+First we need to copy the client certificate files to the Silverstripe instance. You will need to copy:
+
+- `client-key.pem`
+- `client-cert.pem`
+- `ca-cert.pem`
+
+
+Make sure to only copy `client-key.pem`, `client-cert.pem`, and `ca-cert.pem` to avoid leaking your credentials!
+
+
+On your Silverstripe instance:
+
+ :::bash
+ # Secure copy over SSH via rsync command. You may use an alternative method if desired.
+ rsync -avP user@db1.example.com:/path/to/client/certs /path/to/secure/folder
+
+ # Depending on your web server configuration, allow web server to read to SSL files
+ sudo chown -R www-data:www-data /path/to/secure/folder
+ sudo chmod 750 /path/to/secure/folder
+ sudo chmod 400 /path/to/secure/folder/*
+
+### Setting up _ss_environment.php to use SSL certificates
+
+
+`SS_DATABASE_SERVER does not accept IP-based hostnames. Also, if the domain name of the host does not match the common name you used to generate the server certificate, you will get an `SSL certificate mismatch error`.
+
+
+Add or edit your `_ss_environment.php` configuration file. (See [Environment Management](/getting_started/environment_management) for more information.)
+
+ :::php
+ ');
+
+ // These define the paths to the SSL key, certificate, and CA certificate bundle.
+ define('SS_DATABASE_SSL_KEY', '/home/newdrafts/mysqlssltest/client-key.pem');
+ define('SS_DATABASE_SSL_CERT', '/home/newdrafts/mysqlssltest/client-cert.pem');
+ define('SS_DATABASE_SSL_CA', '/home/newdrafts/mysqlssltest/ca- cert.pem');
+
+ // When using SSL connections, you also need to supply a username and password to override the default settings
+ define('SS_DEFAULT_ADMIN_USERNAME', 'username');
+ define('SS_DEFAULT_ADMIN_PASSWORD', 'password');
+
+
+When running the installer, make sure to check on the `Use _ss_environment file for configuration` option under the `Database Configuration` section to use the environment file.
+
+## Conclusion
+
+That's it! We hope that this article was able to help you configure your remote MySQL SSL secure database connection.
\ No newline at end of file
diff --git a/docs/en/00_Getting_Started/03_Environment_Management.md b/docs/en/00_Getting_Started/03_Environment_Management.md
index 05946a051..b6f25e9d5 100644
--- a/docs/en/00_Getting_Started/03_Environment_Management.md
+++ b/docs/en/00_Getting_Started/03_Environment_Management.md
@@ -85,3 +85,8 @@ SilverStripe core environment variables are listed here, though you're free to d
| `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching). Must be a CacheInterface or CacheFactory class name |
| `SS_IGNORE_DOT_ENV` | If set the .env file will be ignored. This is good for live to mitigate any performance implications of loading the .env file |
| `SS_BASE_URL` | The url to use when it isn't determinable by other means (eg: for CLI commands) |
+| `SS_CONFIGSTATICMANIFEST` | Set to `SS_ConfigStaticManifest_Reflection` to use the Silverstripe 4 Reflection config manifest (speed improvement during dev/build and ?flush) |
+| `SS_DATABASE_SSL_KEY` | Absolute path to SSL key file |
+| `SS_DATABASE_SSL_CERT` | Absolute path to SSL certificate file |
+| `SS_DATABASE_SSL_CA` | Absolute path to SSL Certificate Authority bundle file |
+| `SS_DATABASE_SSL_CIPHER` | Optional setting for custom SSL cipher |
diff --git a/src/Dev/Install/MySQLDatabaseConfigurationHelper.php b/src/Dev/Install/MySQLDatabaseConfigurationHelper.php
index 97c6d35d7..000eb9b70 100644
--- a/src/Dev/Install/MySQLDatabaseConfigurationHelper.php
+++ b/src/Dev/Install/MySQLDatabaseConfigurationHelper.php
@@ -6,6 +6,9 @@ use mysqli;
use PDO;
use Exception;
use mysqli_result;
+use SilverStripe\Core\Config\Config;
+use SilverStripe\ORM\Connect\MySQLiConnector;
+use SilverStripe\ORM\Connect\PDOConnector;
/**
* This is a helper class for the SS installer.
@@ -30,11 +33,29 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper
try {
switch ($databaseConfig['type']) {
case 'MySQLDatabase':
- $conn = @new MySQLi(
+ $conn = mysqli_init();
+
+ // Set SSL parameters if they exist. All parameters are required.
+ if (array_key_exists('ssl_key', $databaseConfig) &&
+ array_key_exists('ssl_cert', $databaseConfig) &&
+ array_key_exists('ssl_ca', $databaseConfig)
+ ) {
+ $conn->ssl_set(
+ $databaseConfig['ssl_key'],
+ $databaseConfig['ssl_cert'],
+ $databaseConfig['ssl_ca'],
+ dirname($databaseConfig['ssl_ca']),
+ array_key_exists('ssl_cipher', $databaseConfig)
+ ? $databaseConfig['ssl_cipher']
+ : Config::inst()->get(MySQLiConnector::class, 'ssl_cipher_default')
+ );
+ }
+ @$conn->real_connect(
$databaseConfig['server'],
$databaseConfig['username'],
$databaseConfig['password']
);
+
if ($conn && empty($conn->connect_errno)) {
$conn->query("SET sql_mode = 'ANSI'");
return $conn;
@@ -46,10 +67,31 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper
}
case 'MySQLPDODatabase':
// May throw a PDOException if fails
+
+ // Set SSL parameters
+ $ssl = null;
+
+ if (array_key_exists('ssl_key', $databaseConfig) &&
+ array_key_exists('ssl_cert', $databaseConfig)
+ ) {
+ $ssl = array(
+ PDO::MYSQL_ATTR_SSL_KEY => $databaseConfig['ssl_key'],
+ PDO::MYSQL_ATTR_SSL_CERT => $databaseConfig['ssl_cert'],
+ );
+ if (array_key_exists('ssl_ca', $databaseConfig)) {
+ $ssl[PDO::MYSQL_ATTR_SSL_CA] = $databaseConfig['ssl_ca'];
+ }
+ // use default cipher if not provided
+ $ssl[PDO::MYSQL_ATTR_SSL_CA] = array_key_exists('ssl_ca', $databaseConfig)
+ ? $databaseConfig['ssl_ca']
+ : Config::inst()->get(PDOConnector::class, 'ssl_cipher_default');
+ }
+
$conn = @new PDO(
'mysql:host='.$databaseConfig['server'],
$databaseConfig['username'],
- $databaseConfig['password']
+ $databaseConfig['password'],
+ $ssl
);
if ($conn) {
$conn->query("SET sql_mode = 'ANSI'");
diff --git a/src/Dev/Install/install5.php b/src/Dev/Install/install5.php
index cce0726ea..ebcfb7528 100755
--- a/src/Dev/Install/install5.php
+++ b/src/Dev/Install/install5.php
@@ -138,6 +138,18 @@ if (isset($_REQUEST['db'])) {
"password" => getenv('SS_DATABASE_PASSWORD') ?: "",
"database" => $_REQUEST['db'][$type]['database'],
);
+
+ // Set SSL parameters if they exist
+ if (getenv('SS_DATABASE_SSL_KEY') && getenv('SS_DATABASE_SSL_CERT')) {
+ $databaseConfig['ssl_key'] = getenv('SS_DATABASE_SSL_KEY');
+ $databaseConfig['ssl_cert'] = getenv('SS_DATABASE_SSL_CERT');
+ }
+ if (getenv('SS_DATABASE_SSL_CA')) {
+ $databaseConfig['ssl_ca'] = getenv('SS_DATABASE_SSL_CA');
+ }
+ if (getenv('SS_DATABASE_SSL_CIPHER')) {
+ $databaseConfig['ssl_ca'] = getenv('SS_DATABASE_SSL_CIPHER');
+ }
} else {
// Normal behaviour without the environment
$databaseConfig = $_REQUEST['db'][$type];
diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php
index a8455bf3e..6f2f0f8ad 100644
--- a/src/Dev/SapphireTest.php
+++ b/src/Dev/SapphireTest.php
@@ -440,7 +440,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
{
$filename = ClassLoader::inst()->getItemPath(static::class);
if (!$filename) {
- throw new LogicException("getItemPath returned null for " . static::class);
+ throw new LogicException("getItemPath returned null for " . static::class
+ . ". Try adding flush=1 to the test run.");
}
return dirname($filename);
}
diff --git a/src/Forms/TreeDropdownField.php b/src/Forms/TreeDropdownField.php
index ef972965d..dc6a13788 100644
--- a/src/Forms/TreeDropdownField.php
+++ b/src/Forms/TreeDropdownField.php
@@ -69,6 +69,13 @@ class TreeDropdownField extends FormField
'tree'
);
+ /**
+ * @config
+ * @var int
+ * @see {@link Hierarchy::$node_threshold_total}.
+ */
+ private static $node_threshold_total = 30;
+
/**
* @var string
*/
@@ -488,7 +495,12 @@ class TreeDropdownField extends FormField
}
// Create marking set
- $markingSet = MarkedSet::create($obj, $this->getChildrenMethod(), $this->getNumChildrenMethod(), 30);
+ $markingSet = MarkedSet::create(
+ $obj,
+ $this->getChildrenMethod(),
+ $this->getNumChildrenMethod(),
+ $this->config()->get('node_threshold_total')
+ );
// Set filter on searched nodes
if ($this->getFilterFunction() || $this->search) {
diff --git a/src/ORM/Connect/DBConnector.php b/src/ORM/Connect/DBConnector.php
index c5af38205..039a6accf 100644
--- a/src/ORM/Connect/DBConnector.php
+++ b/src/ORM/Connect/DBConnector.php
@@ -3,6 +3,7 @@
namespace SilverStripe\ORM\Connect;
use SilverStripe\Core\Config\Config;
+use SilverStripe\Core\Config\Configurable;
use SilverStripe\View\Parsers\SQLFormatter;
/**
@@ -11,6 +12,8 @@ use SilverStripe\View\Parsers\SQLFormatter;
abstract class DBConnector
{
+ use Configurable;
+
/**
* List of operations to treat as write
* Implicitly includes all ddl_operations
diff --git a/src/ORM/Connect/MySQLiConnector.php b/src/ORM/Connect/MySQLiConnector.php
index 8ca85f91b..6b283be7b 100644
--- a/src/ORM/Connect/MySQLiConnector.php
+++ b/src/ORM/Connect/MySQLiConnector.php
@@ -12,6 +12,14 @@ use mysqli_stmt;
class MySQLiConnector extends DBConnector
{
+ /**
+ * Default strong SSL cipher to be used
+ *
+ * @config
+ * @var string
+ */
+ private static $ssl_cipher_default = 'DHE-RSA-AES256-SHA';
+
/**
* Connection to the MySQL database
*
@@ -68,23 +76,31 @@ class MySQLiConnector extends DBConnector
$connCharset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_charset');
$connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation');
- if (!empty($parameters['port'])) {
- $this->dbConn = new MySQLi(
- $parameters['server'],
- $parameters['username'],
- $parameters['password'],
- $selectedDB,
- $parameters['port']
- );
- } else {
- $this->dbConn = new MySQLi(
- $parameters['server'],
- $parameters['username'],
- $parameters['password'],
- $selectedDB
+ $this->dbConn = mysqli_init();
+
+ // Set SSL parameters if they exist. All parameters are required.
+ if (array_key_exists('ssl_key', $parameters) &&
+ array_key_exists('ssl_cert', $parameters) &&
+ array_key_exists('ssl_ca', $parameters)) {
+ $this->dbConn->ssl_set(
+ $parameters['ssl_key'],
+ $parameters['ssl_cert'],
+ $parameters['ssl_ca'],
+ dirname($parameters['ssl_ca']),
+ array_key_exists('ssl_cipher', $parameters)
+ ? $parameters['ssl_cipher']
+ : self::config()->get('ssl_cipher_default')
);
}
+ $this->dbConn->real_connect(
+ $parameters['server'],
+ $parameters['username'],
+ $parameters['password'],
+ $selectedDB,
+ !empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port")
+ );
+
if ($this->dbConn->connect_error) {
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
}
diff --git a/src/ORM/Connect/PDOConnector.php b/src/ORM/Connect/PDOConnector.php
index 5bf70f106..753957488 100644
--- a/src/ORM/Connect/PDOConnector.php
+++ b/src/ORM/Connect/PDOConnector.php
@@ -21,6 +21,14 @@ class PDOConnector extends DBConnector
*/
private static $emulate_prepare = false;
+ /**
+ * Default strong SSL cipher to be used
+ *
+ * @config
+ * @var string
+ */
+ private static $ssl_cipher_default = 'DHE-RSA-AES256-SHA';
+
/**
* The PDO connection instance
*
@@ -171,6 +179,20 @@ class PDOConnector extends DBConnector
$options = array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . $charset . ' COLLATE ' . $connCollation
);
+
+ // Set SSL options if they are defined
+ if (array_key_exists('ssl_key', $parameters) &&
+ array_key_exists('ssl_cert', $parameters)
+ ) {
+ $options[PDO::MYSQL_ATTR_SSL_KEY] = $parameters['ssl_key'];
+ $options[PDO::MYSQL_ATTR_SSL_CERT] = $parameters['ssl_cert'];
+ if (array_key_exists('ssl_ca', $parameters)) {
+ $options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca'];
+ }
+ // use default cipher if not provided
+ $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default');
+ }
+
if (self::is_emulate_prepare()) {
$options[PDO::ATTR_EMULATE_PREPARES] = true;
}
diff --git a/templates/forms/HtmlEditorField_holder_small.ss b/templates/forms/HtmlEditorField_holder_small.ss
new file mode 100644
index 000000000..21eec8dc5
--- /dev/null
+++ b/templates/forms/HtmlEditorField_holder_small.ss
@@ -0,0 +1,5 @@
+
+ <% if $Title %><% end_if %>
+ $Field
+ <% if $RightTitle %><% end_if %>
+
diff --git a/tests/php/Security/MemberAuthenticatorTest.php b/tests/php/Security/MemberAuthenticatorTest.php
index d8e4c2f05..6a203681f 100644
--- a/tests/php/Security/MemberAuthenticatorTest.php
+++ b/tests/php/Security/MemberAuthenticatorTest.php
@@ -3,6 +3,7 @@
namespace SilverStripe\Security\Tests;
use SilverStripe\Control\Controller;
+use SilverStripe\Control\NullHTTPRequest;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
@@ -42,7 +43,6 @@ class MemberAuthenticatorTest extends SapphireTest
$this->defaultUsername = null;
$this->defaultPassword = null;
}
- DefaultAdminService::clearDefaultAdmin();
DefaultAdminService::setDefaultAdmin('admin', 'password');
}
@@ -149,6 +149,37 @@ class MemberAuthenticatorTest extends SapphireTest
);
}
+ public function testExpiredTempID()
+ {
+ DefaultAdminService::clearDefaultAdmin();
+
+ $authenticator = new CMSMemberAuthenticator();
+
+ // Make member with expired TempID
+ $member = new Member();
+ $member->Email = 'test1@test.com';
+ $member->PasswordEncryption = "sha1";
+ $member->Password = "mypassword";
+ $member->TempIDExpired = '2016-05-22 00:00:00';
+ $member->write();
+ Injector::inst()->get(IdentityStore::class)->logIn($member, true);
+
+ $tempID = $member->TempIDHash;
+
+ DBDatetime::set_mock_now('2016-05-29 00:00:00');
+
+ $this->assertNotEmpty($tempID);
+ $this->assertFalse(DefaultAdminService::hasDefaultAdmin());
+
+ $result = $authenticator->authenticate(array(
+ 'tempid' => $tempID,
+ 'Password' => 'notmypassword'
+ ), Controller::curr()->getRequest(), $validationResult);
+
+ $this->assertNull($result);
+ $this->assertFalse($validationResult->isValid());
+ }
+
/**
* Test that the default admin can be authenticated
*/