views:

143

answers:

2

Hey All,

I'm contemplating the best way to implement the conditional permissions i.e. Users and Teams are m-to-m. But each Team also has a 1-to-m "Leader" relationship with the User table as well.

For simplicity sake, let's say we have two permission levels, "User" and "Admin". Then lets say, only certain Team administration task i.e. group emails, can only be sent by that team's leader.

At the moment, every action that is specific to team leaders queries the database to check it the current user is the team leader (keeping in mind a user may lead many Teams). I personally don't like seeing "if($user->isLeader($team))" all over the place.

I have considered setting a list of teams lead by a user in the user session on login (ala phpBB) or using a symfony filter to do the same.

However, in the first approach, the data can become stale in the case where another user may change a team's leader. The second approach requires an extra database query on every page load.

Any better ideas? note: there are multiple app in the one project that need to share the same permission model (i.e. backend and api)

A: 

I have achieved something similar by over-riding the hasCredential method in myUser to execute custom checks when a certain credential is required.

eg:

public function hasCredential($credential, $useAnd = true) {

  // make sure the symfony core always passes the team leader permission, we handle it later ourselves
  $this->addCredential('team_leader');
  $ret = parent::hasCredential($credential, $useAnd);

  // if other checks have failed, return false now, no point continuing
  if (!$ret) return false;

  if ($credential == 'team_leader' || (is_array($credential) && in_array('team_leader', $credential))) {
    // do stuff here. in this example, we get the object from a route and check the user is a leader
    $route = sfcontext::getinstance()->getRequest()->getAttribute('sf_route');
    if (!$route instanceof sfObjectRoute) {
      throw new sfConfigurationException(sprintf('team_leader credential cannot be used against routes of class %s - must be sfObjectRoute', get_class($route)));
    }
    return $this->isLeader($route->getObject());
  }
  return $ret;
}

You can then add the "team_leader" credential to security.yml like you would any other.

Obviously this depends on you using sfObjectRoutes, so may not be that suitable if you can't do this and can't adapt what you are using, but I think its a nice solution when you can use it!

If you are worried about extra queries, you could look at wrapping some caching around the isLeader calls.

benlumley
A: 

I see 2 options:

Override sfDoctrineGuardUser::getGuardUser()

Override the getGuardUser method of sfDoctrineGuardUser to fetch the Teams when it get's the user. This requires an additional left join, but it saves you later queries.

/**
* Overridden getGuardUser to automatically fetch Teams with User
* @see plugins/sfDoctrineGuardPlugin/lib/user/sfGuardSecurityUser#getGuardUser()
*/
public function getGuardUser()
{
  if (!$this->user && $id = $this->getAttribute('user_id', null, 'sfGuardSecurityUser'))
  {
    $this->user = sfGuardUserTable::findUserByIdWithTeams($id); //write this query to fetch a user with his teams

    if (!$this->user)
    {
      // the user does not exist anymore in the database
      $this->signOut();

      throw new sfException('The user does not exist anymore in the database.');
    }
  }

  return $this->user;
}

You can then add the user's credentials based on his teams via a filter or by overriding hasCredential.

Invalidate cached team statuses

If you don't want any additional queries, the only additional option I see is to use a global cache. This would involve the following:

  • Cache the user's teams or team based credentials on sign on
  • When a team leader is removed or added, do something like apc_add(team_invalid_[team_id], true, [length_of_session])
  • Whenever the user's credentials are added (via one of the methods described in the first option), check the cache to see if they are invalid.
jeremy
This is very close to what I am after.I will likely implement a similar solution using memcache.
xzyfer