Yii2 authentication based on record level - activerecord

Within yii2 I need some RBAC access control also on record level. As a yii2 beginner I'm searching for the best point to enter the logic but struggle through the documents.
Example:
A table Children mentions a child, besides lots of other children.
A child usually has two parents in table Parents.
Besides other access control with yii2-admin/user these two parents can view and manipulate the record of their own child/ren but not others.
the logged in user is a parent.
Example table Children:
|id|name|age|
|1|Max|10|
|2|Moritz|11|
|3|Lena|8|
...
Example table Parents:
|id|relation|name|
|1|mother|Anna|
|2|father|Paul|
|3|mother|Lisa|
...
Example table Xref (Relation to Children and Parents):
|child_id|parent_id|
|1|1|
|1|2|
|2|3|
|3|1|
|3|2|
...
I think the activeRecord class Children would be the the right place for a behaviour like that, right?
Does someone have an example code to point me to the right direction for an efficient code,
where Paul and Anna could modify the record for 'Max' but not for 'Moritz'?

I hardly ever use relational DBs with Yii2 but I will illustrate what I meant as best as possible:
I'm going to illustrate the Parent Class as the child will be roughly similar (appart from the relationship direction):
class ParentModel extends ActiveRecord
{
/**
* #return string the name of the table associated with this ActiveRecord class.
*/
public static function tableName()
{
return 'Parents';
}
public function getChildren()
{
// ParentModel has_many ChildModel via Xref.child_id -> id
return $this->hasMany(ChildModel::className(), ['id' => 'child_id'])
->viaTable('Xref', ['parent_id' => 'id']);
}
}
With this you can get all children by using (for example):
$user = ParentModel::findOne($userID);
$children = $user->children;
You can then use all RBAC functionalities to make sure you users can't access an edit form action/view. Or use RBAC rules to make sure only the appropriate content is being handled.
Or you could write your own checks to make sure (for example) that the children being handled really belong to the Parent accessing the controller action. (by say... comparing the user's children to the ones being handled, though depending on your structure this could be handled by RBAC Rules)
But regardless of the actions, that logic should remain on the controller layer. On some very rare occasions you might have to put access right logic in your models but the models should have that logic separated and not related to the RBAC system. I've personally had this issue with graph traversal logic which is on an ActiveQuery level. But that's another issue in it's own right.

Related

Laravel polymorphic hasMany relationship

From Laravel's docs, the model polymorphism is defined as follows:
Polymorphic relations allow a model to belong to more than one other model on a single association
Sounds like it's designed to work with belongsTo instead of hasMany side. Here's a scenario that I want to achieve:
In my system, there are many project types, each projec type will have its own invoice field layout. Let's say we have a Project model that has a type field, whose value could be contract or part-time. We have another two tables called ContractInvoice and PartTimeInvoice to define their respective field layout, both of these invoice tables have a project_id referencing a project record. What I want to do is I want a universal interface to retrieve all invoices given a project, something like $project->invoices.
Current solution
I can't figure out how to achieve this via polymorphism. So what I am currently doing is kind silly, using a switch statement in my invoice() method on Project model class:
switch ($this->type) {
case 'contract':
$model = 'App\ContractInvoice';
break;
case 'part-time':
$model = 'App\PartTimeInvoice';
break;
}
return $this->hasMany($model);
I feel like there must be a better way to do this. Can someone please shed some light?
I don't see how a polymorphic relationship would be beneficial in this case. If you had different project type models and a single invoices table, then the invoices could morphTo the projects. But as you've described it, the switch statement sounds like it is adequate. You could achieve the same means using when conditionals like:
public function invoices()
{
return $this->when($this->type === 'contract', function () {
return $this->hasMany(ContractInvoice::class);
})->when($this->type === 'part-time', function () {
return $this->hasMany(PartTimeInvoice::class);
});
}
The type attribute on the Project model and the separate invoice tables are defining a rigid relationship between them, which goes against the idea of polymorphism. Think likes for comments and posts.

Laravel Relationship with OR case

Assume I have a User model, and also I have Couple model which forms of 2 users, father_id and mother_id which are essentially user_ids
On User model, I have
public function kids() {
return $this->hasMany('App\Kid', 'father_id');
}
However, I want to check if user_id is either father_id or mother_id, return the related Kid model.
Is there a way to achieve it with a single relationship? What is the proper way of handling this scenario, so I can use $user->kids that would check for both cases?
There is a way, but you wouldn't typically use it to "check" if there are related models.
If you have a field that determines if the model is representing a father or mother, such as is_father, you could do:
public function kids()
{
return ($this->is_father)
? $this->hasMany(Kid::class, 'father_id')
: $this->hasMany(Kid::class, 'mother_id');
}
Essentially, the relationship method MUST return a relationship instance. But you can do logic before you return this.
NOTE: The relationship is cached, so even if the is_father value changes in the same thread run, it will utilize the same relationship that it did before. This can cause unwanted bugs.

Relationship mapping with NeoEloquent

I'm tinkering with Neo4j 2.3.0 and Laravel 5.1 using NeoEloquent. I've set up a couple of dummy nodes and some relationships between them:
image of Neo4j model - apologies, I cannot insert images directly yet :)
So articles can use a template. The inverse of this relationship is that a template is used by an article.
I've set up the classes like so:
Class Template extends Model
{
public function articles()
{
return $this->hasMany('App\Article', 'USED_BY');
}
}
And:
Class Article extends Model
{
public function template()
{
return $this->belongsTo('App\Template', 'USES');
}
}
So far, so good, I think.
I have a page where I am wanting to eventually list all of the articles in the system, along with some useful metadata, like the template each ones uses. For this, I have set something up in the controller:
$articles = array();
foreach (Article::with('template')->get() as $article) {
array_push($articles, $article);
}
return $articles;
Not the most elegant, but it should return the data for both the article and it's associated template. However:
[{"content":"Some test content","title":"Test Article","id":28,"template":null},{"content":"Some flibble content","title":"Flibble","id":31,"template":null}]
So the question is - why is this returning null?
More interestingly, if I set up the relationship to the same thing in BOTH directions, it returns the values. i.e. if I change the USED_BY to USES, then the data is returned, but this doesn't make sense from an architectural point of view - a template does not 'use' an article.
So what am I missing?
More interestingly, if I set up the relationship to the same thing in BOTH directions, it returns the values.
That's correct, because this is how it operates. It is worth knowing that the relationship methods you have defined represent the relationship itself, which means for both models Template and Article to target the USED_BY relationship from any side it has to be the same in articles() and template.
The solution would be to use something like USES (or any notion you like) on both sides. This reference should help you make good decisions regarding your relationships.
On the other hand, if you still wish to have different relations on the sides then kindly note that in your model (image) both relationships are in outgoing direction. i.e. Fibble-[:USES]->Template and Template-[:USED_BY]->Fibble which means template() should be an outgoing relationship such as hasOne instead of belongsTo which is incoming.

