ZF2 - Assuming other user's identity - php

Before I dive into reinventing the wheel, I'd first like to check if ZF2 supports, either out-of-the-box or with a 3rd party library, this particular use case where admins log in as other user, or assume their identity.
If not, as I'm not familiar with ZF2 internal design, how would I go into implementing this, with the only constraint being that the system is already built, so I can't change components (controllers, auth services, etc) into supporting it.
My first thought would be to make a mechanism to switch the logged user information stored in the session storage, with the one whose identity I want to assume. Then, write to the session, under a different namespace, the original user information (admin) so that it can be reverted.
Going by this approach, I am expecting components like Zend\Authentication\AuthenticationService return the user whose identity I'm assuming. So, in every call I make to $this->identity()->getId() (identity being a controller plugin for AuthenticationService, that returns the User) in other controllers, the business logic will work normally.
Having said this, the questions would be:
Is there a solution already for this?
Is my approach correct in assuming that by overwriting the session storage I can assume other user ID and expect ZF2 components to work accordingly, or is there any considerations regarding ZF2 internal design/infrastructure I haven't taken in consideration that I should?
Maybe there's a better way to do this?

I think you would need to create your own AuthenticationAdaptor.
class AdminUserLoginAsUser implements \Zend\Authentication\Adapter\AdapterInterface
{
/**
* #var User
*/
private $userToLoginAs;
/**
* #var AdminUser
*/
private $adminUser;
public function __construct(User $userToLoginAs, AdminUser $adminUser)
{
$this->userToLoginAs = $userToLoginAs;
$this->adminUser = $adminUser;
}
/**
* Performs an authentication attempt
*
* #return \Zend\Authentication\Result
* #throws \Zend\Authentication\Adapter\Exception\ExceptionInterface If authentication cannot be performed
*/
public function authenticate()
{
return new \Zend\Authentication\Result(
Result::SUCCESS, $this->user, [
'You have assumed control of user.',
]
);
}
}
The above class will allow you to login as another user when used with Zend's AuthenticationService class.
You will need some way of using Zend's AuthenticationService class and I would recommend using an AuthManager that wraps around the AuthenticationService.
/**
* The AuthManager service is responsible for user's login/logout and simple access
* filtering. The access filtering feature checks whether the current visitor
* is allowed to see the given page or not.
*/
class AuthManager
{
/**
* Authentication service.
* #var \Zend\Authentication\AuthenticationService
*/
private $authService;
/**
* Session manager.
* #var Zend\Session\SessionManager
*/
private $sessionManager;
/**
* Contents of the 'access_filter' config key.
* #var array
*/
private $config;
/**
* Constructs the service.
*/
public function __construct($authService, $sessionManager, $config)
{
$this->authService = $authService;
$this->sessionManager = $sessionManager;
$this->config = $config;
}
/**
* Performs a login attempt. If $rememberMe argument is true, it forces the session
* to last for one month (otherwise the session expires on one hour).
*/
public function login($email, $password, $rememberMe)
{
// Check if user has already logged in. If so, do not allow to log in
// twice.
if ($this->authService->getIdentity()!=null) {
throw new \Exception('Already logged in');
}
// Authenticate with login/password.
$authAdapter = $this->authService->getAdapter();
$authAdapter->setEmail($email);
$authAdapter->setPassword($password);
$result = $this->authService->authenticate();
// If user wants to "remember him", we will make session to expire in
// one month. By default session expires in 1 hour (as specified in our
// config/global.php file).
if ($result->getCode()==Result::SUCCESS && $rememberMe) {
// Session cookie will expire in 1 month (30 days).
$this->sessionManager->rememberMe(60*60*24*30);
}
return $result;
}
public function loginAsUser($user)
{
// Check if user has already logged in. If so, do not allow to log in
// twice.
if ($this->authService->getIdentity() !== null) {
throw new \Exception('Not logged in.');
}
// First need to logout of current user
$this->authService->clearIdentity();
$authAdapter = $this->authService->setAdapter(new AdminUserLoginAsUser($user, $this->authService->getIdentity()));
return $this->authService->authenticate();
}
/**
* Performs user logout.
*/
public function logout()
{
// Allow to log out only when user is logged in.
if ($this->authService->getIdentity()==null) {
throw new \Exception('The user is not logged in');
}
// Remove identity from session.
$this->authService->clearIdentity();
}
}
To see how to plug it all together I would recommend looking at the following resources:
https://olegkrivtsov.github.io/using-zend-framework-3-book/html/en/User_Management__Authentication_and_Access_Filtering.html
https://github.com/olegkrivtsov/using-zf3-book-samples/tree/master/userdemo/module/User
The resources are for zf3 but I think the authenticating of users and managing authentication is very similar to zf2.

