How to cache the results of a method on the server?


#1

Hi, in my app, I wrote a method which checks whether the user have permission to view a particular page. In every page visit, this method will be called and if the user does not have permission, he will be sent back to the home page.

The problem is, this method slow things down. It takes time to do this check and moving between pages in the app is very slow. Here’s the method :

'users.havePermission'(currentRoute) {
  check(currentRoute, String);
  const loggedInUser = Meteor.user();

  const permissions = {
    home: [ 'superAdmin', 'adviser', 'admin' ],
    'page.1': [ 'superAdmin', 'adviser' ],
    'page.2': [ 'superAdmin', 'adviser', 'admin' ],
    'page.3': [ 'superAdmin', 'adviser', 'admin' ],
    'page.4': [ 'superAdmin', 'adviser' ],
    'page.5': [ 'superAdmin' ],
    'page.6': [ 'superAdmin', 'adviser' ],
    'page.7': [ 'superAdmin', 'admin' ],
    'page.8': [ 'superAdmin', 'admin' ],
    'page.9': [ 'superAdmin' ],
    'page.10': [ 'superAdmin', 'adviser', 'admin' ],
    'page.11': [ 'superAdmin' ],
    'page.12': [ 'superAdmin' ],
    'page.13': [ 'superAdmin', 'admin' ],
    'page.14': [ 'superAdmin' ],
    'page.15': [ 'superAdmin', 'admin' ],
    'page.16': [ 'superAdmin' ],
    'page.17': [ 'superAdmin', 'admin' ],
    'page.18': [ 'superAdmin' ],
    'page.19': [ 'superAdmin' ],
    'page.20': [ 'superAdmin', 'adviser', 'admin' ]
  };

  const permissionForCurrentRoute = permissions[currentRoute];

  return Roles.userIsInRole(loggedInUser, permissionForCurrentRoute);
}

Here’s how I call the method :

Meteor.call('users.havePermission', currentRoute, (error, userHavePermission) => {

    if (!userHavePermission) {
      // If not, send directly to the home page without any warning.. Boom!
      FlowRouter.redirect('/');
    } else {

      // If everything is in order, let him pass!
      const userName = loggedInUser.profile.name;
      const loggedInUserEmail = loggedInUser.emails[0].address;
      LocalState.set('LOGGED_IN_USER_EMAIL', loggedInUserEmail);

      const isAdmin = loggedInUser.roles.includes('admin');
      const isSuperAdmin = loggedInUser.roles.includes('superAdmin');

      onData(null, {notifications, userName, currentRoute, isAdmin, isSuperAdmin,
        uuid, RouteTransition});

      return clearAllNotifications;
    }
  });

Strangely, this method sometimes (Not all the time) returns an error like this TypeError: Cannot read property 'includes' of undefined which might be contributing to the delay. No idea.

My main question is, is it possible to speed things up by caching this on the server? How can I do it?


#2

I guess one of these packages would be useful for your use case


#3

Tangential comment: You may be getting your TypeError due to unready subscriptions. One possible case where this error would appear is when you are trying to call this method before the client has added the roles field to the Meteor.users collection.

Your declaration: const permissions = ... should be done globally outside the scope of the method, since it is the same of all of the users.

Since you are hiding your implementation of userIsInRole, it’s hard to pinpoint where the slowdown is.

If you are interested in caching information, I suggest looking into Meteor.onConnection. You can keep a global object RolesCache. Nevermind, use Accounts.onLogin instead, since your user must be logged in for this to make sense. Another option is to use Accounts.validateLoginAttempt, and add the user’s role information to the cache IFF the login attempt was going to succeed. I like this a little bit better since I’m sure you get the (full?) user object as an argument, so you don’t need to explicitly query the user. UPDATE: according to the docs, the onLogin callback also gets the same argument! https://docs.meteor.com/api/accounts-multi.html

Accounts.onLogin(()=>{
 RolesCache[currentUser.id] = currentUser.roles;
})
Meteor.methods({
 'users.havePermission': function(){
  //RolesCache[this.userId] === your roles object, no query needed; fast!
 }
})

If the roles field is dynamic at all, you must not forget to update your RolesCache whenever the roles object is updated on the server. You do not want user permissions to be out-of-date…


#4

Great thanks I’ll check it out. The method userIsInRole is from the Roles package https://github.com/alanning/meteor-roles :slight_smile: And the roles field is not dynamic I think. Need to study that package a bit.


#5

Ok, I checked out the source, it seems like Roles.userIsInRole is implemented to be “fast”, since it avoids running a query twice, as you are passing in a user object.

Keep in mind that running even one query per each route will give you an extra 200+ms slow down!

There’s another option. Get rid of the method all-together.

I think this might be a slightly better implementation (and might even be faster). I’m assuming that each route has a subscription that needs to be run. For example: going to /posts/:id will run a subscription for the details of Post with that id. This is a very common pattern.

If you do this, then perhaps you could implement your authentication logic inside the subscription? Then, you have it all in one place. Instead of using a method to do an authentication check and only an authentication check, you could include the check as part of the data subscription. This is how I would implement it at first.

Meteor.publish('page.12.contents', function(){
  makeSureUserHasPermission(this.userId); //throw inside here.
  return SecurePosts.find({page: 12, ...otherStuff})
})

#6

@streemo Yep your right. I already to authentication in publications and the page won’t load if the user does not have permission. Anyway, is it secure enough? (Sorry I’m a bit new and also paranoid about security :slight_smile: )


#7

Publications are implemented as methods, so I would imagine that it’s just as secure. If someone isn’t authenticated as a user, then this.userId will return null. They could manipulate their client to fake this.userId, but at the end of the day, they must pass the authentication test to fool the server, which requires an id-password pair.

What’s more is that Meteor takes care of cleaning up data from publications which are invalidated. If you ditch publications and use methods directly, then it’s up to you do collect the garbage.