0

Inconsistency with 'Readable If' and 'Writable If'

I wrote a comprehensive user permissions application that validates user permissions based on:

Entity > Location > Permission

 

Using a Global Function, I wrote this:

#{ obtain list of user permissions for entity or building NEW }#;
function permCheckN(ct : text,rid : number,permissionCompare : text) do
    #{
    Global Variables
}#;
    #{
    Entity permission checking variables
}#;
    let obtainUserEntityAccess := (select int_entity_access where 'System User' = thisUserID());
    let allEntityAccessCheck := first(obtainUserEntityAccess.int_is_sys_record = true) and
        first(contains(obtainUserEntityAccess.int_entity_name, "ALL"));
    let thisEntitySearch := first(obtainUserEntityAccess[int_is_sys_record = true or Entity = number(rid)]);
    let entityAccess := if thisEntitySearch != null then
            true
        else
            false
        end;
    let listEntityPerms := thisEntitySearch.int_permissions_list;
    #{
    Building permission checking variables
}#;
    let obtainUserBuildingAccess := (select int_building_access where 'System User' = thisUserID());
    let thisBuildingSearch := first(obtainUserBuildingAccess[int_is_sys_record = true or Building = number(rid)]);
    let buildingAccess := if thisBuildingSearch != null then
            true
        else
            false
        end;
    let listBuildingPerms := thisBuildingSearch.int_permissions_list;
    #{
    Check the user's permission
}#;
    if isAdminMode() then
        true
    else
        switch upper(ct) do
        case "E":
            if entityAccess = true or rid = 0 then
                if contains(listEntityPerms, upper(permissionCompare)) then
                    true
                else
                    false
                end
            end
        case "B":
            if buildingAccess = true or rid = 0 then
                if contains(listBuildingPerms, upper(permissionCompare)) then
                    true
                else
                    false
                end
            end
        default:
            false
        end
    end
end;

Ultimately, the function will return a Boolean true or false if the user has the appropriate permissions. 

I call function:

permCheckN("B", int_building, "Personnel (Read)")

let's assume the returned result is TRUE. If this function is within 'Readable If' for a field and the output is true, the field is visible.

However, if I place the exact formula within 'Writable If', regardless of the output, the field will still be read-only. 

 

I've even tried putting this function within a formula, and then trying

'Formula Field' = true;

but that still does not work. I am quite bummed, because a lot of work gone into this permissions structure, but its practically unusable due to a programming bug (I assume) within Ninox.

 

Doe anyone have any suggestions?

14 replies