Does Eloquent handle caching of related entities?

I'm exploring Laravel's Eloquent as a drop-in replacement for my project's current, home-grown active record data layer. Currently, I have a class User that supports a many-to-many relationship with another class, Group. My current implementation looks something like:
class User {
protected $_groups; // An array of Group objects to which this User belongs
public function __construct($properties = []){
...
}
public function groups() {
if (isset($_groups))
return $_groups;
else
return $_groups = fetchGroups();
}
private function fetchGroups() {
// Lazily load the associated groups based on the `group_user` table
...
}
public function addGroup($group_id) {
// Check that the group exists and that this User isn't already a member of the group. If so, insert $group_id to $_groups.
...
}
public function removeGroup($group_id) {
// Check that the User is already a member of the group. If so, remove $group_id from $_groups.
...
}
public function fresh() {
// Reload user and group membership from the database into this object.
...
}
public function store() {
// Insert/update the user record in the `user` table, and insert/update/delete records in `group_user` based on the contents of `$_group_user`.
...
}
public function delete() {
// If it exists, delete the user record from the `user` table, and delete all associated records in `group_user`.
...
}
}
As you can see, my class:
Performs lazy loading of related groups, caching after the first time they're queried;
Maintains an internal representation of the User's relationship with their Groups, updating in the database only when store is called;
Performs sanity checks when establishing relationships, making sure that a Group exists and is not already related to the User before creating a new association.
Which, if any of these things, will Eloquent automatically take care of for me? Or, is my design flawed in some way that Eloquent can solve?
You can assume that I will re-implement User as User extends Illuminate\Database\Eloquent\Model and use Eloquent's belongsToMany as a replacement for my current fetchGroups method.
Eloquent caches the results of relationships internally, yes. You can see that in action in the Model::getRelationValue() method.
Eloquent also provides you with methods to help you manage the many-to-many relationship. You could implement this functionality inside your existing API. However, here are some things to look out for:
When using attach(), detach(), etc., the queries are performed immediately. Calling the parent User::save() method would only save the User's details, not the many-to-many relationship information. You could work around this by storing the IDs passed to your API temporarily, and then act upon them when you call User::store().
No sanity checks are performed when using attach/detach/etc. It would be good to apply these in your API if they're needed.
Adding or removing an ID to/from a many-to-many relationship will not affect the cached results of the initial relationship query. You would have to add in logic to insert or remove the related models into the collection.
For example, let's say a User has two Groups. When I load the user, I can access those groups using $user->groups. I now have a Collection of Groups cached inside the User model. If I call for $user->groups again, it will returned this cached Collection.
If I remove one Group using $user->detach($groupId), a query will be performed to update the join table, but the cached Collection will not change. Same goes for adding a group.

Can I include a convenience query in a Doctrine 2 Entity Method?

I'd like to include some additional functions in my Doctrine 2 entities to contain code that I'm going to have to run quite frequently. For example:
User - has many Posts
Post - has a single user
I already have a function $user->getPosts(), but this returns all of my posts. I'm looking to write a $user->getActivePosts(), which would be like:
$user->getPosts()->where('active = true') //if this were possible
or:
$em->getRepository('Posts')->findBy(array('user'=>$user,'active'=>true)) //if this were more convenient
As far as I can tell, there's no way to get back to the entity manager though the Entity itself, so my only option would be
class User {
function getActivePosts() {
$all_posts = $this->getPosts();
$active_posts = new ArrayCollection();
foreach ($all_posts as $post) {
if ($post->getActive()) {
$active_posts->add($post);
}
}
return $active_posts;
}
However, this requires me to load ALL posts into my entity manager, when I really only want a small subset of them, and it requires me to do filtering in PHP, when it would be much more appropriate to do so in the SQL layer. Is there any way to accomplish what I'm looking to do inside the Entity, or do I have to create code outside of it?
I think you should implement the method on the PostRepository rather than on the entity model.
I try to keep all model related logic in the repositories behind "domain specific" methods. That way if you change the way you represent whether a post is active or not, you only have to change the implementation of a single method instead of having to find all the active = true statements scattered around in your application or making changes in an "unrelated" entity model.
Something like this
PostRepository extends EntityRepository {
public function findActiveByUser($user){
// whatever it takes to get the active posts
}
}

Resources