Self validation of links from HTML-Helper? - cakephp

How can I prevent links automaticly from beeing displayed in the Template ctp files?
I will give you an example:
User(id = 1) is allowed to see teamcalendars/view/1
User(id = 2) is not allowed to see teamcalendars/view/1.
User1 is member of the team 1 and should see and follow the link. User2 is not member in any teams and neither should see the link to the calender nor follow it. But I would like to place the link in the teams/index file where both users can go to and see all teams, but with different options per team.
If User2 follows the link (or types it into the browser manually), the controller will return a redirect and a error message about missing privileges. User2 will anyhow never get there. But how do I prevent cake from displaying the link for User2 (its missleading)?
Is there a possibility to connect the link to the controller and action and the id of the object where it is leading to, so I don't neet to take care of building and passing variables for each view just to decide which links can be displayed?
Sorry for not providing any code, but I think anyone knows how to send an array from the Controller to the View and how to validate it with if(){echo $this->Html->link()}, which is what I am doing currently.
Thank you for any help or remarks in advance.

One option is to define your own HtmlHelper and override the link function, such that it checks the permissions on the link first and only outputs it if they are allowed access. Something like the following:
namespace App\View\Helper;
use \Cake\View\Helper\HtmlHelper;
// Or, if you're already using a third-party HTML helper, something like
// use BootstrapUI\View\Helper\HtmlHelper as HtmlHelper;
class MyHtmlHelper extends HtmlHelper
{
function link($title, $url = null, array $options = [])
{
if (checkMyPermissions($url)) {
return parent::link($title, $url, $options);
}
}
}
And then in your AppController:
use App\View\Helper\MyHtmlHelper;
public $helpers = [
'Html' => ['className' => 'MyHtmlHelper'],
// ... and all your other helpers
];

Related

CakePHP : Check authorizations in views

