Data ACLs - Limit access to data

Data ACLs give more granular control over users' access to data objects, therefore making apps more secure. This is achieved by specifying read and/or write permissions on data buckets and their relationships when querying DB or OnlineDB.

ACLs vs sync rules

ACLs specify users' access to data objects, whereas sync rules specify which data syncs to users' devices. This document describes ACLs. You can learn more about sync rules here.

Data ACLs on Web

For apps on web it is strongly recommended to implement data ACLs to limit data a single compromised web user could have access to.

Data Buckets

Data ACLs are specified on data buckets. It is important that you are familiar with these before moving on to the next sections. Read up about them here:

pageData Buckets

Data ACLs syntax

Data ACLs read and write attributes that can be specified for the data buckets global-bucket, bucket, and the model and has-many tags within these buckets.

Note: These are only “allow” rules - no “deny” rules are supported. This means that if any rule matches, the operation is allowed.

read="any|none|online|offline"

TypeDescription

any (default)

Allow syncing of the data, and allow OnlineDB querying of the data.

none

Disable syncing of the data, and disable OnlineDB querying of the data.

online

Disable syncing of the data, and allow OnlineDB querying of the data.

offline

Allow syncing of the data, and disable OnlineDB querying of the data.

Attachments require online access

Downloading attachments requires online access, since they are not stored on a device. The recommended rule for attachments is read="any".

write="any|none|create|update|delete"

  • You can specify one, or a comma-separated combination of the above. E.g. write="none" or write="create,update"

TypeDescription

any (default)

Allow any write to an object, as long as the user still has access to the object afterwards.

none

Make the data read-only - don’t allow creating, updating or deleting.

create

Allow the user to create new objects, but not modify existing ones. This is mostly useful for use cases such as creating logs or audit events.

update

Allow updating existing objects in the bucket.

delete

Allow deleting existing objects in the bucket.

Object-specific bucket roots

Object-specific bucket access rules include their root objects by default. To overwrite this behavior, read and write attributes can be specified for the root object, by adding the a root tag inside a bucket, e.g.:

data_rules.xml
<bucket via="self/region">
    <!-- The root is region in this case -->
    <root write="none" read="any" />
    
    <has-many name="clients" />
</bucket>

If not specified (the tag is not present, or attributes on the tag are not present), root object access permissions default to:

<root read="any" write="update,delete" />

Root permissions have some important details:

  • Only a single root tag is allowed per bucket.

  • When the via path to the bucket root is a belongs-to relationship, write="create" is not a valid permission. Additionally, the write="any" permission also does not allow creates on root objects in this case (but it does allow updates and deletes). The reason for this is that the relationship from the user to the root object would need to be set before creating the root object:

<bucket via="user/region">
   <root write="any" />
</bucket>

let region = DB.region.create();
region.save(); // Not allowed, since user.region() isn't set yet

user.region(region);
user.save(); // Not allowed, unless the region was already saved
  • This does not apply to bucket roots where the via path to the root is a has-many relationship. In this case, write="create" and write="any" are valid permissions and writes are supported if a single operation creates the root object and assigns it to the user/bucket. To illustrate with an example:

<bucket via="user/jobs">
   <root write="any" />
</bucket>

DB.job.create({user: user}).save();  // Allowed, because a single operation
                                     // creates and assigns the user

Examples

Read-only access to all globally synced models:

By specifying write="none" on this global bucket, its data is synced to all users and they have read-only access.

data_rules.xml
<data-rules version="3">
    <global-bucket write="none">
        <!-- These are synced and are read-only -->
        <model name="category" />
        <model name="subcategory" />
    </global-bucket>
</data-rules>

Role-specific access rules:

In this example, only admins should be able to update certain data, while all other users have read-only access. Here we define the read="none" rule on the data bucket that provides write access for admins, since another data bucket already allows synching and unrestricted read access to these objects for all users (including admins).

data_rules.xml
<data-rules version="3">
    <global-bucket write="none">
        <model name="category" />
        <model name="subcategory" />
    </global-bucket>
    
    <!-- For admins: Allow write access and disable syncing and read access, 
    to avoid duplication with the above bucket. -->
    <global-bucket via="self[role == 'admin']" write="any" read="none">
        <model name="category" />
        <model name="subcategory" />
    </global-bucket>
</data-rules>

Allow users to create new regions, but not update their own

In this example, a user belongs-to a region, and the region has-many clients. Users have unrestricted access to their region's clients, but cannot update their own region (and thereby access other region's clients). However, they can create new regions.

data_rules.xml
<data-rules version="3">
    <bucket via="self/region">
        <!-- Prevent writes to the user's region (the bucket root) -->
        <root write="none" />
        
        <!-- Unrestricted access to clients belonging to the user's region -->
        <has-many name="clients" />
    </bucket>
    
    
    <global-bucket>
        <!-- Allow users to create new regions -->
        <model name="region" write="create" />
    </global-bucket>
</data-rules>

Prevent writes to objects after they've reached a certain state

Sometimes it may be necessary to "lock objects" after they have reached a certain state. In this example, we want to prevent technicians from updating jobs once they have been marked as completed.

data_rules.xml
<data-rules version="3">
    <bucket via="self[technician == true]/region">
        <!-- Technicians cannot update jobs in their region once they are completed -->
        <has-many name="jobs" condition="completed == true" write="none" />
        <!-- Allow technicians to update jobs in their region 
        if they are not marked as completed -->
        <has-many name="jobs" condition="completed == false" write="any" />
    </bucket>