null
    • SMoore
    • 7 days ago
    • Reported - view

    bump.. hoping for input from someone.

    • Fred
    • 7 days ago
    • Reported - view

    Have you email Ninox support?

    I tried with a Yes/No field in another table and had no issues using just the name or = true or = 1.

    • SMoore
    • 3 days ago
    • Reported - view

    Hi Fred,

     

    I finally sent my inquiry to support. Feel free to test the bug I discovered yourself with the attached database.

    I assume this needs a deeper investigation by the development team.

    1. Create a new record under 'System User' and set 'Ninox User' as yourself from the dropdown.
    2. Add a user permission to yourself.
    2a. Check 'System-Wide Access' (true)
    2b. Set 'Role' to 'Role 1 - System'
    3. Navigate to Test_Error page
    3a. Field 'global function that returns true or false' should say "Yes"
    4. Try to fill out Readable If, Writable If, and Read and Writable If.

    You will find that the Writable If fields cannot be modified. If you enter admin mode, click on the fields and look at "Display field only, if" and "Writable if" for each of the fields, you will find the logic within them is identical. 

    Thus, there is some backend underlying bug with Writable If.

      • SMoore
      • 3 days ago
      • Reported - view

       I wonder if  is interested in checking this out. Please see above :)

      • Ninox developper
      • Jacques_TUR
      • 2 days ago
      • Reported - view

       It seems that using comments with the #{ }# syntax causes the function to crash when executed in the trigger. Removing the comments makes everything work fine, but as soon as a single comment is added back, it stops working.
      Can you ask support where this issue comes from?

      • John_Halls
      • 2 days ago
      • Reported - view

       The first thing I did was take all the comments out, and tidy up the function but there is definitely something odd going on.

      Regards John

      • Ninox developper
      • Jacques_TUR
      • 2 days ago
      • Reported - view

       

      Upon checking, the comments bug is related to Ninext. I will look into this further. However, I've noticed that the select function in this trigger consistently returns an empty table, and that's why your function isn't working.

      With some reflection, indeed, using select in a trigger could significantly decrease the performance of the database both locally and on the server.

      • John_Halls
      • 2 days ago
      • Reported - view

       Not mine! Agreed it needs a re-write but I didn’t understand it well enough on first sight to do so

    • John_Halls
    • 2 days ago
    • Reported - view

    Hi  This is the oddest thing. I have tried so many permutations of your function. I have scrunched it down from

    function permCheck(group_id_val : number,record_location_id : number,permission_text : text) do
        #{ ---------------------------------------- }#;
        #{
            variables set for permission checking
        }#;
        let def_loc_check := number(record_location_id);
        #{
            find the referenced (permission_text) permission id
        }#;
        let permission_id := number(first((select Permission)[upper('Permission Name') = upper(permission_text)]).Id);
        let curent_user := user();
        #{
            locate the current user in system users table
        }#;
        let return_user_id := first((select 'System User')['Ninox User' = curent_user]).Id;
        #{
            return all permissions User Permissions linked to user_id and group_id_val
        }#;
        let matched_user_permission_group := (select 'User Permission')['System User' = return_user_id and group_id = group_id_val];
        #{
            compare permissions
        }#;
        switch group_id_val do
        case 1:
            if cnt(matched_user_permission_group[contains(return_permission_ids, permission_id)]) > 0 then
                1
            else
                0
            end
        case 2:
            if cnt(matched_user_permission_group[contains(return_entity_ids, record_location_id) and
                            contains(return_permission_ids, permission_id)]) > 0 then
                1
            else
                0
            end
        case 3:
            if cnt(matched_user_permission_group[contains(return_building_ids, record_location_id) and
                            contains(return_permission_ids, permission_id)]) > 0 then
                1
            else
                0
            end
        default:
            0
        end
    end;
    
    

    To

    function permCheck(group_id_val : number,record_location_id : number,permission_text : text) do
        let permission_id := number(first(select Permission where upper('Permission Name') = upper(permission_text)).Id);
        let return_user_id := number(first(select 'System User' where 'Ninox User' = user()).Id);
        let matched_user_permission_group := (select 'User Permission' where 'System User' = return_user_id and group_id = group_id_val);
        switch group_id_val do
        case 1:
            cnt(matched_user_permission_group[contains(return_permission_ids, permission_id)])
        case 2:
            cnt(matched_user_permission_group[contains(return_entity_ids, record_location_id) and
                            contains(return_permission_ids, permission_id)])
        case 3:
            cnt(matched_user_permission_group[contains(return_building_ids, record_location_id) and
                            contains(return_permission_ids, permission_id)])
        default:
            0
        end;
    

    I have moved from true / false to 1 / 0 and there are only two versions that make it work. The first is to just return a 1, the other is

    sign(cnt(matched_user_permission_group[contains(return_permission_ids, permission_id)]))

    The inclusion of sign() does work, over first(), number() but you can't use it as sign() return 1 when the value is 0.

    Regards John

    • SMoore
    • 2 days ago
    • Reported - view

    Wow! I didn't expect so many people to troubleshoot this. I really appreciate it.

     

    Still waiting to hear back from support. 

    • Ninox developper
    • Jacques_TUR
    • 2 days ago
    • Reported - view

    To put it simply, the permCheck function uses select statements to verify permissions in the tables. However, the select function does not work in the Writable If trigger that calls the permCheck function.

    Therefore, we need to find a solution that does not use select to access permissions.

    • John_Halls
    • 12 hrs ago
    • Reported - view

    I've looked into this a bit more and  is correct. The Ninox table level and field level Writable if... sections do not like select commands being used to return a value. Worth writing to support regarding this.

    Regards John

      • SMoore
      • 10 hrs ago
      • Reported - view

        I see..

       

      Thinking of an alternative approach... I wonder if I could create a Page with a Global Variable in Memory Binding that I could store all the user's permissions in as an object. 

       

      This could be populated in Trigger after open in the app settings. Then, use my global permCheck() function to compare against the Page.. Something like:

      record('Settings',1).'current_user_perms'

      Do you think this will work? It's no longer a Select, instead referencing a direct location. I just wonder how a global memory variable will handle this. 

      • Fred
      • 8 hrs ago
      • Reported - view

      I was thinking along the same line, but instead of creating a global variable. You can create a parent table above your security tables. The parent will have only 1 record and all of the child tables will be linked to it. Then you can always do a record(ParentSecurity,1) anywhere and get all of your security tables.