CakePHP 3.5 Auth use multiple tables - cakephp

I have an Auth process which works fine with one userModel. But not only because of my DB schema I need to have one login method/action which works with multiple models.
So far I've tried everything I was able to think of or find online - for example editing this Cake 1.3 solution into Cake 3 and a few more hints I was able to find.
However, I'm not able to figure it out.
Thank you for any answer.
My AppController component load:
$this->loadComponent('ExtendedAuth', [
'authenticate' => [
'Form' => [
//'userModel' => 'Admins',
'fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Admins',
'action' => 'login'
],
// If unauthorized, return them to page they were just on
'unauthorizedRedirect' => $this->referer(),
]);
My ExtendedAuthComponent:
class ExtendedAuthComponent extends AuthComponent
{
function identify($user = null, $conditions = null) {
$models = array('Admins', 'Users');
foreach ($models as $model) {
//$this->userModel = $model; // switch model
parent::setConfig('authenticate', [
AuthComponent::ALL => [
'userModel' => $model
]
]);
$result = parent::identify(); // let cake do its thing
if ($result) {
return $result; // login success
}
}
return null; // login failure
}
}
EDIT1: Description of situation
I have two separate tables (Admins, Users). I need just one login action which tries to use Admins table prior to Users. Because of the application logic I can't combine them to one table with something like 'is_admin' flag. So basically what I need is instead of one specific userModel set in Auth config, I need a set of models. Sounds simple and yet I'm not able to achieve it.
EDIT2: Chosen solution
Based on the answer below, I decided to update my schema. Auth users table is just simplified table with login credentials and role and other role-specific fields are then in separate tables which are used as a connection for other role-specific tables. Even though the answer is not exactly a solution for the asked question, it made me think more about any possible changes of the schema and I found this solution because of it so I'm marking it as a solution. I appreciate all comments as well.

As Mark already said in a comment: Don't use two users tables. Add a type field or role or whatever else and associated data in separate tables if it's different like admin_profiles and user_profiles.
Don't extend the Auth component. I wouldn't recommend to use it anymore any way because it's going to get deprecated in the upcoming 3.7 / 4.0 release. Use the new official authentication and authorization plugins instead.
If you insist on the rocky path and want to make your life harder, well go for it but then you should still not extend the auth component but instead write a custom authentication adapter. This is the right place to implement your custom 2-table-weirdness. Read this section of the manual on how to do it.

Related

Cakephp4, how to load associated data in an Entity object?

I have a Users table and a Roles table.
A user has one role. So in UsersTable.php:
$this->belongsTo('Roles', [
'foreignKey' => 'role_id',
'joinType' => 'INNER',
]);
Now in User.php (Entity!!) I need the role name of the users role.
But I have only the User Entity in which is no associated data.
I now have:
public function getRole()
{
$q = TableRegistry::getTableLocator()->get('Roles');
$roles = $q->find('list')->toArray();
return $roles[$this->role_id];
}
This works, but TableRegistry is marked obsolete in cake4, and I can't find any other way the make this work. What is the propper way of doing this?
If you want to use both the User and Role data in a controller you can just do for example
$user = $this->Users->get($id, [
'contain' => ['Roles'],
]);
and the resulting object has $user->role defined as a Role entity.
However, I have a feeling you already know this and have another issue that may or may not overlap with a problem I was solving just recently. Check out ndm's answer to my question here and hopefully it helps you! CakePHP 4.1 User entity as authorization identity associated fields
Apologies if that's not your scenario, I just found this current thread while looking for an answer to my problem, and it was definitely the closest to my own issue.

Make CakePHP consider two different routes/models for same path?

I have a website that has users and entries, both of which are stored in a database. Every entry has its own page, using its slug, and every user has a public profile, using its username. Their respective URLs might look something like this:
Entry: https://example.com/hello-world
User: https://example.com/test-user
Refactoring said website with CakePHP 3.4 (the original being built with “vanilla” PHP), I implemented the following routes:
$routes->connect('/:slug',
['controller' => 'Entries', 'action' => 'view'],
['pass' => ['slug']]
);
$routes->connect('/:username',
['controller' => 'Users', 'action' => 'view'],
['pass' => ['username']]
);
The entry pages work like a charm — no problem there — but when I try to access a user profile, Cake throws a RecordNotFoundException. This makes sense, since it’s looking for an entry that does no exist.
I was hoping switching from firstOrFail to find in the EntriesController would allow the application to continue with the next route in line (because no exception would be thrown), but the result is actually worse: It tries to render the entry view without an object, causing PHP notices on an otherwise empty layout.
I have read the CakePHP documentation (“Book”), but could not find a solution to this (I would assume rather generic) problem. I have also tried many other (often less obvious) route setups, but no luck there either.
Now my mind keeps going to something like a EntryOrUserController, but I doubt that would be the best solution, or even a good one. Frankly, I think it’s silly. I guess I am really hoping for some controller or middleware function that does exactly what I want out of the box, but any elegant solution would do.
P.S. I do realize that the default CakePHP/MVC way of going about this would be to have URLs a bit more like this:
Entry: https://example.com/entries/hello-world
User: https://example.com/users/test-user
…but that is not an option in this case, so thanks but no. ☺
Thanks to ndm for pointing me in the right direction. After some tinkering I have a solution that works nicely. Posting it here because it might be of use to others. There are three steps.
1. Create custom routing class
/src/Routing/Route/SlugRoute.php:
namespace App\Routing\Route;
use Cake\Routing\Route\Route;
use Cake\ORM\Locator\LocatorAwareTrait;
class SlugRoute extends Route
{
use LocatorAwareTrait;
public function parse($url, $method = '')
{
$params = parent::parse($url, $method);
if (!$params ||
!isset($this->options['model']) ||
!isset($this->options['pass'][0])
) {
return false;
}
$count = $this
->tableLocator()
->get($this->options['model'])
->find()
->where([
$this->options['pass'][0] => $params['pass'][0]
])
->count();
if ($count !== 1) {
return false;
}
return $params;
}
}
2. Apply new routing class to relevant routes
$routes->connect('/:slug',
['controller' => 'Entries', 'action' => 'view'],
['pass' => ['slug', 'name'], 'routeClass' => 'SlugRoute', 'model' => 'Entries']
);
$routes->connect('/:username',
['controller' => 'Users', 'action' => 'view'],
['pass' => ['username'], 'routeClass' => 'SlugRoute', 'model' => 'Users']
);
3. Consider a few things
I’m also telling the routes which models to use. It would probably be nice if the routing class figures this out by itself.
The routing class assumes we want to do a lookup on the first value of the pass array. That’s fine in this case (with only slug and username being passed), but it’s not very transparent and easily broken.

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 2.x Custom Authentication

So I am working with cakePHP 2.3 and I try to use different Frameworks when possible a) to keep my 41 Year old mind aware b) To make sure I use every tool in the shed for myself and my customer.
I have a personal SaaS app Im building and need to know the best way to add "where site_id = 2" to the authentication calls basically based on how they are viewing the app i.e. subdomain or domain sets a particular site_id in AppController.
I have looked for custom authentication but I havent seen anything that stood out. I also have a roles column & table which is comma delim I need to join in the auth request
Any good how to's or pointers would be great
Thanks
I'm just taking a shot in the dark here with limited info but i think this is somewhere around the woods of what your looking for.
Locate your cake build and navigate to /lib/Cake/Controller/Component/Auth/BaseAuthenticate.php
Locate:
public $settings = array(
'fields' => array(
'username' => 'username',
'password' => 'password'
),
'userModel' => 'User',
'scope' => array(),
'recursive' => 0,
'contain' => null,
);
and make your mods there.

