2
\$\begingroup\$

I am working on a MERN stack app that is a CRM with a couple of modules. Now when the user sends x request to the server to do a supposed action (such as delete something), the server first checks if the user has access by checking the MongoDB database users' collection's permissions property for an array of "permissions" that looks something like this:

[
  { name: "Leads", level: 1 },
  { name: "Conversations", level: 1 },
  { name: "Salesbot", level: 0 },
  { name: "Reporting", level: 1 },
  { name: "Contacts", level: 4 },
]

This states the level of permission this user has for each module. There are 5 possible levels conveying different amounts of access, these are:

  1. None
  2. Accepted only
  3. Read
  4. Write
  5. All

Each level automatically grants access to the levels that would be before it. So if the user has write permission, he would logically, also have read permission as well.

I wrote a function that I call every time to check if the user has X relevant permission before doing something on the server.

The function is as follows:

const checkPermission = (permissions, checkFor) => {
  for (let i = 0; i < permissions.length; i++) {
    const permission = permissions[i];

    if (
      permission.name === checkFor.name &&
      permission.level >= checkFor.level
    ) {
      return true;
    }
  }

  return false;
};

and then I call it this way:

checkPermission(
  [
    { name: "Leads", level: 1 },
    { name: "Conversations", level: 1 },
    { name: "Salesbot", level: 0 },
    { name: "Reporting", level: 1 },
    { name: "Contacts", level: 4 },
  ],
  { name: "Contacts", level: 3 }
);

So this function call checks to see if the user has write access in the contacts module, and if they do, then it returns true, if they don't, then false.

I welcome any sort of improvements to the code, but most importantly, I would welcome improvements to the logic of permissions if there are any. I came up with this permissions levels system on my own without much research, so there could be many things in this that I would be neglecting unknowingly that you guys can point out. Perhaps an entirely new way of handling permissions or whatever, I welcome any sort of improvements.

\$\endgroup\$

2 Answers 2

2
+150
\$\begingroup\$

The best approach will depend on how scalable / flexible you want your approach to be. If your app works perfectly as-is and you never intend to make any changes, the current permissions system may be sufficient. But let's pretend you have a client and they ask some questions.

  1. What if I have millions of users?
  2. Could we work with permissions that can't be ordered?
    • An example might be Append and Delete for some queue of requests: clients might only be allowed to Append new requests, while some processing server might only be allowed to Delete requests as they are finished.
  3. Can your solution scale with a large number of permissions?
  4. What if I want to set one permission without considering the other permissions?

The current approach might have a tough time on these questions. To solve (1), you can make permissions an unordered map (dictionary with keys) from each user string to each user level. Unordered maps have O(1) search, insertion, and deletion time on average. To solve the next 3 questions, you could replace level with a more sophisticated policy object. Your policy could be an unordered map from actions (strings) to whether they are allowed (true or false). This would allow users to easily share a commonly used policy. Since nonexistent object members are undefined in JavaScript, this has the added bonus of instantly rejecting requests where actions not in the policy (invalid or unspecified) are requested. If we deny all requests by default, this has the added advantage of making it easy to allow a single action or a small group of actions, knowing that all other actions will be rejected by default. I would suggest user instead of name since that is what Passport uses. You can also use TypeScript for more readability and automated checking:

// checking policies, permissions, requests
type policy = {
  [action: string]: boolean;
};

const checkPolicy = (policy: policy, action: string) => {
  return policy[action] === true;
};

type permissions = {
  [user: string]: policy;
};

type request = {
  user: string;
  action: string;
};

const checkPermission = (permissions: permissions, request: request) => {
  const user = request.user;
  if (permissions.hasOwnProperty(user))
    return checkPolicy(permissions[user], request.action);
  return false;
};

// testing
const testPerms = {
  Leads: {
    accepted: true,
  },
  Conversations: {
    accepted: true,
  },
  Salesbot: {},
  Reporting: {
    accepted: true,
  },
  Contacts: {
    accepted: true,
    read: true,
    write: true,
    all: true,
  },
  Unordered: {
    accepted: true,
    read: false,
    write: true,
    all: false,
  },
};

let test1 = checkPermission(testPerms, { user: 'Contacts', action: 'write' });
let test2 = checkPermission(testPerms, { user: 'Contacts', action: '?' });
let test3 = checkPermission(testPerms, { user: 'Salesbot', action: 'write' });
let test4 = checkPermission(testPerms, { user: 'Unordered', action: 'read' });
let test5 = checkPermission(testPerms, { user: 'Unordered', action: 'write' });

In my opinion, this adjusted approach is also more readable / explicit. I put together a small app in StackBlitz to demonstrate the results. Let's think of some other potential client questions:

  1. Can requests be subjected to multiple contextual policies?
  2. Can policies correspond to particular databases / resources, perhaps using regular expression matching?
  3. Can we add blacklists (deny) in addition to whitelists (allow)?

At that point, you might as well use the Amazon Web Services (AWS) Identity and Access Management (IAM) system, with its policy elements and complex evaluation logic. I lifted the request / policy / action terminology from AWS IAM, as it is a highly sophisticated and widely used permissions system.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Wow, this is actually a huge upgrade to what I had previously, and thanks for the additional recommendations + possible client questions. I will wait for longer to see if any other answer comes that may have the bounty, but for now, this seems to be the best answer here so far out of the two, although the second one also gives great suggestions. \$\endgroup\$ Sep 6 at 6:47
  • 1
    \$\begingroup\$ Happy to help. Another good permissions example is Linux. It makes read, write, and execute independent of each other - look up chmod if you’re interested. Linux also uses the append / delete motif for certain special log files. \$\endgroup\$ Sep 6 at 20:30
1
\$\begingroup\$

an array of "permissions" that looks something like this:

[
  { name: "Contacts", level: 4 },
  { name: "Conversations", level: 1 },
  { name: "Leads", level: 1 },
  { name: "Reporting", level: 1 },
  { name: "Salesbot", level: 0 },
]

[I took the liberty of ordering those.]

I'm willing to believe that number of permissions, P, is "small". Nonetheless, linear scan over that array has time complexity of O(P). Prefer to store it as an object (map), to enjoy O(1) constant lookup time and to simplify the (now loopless) calling code.

In general, if item order in a piece of source code doesn't matter, then impose some clear and arbitrary order like alphabetic or chronologic. That will clarify to the Gentle Reader that order is arbitrary, and it reduces merge conflicts when maintainers in multiple feature branches make changes to the same source file.

The OP code had the risk of some maintenance engineer appending

  { name: "Leads", level: 2 },

without noticing that another entry would now be silently ignored. Better to clear out such cruft at once. For small P it's not so bad, but as soon as a list grows to greater than a screenful such unfortunate edits become inevitable.


Consider using an enum to name those integer access levels.

const Level = Object.freeze({
    NONE:   0,
    ...
    ALL:    4,
});
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the great suggestions, I really liked your suggestion of using enums, would certainly have it implemented when I update my code. \$\endgroup\$ Sep 6 at 6:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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