I want want to restrict the records based on the user logged in viewing the records.
For example I have following two models:
Assignments
Users
So if a student (user) is viewing the summary of assignments he should be able to see only his assignments and able to perform view/delete/edit only his assignment.
But if a teacher (user) is viewing summary of assignments then he should see all assignments and can perform add/edit/delete on all assignments.
I am already aware I can do this by putting group conditions in find and then appropriate in code in view/edit/delete actions also.
My Question is - what is the best ways to handle scenarios like this in cakephp? Putting conditions everywhere don't seems good way to me.
Consider as two separate problems to solve
Access control
The first is how to deny students who e.g. just manipulate the url to attempt to view/edit/delete things they don't own. For that use isAuthorized, there's an example in the book, adapted to the info in the question that'd be:
// app/Controller/AppController.php
public $components = array(
'Session',
'Auth' => array(
'authorize' => array('Controller') // Added this line
)
);
public function isAuthorized($user) {
// Teachers can access/do everything - adapt to whatever identifies a teacher
if ($user['is_teacher'])) {
return true;
}
// Anyone logged in can access the index
if ($this->action === 'index') {
return true;
}
// The owner of a whatever can view, edit and delete it
$id = $this->request->params['pass'][0];
$owner = $this->{$this->modelClass}->field('user_id', array('id' => $id));
if ($owner === $user['id'])) {
return true;
}
// Default deny
return false;
}
Restrict student data
The event system, available since 2.1, is an easy way to enforce the data restriction mentioned. Again there's a relevant example in the book, adapted to the information in the question, that'd be:
// app/Controller/AssignmentsController.php
public function beforeFilter() {
if (!$this->Auth->user('is_teacher')) {
$currentUser = $this->Auth->user('id');
$this->Assignment->getEventManager()->attach(
function(CakeEvent $e) use ($currentUser) {
$e->data[0]['conditions']['user_id'] = $currentUser;
return $e->data[0];
},
'Model.beforeFind'
);
}
}
This will add a user_id condition to all finds, therefore the index listing will show only their own assignments, whereas for a teacher it will show all assignments.
I would create 2 separate models - StudentAssignments and TeacherAssignments. both models can extend Assignments model. you can then filter your conditions in the beforeFind of each model.
This way, you have decoupled models and you can manipulate them appropriately.
Example:
App::uses('Assignment', 'Model');
class StudentAssignments extends Assignment
{
function beforeFind( $queryData ) {
$queryData['conditions'] = array_merge( (array)$queryData['conditions'],
array( $this->alias.'.user_id' => CakeSession::read('User.id') ) );
return parent::beforeFind($queryData);
}
}
Then you can call $this->StudentAssignments->find('all'); to pull all assignments for a single user
Related
The title says it all, but to give an example. I have a Member record and a Group. A member can have memberships in many groups and a group can have many members. (So that's many to many and I would have a pivot table for it.)
Now, each group has membership grades. E.g., (Free, Freemium, Premium, Super Premium). So the membership_grade shall belong to the pivot table, right? But here's the problem, not all groups share the same grades. Some might have Free and Freemium only, some might have all.
In the fields.yaml of the Membership pivot model, I defined the membership_grades as a Relation Widget, like this:
pivot[grade]:
label: Membership Grade
span: full
type: relation
nameFrom: name
And in its relationship in Membership.php like this:
public $belongsTo = [
'grade' => [
'Acme\Models\Grade',
]
];
Obviously, this will expose ALL grades, since I'm pulling data from the Grade model. What I want is to expose the grades that is just available on that group, not all.
What I've thought to do (but I didn't, because it seemed impossible) is to try to pull data from the grades relationship of the Group, but how am I suppose to do that? (Since Relation widget manages the relation of the Model, I cannot simply pull data from other sources just like that).
Also I've tried to do scopes but how am I suppose to pass the current Group I'm in? Since it is needed as the filter, like this:
// Membership.php
public $belongsTo = [
'grade' => [
'Acme\Models\Grade',
'scope' => 'filteredIt'
],
// added this relationship to try the scopes approach
'group' => [
'Acme\Models\Group'
]
];
// Grade.php
public function scopeFilteredIt($query, Membership $m)
// yes, the second parameter in the scope will be the
// current Membership model. I've tried it.
{
// this won't work, since we want the overall relation filter;
// an instance of Membership won't help.
// this would work if I can find a way to pass the
// current Group (record) selected, and get its grades, then use it here.
return $query->whereIn('id', $m->group->grades->pluck('id')->all());
}
Any thoughts?
I have noticed some post values during pivot model ajax call.
When you add new record and when your pivot model opens post values are like this
Array (
[_relation_field] => groups
[_relation_extra_config] => W10=
[foreign_id] => 1
[_session_key] => VrSCoKQrSkIsZNGIju5QIqpdbS3AADoGQRHAsv1e
)
So good thing is that we can now get foreign_id as it will be your selected group id
and we can use it at creation time and for update time you know we have relation so we use that.
public function scopefilteredIt($query, Membership $m)
{
// we are checking relation is there or not
if($m->group) {
// yes group is there we use it
return $query->whereIn('id', $m->group->grades->pluck('id')->all());
}
else {
// seems new record then use foreign_id
$foreign_id = post('foreign_id'); //<-this will be your selected group id
if($foreign_id) { // <- double check if its there
$group = Group::find($foreign_id);
return $query->whereIn('id', $group->grades->pluck('id')->all());
}
}
return $query;
}
please comment if you get any issue.
to check post
public function scopefilteredIt($query, Membership $m)
{
// will show flash message with post data array
$post = print_r(post(), true);
\Flash::success($post);
// we are checking relation is there or not
if($m->group) {
// yes group is there we use it
return $query->whereIn('id', $m->group->grades->pluck('id')->all());
}
else {
// seems new record then use foreign_id
$foreign_id = post('foreign_id'); //<-this will be your selected group id
if($foreign_id) { // <- double check if its there
$group = Group::find($foreign_id);
return $query->whereIn('id', $group->grades->pluck('id')->all());
}
}
return $query;
}
I am trying to implement RBAC in my project by following the tutorial* on the Yii website. However I am confused when trying to implement the permissions by group.
For this example I have added a group field into the user table and have defined two groups, user (2) and admin (1).
I then created a console command which looks like this:
class RbacController extends Controller
{
public function actionInit()
{
$auth = \Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$search = $auth->createPermission('search');
$search->description = 'Search';
$search->ruleName = $rule->name;
$auth->add($search);
$user = $auth->createRole('user');
$user->ruleName = $rule->name;
$auth->add($user);
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $user);
}
}
And I have this file: rbac/UserGroupRule.php
class UserGroupRule extends Rule
{
public $name = 'userGroup';
public function execute($user, $item, $params)
{
// return true; // force return to true for test
if(!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->group;
if($item->name === 'search') {
return $group == 1;
}
return false;
}
}
I'm trying to test the permission with if(\Yii::$app->user->can('search')).
Firstly, I wonder why the console command is required here as I can't see where it's being used.
The $item parameter in the execute method has the value of search, but the tutorial shows that it expects this value to be role type.
Regardless of what I return in the execute method, it seems to return false.
Can anyone answer these questions?
http://www.yiiframework.com/doc-2.0/guide-security-authorization.html
I guess you have an authManager with DbManager ?
'authManager' => [
'class' => 'yii\rbac\DbManager',
],
to init the rbac from the console just use yii rbac/init in a console (in correct project dir) then the database entries were done (before that the rbac tables should be empty)
if you haven't done yet create the tables with
yii migrate --migrationPath=#yii/rbac/migrations
$item is just the auth permission or role entry. The rule is called for every entry, if you have added a rule. In your case for permission "search" and roles "user" and "admin" the rules is executed.
your have added entries with rule checking. So if you e.g. check if the user can "search" by e.g.
if (\Yii::$app->user->can('search')) {
// can search
}
then the rule is checked or executed (which is your UserGroupRule). And in your case it would return true for admins and false for user given by the group field.
edit:
I hope you have added this to your components in your config file.
return [
// ...
'components' => [
'authManager' => [
'class' => 'yii\rbac\DbManager',
],
// ...
],
];
You have created 2 roles in your rbac (user/admin) and as far as i understand your are using a group column in the User table to allocate those roles to the user. And in your code you will need to have to check the permissions or roles. So from the DB the correct Entry is selected and if a Rule is attached this rule is then executed. And this checks the current user group and returns true or false. So in your case no assignments to those roles or permissions are done. It uses the Rule to return true or false depending on the user group. But here are other extensions search for yii2admin or yii2rbac, where you can also assign user to roles/permissions etc by database entries.
I would say you should get more help where you can "chat" e.g. the yii chat which is linked on the yii homepage.
I'm implementing relationships in Eloquent, and I'm facing the following problem:
An article can have many followers (users), and a user can follow many articles (by follow I mean, the users get notifications when a followed article is updated).
Defining such a relationship is easy:
class User extends Eloquent {
public function followedArticles()
{
return $this->belongsToMany('Article', 'article_followers');
}
}
also
class Article extends Eloquent {
public function followers()
{
return $this->belongsToMany('User', 'article_followers');
}
}
Now, when listing articles I want to show an extra information about each article: if the current user is or is not following it.
So for each article I would have:
article_id
title
content
etc.
is_following (extra field)
What I am doing now is this:
$articles = Article::with(array(
'followers' => function($query) use ($userId) {
$query->where('article_followers.user_id', '=', $userId);
}
)
);
This way I have an extra field for each article: 'followers` containing an array with a single user, if the user is following the article, or an empty array if he is not following it.
In my controller I can process this data to have the form I want, but I feel this kind of a hack.
I would love to have a simple is_following field with a boolean (whether the user following the article).
Is there a simple way of doing this?
One way of doing this would be to create an accessor for the custom field:
class Article extends Eloquent {
protected $appends = array('is_following');
public function followers()
{
return $this->belongsToMany('User', 'article_followers');
}
public function getIsFollowingAttribute() {
// Insert code here to determine if the
// current instance is related to the current user
}
}
What this will do is create a new field named 'is_following' which will automatically be added to the returned json object or model.
The code to determine whether or not the currently logged in user is following the article would depend upon your application.
Something like this should work:
return $this->followers()->contains($user->id);
I am building a Cake PHP application. Different users have different properties so I use two objects to store a user for example
User hasOne Student / Student belongs to User
User hasOne Lecturer / Lecturer belongs to User
The profile edit page will allow the User to edit all their details for both objects. I've set up the form and used saveAll so save both objects. My problem is dynamically populating the dropdown menus depending on which role the user has.
For example the counties field. Admin does not have an address whereas Student and Lecturer do. I have setup my Country model to find all my counties and put them into opt-groups in the select box (sorting them by country as shown here Dropdown list with dyanmic optgroup)
I can do this fine inside the Students/LecturersController as they allow me to access the Country model as I set $uses variable. I do not want to do this inside the UsersController as not all user roles have an address and an address is never stored inside the User object. I tried putting the code in the model but then I don't know how to access other Models inside a Model. Up to now I've had no problem building the app and I feel that I may have made a bad design decision somewhere or there's something I'm not understanding properly.
Essentially I'm asking how do I implement the setForForm() function below.
public function edit() {
//get user
$user = $this->Auth->user();
//get role
$role = $user['User']['role'];
if ($this->request->is('post') || $this->request->is('put')) {
//get IDs
$userID = $user['User']['id'];
$roleID = $user[$role]['id'];
//set IDs
$this->request->data[$role]['user_id'] = $userID;
$this->request->data[$role]['id'] = $roleID;
$this->request->data['User']['id'] = $userID;
//delete data for role that is not theirs
foreach ($this->request->data as $key => $value) {
if($key !== 'User' && $key !== $role) {
unset($this->request->data[$key]);
}
}
if ($this->User->saveAll($this->request->data)) {
//update logged in user
$this->Auth->login($this->User->$role->findByUserId($userID));
$this->Session->setFlash('Changes saved successfully.');
} else {
$this->Session->setFlash(__('Please try again.'));
}
}
//set role for easy access
$this->set(compact('role'));
//sets required variables for role form
$this->User->$role->setForForm();
//fills in form on first request
if (!$this->request->data) {
$this->request->data = $user;
}
//render form depending on role
$this->render(strtolower('edit_' . $role));
}
//this is the method I would like to implement in the Student/Lecturer model somehow
public function setForForm() {
$counties = $this->Country->getCountiesByCountry();
$homeCounties = $counties;
$termCounties = $counties;
$this->set(compact('homeCounties', 'termCounties'));
}
Not sure if I get your question correct, but I think what you want is the following:
User hasOne Student / Student belongs to User
and
Student hasOne Country / Country belongsTo Student
(and the same for Lectureres)
then from your UsersController you can do:
$this->User->Student->Country->findCountiesByCountry();
hope that helps
--
EDIT:
if you want to want to use $role instead of Student/Lecturer you would have to do it like this:
$this->User->{$role}->Country->findCountiesByCountry();
In the end I decided to just load counties in the user controller regardless of whether the form will need them or not since Admin is the only user that doesn't need them.
Try something like this:
public function setForForm() {
App::import('model', 'Country');
$Country = New Country();
$counties = $Country->getCountiesByCountry();
$homeCounties = $counties;
$termCounties = $counties;
$this->set(compact('homeCounties', 'termCounties'));
}
I don't know is this the best solution or not but it is working at least :)
Edited
Here is another mistake. Its not recommended to set variable from model to view , you can return data that will be set then in edit function normally set it to the view , but anyways if you need to set from model to the view you can load controller class to your model using App::import(); and use set function.
I've a standard Gii created admin view, which use a CGridView, and it's showing my user table data.
the problem is that user with name 'root' must NOT BE VISIBLE.
Is there a way to add a static where condition " ... and username !='root' " ?
admin.php [view]
'columns'=>array(
'id',
'username',
'password',
'realname',
'email',
.....
user.php [model]
public function search()
{
// Warning: Please modify the following code to remove attributes that
// should not be searched.
$criteria=new CDbCriteria;
$criteria->compare('id',$this->id);
$criteria->compare('username',$this->username,true);
$criteria->compare('password',$this->password,true);
$criteria->compare('realname',$this->realname,true);
$criteria->compare('email',$this->email,true);
......
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
You can use CDbCriteria's addCondition like this:
$criteria->addCondition("username !='root'");
Your best option would be to use Yii scopes which are essentially a saved where clause (or other modification of your existing criteria) that you can apply all over your app and only need to change in one place if your criteria ends up changing later.
What makes them even cooler is that you can string them together with other scopes / criteria changes (from users in grids for instance) without having to keep track of what criteria clause is getting changed by what.
A few examples that might apply to your situation. In your controller you probably have something like this:
$users = User::model()->search()->findAll();
Asgaroth's answer answers what you were asking on the surface. But there is so much more you can do (and do easily) using scopes.
If you add the below to your user model:
class User extends CActiveRecord
{
......
public function scopes()
{
return array(
'active'=>array(
'condition'=>'active=1',
),
'isAdmin'=>array(
'condition'=>'isAdmin=1',
),
);
}
}
then you can retrieve active users (with your users' filters still applied) like this in your controller:
$users = User::model()->active()->search()->findAll();
Or you can retrieve all active admin users (without being filtered by your gridview criteria) like this:
$users = User::model()->active()->isAdmin()->findAll();
Default scopes are just an extension of the same idea:
class User extends CActiveRecord
{
public function defaultScope()
{
return array(
'condition'=>"username != 'root'",
);
}
}
If before your isAdmin scope would return the root user, applying the default scope will eliminate the root user from the models returned, as it applies to every User::model() query you make.