Zend_Acl, How to check a user with multiple roles for resource access - zend-framework

i am implementing RBAC for my app, and everything is managed from database.
for example i am storing all resources/permissions in a table called permission , all roles in role table, and another table called role_permission to define which role have access to which resources/permissions.
the purpose for going with this approach is because i want the administrator of the app to create the role and assign the permission to role by himself.
User of the app can have multiple roles for example administrator, supervisor, player, referee etc.
I created a model class for Zend_Acl to add roles and resources and assign permission to it.
Below is what i did.
foreach($this->_roles as $role) {
$this->addRole(new Zend_Acl_Role($role['id']));
}
foreach($this->_permissions as $permmission) {
$this->addResource(new Zend_Acl_Resource($permmission['id']));
}
foreach($this->_rolePermissions as $value) {
$this->allow($value['role_id'], $value['permmission_id']);
}
$this->allow($this->_roleAdmin);
it works fine if i want to check wether a permission exist for a particular role for example by using this code.
echo $acl->isAllowed($role, $permission) ? 'allowed' : 'denied';
however i want to check with multiple roles wether the current permission exist for a user with multiple roles.
how am i supposed to check wether the user with multiple roles such as referee, supervisor has the access to resource create report. with isAllowed() you can only check for permission for only 1 role.

The approach I usually take is to create a class that extends Zend_Acl, and extend the isAllowed() function so it can take my user object as a parameter instead. It then loops through that user's roles performing the check for each one. E.g.:
public function isAllowed($roleOrUser = null, $resource = null, $privilege = null)
{
if ($roleOrUser instanceof Users_Model_User) {
// check each of that user's roles
foreach ($roleOrUser->roles as $role) {
if (parent::isAllowed($role, $resource, $privilege)) {
return true;
}
}
return false;
} else {
return parent::isAllowed($roleOrUser, $resource, $privilege);
}
}

Related

How to use custom Roles using Windows Authentication in MVC Application

I am developing an internal MVC Application using Windows Authentication (WA). Authenticating users using WA is straight forward, however; with respect to user Roles, I have the following requirements:
We will use custom Roles ignoring the AD Roles. For example, a user
may have a 'Manager' role in the AD but his app role is set to
'Supervisor'. After the User is authenticated, the system will fetch
the user roles and set the CurrentPrincipal accordingly.
For the above, I plan to have 3 tables including User, Role
and UserRole. The Role table has the custom roles while the
User table consists of company users. The UserRole table will define
the mapping between User and their Role(s). The issue I see with this approach
is to pre-populate all 3 tables. The User table must have the list of all
company employees and is maintained for new/inactive employees. The UserRole
table should be set with each user and his role(s) before he logs in.
In the application, User are assigned to different tasks (for example John is
supervising Vehicles) plus we need to maintain user activity logs. Assuming
the above two points are valid, is it OK to use the ID field in the User
table for this purpose?
There is also a chance that later, we may deploy the application
over the public domain. In such a case, how can we use the existing
User/Role infrastructure for this purpose.
Thanks in advance.
You are in exactly the same boat as me, my friend! I managed to do this through a Custom Authorization Attribute. Here are a couple of points that I have stumbled on through this process.
I did not create my own user table. You can, but you can query AD for users depending on the amount of users on your domain and link it to your Roles / Activities tables using the Guid to search. If you do create a users table that mirrors AD, use the Guid for the user. This way, if the login/name/anything else changes, the Guid stays the same.
Custom authorization attribute:
namespace YourSite.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public class AuthRoleAttribute : ActionFilterAttribute
{
public string RoleName { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!ViewIsAuthorizedActivity(RoleName)) // create a bool function to see if your user is in this role... check your db or whatever
{
string requestMethod = filterContext.HttpContext.Request.HttpMethod;
if (requestMethod == "GET")// I chose two different means of forbidding a user... this just hides the part of the page based on the #if(ViewBag.Authorization = "FORBIDDEN") where you render a partial, else show the view
{
filterContext.Controller.ViewBag.Authorization = "FORBIDDEN";
}
else if (requestMethod == "POST") // This prevents over posting by redirecting them completely away from that controller... also prevents them from posting if they have the page loaded and you remove permission
{ // Hiding a part of the page doesn't matter for the POST if the page is already loaded
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Home" },
{ "action", "Forbidden" },
{ "area", ""}
});
}
base.OnActionExecuting(filterContext);
}
}
}
}
How GETs are handled in the view:
#if (ViewBag.Authorization == "FORBIDDEN")
{
ViewBag.Title = "Forbidden!";
#Html.Partial("~/Views/Forbidden.cshtml");
}
else
<!-- User is not forbidden and have the view here -->
Note that for the POSTs the user is redirected away from the controller to the Forbidden controller.
Attribute on controller:
[AuthRole(RoleName = "Admin")]
public ActionResult YourController()
I also made a extension to the User so things may be hidden in the view if they don't have permission:
public static bool IsAuthorized(this IPrincipal user, string roleName)
{
return Attributes.AuthActivityAttribute.ViewIsAuthorizedByRole(roleName); // function determining if the user is in that role, therefore show what you want to show in the view or don't show it if false
}
Which is called by:
#if (User.IsAuthorized("Admin"))
{
<!-- show something, a link, etc. -->
}
Hopefully this gives you a better head start than I had. Let me know if you have questions.

