Adds group and membership functionality to Rails models. Defines a polymorphic
relationship between a Group model and any member model. Don't need a Group
model? Use named groups instead to add members to named groups such as
The following ORMs are supported:
- ActiveRecord 3.2, 4.x
- Mongoid 3.1, 4.0,
The following Rubies are supported:
- MRI Ruby 1.9.3, 2.0.x, 2.1.x
- JRuby (1.9 mode)
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
$ gem install groupify
Add a migration similar to the following:
class CreateGroups < ActiveRecord::Migration def change create_table :groups do |t| t.string :type # Only needed if using single table inheritance end create_table :group_memberships do |t| t.string :member_type # Necessary to make polymorphic members work t.integer :member_id # The id of the member that belongs to this group t.integer :group_id # The group to which the member belongs t.string :group_name # The named group to which a member belongs (if using) t.string :membership_type # The type of membership the member belongs with end add_index :group_memberships, [:member_id, :member_type] add_index :group_memberships, :group_id add_index :group_memberships, :group_name end end
In your group model:
class Group < ActiveRecord::Base groupify :group, members: [:users, :assignments], default_members: :users end
In your member models (i.e.
class User < ActiveRecord::Base groupify :group_member groupify :named_group_member end class Assignment < ActiveRecord::Base groupify :group_member end
You will also need to define a
GroupMembership model to join groups to members:
class GroupMembership < ActiveRecord::Base groupify :group_membership end
In your group model:
class Group include Mongoid::Document groupify :group, members: [:users], default_members: :users end
In your member models (i.e.
class User include Mongoid::Document groupify :group_member groupify :named_group_member end
Create groups and add members
group = Group.new user = User.new user.groups << group # or group.add user user.in_group?(group) # => true # Add multiple members at once group.add(user, widget, task)
Add to named groups
user.named_groups << :admin user.in_named_group?(:admin) # => true
Remove from groups
users.groups.destroy(group) # Destroys this user's group membership for this group group.users.delete(user) # Deletes this group's group membership for this user
Check if two members share any of the same groups:
user1.shares_any_group?(user2) # Returns true if user1 and user2 are in any of the same groups user2.shares_any_named_group?(user1) # Also works for named groups
Query for groups & members:
User.in_group(group) # Find all users in this group User.in_named_group(:admin) # Find all users in this named group Group.with_member(user) # Find all groups with this user User.shares_any_group(user) # Find all users that share any groups with this user User.shares_any_named_group(user) # Find all users that share any named groups with this user
Check if member belongs to any/all groups
User.in_any_group(group1, group2) # Find users that belong to any of these groups User.in_all_groups(group1, group2) # Find users that belong to all of these groups Widget.in_only_groups(group2, group3) # Find widgets that belong to only these groups widget.in_any_named_group?(:foo, :bar) # Check if widget belongs to any of these named groups user.in_all_named_groups?(:manager, :poster) # Check if user belongs to all of these named groups user.in_only_named_groups?(:employee, :worker) # Check if user belongs to only these named groups
Merge one group into another:
# Moves the members of source into destination, and destroys source destination_group.merge!(source_group)
Membership types allow a member to belong to a group in a more specific way. For example, you can add a user to a group with membership type of "manager" to specify that this user has the "manager role" on that group.
This can be used to implement role-based authorization combined with group authorization, which could be used to mass-assign roles to groups of resources.
It could also be used to add users and resources to the same "sub-group" or "project" within a larger group (say, an organization).
# Add user to group as a specific membership type group.add(user, as: 'manager') # Works with named groups too user.named_groups.add user, as: 'manager' # Query for the groups that a user belongs to with a certain role user.groups.as(:manager) user.named_groups.as('manager') Group.with_member(user).as('manager') # Remove a member's membership type from a group group.users.delete(user, as: 'manager') # Deletes this group's 'manager' group membership for this user user.groups.destroy(group, as: 'employee') # Destroys this user's 'employee' group membership for this group user.groups.destroy(group) # Destroys any membership types this user had in this group # Find all members that have a certain membership type in a group User.in_group(group).as(:manager) # Find all members of a certain membership type regardless of group User.as(:manager) # Find users that are managers, we don't care what group # Check if a member belongs to any/all groups with a certain membership type user.in_all_groups?(group1, group2, as: 'manager') # Find all members that share the same group with the same membership type Widget.shares_any_group(user).as("Moon Launch Project") # Check is one member belongs to the same group as another member with a certain membership type user.shares_any_group?(widget, as: 'employee')
Note that adding a member to a group with a specific membership type will automatically
add them to that group without a specific membership type. This way you can still query
groups and find the member in that group. If you then remove that specific membership
type, they still remain in the group without a specific membership type.
Removing a member from a group will bulk remove any specific membership types as well.
group.add(manager, as: 'manager') manager.groups.include?(group) # => true manager.groups.delete(group, as: 'manager') manager.groups.include?(group) # => true group.add(employee, as: 'employee') employee.groups.delete(group) employee.in_group?(group) # => false employee.in_group?(group, as: 'employee') # => false
But wait, there's more!
Check the specs for a complete list of methods and scopes provided by Groupify.
Using for Authorization
Groupify was originally created to help implement user authorization, although it can be used generically for much more than that. Here are some examples of how to do it.
class Ability include CanCan::Ability def initialize(user) # Implements group-based authorization # Users can only manage assignment which belong to the same group. can [:manage], Assignment, Assignment.shares_any_group(user) do |assignment| assignment.shares_any_group?(user) end end end
# Whatever class represents a logged-in user in your app class User groupify :named_group_member include Authority::UserAbilities end class Widget groupify :named_group_member include Authority::Abilities end class WidgetAuthorizer < ApplicationAuthorizer # Implements group-based authorization using named groups. # Users can only see widgets which belong to the same named group. def readable_by?(user) user.shares_any_named_group?(resource) end # Implements combined role-based and group-based authorization. # Widgets can only be updated by users that are employees of the same named group. def updateable_by?(user) user.shares_any_named_group?(resource, as: :employee) end # Widgets can only be deleted by users that are managers of the same named group. def deletable_by?(user) user.shares_any_named_group?(resource, as: :manager) end end user = User.create! user.named_groups.add(:team1, as: :employee) widget = Widget.create! widget.named_groups << :team1 widget.readable_by?(user) # => true user.can_update?(widget) # => true user.can_delete?(widget) # => false
class PostPolicy < Struct.new(:user, :post) # User can only update a published post if they are admin of the same group. def update? user.shares_any_group?(post, as: :admin) || !post.published? end class Scope < Struct.new(:user, :scope) def resolve if user.admin? # An admin can see all the posts in the group(s) they are admin for scope.shares_any_group(user).as(:admin) else # Normal users can only see published posts in the same group(s). scope.shares_any_group(user).where(published: true) end end end end
- Fork it
- Create your feature branch (
git checkout -b my-new-feature)
- Commit your changes (
git commit -am 'Added some feature')
- Push to the branch (
git push origin my-new-feature)
- Create new Pull Request