Related

Controller check in advance if user can access a route + parameters

I've got a notification system where a route name is stored in the DB along with some a list of parameters used to build a URL. When a user clicks on a notification, these values are recalled and are passed into the router to generate a URL for the user to be redirected to.
A notification has the following structure (simplified structure, there is some inheritance at play here)
class Notification {
/** #var int */
protected $id;
/** #var Uid */
protected $uid;
/** #var string */
protected $title;
/** #var string */
protected $routeName;
/** #var array|null */
protected $routeParameters;
/** #var DateTime */
protected $date;
/** #var DateTime|null */
protected $read;
}
However instead of blindly passing these parameters into the clickable object on screen, they pass through the a redirectAction() defined in the NotificationController.
{{ notification.title }}
This so that I can do a couple things.
Mark the notification as read
Make sure that the user who clicked the notification is the user who the notification was delivered to (otherwise throw an AccessDeniedException)
Make sure that old links are handled nicely <-- the issue
So far, there is a section in my controller that reads the following:
/**
* #Route("/notification/action", name="notification_action")
* #param Request $request
* #param RouterInterface $router
* #return RedirectResponse|Response
*/
public function redirectAction(Request $request, RouterInterface $router) {
// * $notification gets recalled from the DB from the 'uid' in the request params
// * Validation is applied to the notification (check if exists, check if for the correct user)
// * Notification is marked as read with the current timestamp
try {
$url = $router->generate($notification->getRoute(), $notification->getParameters() ?? []);
} catch(RouteNotFoundException | MissingMandatoryParametersException | InvalidParameterException $e) {
return $this->render('notification/broken_action.html.twig');
}
return $this->redirect($url);
}
This is in order to attempt to generate a route and nicely handle a route that either
a) doesn't exist
b) no longer works with older parameters.
This works great if I were to change the definition of a route in the future, but it doesn't handle checking if the user has permission to access the route. This includes:
Checking if the user has the appropriate role to access the route (defined in security.yaml)
Checking if they user is denied access by either the #Security annotation or a voter
Validating parameters interpreted by the the #ParamConverter (such as when an entity may not exist anymore causing a 404)
Handling a thrown exception such as AccessDeniedException or NotFoundHttpException from logic within the action itself
I'm having trouble coming up with a way that I can validate these things before redirecting the user to the $url.
So far all I've come up with is performing a dummy request and seeing if that works before then redirecting the user, but that is yuck for obvious reasons (double request will have extended load times, potentially triggering an action in the background, etc.)
Is there a way that I can check for these things in an efficient way before sending the user to the generated URL? I'm happy to let logic within the action itself slide through validation, as most of the time I tend to avoid this in favour for voters or use of the #Security and #ParamConverter annotations instead. It's just a nice to have for edge cases.
...or I'm open to other ideas on better ways to handle clicking on a notification!

Better way to check if subdomain exists without using database query or hosts file

