So, I didn’t user merb-auth for GraffitiWok, partly because it wasn’t when I started. But, actually, the users and roles was one of the more complicated and app-specific parts to GraffitiWok, so I probably would have gone the full custom route anyway.
The particularly interesting part (to me) where Board and Note specific permissions, based on two criteria: ownership of the object (an owner can do anything to that object or to its Notes if the object is a Board) and general permissions for all logged-in or guest users. Actually, the models are built to allow per-user permissions, but its not really implemented.
So, there are actually six relevant models, which I’ll review briefly:
- User - The basic model for users
- GuestUser - A singleton class representing an anonymous user. I’ll actually ignore this in the subsequent discussion
- Role - A role, which just records a name and id. For example, “editor” or “poster”
- UserRole - An intermediate model connecting a Role, a User, and a Board. That is, it says “Bob (User) is an ‘editor’ (Role) on Board #1”
- Permission - A name and id identifying a permission
- RolePermission - An intermediate model connecting a Role with its many permissions.
So, a User has one or more Roles for a given Board (or, uses the default roles) each of which then have a set of permissions, thus, per board, a User has a given set of permissions. The upshot of this is some meta-programming to have code like User.first(:email => ‘bob@example.com’).can_edit?(Note.get(1)). I like it, anyway.
Here’s the interesting code:
def process_permission(permission, obj)
if obj.kind_of?(Note)
board = obj.board
permission += '_note'
else
board = obj
end
if p = Permission.first(:name => permission)
return true if (obj.owner == self && permission != 'create_note')
return true if (permission == 'create_note' && obj.board && obj.board.owner == self)
ur = UserRole.all(:board_id => board.id, :user_id => self.id)
ur = UserRole.all(:board_id => board.id, :user_id => -1) if ur.empty?
return ur.collect{ |r| r.role.permissions}.flatten.include?(p)
end
return false
end
def method_missing(sym, *args)
if sym.to_s =~ /\Acan_([\w_]+)\?\Z/
return process_permission($1, args.first)
end
super
end
def respond_to?(sym, *args)
# repond_to can_{permission}? methods if there is a permission with the
# given name.
return true if super
if sym.to_s =~ /\Acan_([\w_]+)\?\Z/
return (Permission.first(:name => $1) ||
Permission.first(:name => $1 + "_note") ?
true : false)
end
return false
end
The process_permission method is where the real work goes on. If the object is a Note, it actually checks for a permission with the suffix _note on the given Board (with some special coding for can_create_note?). It also uses the default Roles (those for the Board where the User id is -1) if that User has no assigned roles for this Board (a similar thing happens with GuestUser).
For GraffitiWok, I think this approach has worked rather well.