Make sure the future method is in a class "with sharing" or it will be allowed to perform the update-- the default mode of operation for unspecified classes is "without sharing" (with exceptions). Without sharing means the act is performed "as an administrator", thus bypassing the lock.
Edit #2
A better understanding of the forces at play are mentioned on SFDC: Understanding With Sharing, Without Sharing & Unspecified Sharing Classes.
Edit #1
First, see: Using the with sharing or without sharing Keywords.
Next, you'll this answer is correct. I shall offer proof in a pattern that can be replicated in about 10 minutes, depending on your Internet connection.
Step 1: Custom Object
Name: Limited
Organization Wide Defaults: Public Read Only
Custom Fields:
Account__c Lookup(Account__c)
Counter__c Number(18, 0)
Triggers:
trigger updateCounter on Limited__c (before insert, before update) {
for(limited__c record: trigger.new)
if(record.counter__c == null)
record.counter__c = 0;
else
record.counter__c++;
}
Step 2: Custom Profile
Create a new profile from the Read Only profile. Add "Create Edit Delete" permissions to Account and Limited.
Step 3: Future handler
public class FutureHandler {
@future
public static void updateContacts(Set<Id> accountIds) {
update [select id from limited__c where account__c in :accountIds];
}
}
Step 4: Accounts
Organization Wide Defaults: Public Read/Write
Triggers:
trigger InvokeFuture on Account (after update) {
FutureHandler.updateContacts(Trigger.newMap.keySet());
}
Step 5: Set up test data
Create a new account, and add a new "limited to the account. The counter should read 0.
Edit the account, making any change that successfully saves. Look back at the "limited" record, the counter should now read 1.
Step 6: New User
Create a new user with the custom profile from before. Login to the account, find the test data, verify that the user cannot manually edit the "limited" record (click edit, you'll get insufficient privileges). Edit the account, and go back to the "limited", the counter should now read 2. Note This occurs because we are "without sharing", the default model.
Step 7: Update Future handler
As the administrator, update the FutureHandler class:
public with sharing class FutureHandler {
@future
public static void updateContacts(Set<Id> accountIds) {
update [select id from limited__c where account__c in :accountIds];
}
}
Step 8: Test Again
Log back in to the test user, verify they still cannot edit the "limited". Now, edit the account. Once saved, check the "limited". The counter should still be at 2.
Step 9: Verify with debugging
Turn on the Debug Logs for the test user, then log back in as the test user. Try editing the account again. The counter will remain at 2 after this edit. Check the debug logs:
Update failed. First exception on row 0 with id a0J5000000IrBosEAF; first error: INSUFFICIENT_ACCESS_OR_READONLY, insufficient access rights on object id: []
Q.E.D.