1

I'm ingesting 150 objects that each require an user capabilities check, the function isUserAdmin tells me whether or not an user is an admin or not. Inside this function, there's a lot of deeper checks and so on. It'd be really, really nice if I could cache this stuff so I don't have to check for 150 objects.

I'd like to, before anything loads, determine who the user is and set up things that in practice, don't change -- or do they? Assume a super admin changes the privileges of an admin user mid-request but I've already set his privilege information, so, assume this admin has the capability to delete other users, he's running a request to do that, but during that time, the superadmin says "nope, can't do that anymore", but because on that request I already set that user's permissions, this will not take effect and the admin will still be able to delete users.

What's the fix here? Setting information pre-emptively is basically dangerous in an async-by-proxy environment (which everything is) but not doing this makes me perform the same check 150 times in a row.

I feel like this is a chicken & egg problem and every time I'm faced with such issue, I'm not seeing a vital angle. What am I missing?

3
  • 2
    Caching user permissions for the lifetime of the program just means that a user would have to log out and log back in to get the changes. Permissions change very infrequently; real-time uptake is usually not necessary. If users never logging out is a worry, just invalidate the cache periodically, say at midnight every day or once per hour. Jan 7, 2020 at 0:49
  • 1
    There will always be a gap of time between the permissions check and the privileged operation. Even if you recheck everything every time, there is still a tiny time window, since your two operations are not atomic. This is the basis of the TOCTOU vulnerability. You should try to handle the permissions check atomically with the privileged operation, if possible. That would give you the safety net you need to use a cached pre-check to improve performance.
    – John Wu
    Jan 7, 2020 at 3:01
  • @JohnWu I think that's overkill. In my case, I believe that the best that I can do is cache the check which is way more expensive than checking for the...check! Basically, I'll store the check's result and only check if it's still valid (hasn't been invalidated by an upper user, such as a super admin taking the rights of a lower admin). I will then have a number of triggers (such as updating an user's status) that invalidate the cache so I can rebuild it.
    – Daniel M
    Jan 7, 2020 at 5:51

4 Answers 4

6

You're not missing anything.

To get the most up to date state, you need to query it (and even that will be delayed by the latency of your request). Caching it, or waiting for some event/message means you're necessarily working with potentially stale state.

Technically, you get to decide what trade-off to make. Querying every time minimizes the stale time (and inconsistency), but increases load and UI latency. A call-through cache with a timeout decreases load a bit, and is very resilient to bad things happening but will mean longer stale data. An eventing system decreases the staleness along with the load, but tends to be more fragile and difficult to implement.

But all of these are pretty easy compared to the non-technical challenges. Business people want instantaneous consistency with no load or latency. Your biggest challenge will be getting enough context to make the good technical decision, and explaining it sufficiently to the business that they understand and accept the trade-offs.

2
  • 2
    I'd say business people are generally willing to tolerate slightly delayed consistency but are much more sensitive to latency. Relevant quote from Vaughn Vernon: "Domain experts are sometimes far more comfortable with the idea of delayed consistency than are developers. They are aware of realistic delays that occur all the time in their business, whereas developers are usually indoctrinated with an atomic change mentality."
    – casablanca
    Jan 7, 2020 at 8:07
  • @casablanca: business people generally are used to working with people and other businesses that does manual record keeping, where consistency problems happens all the time, and so there are established manual processes to resolve those inconsistencies. This is possible because manual record keeping are very malleable, and easy to tamper with to fix those issues. Computer systems, on the other hand, are often used to enforce privileges, so updates to records are deliberately kept to minimize number of inconsistent states, because each of those inconsistent states adds complexity to the system.
    – Lie Ryan
    Jan 11, 2020 at 6:53
1
  1. If your database supports rolling back changes, you could check at the beginning that the user is permitted to make the request, allow the request to go through, and then check again that they are still allowed to make the request. If they are no longer allowed, roll back the changes, otherwise complete the request. This still has the problem that the request could time out or otherwise end prematurely, which can be mitigated by having the database automatically roll back if the request doesn't complete.
  2. You could implement the permissions checking on the database itself instead of in your web application. In this case, while you would still need to make queries, they would be initiated on the database server so they would not be delayed by network latency.
0

Since user permission update does not occur frequently, I believe it is better to be updating the cache, local storage, cookie, session storage of which ever you are using , anytime user permission is being altered.

0

Use the transaction isolation feature in your database.

Modern database is a smart beast, modern databases uses a technique called multi version concurrency control (MVCC), which roughly translates to that the database essentially takes a snapshot of the data that it's reading within a transaction.

This allows the database system to detect conflicting updates and either block the conflicting changes until the currently running one finishes, or rollback one of the running transactions.

For example, the request serving the admin query can grab an update lock on the subadmin user's object by doing a "select for update" query, so that superuser's update to the admin's privileges doesn't take place until the admin finishes their current operation. If you're concerned of privilege inversion, where the admin can block the superadmin's change of privilege operation for an indefinite amount of time by keeping their operation running, you can make it so that the superuser can force terminate the admin's transaction, and the database will rollback all that admin's operations that are still in transaction and force the admin to redo those operations with the new privileges.

The effect here is that changes become atomic, and the operations don't make decisions based in partially updated data.

1
  • This is exactly what I need, unfortunately, I have no idea whether I can take advantage of this in PHP that needs to work with MySQL / MariaDB. What should I search for to find out more?
    – Daniel M
    Jan 11, 2020 at 12:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.