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

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.

Related

CakePHP 3.5 Auth use multiple tables

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.

How to pull data for and display an Association as a multiple-select checkbox in CakePHP 3?

I'm setting up a user access model with Roles in a separate table, and linked to Users by a UserRoles table.
I currently have the following in Model/Table/UsersTable.php:
$this->belongsToMany('Roles', [
'through' => 'UserRoles'
]);
and the following in Model/Table/RolesTable.php:
$this->belongsToMany('Users', [
'through' => 'UserRoles'
]);
and the following in Model/Table/UserRolesTable.php:
$this->belongsTo('Users', [
'foreignKey' => 'user_id'
]);
$this->belongsTo('Roles', [
'foreignKey' => 'role_id'
]);
I have 3 different roles created, 'viewer', 'creator', and 'administrator'. I've successfully set privileges based on user types. Where I am getting stuck is adding roles to a user via an association form.
Right now I have given Administrator users the ability to edit user information. This works for basic information that I have in the Users table, but I can't figure out how to set up the form field for the associated Role. I would like it to be a checkbox where the Administrator can select each privilege for the user.
I'm currently doing this, which is not giving me what I want:
echo $this->Form->input('Users.role', ['type' => 'checkbox']);
This is giving me a single checkbox with the label "Role". I want to pull each row from my Roles table and list them all as options.
I have a few questions relating to this:
1) This seems really elementary but I'm just not finding it clearly stated. What code do I need in my UsersController to pull the list of all Roles? (not just those associated with the current User, but all objects in the Roles table.)
2) What form input code do I need to display checkboxes with all possible Roles, and show the current user privileges (in my UserRoles table) as already checked off? I think I need something like this in my form:
echo $this->Form->select('User.Role', $options, ['multiple' => 'checkbox']);
...but I can't tell what $options should be, and how to set already-selected values.
I am currently pulling Roles with my User object to be edited:
$user = $this->Users->get($id, [
'contain' => ['Roles'],
'Users.id' => $this->Auth->user('id')
]);
...but I'm having trouble converting it into a checkbox form selection.
Thanks so much.
In order to create a list of checkbox out of some options, you first need to send the options from the controller:
$this->set('roles', $this->Users->Roles->find('list'));
Then, in your template add a multiple => checkbox input:
echo $this->Form->input('roles', ['multiple' => 'checkbox', 'options' => $roles]);
It is not necessary to prefix your input names with User. just name your inputs as the properties of a User entity.

Simplified and manageable ACL implementation in cakephp

