Skip to content

Instantly share code, notes, and snippets.

@dbu
Last active August 29, 2015 14:07
Show Gist options
  • Save dbu/c7fe53a768b33e4ad6c6 to your computer and use it in GitHub Desktop.
Save dbu/c7fe53a768b33e4ad6c6 to your computer and use it in GitHub Desktop.
Cached user provider

A user provider reading users from the filesystem.

This is useful when you have an API with just a few different users (different frontends, not end users). Our data comes from elasticsearch for most API calls, so we don't want to block all access just because MySQL has gone away.

As we have a multi server setup, we have a cronjob to trigger the UserProvider::dumpUsers method regularly.

<?php
namespace Acme\ApiBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* User entity for API consumers.
*
* Using public properties instead of getter/setter for simplicity.
*
* @ORM\Entity()
* @ORM\Table(name="api_user")
*/
class ApiUser implements UserInterface
{
/**
* @var integer
*
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* @var string
*
* @ORM\Column(type="string", length=255, unique=true)
*/
public $username;
/**
* Encrypted password. Must be persisted.
*
* @var string
*
* @ORM\Column(type="string", length=255)
*/
public $password;
/**
* The salt for passwords of this user.
*
* @var string
*
* @ORM\Column(type="string", length=255)
*/
public $salt;
/**
* @var array
*
* @ORM\Column(type="array")
*/
public $roles;
/**
* Constructor
*
* @param string $username The (immutable) user name.
*/
public function __construct($username)
{
$this->username = $username;
$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
}
/**
* Rebuild an instance from var_export
*
* @param array $data The stored values
*
* @return ApiUser
*/
public static function __set_state(array $data)
{
$user = new ApiUser($data['username']);
$user->id = $data['id'];
$user->password = $data['password'];
$user->salt = $data['salt'];
$user->roles = $data['roles'];
return $user;
}
}
<?php
namespace Acme\ApiBundle\Security;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Custom file system based user provider.
*/
class UserProvider implements UserProviderInterface
{
/**
* @var string Filesystem path to the user dump.
*/
private $cacheFile;
/**
* @var Filesystem
*/
private $fs;
/**
* @var UserInterface[] Array of users loaded from the filesystem cache.
*/
private $users;
/**
* @var ManagerRegistry
*/
private $doctrine;
/**
* @param string $cacheFile Path to the user dump file
* @param Filesystem $fs The filesystem service
* @param ManagerRegistry $doctrine Registry to get the repository from.
*/
public function __construct($cacheFile, Filesystem $fs, ManagerRegistry $doctrine)
{
$this->cacheFile = $cacheFile;
$this->fs = $fs;
$this->doctrine = $doctrine;
}
/**
* {@inheritDoc}
*/
public function loadUserByUsername($username)
{
$users = $this->getUsers();
if (isset($users[$username])) {
return $users[$username];
}
throw new UsernameNotFoundException();
}
/**
* We don't do sessions so this method is not used.
*
* @param UserInterface $user The user.
*
* @return UserInterface
*/
public function refreshUser(UserInterface $user)
{
return $user;
}
/**
* Dump the users into the filesystem cache.
*
* @param array $users List of UserInterface.
*
* @return void
*/
public function dumpUsers(array $users)
{
$indexedUsers = array();
foreach ($users as $user) {
$indexedUsers[$user->getUsername()] = $user;
}
$this->users = $indexedUsers;
$this->fs->dumpFile($this->cacheFile, '<?php return ' . var_export($indexedUsers, true) . ';');
}
/**
* Get the users list, loading it from the cache file if necessary.
*
* @return UserInterface[] indexed by the username.
*/
private function getUsers()
{
if (null === $this->users) {
if (!$this->fs->exists($this->cacheFile)) {
$this->warmup();
}
$this->users = require($this->cacheFile);
}
return $this->users;
}
/**
* Warm up the cache file.
*
* @return void
*/
private function warmup()
{
$users = $this->doctrine
->getRepository('AcmeApiBundle:ApiUser')
->findAll()
;
$this->dumpUsers($users);
}
/**
* We can support whatever.
*
* @param string $class Class
*
* @return boolean
*/
public function supportsClass($class)
{
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment