-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[WIP] [Security] Session concurrency control #12009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
66ed331
90eba34
82c5f71
5772a75
c6b1488
9c3aebb
2b5e2ed
5e22c79
5664571
f458bd4
f92286a
52b7ba4
4ca3a47
6224ba0
81c01de
c0edefd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,27 +17,28 @@ | |
* The schema used for the ACL system. | ||
* | ||
* @author Stefan Paschke <stefan.paschke@gmail.com> | ||
* @author Antonio J. García Lagar <aj@garcialagar.es> | ||
*/ | ||
final class Schema extends BaseSchema | ||
{ | ||
/** | ||
* Constructor | ||
* | ||
* @param array $options the names for tables | ||
* @param string $table | ||
*/ | ||
public function __construct(array $options) | ||
public function __construct($table) | ||
{ | ||
parent::__construct(); | ||
|
||
$this->addSessionInformationTable($options); | ||
$this->addSessionInformationTable($table); | ||
} | ||
|
||
/** | ||
* Adds the session_information table to the schema | ||
*/ | ||
protected function addSessionInformationTable(array $options) | ||
protected function addSessionInformationTable($table) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be private |
||
{ | ||
$table = $this->createTable($options['session_information_table_name']); | ||
$table = $this->createTable($table); | ||
$table->addColumn('session_id', 'string'); | ||
$table->addColumn('username', 'string'); | ||
$table->addColumn('expired', 'datetime', array('unsigned' => true, 'notnull' => false)); | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,39 +2,35 @@ | |
|
||
namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; | ||
|
||
use Doctrine\DBAL\Driver\Connection; | ||
use Doctrine\DBAL\Connection; | ||
use Symfony\Component\Security\Http\Session\SessionInformation; | ||
use Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface; | ||
|
||
/** | ||
* @author Stefan Paschke <stefan.paschke@gmail.com> | ||
* @author Antonio J. García Lagar <aj@garcialagar.es> | ||
*/ | ||
class SessionRegistryStorage implements SessionRegistryStorageInterface | ||
{ | ||
protected $connection; | ||
protected $options; | ||
protected $table; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these should be private |
||
|
||
public function __construct(Connection $connection, array $options) | ||
public function __construct(Connection $connection, $table) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing PHPdoc in this constructor |
||
{ | ||
$this->connection = $connection; | ||
$this->options = $options; | ||
} | ||
|
||
/** | ||
* not implemented | ||
*/ | ||
public function getUsers() | ||
{ | ||
throw new \BadMethodCallException("Not implemented."); | ||
$this->connection = $connection; | ||
$this->table = $table; | ||
} | ||
|
||
/** | ||
* Obtains the maintained information for one session. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
* | ||
* @param string $sessionId the session identifier key. | ||
* @param string $sessionId the session identifier key. | ||
* @return SessionInformation a SessionInformation object. | ||
*/ | ||
public function getSessionInformation($sessionId) | ||
{ | ||
$statement = $this->connection->executeQuery( | ||
'SELECT * FROM '.$this->options['session_information_table_name'].' WHERE session_id = :session_id', | ||
'SELECT * FROM '.$this->table.' WHERE session_id = :session_id', | ||
array('session_id' => $sessionId) | ||
); | ||
|
||
|
@@ -46,24 +42,23 @@ public function getSessionInformation($sessionId) | |
/** | ||
* Obtains the maintained information for one user. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
* | ||
* @param string $username The user identifier. | ||
* @param boolean $includeExpiredSessions. | ||
* @return array An array of SessionInformation objects. | ||
* @param string $username The user identifier. | ||
* @param boolean $includeExpiredSessions. | ||
* @return array An array of SessionInformation objects. | ||
*/ | ||
public function getSessionInformations($username, $includeExpiredSessions = false) | ||
{ | ||
$sessionInformations = array(); | ||
|
||
$statement = $this->connection->executeQuery( | ||
'SELECT * | ||
FROM '.$this->options['session_information_table_name'].' | ||
FROM '.$this->table.' | ||
WHERE username = :username'.($includeExpiredSessions ? '' : ' AND expired IS NULL ').' | ||
ORDER BY last_request DESC', | ||
array('username' => $username) | ||
); | ||
|
||
while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) | ||
{ | ||
while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) { | ||
$sessionInformations[] = $this->instantiateSessionInformationFromResultSet($data); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you should call |
||
|
||
|
@@ -78,17 +73,52 @@ public function getSessionInformations($username, $includeExpiredSessions = fals | |
*/ | ||
public function setSessionInformation(SessionInformation $sessionInformation) | ||
{ | ||
$statement = $this->connection->prepare( | ||
'INSERT INTO '.$this->options['session_information_table_name'].' | ||
(session_id, username, last_request, expired) VALUES(:session_id, :username, :last_request, :expired) | ||
ON DUPLICATE KEY | ||
UPDATE username=:username, last_request=:last_request, expired=:expired'); | ||
|
||
$statement->bindValue('session_id', $sessionInformation->getSessionId()); | ||
$statement->bindValue('username', $sessionInformation->getUsername()); | ||
$statement->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); | ||
$statement->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); | ||
$statement->execute(); | ||
$mergeSql = $this->getMergeSql(); | ||
|
||
if (null !== $mergeSql) { | ||
$mergeStmt = $this->pdo->prepare($mergeSql); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undefined property |
||
$mergeStmt->bindValue('session_id', $sessionInformation->getSessionId()); | ||
$mergeStmt->bindValue('username', $sessionInformation->getUsername()); | ||
$mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); | ||
$mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); | ||
$mergeStmt->execute(); | ||
|
||
return true; | ||
} | ||
|
||
$updateStmt = $this->pdo->prepare( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here |
||
"UPDATE $this->table SET username=:username, last_request=:last_request, expired=:expired WHERE session_id = :session_id" | ||
); | ||
$mergeStmt->bindValue('session_id', $sessionInformation->getSessionId()); | ||
$mergeStmt->bindValue('username', $sessionInformation->getUsername()); | ||
$mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); | ||
$mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); | ||
$updateStmt->execute(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could use the DBAL API for this: $this->connection->update($this->table, array(
'username' => $sessionInformation->getUsername(),
'expired' => $sessionInformation->getExpired(),
'last_request' => $sessionInformation->getLastRequest(),
), array('session_id' => $sessionInformation->getSessionId()), array(
'last_request' => 'datetime',
'expired' => 'datetime',
)); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, actually no, given that the update statement is reused a second time |
||
|
||
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in | ||
// duplicate key errors when the same sessioninfo is written simultaneously. We can just catch such an | ||
// error and re-execute the update. This is similar to a serializable transaction with retry logic | ||
// on serialization failures but without the overhead and without possible false positives due to | ||
// longer gap locking. | ||
if (!$updateStmt->rowCount()) { | ||
try { | ||
$insertStmt = $this->pdo->prepare( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here |
||
"INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)" | ||
); | ||
$insertStmt->bindValue('session_id', $sessionInformation->getSessionId()); | ||
$insertStmt->bindValue('username', $sessionInformation->getUsername()); | ||
$insertStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); | ||
$insertStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); | ||
$insertStmt->execute(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could use |
||
} catch (\PDOException $e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DBAL will never throw a PDOException, but a DBALException (PDOException will only be the previous exception) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, DBAL already handles identifying duplicate key errors in recent versions |
||
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys | ||
if (0 === strpos($e->getCode(), '23')) { | ||
$updateStmt->execute(); | ||
} else { | ||
throw $e; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -98,16 +128,45 @@ public function setSessionInformation(SessionInformation $sessionInformation) | |
*/ | ||
public function removeSessionInformation($sessionId) | ||
{ | ||
$this->connection->delete($this->options['session_information_table_name'], array('session_id' => $sessionId)); | ||
$this->connection->delete($this->table, array('session_id' => $sessionId)); | ||
} | ||
|
||
private function instantiateSessionInformationFromResultSet($data) | ||
{ | ||
return new $this->options['session_information_class_name']( | ||
return new SessionInformation( | ||
$data['session_id'], | ||
$data['username'], | ||
(null == $data['last_request']) ? null : new \DateTime($data['last_request']), | ||
(null == $data['expired']) ? null : new \DateTime($data['expired']) | ||
null === $data['last_request'] ? null : new \DateTime($data['last_request']), | ||
null === $data['expired'] ? null : new \DateTime($data['expired']) | ||
); | ||
} | ||
|
||
/** | ||
* Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't fully understand this phrase. Could it be simplified as follows?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The returned SQL is not an |
||
* | ||
* @return string|null The SQL string or null when not supported | ||
*/ | ||
private function getMergeSql() | ||
{ | ||
switch ($this->connection->getDriver()->getName()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should rather be based on the DBAL platform than the DBAL driver name actually |
||
case 'pdo_mysql': | ||
return "INSERT INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . | ||
"ON DUPLICATE KEY UPDATE username = VALUES(username), last_request = VALUES(last_request), expired = VALUES(expired)"; | ||
case 'pdo_oracle': | ||
// DUAL is Oracle specific dummy table | ||
return "MERGE INTO $this->table USING DUAL ON (session_id= :session_id) " . | ||
"WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . | ||
"WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired"; | ||
case 'pdo_sqlsrv': | ||
if (version_compare($this->connection->getWrappedConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>=')) { | ||
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon | ||
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx | ||
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON (session_id = :session_id) " . | ||
"WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . | ||
"WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired;"; | ||
} | ||
case 'pdo_sqlite': | ||
return "INSERT OR REPLACE INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)"; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
$table
param description: the table to use? the table to create?