I went through complete lesson on cakephp's ACL component, but gigantic ACL component do not seem to meet my very simple requirements.
I have only group based access control, three groups are users, managers and administrators the fourth is a anonymous users without logins for which I am not creating any group.
from acl concept it creates three table
aros -> this looks somewhat redundant data copied from groups table, I dont even need to have a group table but just field group_id in users table.
acos -> this is a list of public methods in controllers, I had to use AclExtra plugin to populate over 250+ actions in table, now this is the part which I think un-manageable, I noticed that tool used to populate acos table cannot reliably sync everytime when I do changes in controllers, the same work must be done at remote site for each changes that means terrible thing! this also mean i have to have a database backup during updates and migration.
Other side if I use php file based acos that is again un-manageable because we have to make sure syncing between controller and acl file.
aros_acos -> obviously
can we have a simpler mechanism something like i deny all actions using Auth component and then inside each action or maybe in beforeRender method i can specify what methods are open to what group ?
Thanks
There is an undocumented acl class PhpAcl it is much simpler to use than then the database driven ACL and more dynamic than the ini bassed ACL.
In Config/core.php
/**
* The class name and database used in CakePHP's
* access control lists.
*/
Configure::write('Acl.classname', 'PhpAcl');
// Configure::write('Acl.database', 'default');
This tells your ACL to use the PhpAcl
Then open up Config/acl.php
There are some good instructions there
Assumptions:
In your application you created a User model with the following properties: username, group_id, password, email, firstname,
lastname and so on.
You configured AuthComponent to authorize actions via $this->Auth->authorize = array('Actions' => array('actionPath' =>
'controllers/'),...)
Now, when a user (i.e. jeff) authenticates successfully and requests a
controller action (i.e. /invoices/delete) that is not allowed by
default (e.g. via $this->Auth->allow('edit') in the Invoices
controller) then AuthComponent will ask the configured ACL interface
if access is granted. Under the assumptions 1. and 2. this will be
done via a call to Acl->check() with
array('User' => array('username' => 'jeff', 'group_id' => 4, ...))
as ARO and
'/controllers/invoices/delete'
as ACO.
I wanted to use static names for Groups or Roles so you can add a role field to your user table, and then set up the $map like this:
**
* The role map defines how to resolve the user record from your application
* to the roles you defined in the roles configuration.
*/
$config['map'] = array(
'User' => 'User/username',
'Role' => 'User/role',
);
For my app we aren't using user based permissions only role, so we could remove the User from the $map.
Then you need to set up some roles:
/**
* role configuration
*/
$config['roles'] = array(
'Role/admin' => null,
);
Any role not in this array will get 'Role/default'
Now just set up your permissions, they are pretty self explanatory.
/**
* rule configuration
*/
$config['rules'] = array(
'allow' => array(
'*' => 'Role/admin',
'controllers/Reports/*' => 'Role/default',
'controllers/EurRates/*' => 'Role/default',
'controllers/Posts/index' => 'Role/default',
'controllers/Users/(edit|index)' => 'Role/default',
),
'deny' => array(
'controllers/ProtectedController/*' => 'Role/default',
'controllers/EurRates/(edit|add|delete)' => 'Role/default',
'controllers/Reports/(edit|add|delete)' => 'Role/default',
),
);
That's it, now you can allow or deny permission to actions based on role.

CakePHP permissions : are ACLs overkill

