acts_as_joinable 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +53 -0
- data/app/models/default_permission_set.rb +40 -0
- data/app/models/membership.rb +59 -0
- data/app/models/membership_invitation.rb +42 -0
- data/app/models/membership_request.rb +36 -0
- data/app/models/permission_link.rb +6 -0
- data/lib/acts_as_joinable.rb +28 -0
- data/lib/joinable/acts_as_joinable.rb +326 -0
- data/lib/joinable/acts_as_joinable_component.rb +223 -0
- data/lib/joinable/acts_as_member.rb +43 -0
- data/lib/joinable/acts_as_permissable.rb +29 -0
- data/lib/joinable/feedable_extensions.rb +34 -0
- data/lib/joinable/permissions_attribute_wrapper.rb +164 -0
- metadata +75 -0
data/README.rdoc
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
== ActsAsJoinable
|
2
|
+
|
3
|
+
This plugin adds access control to objects by giving them members, each with configurable permissions.
|
4
|
+
|
5
|
+
== Overview
|
6
|
+
|
7
|
+
Access control is achieved through the addition of three *acts_as* extensions and four models.
|
8
|
+
|
9
|
+
== ActsAs Extensions
|
10
|
+
|
11
|
+
acts_as_joinable - Added to the primary model to which access control is being added.
|
12
|
+
acts_as_joinable_component - Added to all of the models which need to inherit their permissions from their parent joinable.
|
13
|
+
acts_as_member - Added to a User model which can join joinables.
|
14
|
+
|
15
|
+
== Models
|
16
|
+
|
17
|
+
=== DefaultPermissionSet
|
18
|
+
|
19
|
+
A set of permissions which any user automatically receives after joining the joinable.
|
20
|
+
The owner of a joinable can configure these. This allows joinables to be open to all Users.
|
21
|
+
|
22
|
+
The permissions contained by the DefaultPermissionSet specify the *access model* for the joinable.
|
23
|
+
There are three access models:
|
24
|
+
|
25
|
+
* open - Users can join this joinable and receive a membership without submitting a MembershipRequest
|
26
|
+
* closed - Users need to submit a MembershipRequest or accept a MembershipInvitation in order to join this joinable.
|
27
|
+
* private - Users can *only* be added to the joinable after accepting a MembershipInvitation
|
28
|
+
|
29
|
+
The current access model is determined solely by the permission level of the DefaultPermissionSet.
|
30
|
+
If the DefaultPermissionSet has a permission level which allows users to view the contents of the joinable.
|
31
|
+
Then the access model is *open*. If the permission level allows any user to find the joinable, the access
|
32
|
+
model is *closed*. If the permission level doesn't allow users not in the joinable to find the joinable,
|
33
|
+
the access model is *private*.
|
34
|
+
|
35
|
+
=== MembershipRequest
|
36
|
+
|
37
|
+
Any user can submit a request to join any joinable which they can see. The managers of the joinable can
|
38
|
+
then accept that request (and choose an appropriate permission level for the user) or deny it.
|
39
|
+
|
40
|
+
=== MembershipInvitation
|
41
|
+
|
42
|
+
The managers of a joinable can invite users to join a joinable. The invitation includes the permissions that
|
43
|
+
the user will receive upon acceptance.
|
44
|
+
|
45
|
+
NOTE: A user cannot use the permissions given in the invitation until they accept the invitation.
|
46
|
+
|
47
|
+
=== Membership
|
48
|
+
|
49
|
+
Represents a set of permissions for a specific user in a specific joinable. Can be created in 3 ways:
|
50
|
+
|
51
|
+
* Through the accepting of a MembershipRequest by the managers of a joinable
|
52
|
+
* Through the accepting of a MembershipInvitation by the invitee
|
53
|
+
* If the joinable has an *open* access model, a User can create a Membership directly
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class DefaultPermissionSet < ActiveRecord::Base
|
2
|
+
include Joinable::PermissionsAttributeWrapper
|
3
|
+
|
4
|
+
belongs_to :joinable, :polymorphic => true
|
5
|
+
|
6
|
+
after_update :raise_existing_member_permissions
|
7
|
+
|
8
|
+
def access_model
|
9
|
+
if has_permission?(:view)
|
10
|
+
return 'open'
|
11
|
+
elsif has_permission?(:find)
|
12
|
+
return 'closed'
|
13
|
+
else
|
14
|
+
return 'private'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def access_model=(model)
|
19
|
+
case model.to_s
|
20
|
+
when 'open'
|
21
|
+
# Additional permissions are set explicitly so just grant the find and view permissions
|
22
|
+
self.grant_permissions([:find, :view])
|
23
|
+
when 'closed'
|
24
|
+
self.permissions = [:find]
|
25
|
+
when 'private'
|
26
|
+
self.permissions = []
|
27
|
+
else
|
28
|
+
raise "Access model invalid: #{model}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def raise_existing_member_permissions
|
35
|
+
(joinable.memberships + joinable.membership_invitations).each do |membership|
|
36
|
+
membership.permissions = membership.permissions + permissions
|
37
|
+
membership.save!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class Membership < ActiveRecord::Base
|
2
|
+
include Joinable::PermissionsAttributeWrapper
|
3
|
+
|
4
|
+
belongs_to :joinable, :polymorphic => true
|
5
|
+
belongs_to :user
|
6
|
+
|
7
|
+
before_save :prevent_locked_permission_changes, :normalize_owner_permissions
|
8
|
+
|
9
|
+
validates_presence_of :user_id
|
10
|
+
validates_uniqueness_of :user_id, :scope => [:joinable_type, :joinable_id] # Ensure that a User has only one Membership per Joinable
|
11
|
+
after_create :destroy_remnant_invitations_and_requests
|
12
|
+
|
13
|
+
attr_accessor :initiator, :locked
|
14
|
+
|
15
|
+
def locked?
|
16
|
+
locked == 'true'
|
17
|
+
end
|
18
|
+
|
19
|
+
def owner?
|
20
|
+
user == joinable.user
|
21
|
+
end
|
22
|
+
|
23
|
+
def permissions_locked?(current_user)
|
24
|
+
# Don't allow any changes to your own permissions
|
25
|
+
if current_user.eql?(user)
|
26
|
+
return true
|
27
|
+
# Don't allow any changes to the owner's permissions
|
28
|
+
elsif owner?
|
29
|
+
return true
|
30
|
+
else
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def prevent_locked_permission_changes
|
38
|
+
reload if locked?
|
39
|
+
end
|
40
|
+
|
41
|
+
def normalize_owner_permissions
|
42
|
+
if owner?
|
43
|
+
self.permissions = joinable_type.constantize.permissions
|
44
|
+
else
|
45
|
+
self.permissions = permissions.reject {|level| level == :own}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def destroy_remnant_invitations_and_requests
|
50
|
+
# Make sure invitation is the first to be destroyed, for feed purposes.
|
51
|
+
if invitation = joinable.membership_invitation_for(user)
|
52
|
+
invitation.destroy
|
53
|
+
end
|
54
|
+
|
55
|
+
if request = joinable.membership_request_for(user)
|
56
|
+
request.destroy
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class MembershipInvitation < ActiveRecord::Base
|
2
|
+
include Joinable::PermissionsAttributeWrapper
|
3
|
+
|
4
|
+
belongs_to :joinable, :polymorphic => true
|
5
|
+
belongs_to :user
|
6
|
+
belongs_to :initiator, :class_name => "User"
|
7
|
+
validates_presence_of :user_id
|
8
|
+
validates_uniqueness_of :user_id, :scope => [:joinable_type, :joinable_id]
|
9
|
+
|
10
|
+
after_create :match_to_request_or_send_email
|
11
|
+
|
12
|
+
def accept(current_user)
|
13
|
+
return false unless current_user == user
|
14
|
+
create_associated_membership_on_accept(current_user)
|
15
|
+
end
|
16
|
+
|
17
|
+
def decline(current_user)
|
18
|
+
return false unless current_user == user
|
19
|
+
destroy_self_on_decline(current_user)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def create_associated_membership_on_accept(current_user)
|
25
|
+
Membership.create(:joinable => joinable, :user => user, :permissions => permissions)
|
26
|
+
end
|
27
|
+
|
28
|
+
def destroy_self_on_decline(current_user)
|
29
|
+
destroy
|
30
|
+
end
|
31
|
+
|
32
|
+
# When a User has already made a request and then someone invites this User to join the Joinable
|
33
|
+
# accept the invitation automatically on behalf of the user, otherwise notify the user
|
34
|
+
# that they have an invite waiting.
|
35
|
+
def match_to_request_or_send_email
|
36
|
+
if joinable.membership_request_for?(user)
|
37
|
+
accept(user)
|
38
|
+
else
|
39
|
+
UserMailer.invited_to_project_email(self).deliver
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class MembershipRequest < ActiveRecord::Base
|
2
|
+
belongs_to :joinable, :polymorphic => true
|
3
|
+
belongs_to :user
|
4
|
+
|
5
|
+
validates_presence_of :user_id
|
6
|
+
validates_uniqueness_of :user_id, :scope => [:joinable_type, :joinable_id]
|
7
|
+
|
8
|
+
scope :for, lambda {|user|
|
9
|
+
joins("INNER JOIN memberships ON membership_requests.joinable_type = memberships.joinable_type AND membership_requests.joinable_id = memberships.joinable_id")
|
10
|
+
.where("memberships.user_id = ? AND 'manage' = ANY(memberships.permissions)", user.id)
|
11
|
+
}
|
12
|
+
|
13
|
+
after_create :match_to_invitation_or_send_email
|
14
|
+
|
15
|
+
def grant(current_user, permissions)
|
16
|
+
membership = create_associated_membership_on_grant(current_user, permissions)
|
17
|
+
UserMailer.project_membership_request_accepted_email(current_user, membership).deliver
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def create_associated_membership_on_grant(current_user, permissions)
|
23
|
+
Membership.create(:joinable => joinable, :user => user, :permissions => permissions)
|
24
|
+
end
|
25
|
+
|
26
|
+
# If a user requests to join a joinable, make sure their isn't a matching invitation
|
27
|
+
# to join the joinable. If there is, then automatically accept the user into the joinable.
|
28
|
+
# Otherwise notify the managers of the project that their is a new request.
|
29
|
+
def match_to_invitation_or_send_email
|
30
|
+
if invitation = joinable.membership_invitation_for(user)
|
31
|
+
invitation.accept(invitation.user)
|
32
|
+
else
|
33
|
+
UserMailer.project_membership_request_created_email(joinable.who_can?(:manage), self).deliver
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# GEM DEPENDENCIES
|
2
|
+
require 'postgres_ext'
|
3
|
+
|
4
|
+
require 'joinable/acts_as_permissable'
|
5
|
+
require 'joinable/acts_as_joinable'
|
6
|
+
require 'joinable/acts_as_joinable_component'
|
7
|
+
require 'joinable/acts_as_member'
|
8
|
+
|
9
|
+
require 'joinable/permissions_attribute_wrapper'
|
10
|
+
|
11
|
+
module ActsAsJoinable
|
12
|
+
class Engine < Rails::Engine
|
13
|
+
initializer "acts_as_joinable.init" do
|
14
|
+
ActiveRecord::Base.send :extend, Joinable::ActsAsJoinable::ActMethod
|
15
|
+
ActiveRecord::Base.send :extend, Joinable::ActsAsJoinableComponent::ActMethod
|
16
|
+
ActiveRecord::Base.send :extend, Joinable::ActsAsMember::ActMethod
|
17
|
+
end
|
18
|
+
|
19
|
+
config.to_prepare do
|
20
|
+
if defined?(ActsAsFeedable::Engine)
|
21
|
+
require 'joinable/feedable_extensions'
|
22
|
+
FeedableExtensions.add
|
23
|
+
else
|
24
|
+
puts "[ActsAsJoinable] ActsAsFeedable not loaded. Skipping extensions."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,326 @@
|
|
1
|
+
module Joinable #:nodoc:
|
2
|
+
module ActsAsJoinable
|
3
|
+
module ActMethod
|
4
|
+
# Takes one option :component_permissions - a list of all of the permissions grouped by the component they affect
|
5
|
+
# eg. [{:labels => [:view, :apply, :remove, :create, :delete]}]
|
6
|
+
#
|
7
|
+
# The grouped permissions are unpacked to create distinct permissions (eg. view_labels, apply_labels, ...)
|
8
|
+
# These unpacked permissions are put into an array with the singular permissions (eg. find)
|
9
|
+
# and stored in a *permissions* class variable.
|
10
|
+
#
|
11
|
+
# In addition, The grouped permissions are stored in a separate *component_permissions_hashes* class variable.
|
12
|
+
#
|
13
|
+
# NOTE: The permissions are passed in-order because in the view we expect to find certain permission patterns.
|
14
|
+
# eg. the simple project permission level is determined by looking for a string of permissions that span
|
15
|
+
# several components, (labels, writeboards, files, etc...).
|
16
|
+
# TODO: Remove the aforementioned order dependency
|
17
|
+
def acts_as_joinable(options = {})
|
18
|
+
extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
|
19
|
+
include InstanceMethods unless included_modules.include?(InstanceMethods)
|
20
|
+
|
21
|
+
options.assert_valid_keys :component_permissions
|
22
|
+
self.component_permissions_hashes = options[:component_permissions]
|
23
|
+
|
24
|
+
self.permissions = [:find, :view]
|
25
|
+
add_flattened_component_permissions(options[:component_permissions])
|
26
|
+
self.permissions += [:manage, :own]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Add explicit permissions to the permissions class accessor
|
32
|
+
# for each of the entries in the component_permission hashes
|
33
|
+
#
|
34
|
+
# eg. {:labels => [:view, :apply, :remove, :create, :delete]} becomes
|
35
|
+
# [:view_labels, :apply_labels, :remove_labels, :create_labels, :delete_labels]
|
36
|
+
# and is added to self.permissions
|
37
|
+
def add_flattened_component_permissions(component_permissions_hashes)
|
38
|
+
for component_permissions_hash in component_permissions_hashes
|
39
|
+
component_permissions_hash.each do |component_name, component_permissions|
|
40
|
+
component_permissions.each { |component_permission| self.permissions << "#{component_permission}_#{component_name}".to_sym }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
include Joinable::ActsAsPermissable::ClassMethods
|
48
|
+
|
49
|
+
def self.extended(base)
|
50
|
+
base.cattr_accessor :permissions, :component_permissions_hashes
|
51
|
+
|
52
|
+
base.has_many :membership_invitations, :as => :joinable, :dependent => :destroy, :before_add => :add_initiator
|
53
|
+
base.has_many :membership_requests, :as => :joinable, :dependent => :destroy
|
54
|
+
base.has_many :memberships, :as => :joinable, :dependent => :destroy, :order => "id ASC", :before_remove => :add_initiator
|
55
|
+
|
56
|
+
base.has_many :invitees, :class_name => "User", :through => :membership_invitations, :source => :user
|
57
|
+
base.has_many :requestees, :class_name => "User", :through => :membership_requests, :source => :user
|
58
|
+
base.has_many :members, :class_name => "User", :through => :memberships, :source => :user
|
59
|
+
|
60
|
+
base.has_many :non_owner_memberships, :as => :joinable, :class_name => 'Membership', :conditions => "permissions NOT LIKE '%own%'"
|
61
|
+
base.has_one :owner_membership, :as => :joinable, :class_name => 'Membership', :conditions => "permissions LIKE '%own%'"
|
62
|
+
|
63
|
+
base.has_many :permission_links, :as => :joinable, :dependent => :destroy
|
64
|
+
|
65
|
+
base.has_one :default_permission_set, :as => :joinable, :dependent => :destroy
|
66
|
+
|
67
|
+
base.after_create :add_owner_membership
|
68
|
+
|
69
|
+
base.class_eval do
|
70
|
+
# Return all *joinables* that a User is a member of with the appropriate permissions
|
71
|
+
scope :with_permission, lambda {|user, permission| where(with_permission_sql(user, permission)) }
|
72
|
+
|
73
|
+
#scope :open, lambda { where(default_permission_set_permission_exists_sql(joinable_type, joinable_id, 'find')) }
|
74
|
+
|
75
|
+
# TODO: Why is this NULLS LAST? Probably because we want the results in some specific order when joined with users, but couldn't we order manually in the find?
|
76
|
+
scope :with_member, lambda {|user| joins(:memberships).where(:memberships => {:user_id => (user.is_a?(User) ? user.id : user)}).order("memberships.created_at DESC NULLS LAST") }
|
77
|
+
end
|
78
|
+
|
79
|
+
base.accepts_nested_attributes_for :default_permission_set
|
80
|
+
base.accepts_nested_attributes_for :membership_invitations, :allow_destroy => true
|
81
|
+
base.accepts_nested_attributes_for :memberships, :allow_destroy => true, :reject_if => proc { |attributes| attributes['locked'] == 'true' }
|
82
|
+
end
|
83
|
+
|
84
|
+
def permissions_string
|
85
|
+
permissions.join(" ")
|
86
|
+
end
|
87
|
+
|
88
|
+
# Simple Permission Strings - Permission strings for four basic levels of permissions - viewer, collaborator, manager, owner
|
89
|
+
# =============================================
|
90
|
+
# Member can view everything but modify nothing
|
91
|
+
def viewer_permissions_string
|
92
|
+
viewer_permissions.join(" ")
|
93
|
+
end
|
94
|
+
|
95
|
+
def viewer_permissions
|
96
|
+
permissions.select { |permission| permission == :find || permission.to_s.starts_with?("view") }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Member can view everything and modify everything except members
|
100
|
+
def collaborator_permissions_string
|
101
|
+
collaborator_permissions.join(" ")
|
102
|
+
end
|
103
|
+
|
104
|
+
def collaborator_permissions
|
105
|
+
permissions - [:manage, :own]
|
106
|
+
end
|
107
|
+
|
108
|
+
# Member can view everything, modify everything, and manage membership
|
109
|
+
def manager_permissions_string
|
110
|
+
(permissions - [:own]).join(" ")
|
111
|
+
end
|
112
|
+
|
113
|
+
# Member started the joinable
|
114
|
+
def owner_permissions_string
|
115
|
+
permissions_string
|
116
|
+
end
|
117
|
+
# =============================
|
118
|
+
# End Simple Permission Strings
|
119
|
+
|
120
|
+
# Returns the SQL necessary to find all joinables for which the user
|
121
|
+
# has a membership with a specific permission.
|
122
|
+
#
|
123
|
+
# Permissions which require special handling:
|
124
|
+
#
|
125
|
+
# * find - In addition to memberships, invitations and default permission sets are checked for the permission. This is because
|
126
|
+
# a joinable should be able to be found once an invitation has been extended or if it is findable by default. (even if the user isn't a member of it).
|
127
|
+
#
|
128
|
+
# * view_* - This is a class of permissions that start with the word 'view'. When determining if a user can view any aspect of a joinable, we also check
|
129
|
+
# if the project is open.
|
130
|
+
#
|
131
|
+
# * join - This is a faux permission. A user has permission to join a joinable if they have an invitation to view it or if it is viewable by default.
|
132
|
+
#
|
133
|
+
# * collaborate - This is a faux permission. A user has permission to collaborate if they have any additional permissions above the standard viewer permissions.
|
134
|
+
def with_permission_sql(user, permission, options = {})
|
135
|
+
permission = permission.to_sym
|
136
|
+
|
137
|
+
case user
|
138
|
+
when String
|
139
|
+
user_id = user
|
140
|
+
when
|
141
|
+
user_id = user.id
|
142
|
+
end
|
143
|
+
|
144
|
+
joinable_type = options[:type_column] || name
|
145
|
+
joinable_id = options[:id_column] || table_name + ".id"
|
146
|
+
|
147
|
+
if permission == :find
|
148
|
+
"#{membership_permission_exists_sql(user_id, joinable_type, joinable_id, 'find')} OR #{membership_invitation_permission_exists_sql(user_id, joinable_type, joinable_id, 'find')} OR #{default_permission_set_permission_exists_sql(joinable_type, joinable_id, 'find')}"
|
149
|
+
elsif permission.to_s.starts_with?('view')
|
150
|
+
"#{membership_permission_exists_sql(user_id, joinable_type, joinable_id, permission)} OR #{default_permission_set_permission_exists_sql(joinable_type, joinable_id, permission)}"
|
151
|
+
elsif permission == :join
|
152
|
+
"#{membership_invitation_permission_exists_sql(user_id, joinable_type, joinable_id, 'view')} OR #{default_permission_set_permission_exists_sql(joinable_type, joinable_id, 'view')}"
|
153
|
+
elsif permission.to_s.starts_with?('join_and_')
|
154
|
+
default_permission_set_permission_exists_sql(joinable_type, joinable_id, permission.to_s.gsub('join_and_', ''))
|
155
|
+
elsif permission == :collaborate
|
156
|
+
"EXISTS (SELECT id FROM memberships WHERE memberships.joinable_type = '#{joinable_type}' AND memberships.joinable_id = #{joinable_id} AND memberships.user_id = #{user_id} AND memberships.permissions && '{#{(collaborator_permissions - viewer_permissions).join(",")}}')"
|
157
|
+
else
|
158
|
+
membership_permission_exists_sql(user_id, joinable_type, joinable_id, permission)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def membership_permission_exists_sql(user_id, joinable_type, joinable_id, permission)
|
165
|
+
"EXISTS (SELECT id FROM memberships WHERE memberships.joinable_type = '#{joinable_type}' AND memberships.joinable_id = #{joinable_id} AND memberships.user_id = #{user_id} AND #{permission_sql_condition('memberships.permissions', permission)})"
|
166
|
+
end
|
167
|
+
|
168
|
+
def membership_invitation_permission_exists_sql(user_id, joinable_type, joinable_id, permission)
|
169
|
+
"EXISTS (SELECT id FROM membership_invitations WHERE membership_invitations.joinable_type = '#{joinable_type}' AND membership_invitations.joinable_id = #{joinable_id} AND membership_invitations.user_id = #{user_id} AND #{permission_sql_condition('membership_invitations.permissions', permission)})"
|
170
|
+
end
|
171
|
+
|
172
|
+
def default_permission_set_permission_exists_sql(joinable_type, joinable_id, permission)
|
173
|
+
"EXISTS (SELECT id FROM default_permission_sets WHERE default_permission_sets.joinable_type = '#{joinable_type}' AND default_permission_sets.joinable_id = #{joinable_id} AND #{permission_sql_condition('default_permission_sets.permissions', permission)})"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
module InstanceMethods
|
178
|
+
include Joinable::ActsAsPermissable::InstanceMethods
|
179
|
+
|
180
|
+
# Override attributes= to make sure that the user and initiator attributes are initialized before
|
181
|
+
# the membership_invitation and membership before_add callbacks are triggered
|
182
|
+
# since they reference these attributes
|
183
|
+
def attributes=(attributes_hash)
|
184
|
+
super(ActiveSupport::OrderedHash[attributes_hash.symbolize_keys.sort_by {|a| [:user, :user_id, :initiator].include?(a.first) ? 0 : 1}])
|
185
|
+
end
|
186
|
+
|
187
|
+
def acts_like_joinable?
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
attr_accessor :cached_membership_request, :cached_membership_invitation, :cached_membership, :initiator
|
192
|
+
|
193
|
+
# Get the membership request (if any) for a specific
|
194
|
+
# user. This method also supports caching of a membership
|
195
|
+
# request in order to facilitate eager loading.
|
196
|
+
#
|
197
|
+
# eg. For all of the projects on the projects index page,
|
198
|
+
# we want to do something similar to
|
199
|
+
# Project.all(:include => :membership_requests).
|
200
|
+
#
|
201
|
+
# We can't do exactly that however because we only want the membership_requests
|
202
|
+
# related to the current_user, not all users.
|
203
|
+
#
|
204
|
+
# Instead, we fake it by doing a separate query
|
205
|
+
# which gets all the user's membership_requests related to
|
206
|
+
# all the projects being displayed. We then cache the request relevant to
|
207
|
+
# this project in the cached_membership_request instance variable for later use
|
208
|
+
# by the view.
|
209
|
+
def membership_request_for(user)
|
210
|
+
if cached_membership_request != nil
|
211
|
+
cached_membership_request
|
212
|
+
else
|
213
|
+
membership_requests.where(:user_id => user.id).first
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Find out whether this Joinable has a membership request for a certain user and return true, else false
|
218
|
+
def membership_request_for?(user)
|
219
|
+
!membership_request_for(user).nil?
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
# Get the membership invitation (if any) for a specific
|
224
|
+
# user. This method also supports caching of a membership
|
225
|
+
# request in order to facilitate eager loading.
|
226
|
+
#NOTE: See :membership_request_for documentation for an in depth example of this type of behaviour
|
227
|
+
def membership_invitation_for(user)
|
228
|
+
if cached_membership_invitation != nil
|
229
|
+
cached_membership_invitation
|
230
|
+
else
|
231
|
+
membership_invitations.where(:user_id => user.id).first
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Find out whether this Joinable has a membership invitation for a certain user and return true, else false
|
236
|
+
def membership_invitation_for?(user)
|
237
|
+
!membership_invitation_for(user).nil?
|
238
|
+
end
|
239
|
+
|
240
|
+
# Get the membership (if any) for a specific
|
241
|
+
# user. This method also supports caching of a membership
|
242
|
+
# request in order to facilitate eager loading.
|
243
|
+
#NOTE: See :membership_request_for documentation for an in depth example of this type of behaviour
|
244
|
+
def membership_for(user)
|
245
|
+
if cached_membership != nil
|
246
|
+
cached_membership
|
247
|
+
else
|
248
|
+
memberships.where(:user_id => user.id).first
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Find out whether this Joinable has a membership for a certain user and return true, else false
|
253
|
+
def membership_for?(user)
|
254
|
+
!membership_for(user).nil?
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns the timestamp of the last time the memberships were updated for this joinable
|
258
|
+
def memberships_updated_at
|
259
|
+
memberships.maximum(:updated_at)
|
260
|
+
end
|
261
|
+
|
262
|
+
delegate :access_model, :to => :default_permission_set
|
263
|
+
|
264
|
+
def access_model=(model)
|
265
|
+
default_permission_set.access_model = model
|
266
|
+
end
|
267
|
+
|
268
|
+
# Returns true or false depending on whether or not the user has the specified permission for this object.
|
269
|
+
# Will cache the result if uncached.
|
270
|
+
def check_permission(user, permission_name)
|
271
|
+
permission_name = permission_name.to_s.dup
|
272
|
+
|
273
|
+
# Generate a cache path based on the factors that affect the user's permissions
|
274
|
+
# If User has membership
|
275
|
+
# - depends on permissions
|
276
|
+
# Elsif User has been invited
|
277
|
+
# - depends on existence of invitation and the default permissions (when checking the view permission)
|
278
|
+
# Else User doesn't have any membership
|
279
|
+
# - depends on default permissions of the joinable
|
280
|
+
if membership = memberships.where(:user_id => user.id).first
|
281
|
+
key = "membership_#{membership.updated_at.to_f}"
|
282
|
+
elsif self.membership_invitations.where(:user_id => user.id).exists?
|
283
|
+
key = "default_permissions_#{self.default_permission_set.updated_at.to_f}_invitation_exists"
|
284
|
+
else
|
285
|
+
key = "default_permissions_#{self.default_permission_set.updated_at.to_f}"
|
286
|
+
end
|
287
|
+
|
288
|
+
cache_path = "permissions/#{self.class.table_name}/#{self.id}/user_#{user.id}_#{key}"
|
289
|
+
|
290
|
+
if defined?(RAILS_CACHE)
|
291
|
+
permissions = RAILS_CACHE.read(cache_path)
|
292
|
+
if permissions && (value = permissions[permission_name]) != nil
|
293
|
+
return value
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# The permission isn't cached yet, so cache it
|
298
|
+
value = self.class.with_permission(user, permission_name).exists?(self.id)
|
299
|
+
|
300
|
+
if defined?(RAILS_CACHE)
|
301
|
+
if permissions
|
302
|
+
permissions = permissions.dup
|
303
|
+
permissions[permission_name] = value
|
304
|
+
else
|
305
|
+
permissions = {permission_name => value}
|
306
|
+
end
|
307
|
+
RAILS_CACHE.write(cache_path, permissions)
|
308
|
+
end
|
309
|
+
return value
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
|
314
|
+
# Adds an initiator to a membership or invitation to possibly use in feed generation
|
315
|
+
def add_initiator(membership)
|
316
|
+
membership.initiator = (initiator || user)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Adds an permission entry with full access to the object by the user associated with the object if one does not already exist
|
320
|
+
def add_owner_membership
|
321
|
+
Membership.create(:joinable => self, :user => user, :permissions => self.class.permissions_string) unless Membership.where(:joinable_type => self.class.to_s, :joinable_id => self.id, :user_id => user.id).exists?
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
@@ -0,0 +1,223 @@
|
|
1
|
+
module Joinable #:nodoc:
|
2
|
+
module ActsAsJoinableComponent
|
3
|
+
module ActMethod
|
4
|
+
# Inherits permissions of an a target object through the permission_links table
|
5
|
+
# An entry in the permission_links table is calculated by tracing the proxy object through to the target
|
6
|
+
# Takes a hash of params that specify which attached object the proxy should inherit its permissions from
|
7
|
+
def acts_as_joinable_component(options = {})
|
8
|
+
extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
|
9
|
+
include InstanceMethods unless included_modules.include?(InstanceMethods)
|
10
|
+
|
11
|
+
options.assert_valid_keys :polymorphic, :parent, :view_permission
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :view_permission
|
15
|
+
end
|
16
|
+
|
17
|
+
self.view_permission = options[:view_permission]
|
18
|
+
|
19
|
+
# If we inherit permissions from multiple types of objects (polymorphic)
|
20
|
+
if options[:polymorphic]
|
21
|
+
parent_klass = options[:parent] + '_type.constantize'
|
22
|
+
parent_id = options[:parent] + '_id'
|
23
|
+
# Else if we are not, and inherit permissions from only one type of object
|
24
|
+
else
|
25
|
+
parent_klass = options[:parent].camelize
|
26
|
+
parent_id = options[:parent] + '_id'
|
27
|
+
end
|
28
|
+
|
29
|
+
# Rescue in case we haven't got a parent
|
30
|
+
# TODO: this could probably be done better
|
31
|
+
class_eval <<-EOV
|
32
|
+
def next_link
|
33
|
+
begin
|
34
|
+
#{parent_klass}.find_by_id(#{parent_id})
|
35
|
+
rescue
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
EOV
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
include Joinable::ActsAsPermissable::ClassMethods
|
45
|
+
|
46
|
+
def self.extended(base)
|
47
|
+
base.has_one :permission_link, :as => :component, :dependent => :destroy
|
48
|
+
base.after_create :find_joinable_and_create_permission_link
|
49
|
+
|
50
|
+
base.class_eval do
|
51
|
+
scope :with_permission, lambda { |user, permission| select("#{table_name}.*").where(with_permission_sql(user, permission)) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the SQL necessary to find all components for which there is no associated joinable or
|
56
|
+
# the user has a membership with a specific permission.
|
57
|
+
#
|
58
|
+
# Permissions which require special handling:
|
59
|
+
#
|
60
|
+
# * view_* - This is a class of permissions that start with the word 'view'. When determining if a user can view any aspect of a joinable, we also check
|
61
|
+
# if the project is open.
|
62
|
+
#
|
63
|
+
# * join_and_* - This is a class of permissions that start with the words 'join_and_'. When determining if a user will have a certain permission
|
64
|
+
# after they join a project, we need to check the default_permission_set of the project.
|
65
|
+
def with_permission_sql(user, permission, options = {})
|
66
|
+
permission = permission.to_s
|
67
|
+
|
68
|
+
case user
|
69
|
+
when String
|
70
|
+
user_id = user
|
71
|
+
else
|
72
|
+
user_id = user.id
|
73
|
+
end
|
74
|
+
|
75
|
+
component_type = options[:type_column] || name
|
76
|
+
component_id = options[:id_column] || table_name + ".id"
|
77
|
+
|
78
|
+
permission_without_join_and_prefix = permission.gsub('join_and_', '')
|
79
|
+
comparison_permission = permission_without_join_and_prefix == 'view' ? "permission_links.component_view_permission" : "'#{permission_without_join_and_prefix}'"
|
80
|
+
|
81
|
+
if permission.starts_with?('view')
|
82
|
+
"#{no_inherited_permissions_exist_sql(component_type, component_id)} OR #{membership_permission_exists_sql(user_id, component_type, component_id, comparison_permission)} OR #{default_permission_set_permission_exists_sql(component_type, component_id, comparison_permission)}"
|
83
|
+
elsif permission.starts_with?('join_and_')
|
84
|
+
default_permission_set_permission_exists_sql(component_type, component_id, comparison_permission)
|
85
|
+
else
|
86
|
+
"#{no_inherited_permissions_exist_sql(component_type, component_id)} OR #{membership_permission_exists_sql(user_id, component_type, component_id, comparison_permission)}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# All components that don't have a permission link to a joinable
|
93
|
+
def no_inherited_permissions_exist_sql(component_type, component_id)
|
94
|
+
"NOT EXISTS (SELECT * FROM permission_links WHERE permission_links.component_type = '#{component_type}' AND permission_links.component_id = #{component_id})"
|
95
|
+
end
|
96
|
+
|
97
|
+
# All components that have an associated membership with a specific permission
|
98
|
+
#
|
99
|
+
# The view permission requires special handling because it may be customized in the permission_link.
|
100
|
+
# For more information see the *recurse_to_inherit_custom_view_permission* method.
|
101
|
+
def membership_permission_exists_sql(user_id, component_type, component_id, comparison_permission)
|
102
|
+
"EXISTS (SELECT * FROM memberships
|
103
|
+
INNER JOIN permission_links ON memberships.joinable_type = permission_links.joinable_type
|
104
|
+
AND memberships.joinable_id = permission_links.joinable_id
|
105
|
+
WHERE permission_links.component_type = '#{component_type}'
|
106
|
+
AND permission_links.component_id = #{component_id}
|
107
|
+
AND memberships.user_id = #{user_id}
|
108
|
+
AND #{comparison_permission} = ANY(memberships.permissions))"
|
109
|
+
end
|
110
|
+
|
111
|
+
def default_permission_set_permission_exists_sql(component_type, component_id, comparison_permission)
|
112
|
+
"EXISTS (SELECT * FROM default_permission_sets
|
113
|
+
INNER JOIN permission_links ON default_permission_sets.joinable_type = permission_links.joinable_type
|
114
|
+
AND default_permission_sets.joinable_id = permission_links.joinable_id
|
115
|
+
WHERE permission_links.component_type = '#{component_type}'
|
116
|
+
AND permission_links.component_id = #{component_id}
|
117
|
+
AND #{comparison_permission} = ANY(default_permission_sets.permissions))"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
module InstanceMethods
|
122
|
+
include Joinable::ActsAsPermissable::InstanceMethods
|
123
|
+
|
124
|
+
def acts_like_joinable_component?
|
125
|
+
true
|
126
|
+
end
|
127
|
+
|
128
|
+
# Used by unsaved joinable_components to return a list of users
|
129
|
+
# who will be able to view the component once it is saved.
|
130
|
+
# Useful for outputting information to the user while they
|
131
|
+
# are creating a new component.
|
132
|
+
def who_will_be_able_to_view?
|
133
|
+
User.find_by_sql("SELECT users.*
|
134
|
+
FROM users JOIN memberships ON users.id = memberships.user_id
|
135
|
+
WHERE memberships.joinable_type = '#{joinable.class.to_s}'
|
136
|
+
AND memberships.joinable_id = #{joinable.id}
|
137
|
+
AND #{self.class.permission_sql_condition('memberships.permissions', recurse_to_inherit_custom_view_permission)}")
|
138
|
+
end
|
139
|
+
|
140
|
+
def check_permission(user, permission)
|
141
|
+
# You can't ask to join joinable_components so the find permission is actually the view permission
|
142
|
+
permission = :view if permission == :find
|
143
|
+
|
144
|
+
if new_record?
|
145
|
+
if joinable.acts_like?(:joinable)
|
146
|
+
permission = recurse_to_inherit_custom_view_permission if permission == :view
|
147
|
+
joinable.check_permission(user, permission)
|
148
|
+
else
|
149
|
+
# The component isn't contained by a joinable so it is public.
|
150
|
+
true
|
151
|
+
end
|
152
|
+
else
|
153
|
+
self.class.with_permission(user, permission).exists?(id)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns the object that we should inherit permissions from
|
158
|
+
#
|
159
|
+
# Recurses until the target is reached,
|
160
|
+
# if we reach a target that does not act as a joinable, call method again if it is a joinable component,
|
161
|
+
# else fall out as the chain has no valid endpoint (eg. feed -> discussion -> item)
|
162
|
+
def joinable
|
163
|
+
if permission_link.present?
|
164
|
+
permission_link.joinable
|
165
|
+
else
|
166
|
+
parent = next_link
|
167
|
+
|
168
|
+
# Our target is now joinable therefore our target is at the end (eg. feed -> discussion -> [project])
|
169
|
+
if parent && parent.acts_like?(:joinable)
|
170
|
+
return parent
|
171
|
+
|
172
|
+
# Our target is a joinable_component therefore our target somewhere between the beginning and the end (eg. feed -> [discussion] -> ??? -> project)
|
173
|
+
elsif parent && parent.acts_like?(:joinable_component)
|
174
|
+
return parent.joinable
|
175
|
+
|
176
|
+
# We've fallen out because there was either no target or the target was not joinable or a joinable_component
|
177
|
+
else
|
178
|
+
return parent
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# inherited_view_permission is calculated by ascending up the chain of joinable components
|
184
|
+
# while view permission only takes into account the current joinable component.
|
185
|
+
# inherited_view_permission is for external use while view_permission should only be used internally.
|
186
|
+
def inherited_view_permission
|
187
|
+
permission_link.try(:component_view_permission)
|
188
|
+
end
|
189
|
+
|
190
|
+
def view_permission
|
191
|
+
klass_view_permission = self.class.view_permission
|
192
|
+
klass_view_permission = klass_view_permission.call(self) if klass_view_permission.respond_to?(:call)
|
193
|
+
|
194
|
+
# Allow view_permission to be set at the instance level
|
195
|
+
return @view_permission || klass_view_permission
|
196
|
+
end
|
197
|
+
attr_writer :view_permission
|
198
|
+
|
199
|
+
|
200
|
+
# Recurse up the tree to see if any of the intervening joinable_components have a customized view permission
|
201
|
+
# In that case, inherit that customized view permission. This allows searches of the form
|
202
|
+
# Feed.with_permission(:view) where feeds belong to joinable_components with custom view permissions.
|
203
|
+
# The query will then be able to return only the feeds which belong to joinable components that are viewable by the user
|
204
|
+
def recurse_to_inherit_custom_view_permission
|
205
|
+
parent = next_link
|
206
|
+
|
207
|
+
# If we've reached the last component in the chain or if this component provides a view permission
|
208
|
+
if parent.acts_like?(:joinable) || self.view_permission
|
209
|
+
return self.view_permission || :view
|
210
|
+
elsif parent.acts_like?(:joinable_component)
|
211
|
+
return parent.recurse_to_inherit_custom_view_permission
|
212
|
+
else
|
213
|
+
return nil
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Creates a link to the joinable that this component is associated with, if there is one.
|
218
|
+
def find_joinable_and_create_permission_link
|
219
|
+
self.create_permission_link(:joinable => joinable, :component_view_permission => recurse_to_inherit_custom_view_permission) if joinable.acts_like?(:joinable)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Adds useful methods to the User model which includes it.
|
2
|
+
module Joinable #:nodoc:
|
3
|
+
module ActsAsMember
|
4
|
+
module ActMethod
|
5
|
+
def acts_as_member
|
6
|
+
extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
|
7
|
+
include InstanceMethods unless included_modules.include?(InstanceMethods)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def self.extended(base)
|
13
|
+
base.has_many :memberships, :dependent => :destroy
|
14
|
+
base.has_many :membership_requests, :dependent => :destroy
|
15
|
+
base.has_many :membership_invitations, :dependent => :destroy
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
# FIXME: no need to call permission_to? on non-permissables.
|
21
|
+
# We should remove this extra code.
|
22
|
+
def permission_to?(permission, record)
|
23
|
+
if record.acts_like?(:permissable)
|
24
|
+
record.check_permission(self, permission)
|
25
|
+
else
|
26
|
+
if record.acts_like?(:visible_only_to_owner)
|
27
|
+
record.user == self
|
28
|
+
else
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def no_permission_to?(permission, record)
|
35
|
+
!permission_to?(permission, record)
|
36
|
+
end
|
37
|
+
|
38
|
+
def membership_requests_for_managed_joinables
|
39
|
+
MembershipRequest.for(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Abstract Module that is included in both joinable and joinable_component. Includes useful methods that both share.
|
2
|
+
module Joinable #:nodoc:
|
3
|
+
module ActsAsPermissable
|
4
|
+
module ClassMethods
|
5
|
+
def find_with_privacy(record_id, user, options = {})
|
6
|
+
record = find(record_id)
|
7
|
+
|
8
|
+
raise ActiveRecord::RecordNotFound, (options[:error_message] || "Couldn't find #{name}") unless user.permission_to?(:find, record)
|
9
|
+
|
10
|
+
return record
|
11
|
+
end
|
12
|
+
|
13
|
+
def permission_sql_condition(column, permission)
|
14
|
+
"'#{permission}' = ANY(#{column})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
def acts_like_permissable?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a list of users who either do or do not have the specified permission.
|
24
|
+
def who_can?(permission)
|
25
|
+
User.find_by_sql("SELECT * FROM users AS u1 WHERE #{self.class.with_permission_sql('u1.id', permission, :id_column => id)}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module FeedableExtensions
|
2
|
+
def self.add
|
3
|
+
Feed.class_eval do
|
4
|
+
# Filter feeds about public joinables that you haven't joined, unless the feed is actually about you
|
5
|
+
scope :without_unjoined, lambda {|joinable_type, user|
|
6
|
+
where("feeds.scoping_object_type IS NULL OR
|
7
|
+
feeds.scoping_object_type != '#{joinable_type}' OR
|
8
|
+
(feeds.feedable_type = 'User' AND feeds.feedable_id = #{user.id}) OR
|
9
|
+
EXISTS (SELECT * FROM memberships WHERE memberships.joinable_type = '#{joinable_type}' AND memberships.joinable_id = feeds.scoping_object_id AND memberships.user_id = ?)", user.id)
|
10
|
+
}
|
11
|
+
|
12
|
+
acts_as_joinable_component :parent => 'permission_inheritance_target', :polymorphic => true, :view_permission => lambda {|feed| :find if feed.feedable.acts_like?(:joinable) }
|
13
|
+
|
14
|
+
# The scoping_object becomes the parent if the feed is delegated to a non-permissable or the feedable is deleted
|
15
|
+
# eg. a user (non-permissible) leaves a project, the parent of the feed is the project since a user isn't a permissable
|
16
|
+
# eg. a writeboard is destroyed, the parent of the feed is now the project
|
17
|
+
def permission_inheritance_target_type
|
18
|
+
if feedable.acts_like?(:permissable)
|
19
|
+
feedable_type
|
20
|
+
else
|
21
|
+
scoping_object_type
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def permission_inheritance_target_id
|
26
|
+
if feedable.acts_like?(:permissable)
|
27
|
+
feedable_id
|
28
|
+
else
|
29
|
+
scoping_object_id
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# Included in models which have a permissions field (Membership, MembershipInvitation, DefaultPermissionSet)
|
2
|
+
# Wraps the permissions field to make it appear to the outside world as an array of symbols rather than the
|
3
|
+
# string that it is stored in the database as. Also adds helpers for creating complex forms
|
4
|
+
# to configure permissions and validations to ensure consistent ordering of permissions in the database.
|
5
|
+
module Joinable #:nodoc:
|
6
|
+
module PermissionsAttributeWrapper
|
7
|
+
def self.included(base)
|
8
|
+
base.before_save :verify_and_sort_permissions
|
9
|
+
end
|
10
|
+
|
11
|
+
def permissions_string
|
12
|
+
self[:permissions].join(' ')
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns an array of the permissions as symbols
|
16
|
+
def permissions
|
17
|
+
self[:permissions].collect(&:to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
def permissions=(permissions)
|
21
|
+
case permissions
|
22
|
+
when String
|
23
|
+
self[:permissions] = permissions.split(' ')
|
24
|
+
when Array
|
25
|
+
self[:permissions] = permissions
|
26
|
+
else
|
27
|
+
raise "Permissions were not passed to permissions writer in the appropriate format"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Used by advanced permission forms which group permissions by their associated component
|
32
|
+
# or using a single check box per permission.
|
33
|
+
def permission_attributes=(permissions)
|
34
|
+
self.permissions = [] # Reset permissions in anticipation for re-population
|
35
|
+
|
36
|
+
permissions.each do |key, value|
|
37
|
+
key, value = key.dup, value.dup
|
38
|
+
|
39
|
+
# Component Permissions
|
40
|
+
if key.ends_with? "_permissions"
|
41
|
+
grant_permissions(value)
|
42
|
+
# Singular Permission
|
43
|
+
elsif key.chomp! "_permission"
|
44
|
+
grant_permissions(key) if value.to_i != 0
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns an array of the permissions allowed by the joinable
|
50
|
+
def allowed_permissions
|
51
|
+
if self[:joinable_type]
|
52
|
+
self[:joinable_type].constantize.permissions
|
53
|
+
else
|
54
|
+
raise "Cannot get allowed access levels because permission is not attached to a permissable yet: #{inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns true if the object has all the permissions specified by +levels+
|
59
|
+
def has_permission?(*levels)
|
60
|
+
if levels.all? { |level| permissions.include? level.to_sym }
|
61
|
+
return true
|
62
|
+
else
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true if none of the permissions specified by +levels+ are present
|
68
|
+
def doesnt_have_permission?(*levels)
|
69
|
+
if permissions - levels == permissions
|
70
|
+
return true
|
71
|
+
else
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns true if the object has an empty permission set
|
77
|
+
def no_permissions?
|
78
|
+
permissions.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns true if the object only has the permissions in +levels+
|
82
|
+
def only_permission_to?(*levels)
|
83
|
+
if permissions - levels == []
|
84
|
+
return true
|
85
|
+
else
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def grant_permissions(permissions_to_grant)
|
91
|
+
case permissions_to_grant
|
92
|
+
when String
|
93
|
+
permissions_to_grant = permissions_to_grant.split(' ').collect(&:to_sym)
|
94
|
+
when Symbol
|
95
|
+
permissions_to_grant = [permissions_to_grant]
|
96
|
+
end
|
97
|
+
|
98
|
+
self.permissions += permissions_to_grant
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Verifies that all the access levels are valid for the attached permissible
|
104
|
+
# Makes sure no permissions are duplicated
|
105
|
+
# Enforces the order of access levels in the access attribute using the order of the permissions array
|
106
|
+
def verify_and_sort_permissions
|
107
|
+
# DefaultPermissionSet is allowed to have blank permissions (private joinable), the other models need at least find and view
|
108
|
+
self.permissions += [:find, :view] unless is_a?(DefaultPermissionSet)
|
109
|
+
|
110
|
+
raise "Invalid permissions: #{(permissions - allowed_permissions).inspect}. Must be one of #{allowed_permissions.inspect}" unless permissions.all? {|permission| allowed_permissions.include? permission}
|
111
|
+
|
112
|
+
self.permissions = permissions.uniq.sort_by { |permission| allowed_permissions.index(permission) }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Adds readers for component permission groups and single permissions
|
116
|
+
#
|
117
|
+
# Used by advanced permission forms to determine how which options to select
|
118
|
+
# in the various fields. (eg. which option of f.select :labels_permissions to choose)
|
119
|
+
def method_missing(method_name, *args)
|
120
|
+
# add permission_for accessors and mutators
|
121
|
+
|
122
|
+
# NOTE: Don't mess with the method_name variable (e.g. change it to a string)
|
123
|
+
# since upstream methods might assume it is a symbol.
|
124
|
+
# NOTE: Ensure we enforce some characters before the '_permission' suffix because Rails 3 creates
|
125
|
+
if respond_to?(:joinable_type) && joinable_type.present?
|
126
|
+
if method_name.to_s =~ /.+_permissions/
|
127
|
+
return component_permissions_reader(method_name)
|
128
|
+
elsif method_name.to_s =~ /.+_permission/
|
129
|
+
return single_permission_reader(method_name)
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
133
|
+
else
|
134
|
+
super
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Get a string of all of the permissions the object has for a specific joinable component
|
139
|
+
# eg. labels_permissions # returns 'view_labels apply_labels remove_labels'
|
140
|
+
def component_permissions_reader(method_name)
|
141
|
+
for component_permissions_hash in joinable_type.constantize.component_permissions_hashes
|
142
|
+
component_permissions_hash.each do |component_name, component_permissions|
|
143
|
+
if method_name.to_s == "#{component_name}_permissions"
|
144
|
+
return component_permissions.collect {|permission| "#{permission}_#{component_name}"}.select {|permission| has_permission?(permission)}.join(" ")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
raise "Unknown component_permissions_reader #{method_name.inspect}"
|
150
|
+
end
|
151
|
+
|
152
|
+
# Access a single permission
|
153
|
+
# eg. manage_permission # returns true if we can manage
|
154
|
+
def single_permission_reader(method_name)
|
155
|
+
for permission in joinable_type.constantize.permissions
|
156
|
+
if method_name.to_s == "#{permission}_permission"
|
157
|
+
return has_permission?(permission)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
raise "Unknown single_permission_reader #{method_name.inspect}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_joinable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Wallace
|
9
|
+
- Nicholas Jakobsen
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-04-30 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: postgres_ext
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.2.2
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 0.2.2
|
31
|
+
description: Adds access control to objects by giving them members, each with configurable
|
32
|
+
permissions.
|
33
|
+
email: technical@rrnpilot.org
|
34
|
+
executables: []
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- app/models/default_permission_set.rb
|
39
|
+
- app/models/membership.rb
|
40
|
+
- app/models/membership_invitation.rb
|
41
|
+
- app/models/membership_request.rb
|
42
|
+
- app/models/permission_link.rb
|
43
|
+
- lib/acts_as_joinable.rb
|
44
|
+
- lib/joinable/acts_as_joinable.rb
|
45
|
+
- lib/joinable/acts_as_joinable_component.rb
|
46
|
+
- lib/joinable/acts_as_member.rb
|
47
|
+
- lib/joinable/acts_as_permissable.rb
|
48
|
+
- lib/joinable/feedable_extensions.rb
|
49
|
+
- lib/joinable/permissions_attribute_wrapper.rb
|
50
|
+
- README.rdoc
|
51
|
+
homepage: http://github.com/rrn/acts_as_joinable
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.8.25
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: An easy to use permissions system
|
75
|
+
test_files: []
|