Zend Navigation and RBAC - zend-framework2

I am developing a ZF2 based site. I have a main navigation which stays same regardless of the visitor/user status. Need to add another component/nav, which will depend on the user's status and role. For a visitor the items will be
Register
Login
EN (Actually a drop-down, with other available language)
For a logged-in normal user, it will display
Profile
Logout
EN (Language selector as mentioned above)
And for some users with specific roles/permission there will be additional items
I want to use RBAC, as ACL seems bloated, and also just to check if the current logged in user/role has additional items, I need to load the complete ACL (and we got around 15+ different types of roles).
I spent some time thinking how I have achieve this, so following are some ideas I have.
I create an empty navigation container, and create a factory. In the factory, I access the Authentication and RBAC and add the pages depending on the the user's status/role.
I create a fully loaded navigation with all the possible pages, then in the factory, with the help of Authentication and RBAC I hide the pages I don't want to show.
rd option is to use a view helper which will get RBAC via ServiceLayer and generate the navigation. (As discussed in ZF2 how to display tweets in layout and ZF2 : Add a Login widget in the template.
Or I can create a controller-plugin or just a method in module.php, and listen to the MVC_Render or MVC_Dispatch event and generate the desired navigation and add the output to a view variable.
PS: I need to use a partial as I need to add some CSS class to the language selection section. Also the navigation will be displayed in the layout.

I am using ZfcRbac and I am doing it as the following, you can display the navigation based on user roles and the navigation items permission as the following:
First add a permission to your navigation item as the following:
'permission' => 'edit-profile',
Then attach a listener in the onBootstrap as the following:
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$eventManager->getSharedManager()->attach(
'Zend\View\Helper\Navigation\AbstractHelper',
'isAllowed',
array('\Application\Listener\RbacListener', 'accept')
);
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
}
Then create a class Application\Listener\RbacListener as the following:
public function accept(Event $event) {
$event->stopPropagation();
$accepted = true;
$serviceLocator = $event->getTarget()->getServiceLocator()->getServiceLocator();
$rbac = $serviceLocator->get('ZfcRbac\Service\Rbac');
$params = $event->getParams();
$page = $params['page'];
$permission = $page->getPermission();
if ($permission) {
$accepted = $rbac->isGranted($permission);
}
return $accepted;
}
and by this when you display the menu it will be filtered based on the permission and roles, for example if you do echo $this->navigation('navigation')->menu() then only the menu items that the user has permission on will be displayed.

Related

Prevent logged user from editing data which does not belong to him

I have Members in my Umbraco 7 website. Each member can login to the front-end site and edit pages which only the member can edit. I use UmbracoIdentity for managing Members.
I have a custom property Page (MNTP) on my Member doc Type which specify which pages a Member can edit. On user profile page I show a list with the alloed pages for edit from this custom property like this:
var pages = profileModel.MemberProperties.FirstOrDefault(p => p.Alias == "pages").Value;
foreach (var item in pages.Split(','))
{
DetailsPage obj = Umbraco.TypedContent(item) as DetailsPage;
#obj.H1Title
}
When a member clicks on the link (domain.com/edit-page?pageId=3242) I have action method which loads data for the page:
#Html.Action("GetPageDetails", "Edit", new { id = pageId })
What I want to prevent is if a Member change pageId query string manualy and his custom property Pages does not contain this pageId to redirect him to user profile page with the list with his allowed pages.
I need an idea how to restrict logged in member to edit only the pages that are assigned in the property Pages.
I have successfully added the pages IDs in Claims and I can perform the check in each method. But my methods are a lot. Is there some best practice or a solution which does not require to go over all methods.
Thanks

Enforcing a choice prior to viewing MVC and Web Forms pages