Issues with CurrentUserPropertyBinder it cannot always remember user

I have implemented a CurrentUserPropertyBinder (see below) for a web application using FubuMVC.
public class CurrentUserPropertyBinder : IPropertyBinder
{
private readonly Database _database;
private readonly ISecurityContext _security;
public CurrentUserPropertyBinder(Database database, ISecurityContext security)
{
_database = database;
_security = security;
}
public bool Matches(PropertyInfo property)
{
return property.PropertyType == typeof(User)
&& property.Name == "CurrentUser";
}
public void Bind(PropertyInfo property, IBindingContext context)
{
var currentUser = //check database passing the username to get further user details using _security.CurrentIdentity.Name
property.SetValue(context.Object, currentUser, null);
}
}
When I login to my site, this works fine. The CurrentUserPropertyBinder has all the information it requires to perform the task (i.e. _security.CurrentIdentity.Name has the correct User details in it)
When I try and import a file using fineUploader (http://fineuploader.com/) which opens the standard fileDialog the _security.CurrentIdentity.Name is empty.
It doesn't seem to remember who the user was, I have no idea why. It works for all my other routes but then I import a file it will not remember the user.
Please help! Thanks in Advance
NOTE: We are using FubuMVC.Authentication to authenticate the users
I'm guessing your action for this is excluded from authentication; perhaps it's an AJAX-only endpoint/action. Without seeing what that action looks like, I think you can get away with a simple fix for this, if you've updated FubuMVC.Authentication in the past 3 months or so.
You need to enable pass-through authentication for this action. Out of the box, FubuMVC.Auth only wires up the IPrincipal for actions that require authentication. If you want access to that information from other actions, you have to enable the pass-through filter. Here are some quick ways to do that.
Adorn your endpoint/controller class, this specific action method, or the input model for this action with the [PassThroughAuthentication] attribute to opt-in to pass-through auth.
[PassThroughAuthentication]
public AjaxContinuation post_upload_file(UploadInputModel input) { ... }
or
[PassThroughAuthentication]
public class UploadInputModel { ... }
Alter the AuthenticationSettings to match the action call for pass-through in your FubuRegistry during bootstrap.
...
AlterSettings<AuthenticationSettings>(x => {
// Persistent cookie lasts 3 days ("remember me").
x.ExpireInMinutes = 4320;
// Many ways to filter here.
x.PassThroughChains.InputTypeIs<UploadInputModel>();
});
Check /_fubu/endpoints to ensure that the chain with your action call has the pass-through or authentication filter applied.

Updating Zend_Auth storage after editing a user

I use Zend_Auth to authenticate users and then store their details in the default Zend_Auth session. This means that when a user edits his details, these changes won't be reflected in the application until he re-authenticates.
I want to avoid this problem as so:
When the user logs in we only store his user ID in a Zend_Auth session
On each request we fetch the user's details from the database in a preDispatch() hook, using the user ID which was stored upon login in the Zend_Auth session:
class Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
if ($auth->hasIdentity())
{
$id = $auth->getIdentity()->id;
$userModel = new Model_User();
$user = $userModel->fetchOne($id);
// Where do I store this user object ???
}
}
}
The problem is: where do i store this User object? I think we shouldn't use sessions for this, since the goal of sessions is to persist data. There's no need for persistence though, since we re-fetch the data from the database on each request. Only the user ID must be persistent. Would storing the User object in Zend_Registry be an option here?
I think example would be the best to explain how to write new auth details to Zend_Auth storage object:
$userDetails = array('foo' => 'bar');
$storage = new Zend_Auth_Storage_Session();
// set sorage for Zend_Auth
Zend_Auth::getInstance()->setStorage($storage);
// write data to the storage
Zend_Auth::getInstance()->getStorage()->write($userDetails);
// read data from storage
var_dump(Zend_Auth::getInstance()->getStorage()->read());
// edit user's data
$userDetails = array('foo' => 'bar', 'foo', 'bar');
// write new data to storage
Zend_Auth::getInstance()->getStorage()->write($userDetails);
// read new written data from storage
var_dump(Zend_Auth::getInstance()->getStorage()->read());
I think this explains how to set Zend_Auth storage, and change it later.
Use Zend_Session_Namespace to store the object. It can be as temporary or permanent as you wish to make it.
Zend_Auth already uses this in the background as it's default storage mechanism using the namespace of Zend_Auth.
class Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
$session = new Zend_Session_Namespace('user');//instantiate session namespace
if ($auth->hasIdentity())
{
$id = $auth->getIdentity()->id;
$userModel = new Model_User();
$user = $userModel->fetchOne($id);
$session->user = $user;//store the object can be recalled anywhere
}
}
}
Of course Zend_Registry will work as well and as always the choice is yours. You may even find it appropriate to build this functionality into your auth adapter.
I believe using Zend_Registry is fine in your case.

