Zend_Acl rules based on both MVC and Propel models - php

I am having a web project where some access decisions are dependant on the page itself (e.g. /logout which shall only be visible to logged in users) and some are dependant on dynamic model objects (e.g. /article/delete/1234 where we have to check if 1234 was written by the logged in user or if he is an admin).
Now, I am facing the problem of how to bring both things together. No matter how I tried, I cannot rely on any of the two alone:
Some pages do not use any models, so I cannot setup any model rules there
On the other hand, I cannot create dynamic assertions for a modular approach, because Comment is just a comment and not a default/comment. A Comment is not restricted to the default module, it may also be used in the admin module.
With modular ACL I am trying to check for each page if a user is allowed to visit it, e.g.
if (!$acl->isAllowed($user, 'default/secrets', 'mysecrets')) {
$this->forward('auth', 'login');
$this->setDispatched(false);
}
And with dynamic assertions I am checking if somebody is allowed to edit a specific model object.
// $comment has a method getResourceId() returning 'comment'
if ($acl->isAllowed($user, $comment, 'delete')) {
// display a link for deletion
}
Of course it would be nice if the check for
deletion of a specific comment, and
accessing the /comment/delete/???? page
would be the same, but I guess this is not possible and I would have to create two rules:
$acl->allow('member', 'default/comment', 'delete');
$acl->allow('member', 'comment', 'delete', new Acl_Assert_CommentAuthor());
$acl->allow('admin', 'comment', 'delete');
Now, this seems not perfect to me as this can lead to duplicate work in some cases.
Is there some better method to approach this problem? Or is the only method to at least create a coherent naming scheme like: mvc:default/comment, model:comment

The way i did it, custom sql queries that restrict results, functions that check before insert/delete/modify sql
and then an ACL plugin i wrote that checks permission and uses these 5 tables
acl_groups
acl_permession
acl_permession_groups
acl_permession_risource
acl_risource
Code:
class Abc_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
{
/**
* Return whether a given request (module-controller-action) exists
*
* #param Zend_Controller_Request_Abstract $request Request to check
* #return boolean Whether the action exists
*/
private function _actionExists(Zend_Controller_Request_Abstract $request)
{
$dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();
// Check controller
if (!$dispatcher->isDispatchable($request)) {
return false;
}
$class = $dispatcher->loadClass($dispatcher->getControllerClass($request));
$method = $dispatcher->formatActionName($request->getActionName());
return is_callable(array($class, $method));
}
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
// fetch the current user
$controller = $request->controller;
$action = $request->action;
$logger = Zend_Registry::get('log');
//prima controlla se sei autenticato e poi controlla se l'azione esiste, cosi non esponi il sito
$auth = Zend_Auth::getInstance(); /* #var $auth Zend_Auth */
if($auth->hasIdentity()) {
if(! $this->_actionExists($request))//l'azione non esiste?
{
$request->setControllerName('error');
$request->setActionName('pagenotfound');
$logger->notice( " IP: ". $_SERVER['REMOTE_ADDR']. " http://".$_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"]. " ?" .http_build_query($_REQUEST));
return ;
}
$allowed = Abc_Action_Helper_CheckPermission::checkPermission($controller, $action);
if ($allowed !== 1)
{
$request->setControllerName('error');
$request->setActionName('noauth');
}
//fine azione esiste
}else{
$request->setControllerName('user');
$request->setActionName('login');
return ;
}
}//fine preDispatch
}
You can then add your code(which i ommited for shortness) to remember the request and redirect you there after login.

Related

Symfony 5 - Can I use voter with another user than the current logged one?