</data-rules>

Overwriting access on a bucket's root and relationships:

Here we override the access rules defined for the overall bucket (the default: sync, read and write access), by defining various access rules on the bucket's root and relationships. The individual access rules are described below.

data_rules.xml
<data-rules version="3">
    <!-- The bucket has sync, read and write access -->
    <bucket via="self/region">
        <!-- Prevent writes to the user's region (the bucket root) -->
        <root write="none" />

        <!-- locked clients are synced and read-only -->
        <has-many name="clients" condition="locked == true" write="none" />
        <!-- unlocked clients are synced and can be updated -->
        <has-many name="clients" condition="locked == false" write="any" />
        
        <!-- audit_items are too many to sync, so can only be queried via OnlineDB -->
        <has-many name="audit_items" read="online" />
        
        <!-- log_entries are create-only -->
        <has-many name="log_entries" read="none" write="create" />
    </bucket>
</data-rules>

Implications of implementing data ACLs

App performance is affected in certain cases

Large number of OnlineDB queries and individual object lookups in your app can negatively affect app performance, since individual requests are slower to perform with data rules.

Tips to improve app performance with data rules:

  1. Use .include(relationship) when related objects need to be looked up in a query

  2. When doing individual object lookups, use .where('id in ?',ids) to batch the requests

  3. When doing OnlineDB writes, use Batch

OnlineDB access is limited to synced data by default

Note: When migrating your app from sync rules to data rules, additional data rules will automatically be added to your data rules to support existing OnlineDB calls in your app given this implication. See more details here.

The default behavior of data rules is that OnlineDB access is limited to data synced to the device (in other words, as previously specified in the app's sync rules).

With sync rules, it was possible to query any model via OnlineDB, regardless of the model being included in sync_rules.xml. With data rules, only the data specified in data_rules.xml is accessible with OnlineDB.

Let's look at an example. Take the following sync rules definition:

sync_rules.xml
<sync version="2">
    <global-bucket>
        <model name="country" />
        <model name="district" />
    </global-bucket>
</sync>

Since sync rules only specify which data is synced to a device, it would be possible to query another model not included in the sync rules using OnlineDB:

main.js
function init() {
    var sites = OnlineDB.site.all().toArray();
}

With data rules, given the initial sync rules definition, the above OnlineDB query would not return any results. Read or write access to site via OnlineDB would need to be explicitly defined within data rules to support the above query:

data_rules.xml
<data-rules version="3">
    <global-bucket>
        <model name="country" />
        <model name="district" />
        <!-- Allow read access via OnlineDB (write access defaults to any) -->
        <model name="site" read="online" />
    </global-bucket>
</data-rules>

Object writes are only performed if the user has access before and after an update

Note: When migrating your app from sync rules to data rules, additional data rules will automatically be added to your data rules to support existing object writes throughout your app given this implication. See more details here.

With data rules, object writes are only performed if the user has access to the object before and after an update. Of course, for create operations it’s only the after state that is validated.

This means that where updates to objects need to occur by a user, those objects still need to be accessible to the user after the update.

Let's look at a practical example.

Take the following bucket defined in data_rules.xml:

data_rules.xml
<bucket via="self/region">
    <!-- only jobs that are not complete are synched -->
    <has-many name="jobs" condition="complete != true" write="any" />   
    ...
</bucket>

And consider the following in-app logic that marks a job as complete.

main.js
function markComplete(selectedJob) {
    selectedJob.complete = true;
    selectedJob.save();
}

Once a user marks a job as complete, the job would no longer be accessible to the user, as only incomplete jobs are synced to the device. With sync rules, this kind of update is supported. With data rules, however, since the user would no longer have access to the job after this update (of setting the complete boolean to true).

Therefore, it is important to review all conditional buckets and models, and ensure that writes are supported.

Taking the above example, the following additional rule in data_rules.xml would enable updates to a job:

data_rules.xml
<bucket via="self/region">
    <!-- only jobs that are not complete are synched -->
    <has-many name="jobs" condition="complete != true" write="any" /> 
    <!-- sync completed jobs to allow setting the completed flag to true --> 
    <has-many name="jobs" condition="complete = true" write="update" />   
    ...
</bucket>

Additional buckets when migrating to data rules

When migrating to data rules, these rules to support writes to objects that are only conditionally synced, are automatically created. Read more about this here.

Manual tests are required before deploying data rules to an environment with active users

Since data rules control which data is available to users it is recommended you test data users should be able to access, and data they shouldn’t be able to access before deploying to active users. For this, you can use the debug developer console, and specifically run OnlineDB calls and queries. The below section on what happens when access is denied, should also help with these tests.

What happens when access to data is denied

When data rules prevent access to data for a user/device, they can expect the following to happen:

  1. OnlineDB save() operations will throw an "Access denied" error.

  2. OnlineDB destroy() operations will appear to work, but the data will not be touched.

  3. OnlineDB queries will exclude the results.

  4. DB save() operations will show an "Access denied" error in the developer console (in JourneyApps Runtime version 4.85.0 and greater).

Data Rule Reprocessing

Data rule reprocessing follows the same principles as sync rule reprocessing.

Sync rule reprocessing is only applicable if updates have been made to rules that affect what data is synced. Therefore, there are several cases where updates to data rules would not require reprocessing. This includes migrating to data rules, and updating/adding/removing rules that don’t have read="any" or read="offline" set. These data rules take effect immediately after deploying an update.

Last updated