I'm working on a system that needs to know a user's choice before they enter a site. Up till now the choice has been stored in a cookie and checked by JavaScript on the page load - if the cookie doesn't exist then a dialog is shown and the user makes the choice.
Although we'd normally expect the user to arrive at the homepage of the application, they can legally follow a URL to any page within the application, so the JavaScript to check the choice exists on every page.
This has caused problems (almost always fixed by clearing cookies) so we're switching to store the choice in the database. What we need is a neat way of making sure that all pages (MVC and Web Forms) check that the choice has been made and, if it hasn't, either display a dialog or redirect to a page where the choice can be made.
The main thing bothering me is that to cause a redirect using MVC, I need to return a RedirectResult, and this can only be done from an Action. I don't want every action to have code regarding this check - it seems like the kind of thing that should be possible from a base controller (in the same way a base page could cause a Response.Redirect.
Can anyone suggest a good way for all pages to perform a check on the database and then either cause a redirect or show a dialog?
The main thing bothering me is that to cause a redirect using MVC, I
need to return a RedirectResult, and this can only be done from an
Action.
Oh not at all. You could also redirect from a custom action filters.
For example you could write a custom IAuthorizationFilter that will check whether the user made the necessary choice and if not redirect to some given page. The check could be done against a cookie, database or wherever you decide to persist this information:
public class EnsureChoiceHasBeenMadeAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
// get the current user
var user = filterContext.HttpContext.User;
if (user.Identity.IsAuthenticated && !UserMadeAChoice(user.Identity.Name))
{
// if the current user is authenticated and he didn't made a choice
// redirect him to some page without even attempting to execute
// the controller action that he requested
var values = new RouteValueDictionary(new
{
controller = "home",
action = "index"
});
filterContext.Result = new RedirectToRouteResult(values);
}
}
private bool UserMadeAChoice(string username)
{
throw new NotImplementedException();
}
}
Now you have different possibilities:
You decorate the controllers/actions that you want to perform this check with the [EnsureChoiceHasBeenMade] attribute
You register the action filter as a global action filter so that it applies to absolutely all actions
You write a custom filter provider in order to dynamically apply the action filter to some actions based on some dynamic values (you have access to the HttpContext).

manage 2 different Log In sections

I'm creating a site with 2 different sections (main site and admin) and both of them need authentication.
I have the main section already created and it works fine using FormsAuthentication.
now, how do I go about creating the admin section? Can I use FormsAuthentication again?
thanks
user - yes you can.
what you need to do is to create roles (such as webuser and admin) and assign the user to the appropriate role as required (you can do this either when setting the user up initially or later as an edit on their profile). anyway, getting back to the question. inside your controller, you'd then investigate the roles that existed for that logged in user and this would determine which controller actions they had access to as well as determining which view to present, should the action be 'shared' between roles.
within the controller, you can decorate the action with the following code:
[Authorize(Roles="admin")]
public ActionResult IndexAdminOnly() // you'd never have an action named this - purely to make the point
{
// your logic here
}
conversely, you could do it inside the controller:
[Authorize]
public ActionResult Index()
{
if(Roles.IsUserInRole("admin")){
// your admin logic here
}
if(Roles.IsUserInRole("webuser")){
// your webuser logic here
}
}
this is it at it's very simplest. hopefully you can google a few more links to get you over any issues that arise once you get going, or drop a note here.

Issues in trying to combine 'View' and 'Edit' in a single view in ASP.NET-MVC

I have an image gallery which has the following route:
// gallery id
routes.MapRoute(
"gallery-route",
"gallery/{galleryID}/{imageID}/{title}",
new { controller = "Gallery", action = "Index", galleryID = (string)null, imageID = (string) null, title = (string) null},
new { galleryID = #"\d+" }
);
I can have URLS like :
example.com/gallery/4/23 - shows
gallery 4 and image 23
example.com/gallery/4 - shows
gallery 4 and first image in that
gallery
I was trying to make an 'edit in place' mode which lets an administrator edit the images and running into several issues. Currently the editing functionality is non-AJAX.
1) How should i keep a 'sticky' edit mode parameter. There won't be an 'edit' button next to each image. i want the edit mode to be 'sticky', but then I'm finding I either need to set it in session or add a parameter to every single link on the page which is clumsy.
2) I have caching enabled for this view. Therefore if i make a change and refresh - the original cached view remains.
Can anyone give me any thoughts?
why not change the output on the view depending on the users authorisation status. Using inline code and Html helper functions in the ascx to either write out the values in HTML for readonly roles and for editor roles add a post form around input controls with the current values in. Then on the controller handle post in a separate procedure to save the edits.
or simply add an edit view ascx as well as a read view ascx.
Also when the post controller procedure fires replace the cache object with the new data recorded in the post.
finally of you have image caching problems when administering the gallery. Try adding a random string to the query eg:
function GetNewUrl(url)
{
Random rnd = new Random();
return url +"?"+rnd.Next(1000).ToString();
}
You need to flush the cache for the page when a change is made. Don't show the cached page or cache the page when the user is logged in as administrator, as they will have a different view with edit controls etc. For a sticky mode where an administrator can choose to be in edit mode throughout the site this would have to be stored in the session. I use something based on this for caching controller actions, with an additional method to determine whether to cache the output/use the cached output.

