Yii Framework Forum: Approach to Multi-Tenant Application using a Behavior - Yii Framework Forum

Jump to content

Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

Approach to Multi-Tenant Application using a Behavior Rate Topic: -----

#1 User is offline   BenB 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 2
  • Joined: 29-October 11
  • Location:Berlin Area, Germany

Posted 14 March 2012 - 12:41 PM

For a current project I needed a solution to Multi-Tenancy using Yii Framework. So that users belonging to tenant A are not able to access any models owned by tenant B.

Basically I got inspired by this post, an approach to Mutli-Tenancy using defaultScope() of CActiveRecord. Thus I used a Tenant-ID-Field on all my tables, which should be divided by tenant. My User-Class fetches this Tenant-ID on login and publishes it via a getter. I will not post this code. For an exampe code how to implement your user class please refer to the original post.

However I used a slightly different approach on my model-classes. Instead of inheriting from a base class I created a behavior.

As I cannot modify the defaultScope() method using a behavior I needed to rely on Events. Thus as a base class for my behavior I use the CActiveRecordBehavior class. Inherriting from this I can react on the events BeforeFind and BeforeSave. Here is my code:
class MultitenantBehavior extends CActiveRecordBehavior {
    
    public function beforeFind($event) {
        //restrict queries to the actual tenant by manipulating the model's DbCriteria
        $c=$this->owner->getDbCriteria();
        $condition=$c->condition;
        if(strlen($condition)>0) {
            $condition='('.$condition.') AND '
        }
        $condition.='tenantId = '.Yii::app()->user->tenantId;
        $c->condition=$condition;   
    }
    
    
    public function beforeSafe($event) {
        //tie this model to the actual tenant by setting the tenantid attribute
 		$this->owner->tenantId=Yii::app()->user->tenantId;
    }
}





In my code you see to filter all results by find methods I overwrite the beforeFind method. In this I modify the condition property of the DbCriteria instance of the owner of this behavior. This is assumed to be an instance of ActiveRecord. This is technically the same as setting the defaultScope().

To make sure all inserts, updates and deletes set the right Tenant-Id I overwrite the beforeSafe method. In this I set the owners attribute tenant_id. This is the same approach as in the original post.

Now if one of model-classes should be filtered by Tenant I just add the following lines of code to it:
        public function behaviors(){
        return array('Multitenant'=>'path.to.MultitenantBehavior');
    }




The behaviors function is an empty function of CActiveRecord. It can be used in model class to return an array of behaviors, which are by default attached to this model. This ensures that my TenantBehavior is automatically attached on the initialization of an instance of my models.
0

#2 User is offline   sebako 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 54
  • Joined: 15-February 12

Posted 14 March 2012 - 02:06 PM

very nice approach! thanks for sharing it!
0

#3 User is offline   waitforit 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 378
  • Joined: 09-February 11

Posted 14 March 2012 - 02:25 PM

You didn't link to another post but I believe it was referring to my multi tenancy solution that I posted last year.

One thing that I've learned since then is to slighly modify your beforeSave() to return the parent.


public function beforeSafe($event) {
        //tie this model to the actual account by setting the model's tbl_account_id attribute
        $this->owner->tenantId=Yii::app()->user->tenantId;
        return parent::beforeSave();
    }

0

#4 User is offline   BenB 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 2
  • Joined: 29-October 11
  • Location:Berlin Area, Germany

Posted 15 March 2012 - 04:01 AM

View Postwaitforit, on 14 March 2012 - 02:25 PM, said:

You didn't link to another post but I believe it was referring to my multi tenancy solution that I posted last year.


@waitforit: Actually I linked your post. But the forum logic erased all links from my first post. Posted Image

Maybe it works to edit my post, if not here is the link to the original post.


View Postwaitforit, on 14 March 2012 - 02:25 PM, said:

One thing that I've learned since then is to slighly modify your beforeSave() to return the parent.

public function beforeSafe($event) {
        //tie this model to the actual account by setting the model's tbl_account_id attribute
        $this->owner->tenantId=Yii::app()->user->tenantId;
        return parent::beforeSave();
    }


Are you sure this necessary in a Behavior? The parent beforeSave method of the CActiveRecordBehavior in this case is an empyt function returning no value (see here).
0

