Skip to main content

Using Pundit to build in a cool gmail-feature

Rails • Asked by Chris Zempel

I'm building an app with data set up similar to this:

class Company < ActiveRecord::Base
  has_many :products
  has_many :options, through: :products
end

class Product < ActiveRecord::Base
  belongs_to :company
  has_many :options
end

class Option < ActiveRecord::Base
  belongs_to :product
  belongs_to :company, through: :product
end

Now I'm not actually sure if that reverse belongs_to :through works, but you get the gist of it. Each level has "ownership" of the next level down. (Company owns Products that have/own Options)

Each level has one or more Users that need to be able to able to act on behalf of those below it. For example, one of the users who manages at the Company level should have full CRUD access to everything that company and everything it owns.

However, a product manager should only have access to CRUD rights for his product and all the options that product owns. And company managers shouldn't be able to have access to other companies or anything inside of them. Option owners, etc, there are user at all different levels.

I'd like to be able to have gmail-like functionality where a full-access user can jump in and interact with the app as though they were a limited-access user the same way they can access a variety of email accounts.

So what I'm thinking is that, really, it's not a "User" that acts upon the models, but rather the "Identity" of a user that actually affects what behavior they can access, and that it's this "Identity" object I should route all the policies and scoping for user through - so a user can act on behalf of a variety of identities they have access to. My first question:

  • Is this idea of an "Identity" a sensible abstraction, or is there a better way?

The rest of this question might be irrelevant, but typing it out anyway:

  • Assuming this is, the user would act on behalf of a current_identity, and then be able to choose what that is from all their available identities. Going down this path, I'd like to avoid a HABTM relationship between users and identities -- one way I can think of is using an in-between:
class User < ActiveRecord::Base
  has_many :user_identities
end

class UserIdentity < ActiveRecord::Base
  belongs_to :user
  belongs_to :identity

class Identity < ActiveRecord::Base
  has_many :user_identities

  #scoping an identity to a specific subset of the given models, ie, this is an identity for a 'Product' with id: 3
end
  • Or, is there an alternative I can do to avoid needing this extra model (ex: serializing a bunch of user_id's in every identity and checking against that)? And is it worth it?

You want a has_one :company, through: :product on the Option to make that work. belongs_to is only used when you have an _id column on the model.

If you're scoping activity to things which a user owns, you'll need to store the user_id on the records anyways otherwise how can you know who created it? Because of that, it seems that identities overcomplicate things a bit and you'll end up with an unclear mess of things (and likely open yourself up for a lot of security holes). If you simply add a user_id on everything, you can know if the current user can manage that item or not. If you add a role to the User model, you can give them higher levels of access that can skip the lower user_id checks to allow them company level access instead.


But what happens when I have co-owners of a given company who want access to the same level of information & scoping? Storing both of their id's in one column seems wrong to me, which means I'd need another table to maintain information about the group of users. Identity is a vague term.

UserGroup, Group, or Membership? Then inside that, I can attach different access levels.

For now, I'm gonna defer this decision and just stick a single user_id on there, but make sure I don't call it directly.


Yeah if you need multiple levels of access more than global and individual then you will need another tier. You can put the access level on the items and the users or you can put them in a join table.


Ultimately, a user should be able to see and access CRUD options based off of which company they're associated with. So inside this domain, companies are actually the sub-domain inside which a user operates.

class User < ActiveRecord::Base
  has_many :company_connections
  has_many :companies, through: :company_connections
end

class CompanyConnection < ActiveRecord::Base
  belongs_to :user
  belongs_to :company
end

class Company < ActiveRecord::Base
  has_many :company_connections
  has_many :users, through: :company_connections
  has_many :products
  has_many :options, through: :products
end

From there, I could go a variety of ways -- if I wanted role-based access, I could store that on the user. If I wanted item/scope based access, I could also store a reference to that object that on the user, and allow access for that based on it.


Although, I suppose that's best for role-based if a company has consistent roles and scopes users need. For item-based, a vaguer table would serve better:

class UserConnection
  belongs_to :user
  belongs_to :item (?)

not sure how that would work. You could approximate it by:

  def item
    "#{item_class.to_s.constantize}".find(item_id)
  end

Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 27,623+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.