Symfony voter and multiples reasons - php

I will use a simple situation for explain :
I have "news" entity
I have "new categories" entities
In administration, I want to check if I can delete news category.
If you dont have "ROLE_SUPERADMIN", you can't ;
If news category is linked (= used in category), you can't.
When control that ?
If I use Symfony Voters :
class NewsCategoryVoter extends Voter {
....
private function canDelete(NewsCategory $newsCategory, User $user)
{
// Check ROLE and Count in NewsRepository if $newsCategory is used. I have not yet coded this.
return false;
}
I have a problem :
I can't get the reason why he can not remove. In twig and after is_granted('delete', category), idealy :
You can't delete because ...
Can you help me ?
Please, keep in mind that this situation is very simple. In my situation, I have many reasons (> 10) to reject a deletion or modification, almost always because of a relationship in database

Because the Voter is just another service, you can add whatever properties, or other classes/services you want or need to be able to store some sort of reason as to why something did, or did not happen.
public static $reason;
// in the voter, make grant/deny/abstain choices...
if ($user->hasRole('ROLE_SUPER_ADMIN')) {
self::$reason = "is super-admin";
$this->log->debug("FeatureVoter | {$user} is super-admin");
return VoterInterface::ACCESS_GRANTED;
}
// after is_granted()
echo VoterClass::$reason;
I already had logging in the voter, and so some other notification mechanism would be just as easy. Here, I've just added a static variable in the Voter, and can read it out externally. You can trivially make that an array that could be added to, (and cleared before voting started), or noting a reason why something did, or didn't happen in an external service that can be retrieved.

Related

How in SonataAdminBundle get current admin class?

How in SonataAdminBundle
get the current admin class without using AdminPool?
Now I'm trying to do it like this
$entityClass = get_class($entity);
$adminClass = $this->adminPool->getAdminByClass($entityClass);
But this method has a problem. If the entity is associated with several classes of the admin, an exception will be thrown.
Is there a way to find out what exactly the admin service should handle the current route?
Thanks!
If you have multiple admins registered for this entity's class, nothing can choose the correct one for you.
You can still get a specific admin with the method Pool::getAdminByAdminCode(string $code).
For example, an usage for you could be:
if ($entityClass === MultipleAdminRegisteredEntity::class) {
$admin = $this->adminPool->getAdminById('specific_admin_id');
} else {
$entityClass = get_class($entity);
$admin = $this->adminPool->getAdminByClass($entityClass);
}
Please pay attention to the fact that the Pool::getAdminByClass(string $class) returns an Admin and not a class string: you named your variable $adminClass which suggests you made this confusion.
Also note that there is an open issue on Github here: https://github.com/sonata-project/SonataAdminBundle/issues/3908 to determine a way to be able define default admins when there are more than one admin for an entity, so that the Pool:getAdminByClass() method doesn't throw an exception. Nobody seems to have care enough about this to implement it, feel free to contribute there if you want.

CakePHP: authorizing actions based on belongsTo relationships

Let's keep it simple.
A project has just two models:
User (hasMany Project)
Project (belongsTo User)
Users are only allowed to perform actions on the projects which they own. No one else's.
I know how to manually check who the logged in user is and whether or not he/she owns a specific project, but is there a better, more global way to do this? I'm looking for a more D.R.Y. way that doesn't require repeating the same validation inside multiple actions. For example, is there a config setting like maybe...
Configure::write('Enforce_belongs_to', true);
...or maybe a setting/option on the Auth component.
Maybe this is crazy, but I figured I'd ask.
Adding to Nunser's answer, here would be a general concept of how the behavior would be. You can then attach it to the applicable model.
class StrongBelongBehavior extends ModelBehavior
{
public function beforeFind( Model $Model, $query = array() ) {
$query['conditions'] = array_merge( (array)$query['conditions'], array( $Model->alias.'.user_id' => CakeSession::read("Auth.User.id" ) );
return $query;
}
public function beforeSave( Model $Model ) {
$projectId = Hash::get( $Model->data, 'Poject.id' );
if( $projectId ) {
$Model->loadModel('UserProject'); // UserProject is a custom model
$canEdit = $Model->UserProject->projectIDExists( $projectId ); // returns true if projectId belongs to the current user
if ( ! $canEdit ) {
return false;
}
}
return true;
}
}
I'm not sure if what I'm answering is the best-utermost-dry-it's-almost-dehydrating approach, but is the simplest thing I could think of.
In the Project model, create a function that return an array of project ids associated to an user.
class Project extends AppModel {
public function getByUserId($userId) {
$projectsArray = array();
if ($userId != "valid")
//do all the checks, if it's not null, numeric, if the id exists, etc
$projects = $this->Project->find('all', array('conditions'=>
array('user_id'=>$userId)));
if (!empty($projects)) {
foreach($projects as $i => $project)
$projectsArray[] = $project['Project']['id'];
}
return $projectsArray;
}
}
You mention a find('first') in your comment, but I'm assuming you want all the projects related to the user instead of just the first. If not, it's a simple modification of that function. Also, I'm just getting the ids, but you may want an $id=>$name_project array, up to you.
Now, I don't know what you mean by "only allowed to perform actions", is it just edits that are restricted? Or lists or views should be restricted and not even shown to the user if the project is not his/hers?
For the first case, restrict editing, modify beforeSave.
public function beforeSave($options = array()) {
if(!$this->id && !isset($this->data[$this->alias][$this->primaryKey])) {
//INSERT
//not doing anything
} else {
//UPDATE
//check if project inside allowed projects array
$allowed = $this->getByUserId(CakeSession::read("Auth.User.id"));
if (!in_array($this->id, $allowed))
return false; //or throw error and catch it in the controller
}
return true;
}
The code is untested, but in general terms, you prevent the edit of a project that is not "the user's" just before the update of the record. I assume the insert of new projects is free for everyone. According to this post, all saving functions except saveAll pass through this filter first, so you will need to overwrite the saveAll function and add a validation similar to the one in beforeSave (as explained in the answer there).
And for the second part, filtering results so the user isn't even aware that there are other projects instead of his/hers, change beforeFind. The docs talk about restricting results based on user's roles, so I guess we're on the right track.
public function beforeFind($queryData) {
//force the condition
$allowed = $this->getByUserId(CakeSession::read("Auth.User.id"));
$queryData['conditions'][$this->alias.'.user_id'] = $allowed;
return $queryData;
}
Since the $allowed array has just id values, it'll work like an IN clause, but if you change that array structure, be sure to modify these functions accordingly.
And that's it. I'm thinking about the more basic cases here, edit, view, delete... Ups, delete... change the beforeDelete function also, to avoid any evil users who want to delete others property. The logic remains the same (check if project id is in allowed array, if not, return false or throw error), so I won't add the example of that function here. But that's the basic stuff. If for some reason you want to have the allowed projects in the controller, call the getByUserId function in beforeFilter and handle that ids array there. You can even store it in session, but you'll have to have in mind maintaining that session when adding or deleting projects.
If you want a superadmin that can see and edit everything, just add a condition in getByUserId that checks the role of the user, and if it is an admin, return all projects.
Also, keep in mind: maybe Project has many... subprojects (not much imagination), and so, the user related to the project can add subprojects, but the same evil user as before modifies the hidden project_id that subproject has and edits it. In that case, I recommend you also add a validation in Subproject to avoid actions on models related to Project that are not his. If you have the Security component in place and the edit and delete actions can just be reached by forms, this is a minor thing because Security Component well used avoids form tampering. But give it a thought to see if you need to validate "Subproject" instances also.
As Ayo Akinyemi mentioned, you can use all this as a behavior. I haven't personally done so, but it meets the requirements, all the callbacks modified here are what you modify in a behaviour. You'll have to encapsulate the logic, column names (need to be variable an not set hardcoded, like user_id), etc, but it will be reusable in any other cake project you'll have. Something like StrongBelongBehavior or MoreDRYBehavior. And share it if you do it :)
I'm not sure if Auth component has some way of doing what you want, but that would be the best option I guess. Until some enlightens me (I haven't investigate much this issue), this is the solution I'd use.

Working with related model in CakePHP

This is a fairly basic question about CakePHP, but since my knowledge of this framework is rather rusty, it is making me lose a lot of time.
I have a ManyToMany relation between Guest and Present. Whenever a new Guest is created and associated with a present, I would like to mark the Present as taken. If the present is already taken, some error should arise. The reason why I am not just declaring that a Guest hasMany Presents is because in the future things may change and more than one guest could associate to a present, so I prefer to avoid a Db migration.
My Guest::add() action looks like follows. It is called with a POST with the data of a new Guest and the id of an existing Present.
public function add() {
if ($this->request->is('post')) {
$id = $this->request->data['Present']['id'];
$this->Guest->create();
$present = $this->Guest->Present->findById($id);
if ($present['Present']['taken']) {
throw new ForbiddenException();
}
if ($this->Guest->save($this->request->data)) {
if ($this->Guest->Present->saveField('taken', true)) {
// Give the guest a uuid and proceed with a welcome message
$this->Guest->read();
$this->set('uuid', $this->Guest->data['Guest']['uuid']);
}
}
}
else {
throw new ForbiddenException();
}
}
What happens is that a new Guest is created (correct) and associated with the given present (correct) but when I save the taken field a new present is created instead of modifying the given one.
What is the correct way to proceed to update the current Present?
If it is of any help, I am using CakePHP 2.0
For obtaining the model data by the primary key it's better to use theIn addition read method:
$present = $this->Guest->Present->read(null, $id);
The read method sets the model's id attribute so that further calls to other methods affect the same data record, rather than creating a new one. This should solve the problem you are having.
Model callbacks tend to be better suited for these situations. You could add a beforeSave callback to the Guest class to checks if the present is already taken, and not allow the creation if it is. This way the model logic is left in the model layer and you don't need to do any extra work e.g. if the constraint has to be enforced also when existing Guests are saved, or created from different controllers or actions.
It sounds like the ID of the model you are trying to save is losing scope. You should be able to resolve your issue by updating your code:
...
if ($this->Guest->save($this->request->data)) {
$this->Guest->Present->id = $id;
if ($this->Guest->Present->saveField('taken', true)) {
...

Best way to code Achievements system

I'm thinking of the best way to design an achievements system for use on my site. The database structure can be found at Best way to tell 3 or more consecutive records missing and this thread is really an extension to get the ideas from developers.
The problem I have with lots of talk about badges/achievement systems on this website is just that -- it's all talk and no code. Where's the actual code implemention examples?
I propose here a design that I hope people could contribute to and hopefully create a good design for coding extensible achievement systems. I'm not saying this is the best, far from it, but it's a possible starting block.
Please feel free to contribute your ideas.
my system design idea
It seems the general consensus is to create an "event based system" -- whenever a known event occurs like a post is created, deleted, etc it calls the event class like so..
$event->trigger('POST_CREATED', array('id' => 8));
The event class then finds out what badges are "listening" for this event, then it requires that file, and creates an instance of that class, like so:
require '/badges/' . $file;
$badge = new $class;
It then calls the default event passing the data received when trigger was called;
$badge->default_event($data);
the badges
This is then where the real magic happens. each badge has its own query/logic to determine if a badge should be awarded. Each badge is set out in e.g. this format:
class Badge_Name extends Badge
{
const _BADGE_500 = 'POST_500';
const _BADGE_300 = 'POST_300';
const _BADGE_100 = 'POST_100';
function get_user_post_count()
{
$escaped_user_id = mysql_real_escape_string($this->user_id);
$r = mysql_query("SELECT COUNT(*) FROM posts
WHERE userid='$escaped_user_id'");
if ($row = mysql_fetch_row($r))
{
return $row[0];
}
return 0;
}
function default_event($data)
{
$post_count = $this->get_user_post_count();
$this->try_award($post_count);
}
function try_award($post_count)
{
if ($post_count > 500)
{
$this->award(self::_BADGE_500);
}
else if ($post_count > 300)
{
$this->award(self::_BADGE_300);
}
else if ($post_count > 100)
{
$this->award(self::_BADGE_100);
}
}
}
award function comes from an extended class Badge which basically checks to see if the user has already be awarded that badge, if not, will update the badge db table. The badge class also takes care of retrieving all badges for a user and returning it in an array, etc (so badges can be e.g. displayed on the user profile)
what about when the system is very first implemented on an already live site?
There is also a "cron" job query that can be added to each badge. The reason for this is because when the badge system is very first implemented and initilaised, the badges that should have already been earned have not yet be awarded because this is an event based system. So a CRON job is run on demand for each badge to award anything that needs to be. For example the CRON job for the above would look like:
class Badge_Name_Cron extends Badge_Name
{
function cron_job()
{
$r = mysql_query('SELECT COUNT(*) as post_count, user_id FROM posts');
while ($obj = mysql_fetch_object($r))
{
$this->user_id = $obj->user_id; //make sure we're operating on the right user
$this->try_award($obj->post_count);
}
}
}
As the above cron class extends the main badge class, it can re-use the logic function try_award
The reason why I create a specialised query for this is although we could "simulate" previous events, i.e. go through every user post and trigger the event class like $event->trigger() it would be very slow, especially for many badges. So we instead create an optimized query.
what user gets the award? all about awarding other users based on event
The Badge class award function acts on user_id -- they will always be given the award. By default the badge is awarded to the person who CAUSED the event to happen i.e. the session user id (this is true for the default_event function, although the CRON job obviously loops through all users and awards seperate users)
So let's take an example, on a coding challenge website users submit their coding entry. The admin then judges the entries and when complete, posts the results to the challenge page for all to see. When this happens, a POSTED_RESULTS event is called.
If you want to award badges for users for all the entries posted, lets say, if they were ranked within the top 5, you should use the cron job (although bare in mind this will update for all users, not just for that challenge the results were posted for)
If you want to target a more specific area to update with the cron job, let's see if there is a way to add filtering parameters into the cron job object, and get the cron_job function to use them. For example:
class Badge_Top5 extends Badge
{
const _BADGE_NAME = 'top5';
function try_award($position)
{
if ($position <= 5)
{
$this->award(self::_BADGE_NAME);
}
}
}
class Badge_Top5_Cron extends Badge_Top5
{
function cron_job($challenge_id = 0)
{
$where = '';
if ($challenge_id)
{
$escaped_challenge_id = mysql_real_escape_string($challenge_id);
$where = "WHERE challenge_id = '$escaped_challenge_id'";
}
$r = mysql_query("SELECT position, user_id
FROM challenge_entries
$where");
while ($obj = mysql_fetch_object($r))
{
$this->user_id = $obj->user_id; //award the correct user!
$this->try_award($obj->position);
}
}
The cron function will still work even if the parameter is not supplied.
I've implemented a reward system once in what you would call a document oriented database (this was a mud for players). Some highlights from my implementation, translated to PHP and MySQL:
Every detail about the badge is stored in the users data. If you use MySQL I would have made sure that this data is in one record per user in the database for performance.
Every time the person in question does something, the code triggers the badge code with a given flag, for instance flag('POST_MESSAGE').
One event could also trigger a counter, for instance a count of number of posts. increase_count('POST_MESSAGE'). In here you could have a check (either by a hook, or just having a test in this method) that if the POST_MESSAGE count is > 300 then you should have reward a badge, for instance: flag("300_POST").
In the flag method, I'd put the code to reward badges. For instance, if the Flag 300_POST is sent, then the badge reward_badge("300_POST") should be called.
In the flag method, you should also have the users previous flags present. so you could say when the user has FIRST_COMMENT, FIRST_POST, FIRST_READ you grant badge("NEW USER"), and when you get 100_COMMENT, 100_POST, 300_READ you can grant badge("EXPERIENCED_USER")
All of these flags and badges need to be stored somehow. Use some way where you think of the flags as bits. If you want this to be stored really efficiently, you think of them as bits and use the code below: (Or you could just use a bare string "000000001111000" if you don't want this complexity.
$achievments = 0;
$bits = sprintf("%032b", $achievements);
/* Set bit 10 */
$bits[10] = 1;
$achievements = bindec($bits);
print "Bits: $bits\n";
print "Achievements: $achievements\n";
/* Reload */
$bits = sprintf("%032b", $achievments);
/* Set bit 5 */
$bits[5] = 1;
$achievements = bindec($bits);
print "Bits: $bits\n";
print "Achievements: $achievements\n";
A nice way of storing a document for the user is to use json and store the users data in a single text column. Use json_encode and json_decode to store/retrieve the data.
For tracking activity on some of the users data manipulated by some other user, add a data structure on the item and use counters there as well. For instance read count. Use the same technique as described above for awarding badges, but the update should of course go into the owning users post. (For instance article read 1000 times badge).
UserInfuser is an open source gamification platform which implements a badging/points service. You can check out its API here:
http://code.google.com/p/userinfuser/wiki/API_Documentation
I implemented it and tried to keep the number of functions minimal. Here is the API for a php client:
class UserInfuser($account, $api_key)
{
public function get_user_data($user_id);
public function update_user($user_id);
public function award_badge($badge_id, $user_id);
public function remove_badge($badge_id, $user_id);
public function award_points($user_id, $points_awarded);
public function award_badge_points($badge_id, $user_id, $points_awarded, $points_required);
public function get_widget($user_id, $widget_type);
}
The end result is to show the data in a meaningful way through the use of widgets. These widgets include: trophy case, leaderboard, milestones, live notifications, rank and points.
The implementation of the API can be found here: http://code.google.com/p/userinfuser/source/browse/trunk/serverside/api/api.py
Achievements can be burdensome and even more so if you have to add them in later, unless you have a well-formed Event class.
This segues into my technique of implementing achievements.
I like to split them first into 'categories' and within those have tiers of accomplishment. i.e. a kills category in a game may have an award at 1 for first kill, 10 ten kills, 1000 thousand kills etc.
Then to the spine of any good application, the class handling your events. Again imagining a game with kills; when a player kills something, stuff happens. The kill is noted, etc and that is best handled in a centralized location, like and Events class that can dispatch info to other places involved.
It falls perfectly into place there, that in the proper method, instantiate your Achievements class and check it the player is due one.
As building the Achievements class it is trivial, just something that checks the database to see if the player has as many kills as are required for the next achievement.
I like to store user's achievements in a BitField using Redis but the same technique can be used in MySQL. That is, you can store the player's achievements as an int and then and that int with the bit you have defined as that achievement to see if they have already gained it. That way it uses only a single int column in the database.
The downside to this is you have to have them organized well and you will likely need to make some comments in your code so you will remember what 2^14 corresponds to later. If your achievements are enumerated in their own table then you can just do 2^pk where pk is the primary key of the achievements table. That makes the check something like
if(((2**$pk) & ($usersAchInt)) > 0){
// fire off the giveAchievement() event
}
This way you can add achievements later and it will dovetail fine, just NEVER change the primary key of the achievements already awarded.

Using the Data Mapper Pattern, Should the Entities (Domain Objects) know about the Mapper?

I'm working with Doctrine2 for the first time, but I think this question is generic enough to not be dependent on a specific ORM.
Should the entities in a Data Mapper pattern be aware - and use - the Mapper?
I have a few specific examples, but they all seem to boil down to the same general question.
If I'm dealing with data from an external source - for example a User has many Messages - and the external source simply provides the latest few entities (like an RSS feed), how can $user->addMessage($message) check for duplicates unless it either is aware of the Mapper, or it 'searches' through the collection (seems like an inefficient thing to do).
Of course a Controller or Transaction Script could check for duplicates before adding the message to the user - but that doesn't seem quite right, and would lead to code duplication.
If I have a large collection - again a User with many Messages - how can the User entity provide limiting and pagination for the collection without actually proxying a Mapper call?
Again, the Controller or Transaction Script or whatever is using the Entity could use the Mapper directly to retrieve a collection of the User's Messages limited by count, date range, or other factors - but that too would lead to code duplication.
Is the answer using Repositories and making the Entity aware of them? (At least for Doctrine2, and whatever analogous concept is used by other ORMs.) At that point the Entity is still relatively decoupled from the Mapper.
Rule #1: Keep your domain model simple and straightforward.
First, don't prematurely optimize something because you think it may be inefficient. Build your domain so that the objects and syntax flow correctly. Keep the interfaces clean: $user->addMessage($message) is clean, precise and unambiguous. Underneath the hood you can utilize any number of patterns/techniques to ensure that integrity is maintained (caching, lookups, etc). You can utilize Services to orchestrate (complex) object dependencies, probably overkill for this but here is a basic sample/idea.
class User
{
public function addMessage(Message $message)
{
// One solution, loop through all messages first, throw error if already exists
$this->messages[] $message;
}
public function getMessage()
{
return $this->messages;
}
}
class MessageService
{
public function addUserMessage(User $user, Message $message)
{
// Ensure unique message for user
// One solution is loop through $user->getMessages() here and make sure unique
// This is more or less the only path to adding a message, so ensure its integrity here before proceeding
// There could also be ACL checks placed here as well
// You could also create functions that provide checks to determine whether certain criteria are met/unmet before proceeding
if ($this->doesUserHaveMessage($user,$message)) {
throw Exception...
}
$user->addMessage($message);
}
// Note, this may not be the correct place for this function to "live"
public function doesUserHaveMessage(User $user, Message $message)
{
// Do a database lookup here
return ($user->hasMessage($message) ? true
}
}
class MessageRepository
{
public function find(/* criteria */)
{
// Use caching here
return $message;
}
}
class MessageFactory
{
public function createMessage($data)
{
//
$message = new Message();
// setters
return $message;
}
}
// Application code
$user = $userRepository->find(/* lookup criteria */);
$message = $messageFactory->create(/* data */);
// Could wrap in try/catch
$messageService->sendUserMessage($user,$message);
Been working with Doctrine2 as well. Your domain entity objects are just that objects...they should not have any idea of where they came from, the domain model just manages them and passes them around to the various functions that manage and manipulate them.
Looking back over, I'm not sure that I completely answered your question. However, I don't think that the entities themselves should have any access to the mappers. Create Services/Repositories/Whatever to operate on the objects and utilize the appropriate techniques in those functions...
Don't overengineer it from the onset either. Keep your domain focused on its goal and refactor when performance is actually an issue.
IMO, an Entity should be oblivious of where it came from, who created it and how to populate its related Entities. In the ORM I use (my own) I am able to define joins between two tables and limiting its results by specifying (in C#) :
SearchCriteria sc = new SearchCriteria();
sc.AddSort("Message.CREATED_DATE","DESC");
sc.MaxRows = 10;
results = Mapper.Read(sc, new User(new Message());
That will result in a join which is limited to 10 items, ordered by date create of message. The Message items will be added to each User. If I write:
results = Mapper.Read(sc, new Message(new User());
the join is reversed.
So, it is possible to make Entities completely unaware of the mapper.
No.
Here's why: trust. You cannot trust data to act on the benefit of the system. You can only trust the system to act on data. This is a fundamental of programming logic.
Let's say something nasty slipped into the data and it was intended for XSS. If a data chunk is performing actions or if it's evaluated, then the XSS code gets blended into things and it will open a security hole.
Let not the left hand know what the right hand doeth! (mostly because you don't want to know)

Categories