acts_as_joinable 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|