Page 1 of 1

Managing fields display and type depending on permissions

PostPosted: Tue Dec 23, 2008 10:10 am
by GrosBedo
Hello again :) Sorry to bother you again :(

Now I've got a little more complicated problem, but I'm sure there is a simple solution with Xataface: I need to be able to change the way some fields display (and their type) depending on the permissions the user got.

In this system I use an external permission manager. All I need to know is how and where I can programmatically manage the display of these fields ? I've seen in the doc the functions %fieldname%__htmlValue(&$record) and %fieldname%__display(&$record) but I don't know how to use them, and from my own searches on the forum, it looks like it's not for what I intend to do.

Just a remark: I don't want to restrict the use of these fields depending on permissions (I already done that with %fieldname%__permissions(&$record) ;p),
what I want to do is to change the type from, for example, a select to a hidden field and fill the field automatically for the user, because he hasn't the right to do so (I know I can use a BeforeSave trigger, but at least I need to hide this field !).

Thank you very much in advance :)

PostPosted: Tue Dec 23, 2008 3:03 pm
by shannah
One thing to note is that if you simply limit the user's access to a field to be read only, even if it uses a select widget, the user won't see a select widget. He'll just see static text with the value of the field. You can then use a beforeInsert() trigger to set the value how you want it.

That said, it is possible to override a field type programmatically. A logical place to do this is inside the beforeHandleRequest() method of your application delegate class. This is executed before the request is handled.

e.g.

Code: Select all
function beforeHandleRequest(){
   
    $app=& Dataface_Application::getInstance();
    $query = $app->getQuery();
    if ( $query['-table'] == 'people' ){
        $record =& $app->getRecord(); // gets the current record.
        if ( !$record->checkPermission('edit', array('field'=>'role'))){
            $roleField =& $record->_table->getField('role');
            $roleField['widget']['type'] = 'hidden';
        }
    }
   
}


In this example we are concerned with the role field of the people table that is normally a select list, but if the user doesn't have edit permission, we want it to be a hidden field.

So we check the current table, see if we are in the people table. If we are, then we load the current record and check to see if we have the edit permission. If we don't, we load the field array from the Dataface_Table object, and change the widget:type attribute.

Hope this makes sense

-Steve

PostPosted: Mon Dec 29, 2008 6:06 pm
by GrosBedo
Thank you very much for your answer, it works well :)

Just a little bug: the command
Code: Select all
$query = $app->getQuery();

creates an access to the first record in the current table if none is selected (ex: try to list the table, then click on Details directly). This could be a security flaw, so is there a workaround or a way to verify ?

At first, I've made a verification based on -action, but I need to change the visibility of fields for the browse action as well, so i don't know what to do :/

PostPosted: Mon Dec 29, 2008 8:43 pm
by shannah
$app->getQuery() will always return the current query. You may pass this query to various functions to load records. Any function that only loads a single record will load the first record in the matching result set.

This is not a security flaw. This is just the way it works.

The useful thing about using getQuery() instead of just using the $_GET or $_REQUEST variables is that it is guaranteed to have at least a few values. Specifically it will always have '-action' and '-table' so we can always use it to check which table and actions the current request is for.

The $app->getRecord() method will return the current record matched by the query. In situations where the query would match multiple records, this will return the first such record.

Any action that works on a single record (like the edit action) will be working off of this single record so the fact that the query specifies multiple matches is irrelevant - you'll always just be using the first match.

This could be a security flaw, so is there a workaround or a way to verify ?

What is it that you want to verify? In Xataface you are always working in 2 contexts:
1. The current record (specified by the query; uses the -cursor parameter to select which record out of the found set is the current record).
2. The found set (specified by the query; obeys the -skip and -limit parameters to decide which of the found set will be loaded).


At first, I've made a verification based on -action, but I need to change the visibility of fields for the browse action as well, so i don't know what to do :/


I get the feeling that we may be working at this the wrong way. I would employ this method of programmatically changing config attributes sparingly - only in cases where the standard permissions model and configuration parameters fail to do what you need.

In this case, I'm not sure why you aren't just using permissions to limit access to these fields. Denying a user the 'view' permission for a field will effectively prevent him from viewing or editing the field in any action. If you start trying to hide information action by action you're bound to miss something (e.g. what about RSS feeds or exporting to CSV?)