I'm using symfony2 and in my application for student accommodation, i'm creating dynamic subdomains for each university, i'm configuring the virtual host with the wildcard subdomain entry so any subdomain would be valid.
How do i check if a subdomain is registered and belongs to a university, and how do i differentiate it from a randomly typed non user registered subdomain efficiently?
if i go with a database query then every random access from curious users would result in a lot of db queries, and the use of hosts file would be too slow (not the best practice)
Please suggest an efficient way to do this using php or symfony or any other techniques you guys know of
(additional info) there will be a 'free trial' option so that would result in a lot of subdomains, as anyone and every one would start a free trial, a very good example of what i'm trying to achieve woudld be this StudyStays
-thanks
You could cache all each of the subdomain requests (using something like Doctrine cache as a wrapper for whatever caching system you use) so that each subsequent check would only need to check the cache rather than the database.
Also when adding/removing/updating your subdomain object you could update the cache value to keep it all up to date.
app/config/config.yml
Set your provider for Doctrine Cache Bundle, for more info see the docs (you would need to add the Doctrine Cache Bundle to your vendors and AppKernel, obviously).
doctrine_cache:
providers:
acme_subdomain:
type: filesystem # apc, array, redis, etc
namespace: acme_subdomain
Acme\YourBundle\Registry\SubdomainRegistry
Create a registry that can check for the subdomain state and update the cache when required. This example stores the state as a string rather than a boolean as (as far as I know) a "not found" key will return a false rather than a null.
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Persistence\ObjectManager;
class SubdomainRegistry implements SubdomainRegistry
{
const REGISTERED = 'registered';
const UNREGISTERED = 'unregistered';
/**
* #param ObjectManager
*/
protected $manager;
/**
* #param Cache
*/
protected $cache;
public function __construct(ObjectManager $manager, Cache $cache)
{
$this->manager = $manager;
$this->cache = $cache;
}
/**
* #param string $subdomain
* #return boolean
*/
public function isSubdomainRegistered($subdomain)
{
// If subdomain is not in cache update cache
if (!$this->cache->has($subdomain)) {
$this->updateRegistry($subdomain);
}
return self::REGISTERED === $this->cache->get($subdomain);
}
/**
* #param string $subdomain
* #return boolean
*/
public function registerSubdomain($subdomain)
{
$this->cache->set($subdomain, self::REGISTERED);
return $this;
}
/**
* #param string $subdomain
* #return boolean
*/
public function unregisterSubdomain($subdomain)
{
$this->cache->set($subdomain, self::UNREGISTERED);
return $this;
}
/**
* #param string $subdomain
* #return null
*/
private function updateRegistry($subdomain)
{
$object = $this->manager->findOneBy(array('subdomain' => $subdomain);
// $object->isActive() assume you are storing all subdomains after cancelling
// and setting it to inactive. You could put your own real logic here
if (null === $object || !$object->isActive()) {
$this->unregisterSubdomain($subdomain);
return null;
}
$this->registerSubdomain($subdomain);
}
Then when you are registering or unregistering your subdomain you could add a call to the registry in the method.
For example...
$subdomain = new Subdomain();
// Subdomain as a property of subdomain seems weird to me
// but as I can't immediately think of anything better I'll go with it
$subdomain->setSubdomain('acme');
// .. subdomain details
$this->manager->persist($subdomain);
$this->manager->flush();
$this->registry->registerSubdomain($subdomain->getSubdomain());

Cached user class vs seperate class holding necessary cached fields

I need to cache some info about a user who is logged in (such as security groups, name, username, etc.)
Currently I have a separate class to achieve just this, lets call it CurrentUserHelper. Given a user object, it will cache the appropriate data and save store info in the $_SESSION variable.
The issue is I'm finding a bunch of classes relying just on CurrentUserHelper because they only need a couple of common fields. In fact, most of the functions have the same name as my User class. There's a couple of functions in CurrentUserHelper, such as getSecurityGroupsNames(), that contains a cache of all security group names, but there is no reason this function name could not be in the user class also.
Instead, should I create a CachedUser class and pass this around? This class can extend User, but then I can override getName(), getSecurityGroups(), etc, and returned the cached data, and not preform db requests to get the data.
The downside of passing around a CachedUser object is that this kind of hides the fact the data isn't really up to date if a constructor/function is accepting a type User. I also need to find way to handle merging the entity with Doctrine 2, and making sure entities associating themselves with a CachedUser won't break. If I decide to cache some temporary data (such as # of page views since logged in), this property shouldn't be part of the User class, it's more about the current user's session.
If I continue using the CurrentUserHelper class, maybe I should create an interface and have both CurrentUserHelper and User for the common functionality the two classes would share?
Preface: Extension isn't the best way for these sorts of things.. I'm sure you've heard composition over inheritance shouted at you over and over again. In fact, you can even gain inheritance without using extends!
This sounds like a good use-case for the decorator pattern. Basically, you wrap your existing object with another one that implements the same interface, so it has the same methods as the inner object, but this object's method adds the extra stuff around the method call to the inner object, like caching, for example.
Imagine you have a UserRepository:
/**
* Represents an object capable of providing a user from persistence
*/
interface UserProvider
{
/**
* #param int $id
*
* #return User
*/
public function findUser($id);
}
/**
* Our object that already does the stuff we want to do, without caching
*/
class UserRepository implements UserProvider
{
/**
* #var DbAbstraction
*/
protected $db;
/**
* #var UserMapper
*/
protected $mapper;
/**
* #param DbAbstraction $db
* #param UserMapper $mapper
*/
public function __construct(DbAbstraction $db, UserMapper $mapper)
{
$this->db = $db;
$this->mapper = $mapper;
}
/**
* {#inheritDoc}
*/
public function findUser($id)
{
$data = $this->db->find(['id' => $id]);
/** Data mapper pattern - map data to the User object **/
$user = $this->mapper->map($data);
return $user;
}
}
The above is a really simple example. It'll retrieve the user data from it's persistence (a database, filesystem, whatever), map that data to an actual User object, then return it.
Great, so now we want to add caching. Where should we do this, within the UserRepository? No, because it's not the responsibility of the repository to perform caching, even if you Dependency Inject a caching object (or even a logger!).. this is where the decorator pattern would come in useful.
/**
* Decorates the UserRepository to provide caching
*/
class CachedUserRepository implements UserProvider
{
/**
* #var UserRepository
*/
protected $repo;
/**
* #var CachingImpl
*/
protected $cache;
/**
* #param UserRepository $repo
*/
public function __construct(UserRepository $repo, CachingImpl $cache)
{
$this->repo = $repo;
$this->cache = $cache;
}
/**
* {#inheritDoc}
*
* So, because this class also implements UserProvider, it has to
* have the same method in the interface. We FORWARD the call to
* the ACTUAL user provider, but put caching AROUND it...
*/
public function findUser($id)
{
/** Has this been cached? **/
if ($this->cache->hasKey($id))
{
/**
* Returns your user object, or maps data or whatever
*/
return $this->cache->get($id);
}
/** Hasn't been cached, forward the call to our user repository **/
$user = $this->repo->findUser($id);
/** Store the user in the cache for next time **/
$this->cache->add($id, $user);
return $user;
}
}
Very simply, we've wrapped the original object and method call with some additional caching functionality. The cool thing about this is that, not only can you switch out this cached version for the non-cached version at any time (because they both rely on the same interface), but you can remove the caching completely as a result, just by changing how you instantiate this object (you could take a look at the factory pattern for that, and even decide which factory (abstract factory?) depending on a configuration variable).

Symfony 2 - onSecurityAuthenticationSuccess handler gets called on every page load

I have created a security.authentication.success event listener, which should send a line to the logs upon login success. Now every time I load a page which is behind a firewall, I get a successful login message in my logs. If I tried to use
if ($this->container->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY'))
{
$logger->info('Successful login by ' . $username);
}
I get into a recursive madness (xdebug complaining after 10000 nested calls, or whatever high I set it to).
Is there a way to check if the user has just logged in, or if (s)he is using an active session?
Note: I'm using Symfony 2.2 (dev-master)
You have to use the security.interactive_login:
namespace Acme\UserBundle\Listener;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Security\Core\SecurityContext;
use Doctrine\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.1.x
// use Symfony\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.0.x
/**
* Custom login listener.
*/
class LoginListener
{
/** #var \Symfony\Component\Security\Core\SecurityContext */
private $securityContext;
/** #var \Doctrine\ORM\EntityManager */
private $em;
/**
* Constructor
*
* #param SecurityContext $securityContext
* #param Doctrine $doctrine
*/
public function __construct(SecurityContext $securityContext, Doctrine $doctrine)
{
$this->securityContext = $securityContext;
$this->em = $doctrine->getEntityManager();
}
/**
* Do the magic.
*
* #param Event $event
*/
public function onSecurityInteractiveLogin(Event $event)
{
if ($this->securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
// user has just logged in
}
if ($this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
// user has logged in using remember_me cookie
}
// do some other magic here
$user = $this->securityContext->getToken()->getUser();
// ...
}
}
From the documentation:
The security.interactive_login event is triggered after a user has
actively logged into your website. It is important to distinguish this
action from non-interactive authentication methods, such as:
authentication based on a "remember me" cookie.
authentication based on your session.
authentication using a HTTP basic or HTTP digest header.
You could listen on the security.interactive_login event, for example,
in order to give your user a welcome flash message every time they log
in.
The security.switch_user event is triggered every time you activate
the switch_user firewall listener.
http://symfony.com/doc/current/components/security/authentication.html#security-events

Separate Zend Application for control panel?

Shall I create a separate Zend Application for the user backend of a web application?
My main concern is that I have to have a separate Zend_Auth on both the public website (for clients to login) and for employees to manage the site.
Since it appears to me that I can't use multiple Zend_Auth instances in one application this would be the only solution.
The next concern would be that the two Zend_Auth sessions will collide since they run on the same webspace?
Cheers
Actually, Benjamin Cremer's solution won't work, because Zend_Auth_Admin extends a Singleton implementation, so its getInstance() would yield a Zend_Auth instance, not a Zend_Auth_Admin one.
I myself was confronted with this situation, and seeing that the ZF people (at least in ZF1) see authetication as a single entry-point in an application (they could've made it so that Zend_Auth could contain multiple instances, using LSB in php etc.), made a minor modification to Benjamin Cremer's code - you must also override the getInstance():
<?php
class AdminAuth extends Zend_Auth
{
/**
* #var AdminAuth
*/
static protected $_adminInstance;
/**
* #return Zend_Auth_Storage_Interface
*/
public function getStorage()
{
if (null === $this->_storage) {
$this->setStorage(new Zend_Auth_Storage_Session('Zend_Auth_Admin'));
}
return $this->_storage;
}
/**
* Singleton pattern implementation.
*
* #return AdminAuth
*/
public static function getInstance()
{
if (null === self::$_adminInstance) {
self::$_adminInstance = new self();
}
return self::$_adminInstance;
}
}
Zend_Auth implements the Singleton Pattern so there can only exist one instance of this class.
To distinguish whether the current identity is an admin or an user you could use an isAdmin-Flag, or even better implement the Zend_Acl_Role_Interface.
If it is really required by your application to have two Auth-Sessions at the same time (one for a User, on for an Admin) you could 'copy' the Zend_Auth class by extending it and adjust the session storage.
<?php
class Zend_Auth_Admin extends Zend_Auth
{
/**
* Returns the persistent storage handler
*
* Session storage is used by default unless a different storage adapter has been set.
*
* #return Zend_Auth_Storage_Interface
*/
public function getStorage()
{
if (null === $this->_storage) {
$namespace = 'Zend_Auth_Admin'; // default is 'Zend_Auth'
/**
* #see Zend_Auth_Storage_Session
*/
require_once 'Zend/Auth/Storage/Session.php';
$this->setStorage(new Zend_Auth_Storage_Session($namespace));
}
return $this->_storage;
}
}
So you can use two distinct Auth objects for your Session handling
Zend_Auth::getInstance(); // instance for users
Zend_Auth_Admin::getInstance(); // instance for admins

Categories