I have two main tables:
Books (id, author, isbn, ...)
Users (id, username, password, ...)
I am looking at building an application, wherein User1 logs in and can:
view list of all books (eg. only title)
view details (author, isbn, ...) of only certain books that he should have access to
Each user may have access to a certain set of books. I don't need various roles.
I have setup the MVC and the relationships (habtm) for the above. I am now looking at developing the permissions. Do you think the CakePHP ACL solves this problem or is it overkill?
If it is overkill, is there another component or easier way to build the desired functionality?
Yes, ACL is overkill
ACL is a very powerful and flexible system - but it doesn't come free it brings with it complexity. Unless you have a usecase where you absolutely need fine-grained permissions (the two rules you've described do not fit this) - don't use ACL.
Restricting to books a user he has added
This rule is easy to implement - e.g. add to relevant find calls:
$results = $BookModelInstance->find('all', array(
'conditions' => array(
'created_by' => AuthComponent::user('id')
)
));
Restricting to books a user he has bought
This rule is also easy to implement, thought slightly more involved:
$BookModelInstance->bindModel(array(
'hasOne' => array( // Yes, hasOne not hasMany
'MyPurchase' => array(
'className' => 'Purchase',
'foriegnKey' => 'user_id'
)
)
));
$results = $BookModelInstance->find('all', array(
'recursive' => 0, // to join hasOne+belongsTo associations into the query
'conditions' => array(
'MyPurchase.user_id' = AuthComponent::user('id'),
)
));
The bindModel call achieves the equivalent of SELECT .. FROM books LEFT JOIN book_users.. The conditions in the find call will therefore restrict results to books where there is a record of the user purchasing the book.
Putting them both together
A simplistic implementation of automatically applying both these rules would be:
model Book extends AppModel {
public $actsAs = array('Containable');
public $restrictToUser = true;
public function beforeSave($options = array()) {
if (!$this->id) {
// Store who created this book
$this->data[$this->alias]['created_by'] = AuthComponent::user('id');
}
return true;
}
public function beforeFind($queryData) {
if (!$this->restrictToUser) {
// we don't want to apply user-level restrictions
return true;
}
$userId = AuthComponent::user('id');
if (!$userId) {
// we want to restrict to the current user - there isn't one.
return false;
}
// define the association to the purchase table
$this->bindModel(array(
'hasOne' => array(
'MyPurchase' => array(
'className' => 'Purchase',
'foriegnKey' => 'user_id'
)
)
));
//ensure the purchase table is included in the current query
$queryData['contain']['MyPurchase'] = array();
// restrict to rows created by the current user, OR purchased by the current user
$queryData['conditions']['OR'] = array(
$this->alias '.created_by' => $userId,
'MyPurchase.user_id' => $userId
);
return $queryData;
}
}
This requires a field created_by (or equivalent) to be in the books table, and uses containable to ensure that the purchases table (or equivalent) is included in all relevant queries.
Most simple solution: Add a condition in your controller, so:
$this->set('books', $this->Book->find(
'all',
array('conditions' => array('Book.user_id' => $user['User']['id']))
);
Disadvantages: You will likely create duplicate code here since this check has to happen also in other places. Also when you start testing your model you can only test that it returns books, you cannot test a model method like: getMyBooks($userId). So no, not the preferred solution.
Next solution: Check in the model
It could be done by a check in for example your books model. You could just check in the afterfind() method whether the returned records are allowed or not. In your beforefind you could also add an additional condition to all queries.
In general a model should be fat so I would suggest implementing clear methods there like: getAllBooks, getBooksOfUser($User), getLatestBooksOfUser($User) etc.
Why is this a nice implementation? Because you now manage the access levels in a central place. You can test the model and you are sure it does only return books from this user.
With beforeSave etc. you can intervene every save attempt and first check: hey, you want to save this but is this really your book?
ACL solution
But in general it could be wise to implement some ACL solution (preferably the built in one) since that makes you application much more future proof. It allows flexibility, for example:
Each user may have access to a certain set of books. I don't need various roles.
That's true for now but the future can change it. So if you need a quick solution just custom filter the records. But think about the future.

How to add index to database with belongsTo association

I'm looking for the best practice for adding an id to a database table. As an example I have a Member (model) that hasMany Recipe. The Recipe model states that it belongsTo Member:
public $belongsTo = array(
'Member' => array(
'className' => 'Member',
'foreignKey' => 'member_id',
)
);
I hoped that this would be sufficient when saving new recipes to the database, but I see that cake cannot figure out the relationship on its own because I am getting an error ("General error: 1364 Field 'member_id' doesn't have a default value") when using add:
public function add() {
if ($this->request->is('post')) {
$this->Recipe->create();
if ($this->Recipe->save($this->request->data)) {
$this->Session->setFlash(__('The recipe has been saved'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The recipe could not be saved. Please, try again.'));
}
}
}
Now, in this case I can easily get around the problem by adding
$this->request->data['Recipe']['member_id'] = $this->Auth->user('id');
but in general the ids that I will want to add will not be stored with the Auth data. So I would like to know how to access the id when saving something that belongsTo something else.
edit: I think my question was probably not worded well enough. I will restate what I'm looking for:
Say User hasMany A, which hasMany B, which hasMany C. C belongsTo B, A, and User (table for C contains user_id, a_id, and b_id). A user logs in, and picks a certain A, and then a certain B. The user now wants to add a new C. What is the best way to get all the id values for the new C entry? Do I just set a bunch of session variables? Should they all be passed in the URL? Is there some better way?
You need to provide any information that you want to save in the database. This means you either use the Auth information like you already stated or you get the information via a form where you can select from all the members available.
Either way the information has to come from somewhere. It can not create itself out of nothing.
If the user is selecting or picking A, B or C, them, somehow you can store their data and deal with them.
Usually, the common way is using forms. If you are using Javascript, you can set hidden inputs or if you are using AJAX, just using them inside it to call the final Cake functions.
You have to see how the user picks them, and then, store that ID in some variable to play with it. POST, GET, SESSIONS, COOKIES...

Resources