HABTM form validation in CakePHP

I have a Projects table and a Users table which are linked by a HABTM relation. In the "add" new Project page I have a multiple checkbox section to select Users for the new project. I want to have at least one User for the Project. What's the best way to approach this in CakePHP ?
Try this:
// app/models/project.php
/**
* An additional validation check to ensure at least one User is
* selected. Spoofs Cake into thinking that there are validation
* errors on the Project model by invalidating a non-existent field
* on the Project model, then also invalidates the habtm field as
* well, so when the form is re-displayed, the error is displayed
* on the User field.
**/
function beforeValidate() {
if (!isset($this->data['User']['User'])
|| empty($this->data['User']['User'])) {
$this->invalidate('non_existent_field'); // fake validation error on Project
$this->User->invalidate('User', 'Please select at least one user');
}
return true;
}
I stumbled on the same issue, but now - 3 years later - with CakePHP 2.3.
To be clear; Group has and belongs to User. I've had a form like this:
// View/Groups/add.ctp
echo $this->Form->input('name');
echo $this->Form->input('User');
With the validation rule like in user448164's answer:
// Model/Group.php
public $validate = array(
'User' => array(
'rule' => array('multiple', array('min' => 1)),
'message' => 'Please select one or more users'
)
);
That didn't work, after Googling for it, I found this question which couldn't still be the best solution. Then I tried several things, and discovered this to work just fine:
// View/Groups/add.ctp
echo $this->Form->input('name');
echo $this->Form->input('Group.User');
Way too easy solution, but had to dig into it to find out it works this way.
Hopefully it helps somebody one day.
Update for CakePHP 2.4.x (possibly 2.3.x as well)
When I wrote this answer, I was using CakePHP 2.3.x. Back then it worked perfectly for both validating and saving the data. Now when applying the same code on a new project, using CakePHP 2.4.x, it didn't work anymore.
I created a test case, using the following code:
$data = array(
'User' => array(
'Client' => array(8)
),
);
$this->User->create();
$this->User->saveAll($data);
My first thought was: Saving all means saving all "root" models, what actually makes sense to me. To save deeper than just the "root" ones, you'll have to add the deep option. So I ended up with the following code:
$data = array(
'User' => array(
'Client' => array(8)
),
);
$this->User->create();
$this->User->saveAll($data, array('deep' => true));
Works like a charm! Happy coding. :)
Update (2014/03/06)
Struggling with the same problem again, in this case with hasMany instead of habtm. Seems like it behaves the same way. But I found myself looking for this answer again, and got confused.
I'd like to make clear that it's key to use Group.User instead of User in your input. Else it won't use the User model validation.
I've just been looking at his problem myself on a project and came across a slightly more elegant solution, as long as you're only dealing with a habtm relationship and you need to ensure that at least one checkbox is selected.
so for example you're editing a Project and you want it to be associated with at least one user
Add this to beforeValidate()
// check habtm model and add to data
foreach($this->hasAndBelongsToMany as $k=>$v) {
if(isset($this->data[$k][$k]))
{
$this->data[$this->alias][$k] = $this->data[$k][$k];
}
}
In the validation rules add the following:
'User' => array(
'rule' => array('multiple', array('min' => 1)),
'message' => 'Please select one or more users'
)
teknoid's blog has a pretty in depth solution to your issue here. The most Cakey way of doing this would be to add custom validation to your model, as you mention in your comment above. Check out http://teknoid.wordpress.com/2008/10/16/how-to-validate-habtm-data/
From the article, where Tag HABTM Post (:: Project HABTM Users):
First, we validate the Tag model, by
using the data from the form to ensure
that at least one Tag was selected. If
so, we save the Post and the relevant
Tags.
2016 update for CakePhp 2.7
Full answer here : HABTM form validation with CakePHP 2.x
TL;DR;
AppModel.php
public function beforeValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$model][$model]))
$this->data[$this->name][$model] = $this->data[$model][$model];
}
return true;
}
public function afterValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
unset($this->data[$this->name][$model]);
if(isset($this->validationErrors[$model]))
$this->$model->validationErrors[$model] = $this->validationErrors[$model];
}
return true;
}
In the main model of your HABTM :
public $validate = array(
'Tag' => array(
'rule' => array('multiple', array('min' => 1)),
'required' => true,
'message' => 'Please select at least one Tag for this Post.'
)
);
If you are using CakePHP 2.3.x, you may need to add this code to your model in addition to the code that GuidoH provided, otherwise your HABTM model data may not save:
public function beforeSave($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$this->name][$model])){
$this->data[$model][$model] = $this->data[$this->name][$model];
unset($this->data[$this->name][$model]);
}
}
return true;
}
As per my comment on Guido's answer above, I use Guido's answer exactly, however I modify the data with the beforeSave callback before it saves to the database.
I have this issue on Cake 2.4.5+
public function beforeSave($options = array()) {
$temp = $this->data['Group']['User'];
unset($this->data['Group']['User']);
$this->data['User']['User'] = $temp;
return true;
}

Resources