Admin section in ZendFramework application

I have an application at the moment using Zend_Auth for user access. The site has an admin section where I want one user who has the role of admin in my database to be allowed access when he uses his credentials. Is Zend_Acl the only way to do this? As it seems a little complex for what I want to do or would there be any easier solutions to my problem?
I have had a think about this and I am now wondering if it is possible to have two auth controllers one for users and one for my admin section?
I did something like this recently. Create a front-controller plugin for the admin module that checks the user credential. Something like:
class Admin_Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
if ($request->getModuleName() != 'admin'){
return;
}
$auth = Zend_Auth::getInstance();
if (!$auth->hasIdentity()){
// send him to login
}
$user = $auth->getIdentity();
if (!$user->isAdmin()){ // or however you check
// send him to a fail page
}
}
}
I decided to go with the method of having a field of "is_admin" in my database if its set to 1 the user is an admin. I then use this code:
public function init()
{
$this->auth=Zend_Auth::getInstance();
if ($this->auth->getStorage()->read()->is_admin) {
$route = array('controller'=>'admin', 'action'=>'index');
} else {
$route = array('controller'=>'index', 'action'=>'index');
$this->_helper->redirector->gotoRoute($route);
}
}
This redirects the user from the admin area if they are not an admin and allows them access if they are an admin.. A lot easier to implement than ACL for the simple use in my application.

In Zend, is there a better way to check if a user hasIdentity()?

