Manage access control using Redis Bitfields

How to create a high-performance, highly available, and flexible access control system using binary data and bitwise operators in Redis

1 2 Page 2
Page 2 of 2

Putting this into practice

While it’s possible to manually put this logic in front of every route in your application framework, its better suited for routing middleware. Routing middleware is used in a variety of frameworks across languages:

The general flow usually works like this:

  • Request provides the route
  • Identity middleware provides some sort of user identifier
  • Our bit-based capabilities/access control middleware make a decision
  • Destination page is either served or withheld

Any time you’re operating in a middleware situation, high performance is essential—every millisecond counts. While our MULTI/EXEC sequence seems rather long and contains writes, this isn’t a problem for Redis. Because we are dealing with literally the smallest amounts of data possible we can achieve great speed, even on the most modest instances of Redis. On my laptop, for example, the benchmark is showing about 12,000 evaluations per second, so scaling won’t be a problem. More importantly, with this being at the heart of serving your resources, high availability is critical. Using a product like Redis Enterprise will enable a rock-solid, lightning-fast access control layer.

Implementing this in Express.js, for example, would look something like this:

function userCan(req,res,next) {                        // `req,res,next` is the standard Express middle argument
signature (request, response and next middleware)
  const
    tempKey   = ‘userCanTemp’,                          // This can be any value
    routeKey  = rk(routePrefix,req.route.path),         // `rk` joins each argument with a colon, req.route.path is the Express route
    user      = req.query.user;                         // In this example, were getting the user from the query string, but you’d get it form another middleware most likely
  let
    userKey;
                                        
  if (!user) {                                          // if `user` isn’t defined, then we’ll reject
    res.status(403).end();
  } else {
    userKey = rk(userPrefix,user);                      // create the user key with `rk`
  }

  client
    .multi()                                            // start the multi transaction
    .bitop(‘AND’,tempKey,userKey,routeKey)              // The result of `userKey` AND’ed with `routeKey` and stored in `tempKey`
    .bitop(‘XOR’,tempKey,tempKey,routeKey)              // The result of `tempKey` XOR’ed with `routeKey` and stored back in `tempKey`
    .bitcount(tempKey)                                  // count the bits in `tempKey`’s value
    .bitfield(rk(routeKey,’level’),’GET’,’u7’,’9’)      // grabbing a unsigned 7-bit word from routeKey+’:level’
    .bitfield(userKey,’GET’,’u7’,’9’)                   // grabbing a unsigned 7-bit word from `userKey`
    .exec(function(err,responses) {
      if (err) { next(err); } else {                    // handle errors
        let
          capMisses = responses[2];                     // capability misses are stored in the 2nd result
        if (
          (capMisses === 0) &&                          // make sure no bits are misses
          (responses[3] >= responses[4])) {             // that the  user level is greater than the route level
          next();                                       // pass it on to the next middleware
        } else {
          res.status(401).end();                        // otherwise reject it.
        }
      }
    });
}

You can get the raw source code here.

Storing it all

The space efficiency here is pretty amazing! Let’s say your system has 59 different flags and three different user levels, each of seven bits (using this for nice, round numbers): 80 bits per user, or ten bytes. Let’s take a look at the space needed for your user base:

  • 10,000 users @ 10 bytes = 10 kilobytes
  • 100,000 users @ 10 bytes = 1 megabyte
  • 1,000,000 users @ 10 bytes = 10 megabytes

You get the idea. Now this is solely for the data; the key itself will take up 10+ itself, so you need to at least double it. Let’s ramp up the example. Gmail has about a billion user accounts. How much space would it take to store that?

(1,000,000,000 users * 10 bytes = 10,000,000,000) * 2 for key overhead = 20,000,000,000 bytes or 20 gigabytes

20 gigabytes is not an unusual amount of data to store in Redis Enterprise, and this is for a billion users. Of course, you would also need to store values for the routes, but that will be a completely insignificant amount of storage at this scale.

This is only a beginning

Bitmaps are an extremely useful, quick, and compact structure. Combine this technique with a time-to-live and an extra BITOP command and you could create temporary capabilities that auto-expire. Using the INCRBY subcommand of BITFIELD, you could manage credits or even limit users. Redis has tools to manipulate the data at a precise and low level while retaining the ability to distribute and serve with high performance. The possibilities are endless!

Kyle Davis is the technical marketing manager at Redis Labs. Kyle is an enthusiastic full-stack developer and works frequently with Node.js and Redis, documented in his long-running blog series on the pair. Previously, Kyle worked in higher education, helping to develop tools and technologies for academics and administrators. 

New Tech Forum provides a venue to explore and discuss emerging enterprise technology in unprecedented depth and breadth. The selection is subjective, based on our pick of the technologies we believe to be important and of greatest interest to InfoWorld readers. InfoWorld does not accept marketing collateral for publication and reserves the right to edit all contributed content. Send all inquiries to newtechforum@infoworld.com.

1 2 Page 2
Page 2 of 2