Setting Up Companies & Defining User Roles/Priveledges

Hello everyone,

I am quite a meteorjs noob but have been working hard to learn the basics and how everything goes together. I have a goal to create a production web application in meteorjs and reactjs. So the classes and docmentation has been focused on those two. I am having a hard time understanding something, and was hoping that you could give me some of your opinions on the best way to get it accomplished. It will also help me find more documentation and do more research once I have focused my attention.

I was curious how I could sign up entire companies, and provide them with an admin or set of admins that could set the priveledges for all of their users (like what they can edit, what they can see such as certain projects or documents). How would I setup each company in order to make sure that the data that they are seeing and manipulating is different from other companies.

Any help on pointing me in the right direction would be greatly appreciated. Please let me know if I wasnt clear and if you need more info to help me out.

Thanks!! :smile:

Just adding on to my original post, I did some reading on alanning:roles, and it looks like I would just setup a company login as the admin, or company moderator and then they would be able to add users and set priveledges. Is there a better method of doing this?

Also, as said in my original post, each company would be logging into their own dash and fetching and storing only their companies information. Would it be better to just setup something such as subdomains? That way if a company wanted to change something or add a feature that it would be unique to their company?

Thanks again, cant wait to hear all of your suggestions!

In it’s most basic form you’d need to make sure the companyId is present with all the data that is specific for a company. Then you set the companyId in the users profile, or map it to a userId in a separate collection, publish the data filtered by the companyId and set allow/deny based upon it.

But you need to think this through before you start code it. If you can’t draw it, you will not be able to code it either.

As an additional tip, you can also use a group permission library like https://atmospherejs.com/alanning/roles to add roles within companies and do extended validation. In your situation you would be able to set a role using the companyId as the group. Afterwards in any method you would check the user’s role within that group.

Publication-wise just like @ralof said only publish data that is of the same groupId as the user.

Thanks, ralof. I completely agree with you. I am doing mostly research and learning currently at the moment. Once I have a good plan of action I will begin wireframing the application. Do you think I can get in touch with you if I have more questions? Sounds like you have done this plenty of times.

Thank you for the response nlammertyn! Well, my main goal and how I see this, is that we would setup a company in our db then we would grant them access to add/delete users and set specific levels of access to their companies data. For example they may want to provide a client with access to comment on a project but they do not want to allow them to see the projects financial information or list of current issues.

@wals532 I’ve developed a couple of Meteor apps that do exactly what you describe, right down to subdomains for different companies.

The first app was developed before alanning:roles had the group option, so we rolled our own (pretty complex and highly coupled) system of companies, roles, permissions (even scopes for roles in certain contexts). It works well enough now, but took a lot of hours to write and debug.

In the second app, I used alanning:roles and that was significantly quicker/easier to work with. I did run into one issue where client performance was degraded (high cpu use leading to big slowdowns) when using alanning:roles to perform hundreds of unique permission checks on a single page. I worked around it by writing a little custom permission check (just using the same data structures that the alanning:roles packages creates) and UI performance went back to being crisp and snappy.

I’d recommend the alanning:roles package. It’s pretty flexible.

Check out meteor chef’s base repo so see some examples. He creates an admin on startup in ~/imports/startup/server/fixture.js

You can also add a collection hook to before find() and findone, to ensure the selector object has the company/tenant id as part of the selector incase you create a query in your app or server side such as people.find(), and would accidentally include another tenants data. The hook would add in the company selector or could also cancel the query.

This is a fantastic idea thank you sbr464!

@babrahams Thanks for the response, I do think that I will be utilizing the alanning:roles. Would you still recommend using subdomains for companies? Did this prove to make things easier and less messy?

Thanks!

everything should be tied to the current user authorization on the server side, to ensure security. Using subdomains is nice besides adding a customized user experience, it also allows you to send basic/branding type information to unauthenticated users, especially for customer portals, customized login pages. It can also be used to help set context if a user needs to login to a specific tenant/company.

We also use the subdomain as an additional security layer, the subdomain/currently authenticated user and other other items must match up before we allow a successful auth. This helps add another layer to ensure tenants/users don’t mix.

After a user connects on the server, a unique session id is created server side. You can also write to that Meteor session/connection object. So after it authenticates once and does several expensive lookups to the database, we save some helpful information to that connection object (only on server). We have a small method that can reference that info when needed on the client.

// Here is an example of the connection object, after we add the _app key/object


  id: 'zPuT8L8ydK7Jj5PKn',
  close: [Function: close],
  onClose: [Function: onClose],
  clientAddress: '127.0.0.1',
  httpHeaders: {
    'x-forwarded-for': '127.0.0.1',
    'x-forwarded-proto': 'ws',
    host: 'localhost:3050',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36',
    'accept-language': 'en-US,en;q=0.8,da;q=0.6'
  },
  clientIp: '127.0.0.1',
  _app: {
    _tenantId: 'wZ24xNQydL48t9hHm',
    _userId: '6tSa2jvnsSLfNEHRo',
    _connectionId: 'zPuT8L8ydK7Jj5PKn'
  }
}

//
function getTenantIdFromUserId (userId) {
  if (typeof userId === 'string') {
    const userDoc = Meteor.users.findOne({_id: userId});
    if (userDoc && userDoc._id === userId && typeof userDoc.tenant === 'string') {
      return userDoc.tenant;
    }
  }
};

function makeAppTenantUserObj (userId, tenantId, connectionId) {
  if (typeof userId === 'string' && typeof tenantId === 'string' && typeof connectionId === 'string') {
    return {
      _tenantId: tenantId,
      _userId: userId,
      _connectionId: connectionId
    };
  } else {
    return null;
  }
};

function verifyAppTenantObj (appObj = {}, connectionId) {
  const result = typeof connectionId === 'string' && Match.test(appObj, {
    _userId: String,
    _tenantId: String,
    _connectionId: String
  });
  return result && appObj._connectionId === connectionId;
};


Accounts.onLogin(function(user) {
  const appObj = makeAppTenantUserObj(user.user._id, user.user.tenant || getTenantIdFromUserId(user.user._id), user.connection.id);
  if (appObj && verifyAppTenantObj(appObj), user.connection.id) {
    user.connection._app = appObj;
    console.log('appObj valid, setting user tenant');
  } else {
    user.connection._app = null;
    console.log('err - appObj not valid, setting user tenant to null');
  }
});

Meteor.onConnection(function(res) {
  // console.log('res? in onConnection', res);
  try {
    const base = new AuditBase('connection', res);
    base.insertEvent();
  } catch(e) {
    rlog(e, 'err in Meteor.onConnection Audit event');
  }
});

Meteor.methods({
  getUserTenant: function() {
    check(this.userId, String);
    check(this.connection.id, String);
    this.unblock();
    if (verifyAppTenantObj(this.connection._app, this.connection.id)) {
      return this.connection._app;
    } else {
      throw new Meteor.Error('err - invalid tenant for user:', this.userId);
    }
  }
});
1 Like