I am using CakePHP in my project and I am looking for a proper way to check advanced user rights in my views.
I have several pages in which the contents depend of your rights (you can view some blocks or not, edit some infos or not, etc...)
I searched and the only way I found is to implement an Auth Helper, but I thought the best way to to that is to implement methods in my "UserController" (such as canPerformAction($action, $controller = 'default_controller')), am I wrong ? And if I'm right, how to call that methods properly ?
Thanks.
EDIT : More precisions
For example I have an action "editEventProducts" that a user can perform only if he's the event owner and if the event status is <= 2.
I check that in my controller "isAuthorized" function, works like a charm.
But I have a page called "eventDetails", form which you can perfom several actions such as this one, and I want to show the edit button, only if you can do it.
If fact what I need is the output of the "isAuthorized" function for each action that you can call, but can I properly get it from a view ?
Solution
I implemented a Auth helper who does several check such as this one, which is finally a whitelist check, depending of the status of my event, hope it will help, the code :
App::uses('AppHelper', 'View/Helper');
class AuthHelper extends AppHelper {
var $helpers = array('Session');
private $_whitelist = array(
'controller1' => array(
'events' => array(
'action1' => array(1 => true, 2 => true),
'action2' => array(1 => true, 2 => true),
'action3' => array(3 => true),
'action4' => array(6 => true)
)
),
'user' => array(
'controller1' => array(
'action1' => array(1 => true, 2 => true),
'action2' => array(1 => true, 2 => true)
)
)
);
public function canPerformAction ($action, $event_infos, $controller = 'events') {
return isset($this->_whitelist[$this->Session->read('Auth.User.role')][$controller][$action][$event_infos['Event']['state_id']]);
}
}
It sounds to me like you just want to render some parts of a view based on the permissions of the user. Well, in this case I think a helper is the right choice. The user should already have all the permissions he has loaded - except they're very fine grained and you got thousands of permissions.
Check this AuthHelper, it allows you to check if the user is logged in, for a role or a set of roles in a field. Alternatively implement your own solution to match whatever your permission system is.
Note that the helper relies on passing the user data to the view in a view variable. It can be also configured to read the data from the auth part of the session directly.
Here is the example taken from it's documentation:
if ($this->Auth->isLoggedIn()) {
echo __('Hello %s!', $this->Auth->user('username'));
}
if ($this->Auth->isMe($record['Record']['user_id']) {
// or your edit button here
echo '<h2>' . __('Your records') . '</h2>';
}
if ($this->Auth->hasRole('admin') {
echo $this->Html->link(__('delete'), array('action' => 'delete'));
}
What you need is called authorization, and is the process of granting/denying actions usually built on top of an authentication step, which maps HTTP requests to logical users.
The authorization scheme can be implemented in a number of ways, for example with simple role-based rules, where users are grouped exactly for the purpose of assigning rights, or with more complex ACL (access control lists). Both can be adopted at the same time for different parts of the system, depending on your needs.
Whatever scheme you pick, you absolutely need to query it at the beginning of your controllers actions (if applicable, you may and up with a standardized authorization filter in your AppController), because the HTTP request doesn't need to come from a previously sent HTTP page, but could be a (possibly) malicious, hand-craften one. Also, you'll likely need to adjust the UI after the user rights. Maybe you'll better start with a bunch of if statements, and then after some days of work you'll be able to identify your needs and build your libraries/helpers/blocks/whatever to avoid code duplication and easing reading the templates.
If you have predefined user permissions (like 'admin', 'moderator', 'editor', 'publisher'...) you can just read the user role and current action in the controller function isAuthorized and set it to true or false.
If you want custom permissions per user, you can store those values in the database, read them in the isAuthorized function and make your logic to determine if you should allow him or not.
My solution to this was a separate table user_permissions that was something like this:
user_id | action
where action would be `controller/action' or 'view/block' or whatever you want to save there.
I would read all values for current user in the controller and if the current controller/action was found in the array, i'd set isAuthorized to true. You can apply your logic to the blocks also.
You can call function of controller from view using
requestAction(string $url, array $options)
Or you can create your custom Helper which will do this for you!

CakePHP: Scaffolding after having written edit/view/add

I have an application in which we give a very friendly interface for managing data. This is done through many controllers' add/edit/view functions. But now the requirement has come that we should have "super admins" able to edit anything, and scaffolding will give them a quick and dirty manner of changing data. Since scaffolding uses add/edit/view by default, I've unintentionally overwritten the ability to scaffold.
I can't just go and change all my calls to edit/add for our "user friendly" data managing. So I want to essentially ignore the add/edit/view when, for example, a user has a flag of "yes, please let me scaffold". I imagined it would be something like:
public function edit($id) {
if (admin_user) {
$scaffold;
} else {
[user-friendly version code]
}
}
But no dice. How can I achieve what I want?
suppose you already have admin users and you want to scaffold only super-user:
Also suppose you store the information about beeing a super-user or not in a column named super in the users table
in your core.php
Configure::write('Routing.prefixes', array('admin', 'super));
in your appController
public $scaffold = 'super';
beforFilter() {
if($this->Auth->user('super') && !isset($this->params['super'])
$this->redirect(array('super' => true));
}
Now I can't try this code but the idea should work.
edit: we need to check if we are already in a super_action to avoid infinite redirect

View link if depended on user rights

I am working with CakePhp 2.x. I have three Columns:
User | Course | UserCourseRole
Each user can edit multiple courses and one course can be edited by multiple users. So far so good.
If a user wants to see an index of all the courses i want to show a 'edit'-link only next to the courses which he can in fact edit.
How can i realize this? I figured i would have to set some sort of extra field inside the CourseController and check for this field inside the view. Is this the right way to go?
My current Code is
CourseController.php
...
public function index() {
$courses = $this->Course->find('all', array('recursive' => 2));
$this->set('courses', $courses);
}
...
Courses/index.ctp
<!-- File: /app/View/Courses/index.ctp -->
...
<?php foreach ($courses as $course):?>
...
<?php
echo $this->Html->link('edit', array('action' => 'edit', $course['Course']['id']));
?>
...
In beforeRender() or beforeFilter() set $this->Auth->user() as a variable to the view, for example as userData.
$this->set('userData', $this->Auth->user());
Implement a (auth)helper that uses that variable (you can make it configurable as a helper setting) and do your checks like:
if ($this->Auth->hasRole($course['Course']['role']) { /* ... */ }
if ($this->Auth->isLoggedIn() { /* ... */ }
if ($this->Auth->isMe($course['Course']['user_id']) { /* ... */ }
Implement the hasRole() method according to whatever your specific requirements are.
Doing this as helper as a bunch of advantages, it is easy to reuse, overload and adapt to whatever your checks are and you don't use a component in a view plus that you should avoid calling statics and singletons a lot in your app. Also it is pretty easy to read and understand what the code does.
I think the good idea is set some variable or constans after logged (if user has privileges) and uses if statement for check.
if($allow === true) {
echo $html->link('Edit',...
}
or use AuthComponent::user() in Views.
This idea it's not good if we can many kind of admins (admin, moderator, reviewier, etc.)
Maybe someone will have a better solution

how to set default prefix in controller -> skip defining prefix in links

I have the following idea: I'd like to be able to define the default prefix in any given controller. So let's say the default prefix for the CitiesController implements all actions with the "admin" prefix ("admin_index", "admin_add", etc.), but the ProvincesController implements all actions with the
"superadmin" prefix ("superadmin_index", "superadmin_add", etc.)
The problem with this is, every time I want to link to any "city stuff", I have to specify "admin" => "true". Any time I want to link to any "province stuff", I have to specify
"superadmin" => "true".
That's already quite a bit of work initially, but if I decided I wanted to change the prefix from "admin" to "superadmin" for cities, it would be even more work.
So I was wondering if there's to somehow do something along the lines of:
class CitiesController extends AppController {
var $defaultPrefix = "admin"
}
And then in the HTML helper link function, do something like:
class LinkHelper extends AppHelper {
public $helpers = array('Html');
function myDynamicPrefixLink($title, $options) {
// check whether prefix was set (custom function that checks all known prefixes)
if (! isPrefixSet($options)) {
// no clue how to get the controller here
$controller = functionToGetControllerByName($options['controller']);
// check whether controller has a defined default prefix
$prefix = $controller->defaultPrefix;
if ($prefix) {
// set the given prefix to true
$options[$prefix] = true;
}
// use HTML helper to get link
return $this->Html->link($title, $options);
}
}
I just have no clue how to get from the helper to the controller of the given name dynamically.
Another option would be to store the default prefix somewhere else, but for now I feel that the best place for this would be in any given controller itself.
Another idea would be to even have that look up function dependent on both, the controller and the action, and not just the controller.
You should be able to use the Router::connect to supply defaults (see code and documentation on Github: link) to specify default prefixes for certain controllers and even actions.
However, if you want more flexibility than the current Router provides, you can extend your use of the Router::connect by specifying an alternate Route class to use:
Router::connect(
'/path/to/route',
array('prefix' => 'superadmin'),
array('routeClass' => 'MyCustomRouter')
);
Examples of this can be seen in the CakePHP documentation.

Adding a 1 to many file upload to CRUD

My app has sales listing functionality that will allow the user to add 1 or more photos for the product that they want to sell.
I'm attempting to use the upload/filestore_image of ATK with a Join table to create the relationship - my models:
class Model_Listing extends Model_Table {
public $entity_code='listing';
function init(){
parent::init();
$this->addField('name');
$this->addField('body')->type('text');
$this->addField('status');
$this->addField('showStatus')->calculated(true);
}
function calculate_showStatus(){
return ($this->status == 1) ? "Sold" : "For Sale" ;
}
}
class Model_listingimages extends Model_Table {
public $entity_code='listing_images';
function init(){
parent::init();
$this->addField('listing_id')->refModel('Model_Listing');
$this->addField('filestore_image_id')->refModel('Model_Filestore_Image');
}
}
In my page manager class I have added the file upload to the crud:
class page_manager extends Page {
function init(){
parent::init();
$tabs=$this->add('Tabs');
$s = $tabs->addTab('Sales')->add('CRUD');
$s->setModel('Listing',array('name','body','status'),array('name','status'));
if ($s->form) {
$f = $s->form;
$f->addField('upload','Add Photos')->setModel('Filestore_Image');
$f->add('FileGrid')->setModel('Filestore_Image');
}
}
}
My questions:
I am getting a "Unable to include FileGrid.php" error - I want the user to be able to see the images that they have uploaded and hoped that this would be the best way to do so - by adding the file grid to bottom of the form. - EDIT - ignore this question, I created a FileGrid class based on the code in the example link below - that fixed the issue.
How do I make the association between the CRUD form so that a submit will save the uploaded files and create entries in the join table?
I have installed the latest release of ATK4, added the 4 filestore tables to the db and referenced the following page in the documentation http://codepad.agiletoolkit.org/image
TIA
PG
By creating model based on Filestore_File
You need to specify a proper model. By proper I mean:
It must be extending Model_Filestore_File
It must have MasterField set to link it with your entry
In this case, however you must know the referenced ID when the images are being uploaded, so it won't work if you upload image before creating record. Just to give you idea the code would look
$mymodel=$this->add('Model_listingimages');
$mymodel->setMasterField('listing_id',$listing_id);
$upload_field->setModel($mymodel);
$upload_field->allowMultiple();
This way all the images uploaded through the field will automatically be associated with your listing. You will need to inherit model from Model_Filestore_File. The Model_Filestore_Image is a really great example which you can use. You should add related entity (join) and define fields in that table.
There is other way too:
By doing some extra work in linking images
When form is submitted, you can retrieve list of file IDs by simply getting them.
$form->get('add_photos')
Inside form submission handler you can perform some manual insertion into listingimages.
$form->onSubmit(function($form) uses($listing_id){
$photos = explode(',',$form->get('add_photos'));
$m=$form->add('Model_listingimages');
foreach($photos as $photo_id){
$m->unloadDdata()->set('listing_id',$listing_id)
->set('filestore_image_id',$photo_id)->update();
}
}); // I'm not sure if this will be called by CRUD, which has
// it's own form submit handler, but give it a try.
You must be careful, through, if you use global model inside the upload field without restrictions, then user can access or delete images uploaded by other users. If you use file model with MVCGrid you should see what files they can theoretically get access to. That's normal and that's why I recommend using the first method described above.
NOTE: you should not use spaces in file name, 2nd argument to addField, it breaks javascript.

Resources