Currently, I am using Zend_Auth::getInstance()->hasIdentity() to check if my user is logged in in every controller that requires a login. I feel like I am practicing Zend horribly, so I wanted to ask the more experienced and know if this is the proper way to do this? If not, could you please tell me what is?
We use a Controller plugin (bootstrapped in application.ini config file) that handles our authentications. It checks the requested controller/action in the preDispatch() phase and matches against ACL objects (could be fetched out of DB, config files, XML, etc.). If the user does not have the privilege to access the target controller/action, the a message is stored in the session and user is redirected to another page, displaying the access forbidden message.
If the user needs to have authentication to access the target controller/action, user is redirected to the login action by modifying the request object.
Using this plugin there is no need to check for user authentication/ACL in each controller and so all the "Access" code would be enclosed in one file, the "Access Plugin".
In order to check for user identity we mostly use the same method of "Zend_Auth::getInstance()->hasIdenity()", but this just shows if the user is authenticated or not. the '''getIdentity()''' method of Zend_Auth returns the current user identity, but again just the identity and not more. However if you would need more information of the user, you could store the user information in a session.
We implement our users as data models, so each user is defined as an object. after a user is authenticated on the login action, we create the appropriate user object and store it in the user session, like this:
// This could be a sample code in AuthController/processloginAction()
// suppose $username is validated before and stores the username
$user = new Default_Model_User($username);
// now $user is our user object, suppose $log is a Zend_Log instance
$log->info("user id '{$user->getId()}' username: '{$user->getUsername()}' logged in");
$sess = Zend_Session_Namespace('auth');
$sess->user = $user;
From now one, the $user property of the session namespace of 'auth' is the user object with all the information you would need about, not just the identity. and whenever you wanted to check if user is logged in (beside using Zend_Auth) you could check for availability of this value on the user session:
$sess = Zend_Session_Namespace('auth');
if (!isset($sess->user) || !$sess->user) {
// user is not logged in, redirect to login page
}
$user = $sess->user;
/*#var $user Default_Model_User*/
$email = $user->getEmail();
now we checked for authentication, and have access to user information (email, phone, etc.).
I use a method similar to the method described by Herman Radtke in his blog at http://www.hermanradtke.com/blog/more-reliable-authentication-in-zend-framework/. Basically create a controller plugin as farzad mentioned:
class My_Authentication extends Zend_Controller_Plugin_Abstract
{
private $_whitelist;
public function __construct()
{
$this->_whitelist = array(
'index/login'
);
}
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
$controller = strtolower($request->getControllerName());
$action = strtolower($request->getActionName());
$route = $controller . '/' . $action;
if (in_array($route, $this->_whitelist)) {
return;
}
$auth = Zend_Auth::getInstance();
if ($auth->hasIdentity()) {
return;
}
self::setDispatched(false);
// handle unauthorized request...
}
}
and then register that plugin in your bootstrap:
public function run() {
$front->registerPlugin(new My_Authentication());
}
I generally take this approach a little farther and integrate the Zend_Acl into the system as well. To do that I would define the plugin below:
class My_Acl_Authentication extends Zend_Controller_Plugin_Abstract
{
private $_acl;
public function __construct($acl)
{
$this->_acl = $acl
}
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
$controller = strtolower($request->getControllerName());
$action = strtolower($request->getActionName());
$route = $controller . '/' . $action;
if (in_array($route, $this->_whitelist)) {
return;
}
$auth = Zend_Auth::getInstance();
$role = 'anonymous';
if ($auth->hasIdentity()) {
$role = $auth->getStorage->read()->role;
}
if ($this->_acl->isAllowed($role, $route)) {
return;
}
self::setDispatched(false);
// handle unauthorized request...
}
}
If you go this route there is some more work to be done, specifically you have to setup the ACL and then you also have to store the user's role in the auth storage.
Thats perfectly ok to do so but to save you from repeating that code you can extend all your controllers from a class A which is subclass of Zend_Controller_Action . Then inside this class declare a method
protected function hasIdentity()
{
return Zend_Auth::getInstance()->hasIdentity();
}
Now in your controller which is subclass of A you can simply do $this->hasIdentity(); instead