Usually I use a voter on the user who is connected (me) to see if I can do an action or not.
if ($this->isGranted('TRAVEL', $city)) {
$this->goToCity($this->getUser(), $city);
}
But this time I would like to use my voter on a user that I will search in the database, to check if this user can or can't do this same action.
$another_user = $doctrineStuff->getRepo(Foo:bar)->getUser(123);
if ($this->isGranted('TRAVEL',$city, $another_user)) {
$this->goToCity($another_user, $city);
}
Is this possible?
How to go about it, I can't find anything about this problem. Maybe except simulating a home-made voter that doesn't check a user from his token but from his instance.
The basic idea is to create your own security token for the user and then use the AccessDecisionManager::decide method instead of the AuthorizationChecker::isGranted method.
class SomeController {
public function someAction(
AccessDecisionManagerInterface $adm,
EntityManagerInterface $em)
{
$user = $em->find(User::class,123);
$token = new UsernamePasswordToken($user,'firewall',$user->getRoles());
if ($adm->decide($token,['TRAVEL'],$city) {
whatever;
Don't worry about the firewall name. It won't be used by the voter system unless you have a very unusual voter. It's used in this example because the TokenInterface is surprisingly complex (13 methods!) so it easiest to use an existing implementation. You could refine things a bit by making your own token class:
class UserToken extends AbstractToken {
public function __construct(User $user) {
parent::__construct($user->getRoles());
$this->setUser($user);
It's also worth noting that the suggestion to directly use the access decision manager comes from the isGranted code:
# AuthorizationChecker
final public function isGranted(mixed $attribute, mixed $subject = null): bool
{
$token = $this->tokenStorage->getToken();
if (!$token || !$token->getUser()) {
$token = new NullToken();
}
return $this->accessDecisionManager->decide($token, [$attribute], $subject);
}
}
As time goes by you might want to keep an eye on any tweaks here. Tweaking the Security component seems to be a bit of a hobby for some of the core Symfony developers. But I don't expect any breaking changes.

Module view not found

I'm experiencing a problem using the HMVC implementation of Codeigniter, with modules.
I created an authentication module, called autenticacion, which, for obvious reasons, in order to check session validity and expiration, is always used within the MX_Controller, this way:
Modules::run('autenticacion/logout', $user, true);
or
Modules::run('autenticacion/update', $user);
depending on certain conditions.
Ever since I added this athentication implementation, the normal access to other modules was broken.
Now if I try to, for instance, access
www.domain.com/items
having that I coded this module files:
modules/items/controllers/Items.php (this extends MX_Controller)
, and the view:
modules/items/views/items_view.php
The view won't be found despite it's loaded within the Items controller.
If I print out $this in the Items constructor, the MY_Loader instance displays this property:
[_module:protected] => autenticacion
I understand this means the items module is not being loaded within the loader, despite I can reach its controller. The other module (autenticacion) seems that is messing it all.
How could I fix this?
EDIT:
This is what I changed in MX_Controller in order to handle session checks and updates:
<?
class MX_Controller
{
public $autoload = array();
public function __construct()
{
// Validación de la autenticación/caducidad de la sesión
$this->_validar_sesion();
// Original actions
//....
}
//... method __get()
/**
* Checks whether the user has not yet created a valid authenticated user session, or if it has expired.
* In both cases, it redirects to the authentication page, deleting the expired session if it was already created.
*/
private function _validar_sesion()
{
if (stristr(get_class($this), 'autenticacion')) return;
require_once APPPATH . 'third_party/usuarios/Usuario.php';
$this->load->model('autenticacion/autenticacion_model');
// Get user instance from session
$usuario = unserialize($this->session->userdata('usuario'));
// No authenticated session yet: redirecting to the authentication page
if (!stristr(current_url(), 'autenticacion') && ! $this->session->logged_in) {
$this->load->library('uri');
$uri = $this->uri->uri_string();
redirect(base_url() . 'autenticacion' . ($uri ? '?redirect=' . $uri : ''));
}
// There is already an authenticated session, and the request is not coming from the authentication page
elseif (!stristr(current_url(), 'autenticacion')) {
// Check session expiration, in which case, we do logout
if ($this->autenticacion_model->sesion_caducada($usuario, config_item('sess_companyname_expiration'))) {
// This will delete the session from DB, destroy it, and redirect to the authentication page
Modules::run('autenticacion/logout', $usuario, true);
}
// Session has not expired yet: we update the session timestamp in DB to extend the expiration time
else {
Modules::run('autenticacion/update', $usuario);
}
}
}
}

Restrictions Within MVC Service Layer

I have recently dived into OOP & PHP MVC Application Design. At the moment I am learning a lot but I have one thing that is currently bugging me.
I read and now understand why it isn't wise to place http redirects within a service layer. We do not know what the controller will need to do once the service is complete, etc. etc. I also read that the service should not do anything outside of its purpose. Example: User Registration should only create a new user, using input passed by controller, but I am wondering if it is also fine to set flash messages within the service layer.
My application displays a lot of flash messages session based notifications for users. All of them are based on service related input validation checks, and produce alerts similar to the following
The username xxxxxx is already in use
Usernames Should be > 5 Characters
Should/can this be defined/set within the service class or is there something wrong with that? I have a Alert Helper function that handles setting the alerts. I can easily use my dependency injector to make it available I am just wondering if there is an issue with doing that.
I made the mistake of implementing all redirects within the services and I just finished removing all of them and placing them in the controllers, I don't want to make the same time consuming mistake so I am looking for advice here.
Thank you in advance for the help.
EDIT - CODE EXAMPLE
<?php
/**
*-----------------------------------------------------------------
*
* LOGIN CONTROLLER
*
*/
namespace Controller\www;
use \Helper\Controller;
class Login extends Controller {
public $dependencies = ['arena', 'login', 'site'];
/**
* Login
*
* Login Runs Through Various Checks Including If User is Banned, Account is Locked,
* or Forgot Password Request Is Active. Then the Entered Password is Matched & if Valid
* User is Logged In
*/
public function index() {
// Define Default
$username = '';
/**
* User Login
*
* If Successful, Login User, Redirect Home
* Else Set Error Alerts
*/
if ($this->form->post('login')) {
// Define and Sanitize Post Data
$username = $this->input->get('username');
$password = $this->input->get('password');
// Login Service Layer
$login = $this->factory->make('user/login');
// If Successful Redirect Home - Else Set Errors
if ($login->user($username, $password) === true) {
$this->redirect->home();
}
$this->alert->error($login->get('errors'));
}
/**
* Define Site Title & Display Page
*/
$this->view->sitetitle('login');
$this->view->display('www/login', [
'video' => $this->arena->video(),
'username' => $this->input->set($username)
], ['notifications' => 'user/forgotpassword']);
}
}
Service Layer
/**
*-----------------------------------------------------------------
*
* USER LOGIN SERVICE LAYER
*
*/
namespace Service\User;
use \Helper\Service;
class Login extends Service {
public $dependencies = ['login', 'mail', 'time', 'user', 'vars'];
/**
* Handles Entire Login Process For Site Users
*
* #params all User Submitted Form Data
*/
public function user($username = '', $password = '') {
// Validate $_POST Form Data
$this->validateInput($username, $password);
/**
* No Errors Produced - Complete Form Submission
*
* We Are Not Using `elseif` Between Forgot Password & Normal Login
* After a Forgot Password Code is Generated User May Remember Old Passwords
* We Need to Ensure Users Can Still Login Using Account Password As Well
*/
if (!$this->errors()) {
/**
* User Input Password Matches Account Password
*/
if ($this->input->verifyhash($password, $this->user->get('info.password'))) {
$this->login->user();
return true;
}
/**
* If We Have Not Been Redirected Login Was Unsuccessful
*/
$message = $forgotPW ? 'Forgot Password Code Invalid - Login Lost Incorrect' : 'Login Unsuccessful - Incorrect Username or Password';
$this->log->error($message, ['Username' => $username, 'Password' => $password]);
$this->error('Incorrect Username or Password');
}
/**
* If We Have Made It This Far Login Was Unsuccessful - Log Unsuccessful Attempt
*/
$this->login->logAttempt();
return false;
}
/**
* Validate $_POST Data
*
* #params all User Submitted Form Data
*/
private function validateInput($username = '', $password = '') {
// Display Error if Username is Empty
if (!$username) {
$this->error('Please enter a username');
}
// Display Error if Password is Empty
elseif (!$password) {
$this->error('Please enter a password');
}
// Search DB For User With Matching Username - If User Not Found Display/Log Error, Else Set User
else {
$user = $this->user->info($username, 'username', '', '`userid`');
if (!$user) {
$this->error('The username ' . $username . ' does not exist');
$this->log->error('User Not Found When Attempting to Login', ['username' => $username]);
} else {
$this->user->set('user', $user['userid']);
}
}
}
}
In order to answer your question, I think it's best to break down the concept of MVC into a very basic form, and its individual parts. I apologise in advance if this comes across as being somewhat condescending.
View
The view of the application displays anything and everything. If something is going to be displayed, it should be done in this layer
Controller
The controller is a mediator between the view and the model. It takes input from the view, applies logic/rules to it (where required), and interacts with the model to then get data to pass back to the view.
Model
This is where the loading and saving of data are done. The majority of the validation should have been done as part of the rules in the controller, and this should only pass details of any errors during loading or saving back to the controller should the arise. If there are no errors, it should return the relevant data, or a success status back to the controller.
With those points in mind, the model should not set flash messages to the session, that should be done within the controller depending on the result from the model.
Look at redirects and alerts as specific to one particular form of UI, and it should be obvious that there's no place for them in the Model. Simply always try to picture an alternative interface for your application; e.g. a command line interface for administrative tasks or a REST API. Redirects obviously have no place in either of these alternatives. Alerts are debatable... at the very least the form of the alert will be very different. Your Model will need to be able to pass back some status code to your Controller or View, and then it's the job of the Controller to react to "negative" events and the job of the View to visualise any alerts if necessary.
For example, your model may do something like this:
public function registerUser(User $user) {
...
if (!$successful) {
throw new EmailAlreadyRegisteredException;
}
return true;
}
The controller may then look like this:
public function userRegistration(Request $request) {
try {
$user = User::fromRequest($request);
$this->services->get('Users')->registerUser($user);
$this->view->render('registration_successful', $user);
} catch (InvalidUserData $e) {
$this->view->render('registration_form', $request, $e);
} catch (EmailAlreadyRegisteredException $e) {
$this->view->render('registration_failed', $user, $e);
}
}
The "alert" is passed around as an exception. It's just a method for the Model to signal to its callers what happened. It's up to the callers then to react to and visualise those events. You should certainly not expect any particular type of visualisation in the Model. So you don't want to hardcode specific HTML encoded messages or such. You don't even want to touch human languages at all, that's all the job of the View.

Get security token for non-logged user with Symfony

How can I get a security token for any user, not only the one currently logged in ?
I would like to be able to call isGranted() on a user fetched from the database
isGranted() comes from the Security service, so it would be hard/unnecessary to use that to get Roles without adjusting the state of the session.
Don't get me wrong, it's definitely possible... This would work, for example:
public function strangeAction()
{
// Get your User, however you normally get it
$user = $userRepository->find($id);
// Save the current token so you can put it back later
$previousToken = $this->get("security.context")->getToken();
// Create a new token
$token = new UsernamePasswordToken($user, null, "main", $user->getRoles());
// Update the security context with the new token
$this->get("security.context")->setToken($token);
// Now you have access to isGranted()
if ($this->get("security.context")->isGranted("ROLE_SOMETHING"))
{ /* Do something here */ }
// Don't forget to reset the token!
$this->get("security.context")->setToken($previousToken);
}
...but that really makes no sense.
In reality, you don't need the token. A much better way of doing this would be to add an isGranted() method into your User entity:
// Namespace\YourBundle\Entity\User.php
class User
{
...
public function isGranted($role)
{
return in_array($role, $this->getRoles());
}
...
}
Now you can get those roles in your controllers:
public function notSoStrangeAction()
{
// Get your User, however you normally get it
$user = $userRepository->find($id);
// Find out if that User has a Role associated to it
if ($user->isGranted("ROLE_SOMETHING"))
{ /* Do something here */ }
}
I had the same requirements a while ago. So I implemented it myself. Since you require the hierarchy information from the container it is not possible advised to extend the user entity with this functionality though.
// first check if the role is inside the user roles of the user
// if not then check for each user role if it is a master role of the check role
public function isGranted($user, $checkrole){
$userroles = $user->getRoles();
if (in_array($checkrole, $userroles)){return true;}
foreach ($userroles as $userrole){
if ($this->roleOwnsRole($userrole, $checkrole)){return true;}
}
return false;
}
// recursively loop over the subroles of the master to check if any of them are
// the suggested slave role. If yes then the masterrole is a master and has
// the same grants as the slave.
private function roleOwnsRole($masterRole, $slaveRole, $checkvalidityroles=true, $hierarchy=null)
{
if ($hierarchy===null){$hierarchy = $this->container->getParameter('security.role_hierarchy.roles');}
if ($masterRole === $slaveRole){ return false; }
if($checkvalidityroles && (!array_key_exists($masterRole, $hierarchy) || !array_key_exists($slaveRole, $hierarchy))){ return false; }
$masterroles = $hierarchy[$masterRole];
if(in_array($slaveRole, $masterroles)){
return true;
}else{
foreach($masterroles as $masterrolerec){
if ($this->roleOwnsRole($masterrolerec, $slaveRole, false, $hierarchy)){return true;}
}
return false;
}
}
I think the best way is to call AccessDecisionManager manually - like $securityContext->isGranted() does as well but for the currently logged in user. This is good too if you are using Symfony Voters to determine access.
$token = new UsernamePasswordToken($userObject, 'none', 'main', $userObject->getRoles());
$hasAccess = $this->get('security.access.decision_manager')->decide($token, array('voter'), $optionalObjectToCheckAccessTo);

how to get role from Zend_Auth/Zend_ACL when using a Doctrine adapter? getting all work together

I'm using Zend_Auth with a project using doctrine.I believe every bootstrapping is done correctly and i can log in.
my adapter looks like this:
class Abra_Auth_Adapter_Doctrine implements Zend_Auth_Adapter_Interface {
protected $_resultArray;
private $username;
private $password;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
//based on feedbacks as response authenticate has changed to this
public function authenticate() {
$q = Doctrine_Query::create()
->from("Abra_Model_User u")
->leftJoin("u.Role r")
->where("u.username=? AND u.password=?", array($this->username,$this->password));
$result = $q->execute();
if (count($result) == 1) {
return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $result->get("Mylibrary_Model_User"), array());//autoloaderNamespaces[] = "Mylibrary_" in application.ini
} else {
return new Zend_Auth_Result(Zend_Auth_Result::FAILURE, null, array("Authentication Unsuccessful"));
}
}
my Abra_Controller_Pluging_Acl looks like this
class Abra_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract {
public function preDispatch(Zend_Controller_Request_Abstract $request) {
parent::preDispatch($request);
$controller = $request->getControllerName();
$action = $request->getActionName();
$module = $request->getModuleName();
$auth = Zend_Auth::getInstance();
if($auth->hasIdentity()){
$identity = $auth->getIdentity();
$roles = $identity["Role"];
$role = $roles["name"];
$role = (empty ($role) || is_null($role))? "regular" : $role ;
} else {
$role = "guest";
}
}
now having Doctrine_Event Fatal error: spl_autoload() [function.spl-autoload]: Class Doctrine_Event could not be loaded. i've seen this post here and i'm wondering how that can affect my using of Zend_Session, and it's true that i have apc.dll enabled in my php.thanks a lot for reading this
How to get the role: In your adapter, on successful login, rather than returning only the username field, how about returning the whole user object? Then the whole thing will be available when you call Zend_Auth::getIdentity().
Question 1: If you treat controllers as resources and the ACL rules are going to be different per module, then the resource names should reflect the module, as well. This will address the issue of modules with identical controller names.
Question 2: I am not sure I am understanding correctly. Zend_Auth and its storage will take care of keeping the uer identity in its own session namespace. However, I have run into the issue of what to do when the user record in the db changes - say, the user modifies his full name in his profile during his logged-in session - and you are displaying that full name in your site template, pulled from Zend_Auth::getIdentity(). As a user, I would expect the change to be reflected in the visible interface, but the change has only occurred back in the db, not in the session.
What I have done in the past is to create an additional auth adapter that fetches the new user record and always returns success. When the user updates his profile, I call Zend_Auth::authenticate() using this trivial adapter. The session storage gets updated and all is well with the world.
[This approach is almost certainly a hack, so I'd be interested in hearing alternative approaches. I'm sure I can set a value in the session storage directly, but when I last tried it, I couldn't make it quite work. So resorted to the additional adapter workaround.]

Categories