If a user is not permitted to see or edit a field, then it is best to use permissions to enforce this. Any other method will not be consistently applied across the entire application.

-Steve

PostPosted: Tue Dec 30, 2008 6:32 am
by GrosBedo
shannah wrote:$app->getQuery() will always return the current query. You may pass this query to various functions to load records. Any function that only loads a single record will load the first record in the matching result set.

This is not a security flaw. This is just the way it works.


The problem is that it will load the first record in the table even if none is accessible from his permissions.

Try it: create a table, then a record, then a user which shouldn't access to it via permissions in the ApplicationDelegate, then with this account log in this table. In the list, you will see: "0 match from a total of 1 records in the table". Now click on details -> you'll have access to the first entry in the database even if you shouldn't (because of the $app->getQuery(), it creates a connection to the first record bypassing the permissions settings).

I found a workaround by creating a dummy first entry in the table.

I get the feeling that we may be working at this the wrong way. I would employ this method of programmatically changing config attributes sparingly - only in cases where the standard permissions model and configuration parameters fail to do what you need.

In this case, I'm not sure why you aren't just using permissions to limit access to these fields. Denying a user the 'view' permission for a field will effectively prevent him from viewing or editing the field in any action. If you start trying to hide information action by action you're bound to miss something (e.g. what about RSS feeds or exporting to CSV?)

If a user is not permitted to see or edit a field, then it is best to use permissions to enforce this. Any other method will not be consistently applied across the entire application.


I used permissions of course, as the first method. But I need for this work to do a very precise field by field configuration (some must be hidden depending on user's right, and there are 3 different levels), and some must be autocompleted, and some must be modifiable, and some must be viewable but not editable.

So I've used permissions to prevent editing by the wrong users, and I've put some in read-only for the middle level.

But for example I need some read-only field to be hidden, while others have to be shown to this level of user, in edit action, browse and list.

And I need all these configurations only on one table, every other have a different working scheme (and much more simple).

In fact I'm trying to make a sort of collaborative system with complex rights management (but only on 3 levels).

Maybe I indeed miss something, but I'm trying my best to reach my goal with the most proper code and xataface-like code, but I'm for sure not a guru of this system :/

//EDIT:

In fact, I basically need several fields.ini depending on user's rights, like: fields-admin.ini
fields-moderator.ini
fields-user.ini

This would be the perfect thing for me :) I hope it's clearer now.

PostPosted: Tue Dec 30, 2008 10:04 am
by shannah
The problem is that it will load the first record in the table even if none is accessible from his permissions.

Try it: create a table, then a record, then a user which shouldn't access to it via permissions in the ApplicationDelegate, then with this account log in this table. In the list, you will see: "0 match from a total of 1 records in the table". Now click on details -> you'll have access to the first entry in the database even if you shouldn't (because of the $app->getQuery(), it creates a connection to the first record bypassing the permissions settings).


I can't seem to reproduce this behavior. As long as the user doesn't have the "view" permission, it says permission denied when I click on the details tab (running the test case exactly as you describe it).

In fact if it says "found 0 of 1 records" when you are in list view, it should, in fact, say that no records were found in both list view and the details tab. The way it should work if you have a table with only one record, to which you do not have "view" permission:

The list tab should say "found 1 of 1 records", but none will be listed (they are hidden by javascript). (if the user has the 'list' permission - without list permission it will just say "permission denied").

The view tab should say "Permission denied".

Are you using security filters to filter your results? or
Are you returning different permissions depending on the current action? What does your getPermissions() method look like?

PostPosted: Wed Dec 31, 2008 6:25 am
by GrosBedo
shannah wrote:I can't seem to reproduce this behavior. As long as the user doesn't have the "view" permission, it says permission denied when I click on the details tab (running the test case exactly as you describe it).


In fact, the user must have access to the table, but only to some records filtered by its username.

shannah wrote:In fact if it says "found 0 of 1 records" when you are in list view, it should, in fact, say that no records were found in both list view and the details tab.


Indeed, I updated my xataface install from v1.0b3 to 1.0.7 and now it says "No records matched your request."

shannah wrote:Are you using security filters to filter your results? or
Are you returning different permissions depending on the current action? What does your getPermissions() method look like?


I'm using both: permissions to set user's rights to edit/create/other action, and security filters to filter the records shown to the user.