#5 User is offline   natron797 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 10
  • Joined: 21-June 12

Posted 21 June 2012 - 12:17 PM

@BenB,

I like how you have done this. When i try to implement this i run in to problems when a user logs in the beforeSafe()
function gets called and then appends the where clause. I need the where clause not to work when logging into the application so it can set the tennantid var.
0

#6 User is offline   waitforit 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 378
  • Joined: 09-February 11

Posted 23 June 2012 - 11:51 PM

Add a conditional.

if(!Yii::App()->isGuest)
then ...
0

#7 User is offline   natron797 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 10
  • Joined: 21-June 12

Posted 24 June 2012 - 03:38 PM

View Postwaitforit, on 23 June 2012 - 11:51 PM, said:

Add a conditional.

if(!Yii::App()->isGuest)
then ...


Thanks waitforit!! so i could use the beforeSave() like the beforeFind() to make sure that all inserts and updates to the tables get appended with a tenantid ? I have tried and doesn't seem to be working.

I got it working. Thanks again.

Natron797
0

#8 User is offline   zeroByte 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 40
  • Joined: 17-February 12

Posted 03 August 2012 - 02:50 AM

I implemented something similar and I can tell you that beforeFind() is not invoked, if you access models using relations.
I.e. if you have a model A you restrict acces to, A::model->findAll(...) will work fine. However, if you have another model B (which can also be restricted - that actually doesn't matter) and you do B::model()->with('relationToA')->findAll(...), the models of A will be selected without your limitation. If model B isn't restricted, the users see content they're not supposed to see.

What works is using the defaultScope() - the downside is that the developer has to set the defaultScope() for each model because it isn't applyable by behaviors.

Another approach to this problem are restrictions on models with a permission system. I prefer ACL and wrote my own extension for that, docs are here.
The flexibility is higher - so you can assign users to several tenants, accumulating the permissions of the user. As the permission system is entirely independent of your database, you don't have to add your own logic or structures into your design.

Note that what you described above works with my extension out of the box - you don't need to adjust things. (by default, nobody but the owner has access on his own objects - that includes RUD)

Regards,
0

#9 User is offline   jbowler 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 16
  • Joined: 05-August 12

Posted 23 August 2012 - 02:23 PM

I am trying to implement this, using the same behavior code that you provide, but I'm getting an error:

Undefined property: EWebUser::$orgId (btw, I'm using "orgId" instead of "tenantId".)

So, it's not able to access my "EWebUser" class, below.

<?php 
class EWebUser extends CWebUser{
 
    protected $_model;
    
    

	function getName(){
		$user = $this->loadUser($this->id);
		return $user->name;
	} 
	
 	
 	function getOrgId(){
 		$org = $this->loadUser($this->id);
 		return $org->orgId; 	
 	}
 	
 	function getIsAdmin(){
 		$admin = $this->loadUser($this->id);
 		return $admin->admin; 	
 	}
 	
 
    
    protected function loadUser()
    {
        if ( $this->_model === null ) {
                $this->_model = Users::model()->findByPk( $this->id );
        }
        return $this->_model;
    }
    


Any ideas?
0

#10 User is offline   natron797 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 10
  • Joined: 21-June 12

Posted 23 August 2012 - 02:28 PM

Try declaring your $orgId var
0

#11 User is offline   jbowler 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 16
  • Joined: 05-August 12

Posted 23 August 2012 - 04:06 PM

Thanks for your response.

Declaring the variable ("protected $orgId") doesn't help - same error.

fwiw, when I declare it and give it a value, like the below, does "work", in that if my orgId is 1, the behavior is added properly, and there are no errors. So, the problem is in the getOrgId function ...

public $orgId = 1;


I'll keep digging.
0

#12 User is offline   natron797 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 10
  • Joined: 21-June 12

Posted 23 August 2012 - 04:21 PM

are you setting the var when they login?
0

#13 User is offline   jbowler 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 16
  • Joined: 05-August 12

Posted 23 August 2012 - 04:37 PM

No, the methods are simply available in Yii::app()->user, I can use getName by doing Yii::app()->user->name. These methods work elsewhere, like in my view files.

fwiw, if I cheat, and put "return 1" at the top of my getOrgId method, it does return "1".
0

#14 User is offline   jbowler 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 16
  • Joined: 05-August 12

Posted 23 August 2012 - 04:42 PM

btw, here's my "MultitenantBehavior extends CActiveRecordBehavior" content:

class MultitenantBehavior extends CActiveRecordBehavior {


public function beforeFind($event) {

//filter out non logged in users, removing the below line will break login
//if(user()->isGuest()){return}

//restrict queries to the actual tenant by manipulating the model's DbCriteria
$c=$this->owner->getDbCriteria();
$condition=$c->condition;
if(strlen($condition)>0) {
$condition='('.$condition.') AND ';
}
$condition.='orgId = '.user()->orgId;
$c->condition=$condition;
}


public function beforeSave($event) {
//tie this model to the actual tenant by setting the tenantid attribute
$this->owner->orgId=Yii::app()->user->orgId;
}
}

Please note that I have a global shortcut file that shortens Yii:app()->getUser() to just "user()" - works fine. Just in case, I did try this with the full name, with the same result.

This post has been edited by jbowler: 23 August 2012 - 04:44 PM

0

#15 User is offline   natron797 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 10
  • Joined: 21-June 12

Posted 24 August 2012 - 08:55 AM

try putting the orgId var into a session when you set it and then use that session in the class MultitenantBehavior. this is what i had to do to get it to work. It's all coming back to me now. Let me know if that worked.
0

#16 User is offline   jbowler 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 16
  • Joined: 05-August 12

Posted 24 August 2012 - 11:28 AM

I added this line in the MultitenantBehavior file:

if(empty(user()->orgId)){return;}


in place of this

if(user()->isGuest()){return}


and that fixed it.

It seems like the problem was occurring at login. I confused myself as I constantly changed the code around, after logging in.

Thanks for your help, I really appreciate it.
0

#17 User is offline   natron797 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 10
  • Joined: 21-June 12

Posted 24 August 2012 - 11:41 AM

Anytime. Glad you got it fixed!!
0

#18 User is offline   jbowler 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 16
  • Joined: 05-August 12

Posted 24 August 2012 - 12:07 PM

I'm going to be "that guy" and post another question. I've got this working, almost. It displays the right data, but the pagination text reads "displaying 1-6 of 7 results", the 7th result belonging to another "tenant". So, the "paginator" is counting all the records in the model. How can I apply the behavior to this:

$model=new Users('search');


I tried a function similarly named to the above, and called it "beforeSearch", but no success .. I'll let you know if I figure this out.
0

#19 User is offline   jbowler 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 16
  • Joined: 05-August 12

Posted 24 August 2012 - 12:28 PM

I tried using default scope, and that seems to work better, but only works with SELECT statements, though.

My project is becoming a patchwork ... is there anyone else trying to setup a MultiTenant architecture finding this, too? Or, am I doing things the wrong way ...

Basically, now I'm looking at needing Default Scope for Selects, and Behavior for Saves ...
0

#20 User is offline   Gerhard Liebenberg 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 311
  • Joined: 07-January 12
  • Location:Stillbay - Western Cape - South Africa

Posted 07 February 2013 - 04:50 PM

View PostzeroByte, on 03 August 2012 - 02:50 AM, said:

I implemented something similar and I can tell you that beforeFind() is not invoked, if you access models using relations.
I.e. if you have a model A you restrict acces to, A::model->findAll(...) will work fine. However, if you have another model B (which can also be restricted - that actually doesn't matter) and you do B::model()->with('relationToA')->findAll(...), the models of A will be selected without your limitation. If model B isn't restricted, the users see content they're not supposed to see.

What works is using the defaultScope() - the downside is that the developer has to set the defaultScope() for each model because it isn't applyable by behaviors.

Another approach to this problem are restrictions on models with a permission system. I prefer ACL and wrote my own extension for that, docs are here.
The flexibility is higher - so you can assign users to several tenants, accumulating the permissions of the user. As the permission system is entirely independent of your database, you don't have to add your own logic or structures into your design.

Note that what you described above works with my extension out of the box - you don't need to adjust things. (by default, nobody but the owner has access on his own objects - that includes RUD)

Regards,



Another problem with filtering records with behavior and beforeFind(), seems to be that the resulting CGridView's pagination is not working properly.
0

Share this topic:


Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users