Using Pundit to build in a cool gmail-feature
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