Here's my conf/ApplicationDelegate.php:

Code: Select all
class conf_ApplicationDelegate {
    /**
     * Returns permissions array.  This method is called every time an action is
     * performed to make sure that the user has permission to perform the action.
     * @param record A Dataface_Record object (may be null) against which we check
     *               permissions.
     * @see Dataface_PermissionsTool
     * @see Dataface_AuthenticationTool
     */
     function getPermissions(&$record){
         global $qls;
         // Managing users permissions, this is the most important section for security as it will not only hide but completely disable any access based on the user's role
         if ( $qls->user_info['username'] == '' ) {
            header('Location: login.php');
            return Dataface_PermissionsTool::NO_ACCESS();
          } elseif ( $qls->user_info['username'] != '' and $qls->user_info['auth_admin'] != 1 ) {
            return Dataface_PermissionsTool::getRolePermissions('READ ONLY');
          } elseif ( $qls->user_info['username'] != '' and $qls->user_info['auth_admin'] == 1 ) {
            return Dataface_PermissionsTool::getRolePermissions('ADMIN');
          } else {
            return Dataface_PermissionsTool::NO_ACCESS();
          }
             // if the user is null then nobody is logged in... no access.
             // This will force a login prompt.
      }

    function getPreferences(){
        global $qls;
        // These are preferences which will only hide some fields or tables based on user's role. DO NOT USE IT FOR SECURITY PURPOSE ! This is not a good way to manage users permissions as it will only hide, but won't disable so smart users can still access wrong authorizations
       
        // Hiding user and usergroup fields for all tables for users, the admin will still see them
        $mytable =& Dataface_Table::loadTable('students_tests');
        $user = $qls->user_info['username'];
        $group = $qls->user_info['group_id'];
        $mask = $qls->user_info['mask_id'];
        if ( $user != '' and $qls->user_info['auth_admin'] != 1 ){
             //We apply the security filter to non admin users.
            if ($mask == 2) { $mytable->setSecurityFilter(array('stage'=>$group)); } // Mask 2 is the professor's group, and they need to be able to access everybody's files.
            if ($mask == 3) { $mytable->setSecurityFilter(array('user'=>$user)); } // Mask 3 is the student's group, and they should only be able to see their documents and documents assigned by professors and admins to them, so the view is filtered by username for them.
            Dataface_PermissionsTool::getRolePermissions('READ ONLY');
        }

        // Hiding some advanced tabs and menus for students
          if ( $qls->user_info['auth_admin'] != 1 and $qls->user_info['mask_id'] != 2 and $_REQUEST['-table'] != 'students_tests' ){
             return array(
                'show_table_tabs'=>0,
                'show_record_tabs'=>0
                );
          }

        return array();  // Mandatory!! getPreferences() must return array.
    }
}


$qls is a global class containing user's informations (because it's managed by an external code).

And here's the permission in the tables/students_tests/students_tests.php

Code: Select all
class tables_students_tests {
    function getPermissions(&$record){
      global $qls;
      if ( $qls->user_info['auth_admin'] == 1 ) {
        return Dataface_PermissionsTool::getRolePermissions('ADMIN');
      } elseif ( $qls->user_info['mask_id'] == '2' ) { // Mask id 2 is the professor's group
        return Dataface_PermissionsTool::getRolePermissions('EDIT');
      } elseif ( $qls->user_info['mask_id'] == '3' ) { // Mask id 3 is the student's group
        return Dataface_PermissionsTool::getRolePermissions('STUDENT');
      } else {
        return Dataface_PermissionsTool::NO_ACCESS();
      }
    }

    function date__permissions(&$record){
      global $qls;
      if ( $qls->user_info['auth_admin'] != 1 ) return Dataface_PermissionsTool::READ_ONLY();
    }

    //Some other fields__permissions functions...

}


Anyway, this bug only happens when I add the line
Code: Select all
$query = $app->getQuery();


in the conf/ApplicationDelegate.php like this:

Code: Select all
    function beforeHandleRequest(){
      $app=& Dataface_Application::getInstance();
      $query = $app->getQuery();
      // Some other inscrutions here
    }


The only workaround I found is to avoid calling $query when the browse action is called, but then the fields aren't filtered in browse:

Code: Select all
    function beforeHandleRequest(){
      global $qls;

      $app=& Dataface_Application::getInstance();
      if ( $_REQUEST['-action'] == 'new' or $_REQUEST['-action'] == 'edit' or $_REQUEST['-action'] == 'view' ){ //This is a fix because when doing a $app->getQuery(); xataface will make accessible the first record of the table even if the user doesn't have the right to do so.
        $query = $app->getQuery();
        // Here we define the fields we show/hide depending on user's rights (mask)
        if ( $query['-table'] == 'students_tests' ){
          $record =& $app->getRecord(); // gets the current record.
          if ($record) { // Avoiding the bug of "Call to a member function getField() on a non-object" when sending the form
            if ( $qls->user_info['auth_admin'] != 1 and $qls->user_info['mask_id'] == 2 ) { // Professor's rights
                $roleField =& $record->_table->getField('date');
                $roleField['widget']['type'] = 'hidden';
            }
            if ( $qls->user_info['auth_admin'] != 1 and $qls->user_info['mask_id'] != 2 ) { // Student's rights
                $roleField =& $record->_table->getField('user');
                $roleField['widget']['type'] = 'hidden';
                $roleField =& $record->_table->getField('date');
                $roleField['widget']['type'] = 'hidden';
            }
          }
        }
      }
    }


That's why I would like to find an alternative, because I would like to hide some of these fields in any actions, not only new and edit (but some must be hidden in new and edit but not in list and browse), and only depending on the user's rights :-/

Anyway, thank you very much for your dedication to fix other people's problems ! I admire that, really :-)

PostPosted: Thu Jan 01, 2009 12:56 pm
by shannah
There is no point setting the widget:type attribute in any action other than edit and new (or any action that is not a form).

If you want to hide a column in list view use visibility:list=hidden
If you want to hide a column in the view tab use visibility:browse=hidden

On another note, looking at the code, the method getQuery() doesn't do anything. It is impossible for the simple act of calling getQuery() to affect the operation of the application. Not sure what is causing the problem, but it cannot be from getQuery().

-Steve

PostPosted: Thu Jan 01, 2009 6:00 pm
by GrosBedo
shannah wrote:There is no point setting the widget:type attribute in any action other than edit and new (or any action that is not a form).

If you want to hide a column in list view use visibility:list=hidden
If you want to hide a column in the view tab use visibility:browse=hidden


Indeed, that's why I use this in the BeforeHandleRequest function:

Code: Select all
$roleField =& $record->_table->getField('date');
                $roleField['visibility]['browse'] = 'hidden';


This is just an example.
And as I said I can't use the fields.ini because I need to to hide this field (and others) based on user's rights.

shannah wrote:On another note, looking at the code, the method getQuery() doesn't do anything. It is impossible for the simple act of calling getQuery() to affect the operation of the application. Not sure what is causing the problem, but it cannot be from getQuery().


Indeed, I'm really sorry I've done a mistake, the following is this line which produces the problem :

Code: Select all
$record =& $app->getRecord();


It seems this line will load the record's informations bypassing the security rules. Is there a way to enforce a security filtering by a field (here username) like I've done for the table ?

PostPosted: Thu Jan 01, 2009 6:30 pm
by shannah
OK.. here is the problem. beforeHandleRequest() is executed before getPreferences(). Therefore you were making the call to getRecord() before you applied your security filters.

A better way to apply the security filters (as long as it is not on your users table), is to place it in the init() method of the table delegate class.

e.g.

Code: Select all
function init(&$table){
    $table->setSecurityFilter(array('foo'=>'bar'));
}


That way it will be applied as soon as the table is loaded. Be careful not to call loadTable() on the same table in this method as it will cause an infinite recursion. Instead use the &$table parameter which is a Dataface_Table object referencing the current table.

-Steve

PostPosted: Thu Jan 22, 2009 10:19 am
by GrosBedo
Perfect :)

And for the visibility:list problem (couldn't change it this way), I've found this thread :

http://xataface.com/forum/viewtopic.php?t=3966#19842

/EDIT: In fact, it seems the above method work, it's just that I filtered by action, and forgot to add list and none to the actions accepted for filtering :roll: So both methods works !

Now my application works beautifully ! Thank you sooooooo much !
I hope I'll be able to repay you one day for your amazing software and support :D