How can I create 1 route with 2 differents user's access in ASP.NET MVC?

How can I do this: I have on page named "Schedule" and it can be accessed through 2 differente ways:
URL 1- www.bla.com/Admin/Schedule
URL 2- www.bla.com/Schedule
"URL 1" will be accessed by users with Admin previlegies and this View will show some Admin stuff, and users must be LoggedOn.
In the otherhand, "URL 2" will be accessed by users NOT LoggedOn and it will NOT show the admin stuff.
But, they are the same page, just with some differences depending on user's access.
I already have AdminController and I intend to put this "Schedule" View as part of this controller. As result, I know if I type "URL 1" it will work. But, if I type "URL 2"? Will I have to create a "ScheduleController" just to handle this?
I wonder if there is a way to resolve this by Global.asax, configuring the routing... I don't know...
Thanks!!!
You can map the /Schedule route to the /Admin/Schedule action from the Global.asax.cs like this:
routes.MapRoute(
"Schedule",
"schedule",
new { controller = "Admin", action = "Schedule" }
);
This will solve your immediate problem of wanting two separate routes resulting in the same action/view.
However, this will not solve your scenario properly. The main issue is that the identity of the logged on user is orthogonal to the route the request takes. In other words, you can't force the admin user to always hit the /Admin/Schedule route, they could just as well hit the /Schedule route and still would expect the same end result. Not only that, but doing it this way will prevent you from using the [Authorize] attribute on the Admin controller or the action to force the user to login and will have to implement custom logic checking which route the action was hit through and decide whether you want to force login or let the user through.
Thus, you have to make a decision:
you share the controller, action and the view and determine whether to show the additional information in the view based on the identity and the role membership of the logged on user (if any). You will have to change the name of the controller then, as /Admin will not reflect the new role this class has;
you share only the view and have two separate controllers and actions - Admin.Schedule and User.Schedule. You will have to put the view in the /views/shared folder and return the same view fromboth actions, potentially passing different model. You'll end up with two routes - /Admin/Schedule and /User/Schedule;
you have two separate controllers, actions and views.
In all three cases, you can still have the rule above pointing to the appropriate controller, if you want to have also the shortest /Schedule route.
Make the View shared and just render it from both controller actions. Pass the appropriate data via the model (or ViewData) so the View knows not to render the admin stuff when rendered from the non-admin controller action.
And, yes, create the Schedule controller. Make the routing simple and handle sharing the generation code on the back end.
AdminController
public ActionResult Schedule( ... )
{
Schedule sched = ... get model ...
return View("Schedule", new SchedViewModel {
Schedule = sched,
Admin = true
} );
}
ScheduleController
public ActionResult Index( ... )
{
Schedule sched = ... get model ...
return View("Schedule", new SchedViewModel {
Schedule = sched,
Admin = false
} ); }
Sounds like you don't really need a different URL if it's the same page. However if for some reason you still want to use 2 different URLs...
Url 1:
routes.MapRoute("ScheduleAdmin", "Admin/Schedule",
new
{
controller = "AdminController",
action = "Schedule"
});
Url 2:
routes.MapRoute("Schedule", "Schedule",
new
{
controller = "ScheduleController",
action = "Index"
});
You didn't make it clear what action you were using for the schedule controller so feel free to change that.

Resources