I'm a Yii developer who want to follow MVC pattern.
In one part of my current project I have to check a special condition and according to it's result show a list of something to user. I want to determine if the user is admin or a guest.
There is two choice for me here, one is to determine two method one for admin and one for other user and check if the user is admin or not in the view, something like this:
$this->widget('zii.widgets.grid.CGridView', array(
'id' => $tableName . "_grid",
'dataProvider' => (Yii::app()->user->id == User::ADMIN) ? $model->search() : $model->getMyList(),
'filter' => $model,
'columns' => array(
....
or change the getMineList method in the model. By this fact that I can't put this statement in the controller which one is better and cleaner?
The best way would to do the logic in the model by changing the getMineList or, as GBD stated, implement it into the search method of the model
Related
I've been developing web apps using Yii 1.1.14 so far, but now it's time for an upgrade.
The company where I work has developed its own Access Control system, and I was really OK with it until I saw what it was really like... A combination of 8 tables in the database (not counting the users table), with a bunch of foreign keys.
1 table for controllers
1 table for the actions
1 table for the menu categories
1 table for types of users
And the other tables basically just connect 2 or 3 of those tables at a time.
It works well, but in my point of view it's highly time consuming to maintain all those tables, and at some point, when your application goes online, if it hits a certain amount of users it could get really slow. specially because 2 of those tables have the user's table primary key as foreign key.
So I've decided that, when I start developing on Yii 2, I'm going to start using RBAC, so I started looking for tutorials online... Only finding many different versions of the same code with author's role, and permissions for create or update posts.
I found a combination of 5 videos on Youtube, but they are about Yii 1 RBAC. They were helpful because I managed to understand most of RBAC's functionality, but I still have some doubts that I'll
enumerate below. And keep in mind that for this Access Control system I'm using the DBManager class.
My Doubts
Yii 1's RBAC used to have 3 tables: auth_assignment, auth_item and auth_item_child. Now in Yii 2 RBAC, a new table appears that is called auth_rule and I still don't understand what that specific table is doing there, how to use it or how to populate it.
I see that it's possible to restrict the user's access to some actions by using the controller's behavior method, and assigning access to some actions depending on the user's role, but when it comes to this I have to split my question into 2:
2.1. First: If you can just restrict the access to actions by setting it up in the behaviors method, then what's the use of saving permissions to the auth_item table?
2.2. Second: If you DO decide to control access according to permissions, then how exactly do you do it, because I find myself writing the following type of code inside of every function and I don't think using RBAC is supposed to be this tedious. There has to be another way.
public function actionView($id)
{
if(Yii::$app->user->can('view-users')){
return $this->render('view', [
'model' => $this->findModel($id),
]);
}else{
#Redirect to a custom made action that will show a view
#with a custom error message
$this->redirect(['//site/notauthorized']);
}
}
Because of the Access Control System that we use right now, when a user logs in, a complex query is executed that will end up returning an array that will be saved as a session variable, and will be used to create a menu with as many dropdownlists as menu categories, that the controllers that the user has access to belong to. How can this be done with RBAC?
I can only really answer 2.2 of your question, as 3 doesn't sound at all like something an RBAC should do. You could, however, get the information you needed from the rules table most likely, provided you followed a naming convention that matched your controllers or actions.
On to answering 2.2 though:
You can simply set the behavior like such:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'actions' => ['view'],
'roles' => ['view-users'], //<-- Note, rule instead of role
],
]
]
}
This doesn't solve a different problem of 'view-own-users' style permissions, as this needs to inspect the ActiveRecord model (well, at least it does in my application). If You want to achieve this, take a look at my post in the Yii forums here:
http://www.yiiframework.com/forum/index.php/topic/60439-yii2-rbac-permissions-in-controller-behaviors/#entry269913
I use it in one of the simplest method,I use them in the behaviours of my controller.
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['sysadmin'],
'actions' => ['index','view','update'],
],
[
'allow' => true,
'roles' => ['staff'],
'actions' => ['index','create','update','view'],
],
],
],
];
}
Here roles are the one created in the auth-item table in the database and they have been assigned for users in auth-assignment table. In the behaviours we just use it as above. In the above code sysadmin can have access to index, view and update action, whereas staff can have access to index,create, update and view action.
Yii2 needs a little setup when it comes to using RBAC under your controllers AccessControl. I got around it by making my own AccessRule file.
namespace app\components;
use Yii;
class AccessRule extends \yii\filters\AccessRule
{
protected function matchRole($user)
{
if (empty($this->roles)) {
return true;
}
foreach ($this->roles as $role) {
if(Yii::$app->authManager->checkAccess($user->identity->code, $role))
return true;
}
return false;
}
then in your controller u can use something like this:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'ruleConfig' => [
'class' => 'app\components\AccessRule'
],
'rules' => [
[
'actions' => ['index', 'resource-type'],
'allow'=> true,
'roles' => ['admin'],
],
],
],
];
}
Where admin is defined as a auth_item and the user is in the auth_item_assignments.
As I have created a new Rbac system for yii2. you can direct permission for a action and action will show you are not authorisez for this action.
By this you find that you will only provide access for action that need to identify.
I uploaded my detail here you can find lot of solution here.
This is the best solution i could come up with when facing the need to filter access by permissions, it's bothersome but can be useful if you're trying to create roles in a productive enviroment and want to use rbac.
use yii\web\ForbiddenHttpException;
if(Yii::$app->user->can('view-users')){
return $this->render('view', [
'model' => $this->findModel($id),
]);
}else{
throw new ForbiddenHttpException('You dont have access to this site');
}
The CMS i'm developing using Cakephp 2.0 has two main Controllers:
Pages
Categories
I'm trying to set the route.php to have the following behavior:
If the user request a Page, the URL should be something like:
http://www.something.com/pages-alias/article-name/id/whatever
If the user address a Category, the URL should be something like:
http://www.something.com/categories-alias/category-name/id/whatever
Please notice that following categories and pages i've used "alias".
To clarify with an example, the URLs for a website of a local restaurant will be:
http://www.something.com/course/wild-boar/68/2013-07-18
Where "course" will substitute "page". And
http://www.something.com/menu/valentine-day/8/2014-01-30
Where "menu" will substitute "category".
The View should not be explicited in the URL nor the Routing rules.
Both the cases will have the view automatically choosen by the controller after some internal check (having subcategory, having only one page or more pages, and so on) so that will be overridden by the controller.
I've got some clues about the use of sort-of "alias" to build the routing rules but unfortunately the documentation was not clear enough to me about how to manage the object to create my own custom route.
So, can someone try to explain it with some example different from the ones available in the CakePhP 2.x documentation?
Thanks in advance to anyone that can be helpful.
For reference i'll paste here the links i've already read:
Routing - Cakephp 2.0 Documentation
Bakery Article from Frank (i suppose this is for the v1.3)
That is what you want probably:
Router::connect(
'/:category_alias/:category_name/:id/:whatever',
array('controller' => 'Article', 'action' => 'view'),
array('pass' => array('category_alias','category_name','id','whatever'),
'id' => '[0-9]+')
);
ofc you can delete this validator for id.. or add more validators :)
Then you can use function in ArticleController.php
public function view($category_alias, $category_name, $id, $whatever) {}
I am using the yii framework and have different user accounts. When I want to have a look at the view page of user 4 I have to enter an url like www.mydomain.com/user/4 for update I have www.mydomain.com/user/update/4.
Is there a way so that I can hide the user id from the url line?
How can I set a route like www.mydomain.com/user/username and www.mydomain.com/user/update/username? Do I have to modify the routes?
What if the username contains an # symbols?
If each user has a unique username, you can describe a route that doesn't show their user id by putting this custom routing rule in config/main:
'urlManager' => array(
'rules' => array(
'user/<username:\S+>' => 'users/view',
)
)
The \S+ should accept "#". Then pass that username to controllers/UsersController like so:
public function actionView($username) {...}
Instead of www.mydomain.com/user/update/username I would use www.mydomain.com/user/<username>/update and put this in the above urlManager rules array:
'user/<username:\S+>/update' => 'users/update',
and this in UsersController:
public function actionUpdate($username) {...}
You can also generate a unique, random number for each user before creation and route based on that number.
I think it's better to encrypt your id before passing it. And decrypt it when you reach there. There are many ways of generating such random/encrpted ids. For more security you should not give name like 'user', that you did. Try to modify that even. Hope you got the point...:)
I've been tasked with rewriting an existing website with large pre-existing link catalog. For argument's sake, let's assume we can't do anything that would change the link catalog. Here's a few examples of the link structure we're working with:
An item page would be:
www.domain.com/widgets/some-totally-awesome-large-purple-widget
A category sub page page would be:
www.domain.com/widgets/purple-widgets
A category parent page page would be:
www.domain.com/widgets/
A custom page may be:
www.domain.com/some-random-page
The various page types are too numerous to write individual Routers for.
Using Router::connect I can easily account for the first and second scenarios using something like:
Router::connect('/{:pageroot}/{:pagekey}', 'Pages::index');
In turn, the Pages::index method looks for entries in our database with the "key" of '/widgets/purple-widgets'.
However, the framework defaults to the '/{:controller}/{:action}/{:args}' route for pages like the third and fourth. I know that this is the correct behavior for the framework. Also, best practice would state that I should write the site to match this behavior. But, that isn't an option here.
What I need is a Router that would allow the third and fourth examples to function the same as the first. All examples should be sent to the Pages::index controller, which in turn queries a database using the URL path as a key.
If you don't have any convention in the URL for what is what, between page, item and category. I'd go with a very generic router.
Router::connect('/{:category}/{:page}/{:item}', 'Pages::any');
Router::connect('/{:category}/{:page}', array('Pages::any', 'item' => null));
Router::connect('/{:category}', array('Pages::any', 'page' => null, 'item' => null));
And in Pages::any() to search for the correct stuff. Is that category a page after all (example 4)? Is that page an item (example 1)?
or
You store the URL somewhere (e.g. a mapping table in the database) and use the pattern version of a lithium Route.
Router::connect(new Route(array(
'pattern' => '#^/(?<path>.+)$#',
'params' => array('controller' => 'pages', 'action' => 'any'),
'keys' => array('path' => 'path'),
// extra stuff, if the path is `tata`, it skips this route and uses
// any of the following ones that matches.
'handler' => function($request) {
if ($request->params['path'] == 'tata') {
return false;
} else {
return $request;
}
}
)));
From that point, you'll get the full URL.
You probably should write a smart Router Helper which is maybe able to process your request based on your db defined routes.
Take a look into: net/http/Router.php
especially connect(), parse() and match()
I would start to write some kind of anonymous function and progress it to a testable Class which is located in /extension.. ?
I am trying to test a controller action that allows edition of user profiles. Among other things I want to test that every logged user can only edit its own profile and not other's. In case of breaking this restriction the action must redirect to a predefined home page.
With this scenario, I have a fixture that creates a user with ID = 1. So I was thinking on testing the restriction this way:
$data = $this->Users->User->read(null, 1);
$this->Users->Auth->login($data);
$this->testAction('/users/edit/2', array('method' => 'get'));
$url = parse_url($this->headers['Location']);
$this->assertEquals($url['path'], '/homepage');
The test passes this assert. So the next step is to check if executing '/users/edit/1', which has the ID of the logged user, shows the form:
$this->testAction('/users/edit/1', array('method' => 'get', 'return' => 'vars'));
$matcher = array(
'tag' => 'form',
'ancestor' => array('tag' => 'div'),
'descendant' => array('tag' => 'fieldset'),
);
$this->assertTag($matcher, $this->vars['content_for_layout'], 'The edition form was not found');
However this assert fails. After digging around with debug() I've found that $this->Auth->user() returns the whole information but $this->Auth->user('id') returns null. Since I use the latter in a comparison within the action, it evaluates as false and causes the
test to fail.
The curious thing is that it happens when testing but not when executing the action in a browser. So, what's the correct way of testing this action?
Thanks!
The actual correct answer should be using mock objects instead of actually login the user in manually:
$this->controller = $this->generate('Users', array(
'components' => array('Auth' => array('user')) //We mock the Auth Component here
));
$this->controller->Auth->staticExpects($this->once())->method('user') //The method user()
->with('id') //Will be called with first param 'id'
->will($this->returnValue(2)) //And will return something for me
$this->testAction('/users/edit/2', array('method' => 'get'));
Using mocks is the most easy way to test a controller, and also the most flexible one
Update 11 March 2015
You can also mock all method of AuthComponent
$this->controller = $this->generate('Users', array(
'components' => array('Auth') // Mock all Auth methods
));
I like Jose's answer, but when faced with a similar situation I want to use the actual AuthComponent and Session to create a test that would give me confidence.
I am using Controller-based authentication, which means that each controller in my app must provide its own isAuthorized() callback. I want to test MyController::isAuthorized(). It seems too easy to get a test to pass using mocks.
So ,instead of using TestCase::generate() to create a mock controller with mock components, I followed Mark Story's excellent article Testing CakePHP Controllers the hard way and provided my own mock controller that logs in a user with the the real CakePHP AuthComponent.
Here's my work. See the method testIsAuthorized() and the class def for MockAnnouncementsController near the top.
It seems to me that CakePHP testing framework assumes that you want to test controllers only through requestAction(). It was not designed to facilitate direct unit-testing of callback implementations like Controller::isAuthorized() within a controller without mocking the AuthComponent and perhaps other components, and that would give me less confidence in test of that particular method. Nevertheless, I think this is a valid use-case for unit-testing parts of a controller that are not actions (e.g. "index", "view"), but cannot be delegated to a component because they must be called by the core framework. I'm still thinking about how I could abstract it to make it available for any controller.
Instead of:
$this->Auth->user('id')
Try one of these:
$this->Auth->data['User']['id']
$this->Session->read('Auth.User.id')
Set it like so:
$this->Users->Session->write('Auth.User',
array('id' => 1,'and_other_fields_you_need' => 'whatever')
);
Mark Story gives me the answer in a CakePHP ticket. Basically I have to log the user like this:
$data = $this->Users->User->read(null, 1);
$this->Users->Auth->login($data['User']);
instead of
$data = $this->Users->User->read(null, 1);
$this->Users->Auth->login($data);