Author: gnat
Date: 2008-08-28 18:29:52 +0100 (Thu, 28 Aug 2008)
New Revision: 4855

Added:
   branches/1.0/docs/cookbook/en/record-based-retrieval-security-template.txt
Modified:
   branches/1.0/docs/cookbook/en.txt
Log:
new article

Added: 
branches/1.0/docs/cookbook/en/record-based-retrieval-security-template.txt
===================================================================
--- branches/1.0/docs/cookbook/en/record-based-retrieval-security-template.txt  
                        (rev 0)
+++ branches/1.0/docs/cookbook/en/record-based-retrieval-security-template.txt  
2008-08-28 17:29:52 UTC (rev 4855)
@@ -0,0 +1,361 @@
+++ Introduction
+
+This is a tutorial & how-to on using a security template and listener to 
restrict a user to specific records, or a range of
+specific records based on credentials and a user table association. Basically 
fine grained user access control.
+
+This template was created for a project which had a few credentials, 
division_manager, district_manager, branch_manager, and salesperson.
+We have a list of accounts, their related sales and all sorts of sensitive 
information for each account. Each logged in user should be allowed
+to only view the accounts and related information based off their credentials 
+ either the division, district, branch or salesperson they are allowed to view.
+So a division manager can view all info for all accounts within his division. 
A salesperson can only view the accounts they are assign.
+
+The template has been a work in progress so the code below may not actually be 
the final code I'm using today. But since it is now working for all situations 
+I'm asking of it, I thought I would post it as is.
+
+++ Template
+<code>
+
+class gsSecurityTemplate extends Doctrine_Template
+{
+    protected $_options = array();
+
+    /**
+     * __construct
+     *
+     * @param string $options
+     * @return void
+     */
+    public function __construct(array $options)
+    {
+        if( !isset($options['conditions']) || empty($options['conditions']) )
+            throw new Doctrine_Exception('Unable to create security template 
without conditions');
+
+        $this->_options = $options;
+    }
+
+    public function setUp()
+    {
+        $this->addListener(new gsSecurityListener($this->_options));
+    }
+}
+
+class gsSecurityListener extends Doctrine_Record_Listener
+{
+    private static
+        $_user_id = 0,
+        $_credentials = array(),
+        $_alias_count = 30;
+
+    protected $_options = array();
+
+    /**
+     * __construct
+     *
+     * @param string $options
+     * @return void
+     */
+    public function __construct(array $options)
+    {
+        $this->_options = $options;
+    }
+
+    public function preDqlSelect(Doctrine_Event $event)
+    {
+        $invoker = $event->getInvoker();
+        $class   = get_class($invoker);
+        $params  = $event->getParams();
+
+        if($class == $params['alias'])
+            return;
+
+        $q       = $event->getQuery();
+
+        // only apply to the main protected table not chained tables... may 
break some situations
+        if(!$q->contains('FROM '.$class))
+            return;
+
+        $wheres = array();
+        $pars   = array();
+
+        $from = $q->getDqlPart('from');
+
+        foreach($this->_options['conditions'] as $rel_name => $conditions)
+        {
+            $apply = false;
+            foreach($conditions['apply_to'] as $val)
+            {
+                if(in_array($val,self::$_credentials))
+                {
+                    $apply = true;
+                    break;
+                }
+            }
+
+            if($apply)
+            {
+                $alias = $params['alias'];
+                $aliases = array();
+                $aliases[] = $alias;
+
+                foreach($conditions['through'] as $key => $table)
+                {
+                    $index = 0;
+                    $found = false;
+                    foreach($from as $index => $val)
+                    {
+                        if(strpos($val,$table) !== false)
+                        {
+                            $found = true;
+                            break;
+                        }
+
+                    }
+
+                    if($found)
+                    {
+                        $vals = explode(' ', 
substr($from[$index],strpos($from[$index],$table)));
+                        $alias = (count($vals) == 2) ? $vals[1]:$vals[0];
+                        $aliases[] = $alias;
+                    }
+                    else
+                    {
+                        $newalias = 
strtolower(substr($table,0,3)).self::$_alias_count++;
+                        $q->leftJoin(end($aliases).'.'.$table.' '.$newalias);
+                        $aliases[] = $newalias;
+                    }
+                }
+
+                $wheres[] = '('.end($aliases).'.'.$conditions['field'].' = ? 
)';
+                $pars[] = self::$_user_id;
+            }
+        }
+
+        if(!empty($wheres))
+            $q->addWhere( '('.implode(' OR ',$wheres).')',$pars);
+    }
+
+    static public function setUserId($id)
+    {
+        self::$_user_id = $id;
+    }
+
+    static public function setCredentials($vals)
+    {
+        self::$_credentials = $vals;
+    }
+}
+</code>
+
+++ YAML schema syntax
+
+Here is the schema I used this template with. I've removed lots of extra 
options, other templates I was using, indexes and table names. It may not work 
out of 
+the box without the indexes - YMMV.
+
+<code>
+---
+Account:
+  actAs:
+    gsSecurityTemplate:
+      conditions:
+        Division:
+          through:  [ Division, UserDivision ]
+          field: user_id
+          apply_to: [ division_manager ]
+        Branch:
+          through:   [ Branch, UserBranch ]
+          field: user_id
+          apply_to: [ branch_manager ]
+        Salesperson:
+          through:  [ Salesperson, UserSalesperson ]
+          field: user_id
+          apply_to: [ salesperson ]
+        District:
+          through:  [ Branch, District, UserDistrict ]
+          field: user_id
+          apply_to: [ district_manager ]
+  columns:
+    id: { type: integer(4), primary: true, autoincrement: true, unsigned: true 
}
+    parent_id: { type: integer(4), primary: false, autoincrement: false, 
unsigned: true}
+    business_class_id: { type: integer(2), unsigned: true }
+    salesperson_id: { type: integer(4), unsigned: true }
+    branch_id: { type: integer(4), unsigned: true }
+    division_id: { type: integer(1), unsigned: true }
+    sold_to: { type: integer(4), unsigned: true }
+
+Division:
+  columns:
+    id: { type: integer(1), autoincrement: true, primary: true, unsigned: true 
}
+    name: { type: string(32) }
+    code: { type: string(4) }
+
+District:
+  actAs:
+    gsSecurityTemplate:
+      conditions:
+        Division:
+          through:  [ Division, UserDivision ]
+          field: user_id
+          apply_to: [ division_manager ]
+  relations:
+    Division:
+      foreignAlias: Districts
+      local: division_id
+      onDelete: RESTRICT
+  columns:
+    id: { type: integer(4), autoincrement: true, primary: true, unsigned: true 
}
+    name: { type: string(64) }
+    code: { type: string(4) }
+    division_id: { type: integer(1), unsigned: true }
+
+Branch:
+  actAs:
+    gsSecurityTemplate:
+      conditions:
+        Division:
+          through: [ Division, UserDivision ]
+          field: user_id
+          apply_to: [ division_manager ]
+        District:
+          through:  [ District, UserDistrict ]
+          field: user_id
+          apply_to: [ district_manager ]
+  relations:
+    Division:
+      local: division_id
+      foreignAlias: Branches
+      onDelete: CASCADE
+    District:
+      foreignAlias: Branches
+      local: district_id
+      onDelete: RESTRICT
+  columns:
+    id: { type: integer(4), primary: true, autoincrement: true, unsigned: true 
}
+    name: { type: string(64) }
+    code: { type: string(4) }
+    district_id: { type: integer(4), unsigned: true }
+    division_id: { type: integer(1), unsigned: true }
+    is_active: { type: boolean, default: true }
+
+#-------------------------------------------------------------------------------------
+User:
+  relations:
+    Divisions:
+      class: Division
+      refClass: UserDivision
+      local: user_id
+      foreign: division_id
+    Districts:
+      class: District
+      refClass: UserDistrict
+      local: user_id
+      foreign: district_id
+    Branches:
+      class: Branch
+      refClass: UserBranch
+      local: user_id
+      foreign: branch_id
+    Salespersons:
+      class: Salesperson
+      refClass: UserSalesperson
+      local: user_id
+      foreign: salespersons_id
+  columns:
+    id: { type: integer(4), autoincrement: true, primary: true, unsigned: true 
}
+    name: { type: string(128) }
+    is_admin: { type: boolean, default: false }
+    is_active: { type: boolean, default: true }
+    is_division_manager: { type: boolean, default: false }
+    is_district_manager: { type: boolean, default: false }
+    is_branch_manager: { type: boolean, default: false }
+    is_salesperson: { type: boolean, default: false }
+    last_login: { type: timestamp }
+
+UserDivision:
+  tableName: user_divisions
+  columns:
+    id: { type: integer(4), autoincrement: true, primary: true, unsigned: true 
}
+    user_id: { type: integer(4), primary: true, unsigned: true }
+    division_id: { type: integer(1), primary: true, unsigned: true }
+
+UserDistrict:
+  tableName: user_districts
+  columns:
+    id: { type: integer(4), autoincrement: true, primary: true, unsigned: true 
}
+    user_id: { type: integer(4), primary: true, unsigned: true }
+    district_id: { type: integer(4), primary: true, unsigned: true }
+
+UserBranch:
+  tableName: user_branches
+  columns:
+    id: { type: integer(4), autoincrement: true, primary: true, unsigned: true 
}
+    user_id: { type: integer(4), primary: true, unsigned: true }
+    branch_id: { type: integer(4), primary: true, unsigned: true }
+
+UserSalesperson:
+  tableName: user_salespersons
+  columns:
+    id: { type: integer(4), autoincrement: true, primary: true, unsigned: true 
}
+    user_id: { type: integer(4), primary: true, unsigned: true }
+    salespersons_id: { type: integer(4), primary: true, unsigned: true }
+
+
+</code>
+
+You can see from the User model that the credentials are set within the db. 
All hardcoded in this situation.
+
+++ Using the template
+
+Once you've built your models from the schema, you should see something like 
the following in your model's setUp function.
+<code>
+
+$gssecuritytemplate0 = new gsSecurityTemplate(array('conditions' => 
array('Division' =>  array( 'through' =>  array( 0 => 'Division',  1 => 
'UserDivision',  ),  'field' => 'user_id',  'apply_to' =>  array( 0 => 
'division_manager',  ),  'exclude_for' =>  array( 0 => 'admin',  ), ), 'Branch' 
=>  array( 'through' =>  array( 0 => 'Branch',  1 => 'UserBranch',  ),  'field' 
=> 'user_id',  'apply_to' =>  array( 0 => 'branch_manager',  ),  'exclude_for' 
=>  array( 0 => 'admin',  1 => 'division_manager',  2 => 'district_manager',  
), ), 'Salesperson' =>  array( 'through' =>  array( 0 => 'Salesperson',  1 => 
'UserSalesperson',  ),  'field' => 'user_id',  'apply_to' =>  array( 0 => 
'salesperson',  ),  'exclude_for' =>  array( 0 => 'admin',  1 => 
'division_manager',  2 => 'district_manager',  3 => 'branch_manager',  ), ), 
'District' =>  array( 'through' =>  array( 0 => 'Branch',  1 => 'District',  2 
=> 'UserDistrict',  ),  'field' => 'user_id',  'apply_to' =>  array( 0 => 
'district_manager',  ),  'exclude_for' =>  array( 0 => 'admin',  1 => 
'division_manager',  ), ))));
+$this->actAs($gssecuritytemplate0);
+
+</code>
+
+The last part you need to use is to provide the template with the running 
user's credentials and id. In my project's session bootstrapping I have the 
following ( I use the symfony MVC framework ).
+<code>
+public function initialize($context, $parameters = null)
+{
+    parent::initialize($context, $parameters = null);
+    gsSecurityListener::setUserId($this->getAttribute('user_id'));
+    gsSecurityListener::setCredentials($this->listCredentials());
+
+}
+</code>
+
+This provides the credentials the user was given when they logged in as well 
as their id.
+
+
+++ User setup
+
+In my case, I create users and provide a checkbox for their credentials, one 
for each type I have. Lets take Division Manager as an example. 
+In my case we have 3 divisions, East, Central, West. When I create a user I 
assign it the West division, and check off that they are a division manager.
+I can then proceed to login, and my account listing page will restrict the 
accounts I see automatically to my division.
+
+++ Querying
+
+Now if you query the Account model, the template is triggered and based on 
your credentials the results will be restricted.
+
+The query below
+<code>
+  $accounts = Doctrine_Query::create()->from('Account 
a')->leftJoin('a.Branches b')->where('a.company_name LIKE ?','A%')->execute();
+</code>
+
+produces the resulting sql.
+
+<code>
+</code>
+
+SELECT ... FROM accounts a2 LEFT JOIN branches b2 ON a2.branch_id = b2.id LEFT 
JOIN divisions d2 ON a2.division_id = d2.id LEFT JOIN user_divisions u2 ON 
d2.id = u2.division_id WHERE a2.company_name LIKE ? AND u2.user_id = ? ORDER BY 
a2.company_name
+
+<code>
+
+The results you get back will always be restricted to the division you have 
been assigned. Since in our schema we've defined restrictions on the Branch and 
Districts as well
+if I were to want to provide a user with a drop down of potential branches, I 
can simply query the branches as I normally would, and only the ones in my 
division would be
+returned to choose from.
+
+
+++ Restrictions
+
+For the time being, this module only protects tables in the FROM clause, since 
doctrine currently runs the query listener for the new tables added to the 
query by the template, 
+and thus we get a pretty nasty query in the end that doesn't work. If I can 
figure out how to detect such situations reliably I'll update the article.
\ No newline at end of file

Modified: branches/1.0/docs/cookbook/en.txt
===================================================================
--- branches/1.0/docs/cookbook/en.txt   2008-08-27 22:00:39 UTC (rev 4854)
+++ branches/1.0/docs/cookbook/en.txt   2008-08-28 17:29:52 UTC (rev 4855)
@@ -5,4 +5,5 @@
 + Plug and Play Schema Information With Templates
 + Taking Advantage of Column Aggregation Inheritance
 + Master and Slave Connections
-+ Creating a Unit of Work Using Doctrine
\ No newline at end of file
++ Creating a Unit of Work Using Doctrine
++ Record Based Retrieval Security Template
\ No newline at end of file


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"doctrine-svn" group.
 To post to this group, send email to [email protected]
 To unsubscribe from this group, send email to [EMAIL PROTECTED]
 For more options, visit this group at 
http://groups.google.co.uk/group/doctrine-svn?hl=en-GB
-~----------~----~----~----~------~----~------~--~---

Reply via email to