increase permissions performance
fixes: CNVS-11425 This is a performance refactor of the permissions. The biggest change is the caching. The cache key is now based on the right so each right will be cached by itself. The goal is to reduce places where we implement caching for permissions and let adheres_to_policy handle it. Another commit is coming to clean up calls to the new methods created here. g/34280 Test Plan: - Make sure permissions all work still. - Make masquerading still works with permissions. - Make sure switing views such as "student view" for a course. Change-Id: I4a30b0aba394cea24c3b60167fc1369a2584f5a4 Reviewed-on: https://gerrit.instructure.com/34278 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com> QA-Review: Cody Cutrer <cody@instructure.com>
This commit is contained in:
parent
7f3df8adfb
commit
e690395a9c
|
@ -317,24 +317,20 @@ class ApplicationController < ActionController::Base
|
|||
alias :authorized_action? :authorized_action
|
||||
|
||||
def is_authorized_action?(object, *opts)
|
||||
return false unless object
|
||||
|
||||
user = opts.shift
|
||||
action_session = nil
|
||||
action_session ||= session
|
||||
action_session = opts.shift if !opts[0].is_a?(Symbol) && !opts[0].is_a?(Array)
|
||||
actions = Array(opts.shift)
|
||||
can_do = false
|
||||
actions = Array(opts.shift).flatten
|
||||
|
||||
begin
|
||||
if object == @context && user == @current_user
|
||||
@context_all_permissions ||= @context.grants_rights?(user, session, nil)
|
||||
can_do = actions.any?{|a| @context_all_permissions[a] }
|
||||
else
|
||||
can_do = object.grants_rights?(user, action_session, *actions).values.any?
|
||||
end
|
||||
return object.grants_any_right?(user, action_session, *actions)
|
||||
rescue => e
|
||||
logger.warn "#{object.inspect} raised an error while granting rights. #{e.inspect}"
|
||||
logger.warn "#{object.inspect} raised an error while granting rights. #{e.inspect}" if logger
|
||||
end
|
||||
can_do
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def render_unauthorized_action
|
||||
|
|
|
@ -153,29 +153,12 @@ module ApplicationHelper
|
|||
return can_do(obj, user, actions)
|
||||
end
|
||||
actions = Array(actions).flatten
|
||||
if (object == @context || object.is_a?(Course)) && user == @current_user
|
||||
@context_all_permissions ||= {}
|
||||
@context_all_permissions[object.asset_string] ||= object.grants_rights?(user, session, nil)
|
||||
return !(@context_all_permissions[object.asset_string].keys & actions).empty?
|
||||
end
|
||||
@permissions_lookup ||= {}
|
||||
return true if actions.any? do |action|
|
||||
lookup = [object ? object.asset_string : nil, user ? user.id : nil, action]
|
||||
@permissions_lookup[lookup] if @permissions_lookup[lookup] != nil
|
||||
end
|
||||
begin
|
||||
rights = object.grants_rights?(user, session, *actions)
|
||||
return object.grants_any_right?(user, session, *actions)
|
||||
rescue => e
|
||||
logger.warn "#{object.inspect} raised an error while granting rights. #{e.inspect}" if logger
|
||||
return false
|
||||
end
|
||||
res = false
|
||||
rights.each do |action, value|
|
||||
lookup = [object ? object.asset_string : nil, user ? user.id : nil, action]
|
||||
@permissions_lookup[lookup] = value
|
||||
res ||= value
|
||||
end
|
||||
res
|
||||
false
|
||||
end
|
||||
|
||||
# Loads up the lists of files needed for the wiki_sidebar. Called from
|
||||
|
|
|
@ -2188,7 +2188,7 @@ class Course < ActiveRecord::Base
|
|||
|
||||
# derived from policy for Group#grants_right?(user, nil, :read)
|
||||
def groups_visible_to(user, groups = active_groups)
|
||||
if grants_rights?(user, nil, :manage_groups, :view_group_pages).values.any?
|
||||
if grants_any_right?(user, :manage_groups, :view_group_pages)
|
||||
# course-wide permissions; all groups are visible
|
||||
groups
|
||||
else
|
||||
|
|
|
@ -289,7 +289,7 @@ class Group < ActiveRecord::Base
|
|||
member = self.group_memberships.create(attrs)
|
||||
end
|
||||
# permissions for this user in the group are probably different now
|
||||
Rails.cache.delete(permission_cache_key_for(user))
|
||||
clear_permissions_cache(user)
|
||||
return member
|
||||
end
|
||||
|
||||
|
@ -304,7 +304,7 @@ class Group < ActiveRecord::Base
|
|||
users.sort_by!(&:id)
|
||||
notification_name = options[:notification_name] || "New Context Group Membership"
|
||||
notification = Notification.by_name(notification_name)
|
||||
users.each {|user| Rails.cache.delete(permission_cache_key_for(user))}
|
||||
users.each {|user| clear_permissions_cache(user) }
|
||||
|
||||
users.each_with_index do |user, index|
|
||||
Instructure::BroadcastPolicy::NotificationPolicy.send_later_enqueue_args(:send_notification,
|
||||
|
|
|
@ -145,6 +145,11 @@ class ActiveRecord::Base
|
|||
@global_asset_string ||= "#{self.class.reflection_type_name}_#{global_id}"
|
||||
end
|
||||
|
||||
# Override the adheres_to_policy permission_cache_key for to make it shard aware.
|
||||
def permission_cache_key_for(user, session, right)
|
||||
Shard.default.activate { super }
|
||||
end
|
||||
|
||||
# little helper to keep checks concise and avoid a db lookup
|
||||
def has_asset?(asset, field = :context)
|
||||
asset.id == send("#{field}_id") && asset.class.base_ar_class.name == send("#{field}_type")
|
||||
|
@ -198,20 +203,18 @@ class ActiveRecord::Base
|
|||
|
||||
def self.clear_cached_contexts
|
||||
@@cached_contexts = {}
|
||||
@@cached_permissions = {}
|
||||
end
|
||||
|
||||
def cached_context_grants_right?(user, session, *permissions)
|
||||
@@cached_contexts ||= {}
|
||||
context_key = "#{self.context_type}_#{self.context_id}" if self.respond_to?(:context_type)
|
||||
context_key ||= "Course_#{self.course_id}"
|
||||
@@cached_contexts[context_key] ||= self.context if self.respond_to?(:context)
|
||||
@@cached_contexts[context_key] ||= self.course
|
||||
@@cached_permissions ||= {}
|
||||
key = [context_key, (user ? user.id : nil)].cache_key
|
||||
@@cached_permissions[key] = nil if session && session[:session_affects_permissions]
|
||||
@@cached_permissions[key] ||= @@cached_contexts[context_key].grants_rights?(user, session, nil).keys
|
||||
(@@cached_permissions[key] & Array(permissions).flatten).any?
|
||||
permissions.flatten!
|
||||
permissions.compact!
|
||||
permissions.uniq!
|
||||
|
||||
if self.respond_to?(:context)
|
||||
self.context.grants_any_right?(user, session, *permissions)
|
||||
elsif self.respond_to?(:course)
|
||||
self.course.grants_any_right?(user, session, *permissions)
|
||||
end
|
||||
end
|
||||
|
||||
def cached_context_short_name
|
||||
|
@ -430,7 +433,7 @@ class ActiveRecord::Base
|
|||
:group => expression,
|
||||
:order => expression
|
||||
)
|
||||
# mysql gives us date keys, sqlite/postgres don't
|
||||
# mysql gives us date keys, sqlite/postgres don't
|
||||
return result if result.keys.first.is_a?(Date)
|
||||
Hash[result.map { |date, count|
|
||||
[Time.zone.parse(date).to_date, count]
|
||||
|
|
|
@ -63,3 +63,9 @@ Delayed::Worker.lifecycle.around(:pop) do |worker, &block|
|
|||
block.call(worker)
|
||||
end
|
||||
end
|
||||
|
||||
Delayed::Worker.lifecycle.before(:perform) do |job|
|
||||
# Since AdheresToPolicy::Cache uses an instance variable class cache lets clear
|
||||
# it so we start with a clean slate.
|
||||
AdheresToPolicy::Cache.clear
|
||||
end
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
Copyright (c) 2014 Raphael Weiner
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -17,7 +17,7 @@ a.check_policy(u)
|
|||
License
|
||||
=======
|
||||
|
||||
Copyright (C) 2011 Instructure, Inc.
|
||||
Copyright (C) 2014 Instructure, Inc.
|
||||
|
||||
This file is part of Canvas.
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|||
spec.email = ["rweiner@pivotallabs.com", "stephan@pivotallabs.com"]
|
||||
spec.summary = %q{The canvas adheres to policy gem}
|
||||
|
||||
spec.files = Dir.glob("{lib,spec}/**/*") + %w(LICENSE.txt Rakefile README.md test.sh)
|
||||
spec.files = Dir.glob("{lib,spec}/**/*") + %w(Rakefile README.md test.sh)
|
||||
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||
spec.require_paths = ["lib"]
|
||||
|
|
|
@ -4,4 +4,5 @@ module AdheresToPolicy
|
|||
require "adheres_to_policy/policy"
|
||||
require "adheres_to_policy/class_methods"
|
||||
require "adheres_to_policy/instance_methods"
|
||||
require "adheres_to_policy/cache"
|
||||
end
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
#
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
module AdheresToPolicy
|
||||
class Cache
|
||||
# Internal: The time to live for the underlying cache. In seconds.
|
||||
CACHE_EXPIRES_IN = 3600
|
||||
|
||||
# Public: Gets the cached object with the provided key. Will call the block
|
||||
# if the key does not exist in the cache and store that returned value
|
||||
# from the block into the cache.
|
||||
#
|
||||
# key - The key to use for the cached object.
|
||||
# block - The block to call to get the value to write to the cache.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# fetch(:key) { 'value' }
|
||||
# # => 'value'
|
||||
#
|
||||
# Returns the value of the cached object from the key.
|
||||
def self.fetch(key, &block)
|
||||
return unless key
|
||||
|
||||
unless value = self.read(key)
|
||||
if block
|
||||
value = block.call
|
||||
self.write(key, value)
|
||||
end
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
# Public: Writes an object to the cache with the provided key. This also
|
||||
# writes to the underlying Rails.cache.
|
||||
#
|
||||
# key - The key to use for the caching the object.
|
||||
# value - The value to cache.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# write(:key, 'value')
|
||||
# # => 'value'
|
||||
#
|
||||
# Returns the value of the cached object from the key.
|
||||
def self.write(key, value)
|
||||
return unless key
|
||||
|
||||
Rails.cache.write(key, value, expires_in: CACHE_EXPIRES_IN)
|
||||
@cache ||= {}
|
||||
@cache[key] = value
|
||||
end
|
||||
|
||||
# Public: Reads an object from the cache with the provided key. This also
|
||||
# reads from the underlying Rails.cache if it is not in the local
|
||||
# cached hash.
|
||||
#
|
||||
# key - The key to use for the caching the object.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# read(:key)
|
||||
# # => 'value'
|
||||
#
|
||||
# Returns the value of the cached object from the key.
|
||||
def self.read(key)
|
||||
return unless key
|
||||
|
||||
@cache ||= {}
|
||||
if @cache.has_key?(key)
|
||||
@cache[key]
|
||||
else
|
||||
@cache[key] = Rails.cache.read(key)
|
||||
end
|
||||
end
|
||||
|
||||
# Public: Clears the local hashed cache.
|
||||
#
|
||||
# key - The key to clear. If none is provided it will clear all keys.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# clear
|
||||
# # => nil
|
||||
#
|
||||
# clear(:key)
|
||||
# # => 'value'
|
||||
#
|
||||
# Returns the value of the cached object from the key deleted.
|
||||
def self.clear(key = nil)
|
||||
if key
|
||||
@cache.delete(key)
|
||||
else
|
||||
@cache = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2011 Instructure, Inc.
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
|
@ -16,89 +16,355 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
module AdheresToPolicy #:nodoc:
|
||||
module AdheresToPolicy
|
||||
module InstanceMethods
|
||||
# Returns all permissions available for a user. If a specific set of
|
||||
# permissions is required, use grants_rights?.
|
||||
def check_policy(user, session=nil, *sought_rights)
|
||||
sought_rights = (sought_rights || []).compact
|
||||
seeking_all_rights = sought_rights.empty?
|
||||
granted_rights = []
|
||||
self.class.policy.conditions.each do |args|
|
||||
condition = args[0]
|
||||
condition_rights = args[1]
|
||||
if (seeking_all_rights && !(condition_rights - granted_rights).empty?) || !(sought_rights & condition_rights).empty?
|
||||
if (condition.arity == 1 && instance_exec(user, &condition) || condition.arity == 2 && instance_exec(user, session, &condition))
|
||||
sought_rights = sought_rights - condition_rights
|
||||
granted_rights.concat(condition_rights)
|
||||
break if sought_rights.empty? && !seeking_all_rights
|
||||
end
|
||||
end
|
||||
# Public: Gets the requested rights granted to a user.
|
||||
#
|
||||
# user - The user for which to get the rights.
|
||||
# session - The session to use if the rights are dependend upon the session.
|
||||
# args - The rights to get the status for.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# granted_rights(user, :read)
|
||||
# # => [ :read ]
|
||||
#
|
||||
# granted_rights(user, :read, :update)
|
||||
# # => [ :read, :update ]
|
||||
#
|
||||
# granted_rights(user, session, :update, :delete)
|
||||
# # => [ :update ]
|
||||
#
|
||||
# Returns an array of rights granted to the user.
|
||||
def granted_rights(user, *args)
|
||||
session, sought_rights = parse_args(args)
|
||||
sought_rights ||= []
|
||||
sought_rights = self.class.policy.available_rights if sought_rights.empty?
|
||||
sought_rights.select do |r|
|
||||
check_right?(user, session, r)
|
||||
end
|
||||
end
|
||||
# alias so its backwards compatible.
|
||||
alias :check_policy :granted_rights
|
||||
|
||||
# Public: Gets the requested rights and their status to a user.
|
||||
#
|
||||
# user - The user for which to get the rights.
|
||||
# session - The session to use if the rights are dependend upon the session.
|
||||
# args - The rights to get the status for.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# rights_status(user, :read)
|
||||
# # => { :read => true }
|
||||
#
|
||||
# rights_status(user, session, :update, :delete)
|
||||
# # => { :update => true, :delete => false }
|
||||
#
|
||||
# Returns a hash with the requested rights and their status.
|
||||
def rights_status(user, *args)
|
||||
session, sought_rights = parse_args(args)
|
||||
sought_rights ||= []
|
||||
sought_rights = self.class.policy.available_rights if sought_rights.empty?
|
||||
sought_rights.inject({}) do |h, r|
|
||||
h[r] = check_right?(user, session, r)
|
||||
h
|
||||
end
|
||||
granted_rights.uniq
|
||||
end
|
||||
|
||||
alias :check_permissions :check_policy
|
||||
|
||||
# Returns a hash of sought-after rights
|
||||
def grants_rights?(user, *sought_rights)
|
||||
session = nil
|
||||
if !sought_rights[0].is_a? Symbol
|
||||
session = sought_rights.shift
|
||||
# Deprecated: Gets the requested granted rights and their status to a user.
|
||||
# Use rights_status if you need the right and their granted
|
||||
# status or granted_rights if you just need an array of all rights
|
||||
# that are granted.
|
||||
#
|
||||
# user - The user for which to get the rights.
|
||||
# session - The session to use if the rights are dependend upon the session.
|
||||
# args - The rights to get the status for.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# grants_rights?(user, :read)
|
||||
# # => { :read => true }
|
||||
#
|
||||
# grants_rights?(user, session, :read, :update, :delete)
|
||||
# # => { :read => true, :update => true }
|
||||
#
|
||||
# Returns a hash with the requested granted rights and their status.
|
||||
def grants_rights?(user, *args)
|
||||
session, sought_rights = parse_args(args)
|
||||
sought_rights ||= []
|
||||
if all_rights = sought_rights.empty?
|
||||
sought_rights = self.class.policy.available_rights
|
||||
end
|
||||
sought_rights.inject({}) do |h, r|
|
||||
granted = check_right?(user, session, r)
|
||||
h[r] = granted if granted || !all_rights
|
||||
h
|
||||
end
|
||||
sought_rights = (sought_rights || []).compact.uniq
|
||||
|
||||
cache_lookup = is_a_context? && !is_a?(User)
|
||||
# If you're going to add something to the user session that
|
||||
# affects permissions, you'd durn well better set :session_affects_permissions
|
||||
# to true as well
|
||||
cache_lookup = false if session && session[:session_affects_permissions]
|
||||
|
||||
# Cache the lookup, iff this is a non-user context and the session
|
||||
# doesn't affect the policies. Since context policy lookups are
|
||||
# expensive (especially for courses), we grab all the permissions at
|
||||
# once
|
||||
granted_rights = if cache_lookup
|
||||
# Check and cache all the things!
|
||||
Rails.cache.fetch(permission_cache_key_for(user), :expires_in => 1.hour) do
|
||||
check_policy(user)
|
||||
end
|
||||
else
|
||||
check_policy(user, session, *sought_rights)
|
||||
end
|
||||
|
||||
sought_rights = granted_rights if sought_rights.empty?
|
||||
res = sought_rights.inject({}) { |h, r| h[r] = granted_rights.include?(r); h }
|
||||
res
|
||||
end
|
||||
|
||||
# user [, session], [, sought_right]
|
||||
# Public: Checks any of the rights passed in for a user.
|
||||
#
|
||||
# user - The user for which to determine the right.
|
||||
# session - The session to use if the rights are dependend upon the session.
|
||||
# rights - The rights to get the status for. Will return true if the user
|
||||
# is granted any of the rights provided.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# grants_any_right?(user, :read)
|
||||
# # => true
|
||||
#
|
||||
# grants_any_right?(user, session, :delete)
|
||||
# # => false
|
||||
#
|
||||
# grants_any_right?(user, session, :update, :delete)
|
||||
# # => true
|
||||
#
|
||||
# Returns true if any of the provided rights are granted to the user. False
|
||||
# if none of the provided rights are granted.
|
||||
def grants_any_right?(user, *args)
|
||||
session, sought_rights = parse_args(args)
|
||||
sought_rights.any? do |sought_right|
|
||||
check_right?(user, session, sought_right)
|
||||
end
|
||||
end
|
||||
|
||||
# Public: Checks all of the rights passed in for a user.
|
||||
#
|
||||
# user - The user for which to determine the right.
|
||||
# session - The session to use if the rights are dependend upon the session.
|
||||
# rights - The rights to get the status for. Will return true if the user
|
||||
# is granted all of the rights provided.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# grants_all_rights?(user, :read)
|
||||
# # => true
|
||||
#
|
||||
# grants_all_rights?(user, session, :delete)
|
||||
# # => false
|
||||
#
|
||||
# grants_all_rights?(user, session, :update, :delete)
|
||||
# # => false
|
||||
#
|
||||
# Returns true if any of the provided rights are granted to the user. False
|
||||
# if any of the provided rights are not granted.
|
||||
def grants_all_rights?(user, *args)
|
||||
session, sought_rights = parse_args(args)
|
||||
return false if sought_rights.empty?
|
||||
sought_rights.none? do |sought_right|
|
||||
!check_right?(user, session, sought_right)
|
||||
end
|
||||
end
|
||||
|
||||
# Public: Checks the right passed in for a user.
|
||||
#
|
||||
# user - The user for which to determine the right.
|
||||
# session - The session to use if the rights are dependend upon the session.
|
||||
# right - The right to get the status for. Will return true if the user
|
||||
# is granted the right provided.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# grants_right?(user, :read)
|
||||
# # => true
|
||||
#
|
||||
# grants_right?(user, session, :delete)
|
||||
# # => false
|
||||
#
|
||||
# grants_right?(user, session, :update)
|
||||
# # => true
|
||||
#
|
||||
# Returns true if any of the provided rights are granted to the user. False
|
||||
# if none of the provided rights are granted.
|
||||
def grants_right?(user, *args)
|
||||
sought_right = args.first.is_a?(Symbol) ? args.first : args[1].to_sym rescue nil
|
||||
return false unless sought_right
|
||||
|
||||
grants_rights?(user, *args)[sought_right]
|
||||
session, sought_rights = parse_args(args)
|
||||
raise ArgumentError if sought_rights.length > 1
|
||||
check_right?(user, session, sought_rights.first)
|
||||
end
|
||||
|
||||
# Used for a more-natural: user.has_rights?(@account, :destroy)
|
||||
def has_rights?(obj, *sought_rights)
|
||||
obj.grants_rights?(self, *sought_rights)
|
||||
end
|
||||
|
||||
def permission_cache_key_for(user)
|
||||
adheres_to_policy_cache_key(['context_permissions', self, user])
|
||||
# Public: Clears the cached permission states for the user.
|
||||
#
|
||||
# user - The user for which to clear the rights.
|
||||
# session - The session to use if the rights are dependend upon the session.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# clear_permissions_cache(user)
|
||||
# # => nil
|
||||
#
|
||||
# clear_permissions_cache(user, session)
|
||||
# # => nil
|
||||
#
|
||||
def clear_permissions_cache(user, session = nil)
|
||||
Cache.clear
|
||||
self.class.policy.available_rights.each do |available_right|
|
||||
Rails.cache.delete(permission_cache_key_for(user, session, available_right))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Internal: Parses the arguments passed in for a session and sought rights
|
||||
# array.
|
||||
#
|
||||
# args - The args containing the session and sought rights.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# parse_args([ session, :read, :write ])
|
||||
# # => session, [ :read, :write ]
|
||||
#
|
||||
# parse_args([ nil, :read, :write ])
|
||||
# # => nil, [ :read, :write ]
|
||||
#
|
||||
# Returns a session object which is nil if it was not provided and an array
|
||||
# of the sought rights.
|
||||
def parse_args(args)
|
||||
session = nil
|
||||
if !args[0].is_a? Symbol
|
||||
session = args.shift
|
||||
end
|
||||
args.compact!
|
||||
args.uniq!
|
||||
|
||||
return session, args
|
||||
end
|
||||
|
||||
# Internal: Checks the right for a user based on session.
|
||||
#
|
||||
# user - The user to base the right check from.
|
||||
# session - The session to use when checking the right status.
|
||||
# sought_right - The right to check its status.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# check_right?(user, session, :read)
|
||||
# # => true, :read
|
||||
#
|
||||
# check_right?(user, nil, :delete)
|
||||
# # => false, :delete
|
||||
#
|
||||
# Returns the rights status pertaining the user and session provided.
|
||||
def check_right?(user, session, sought_right)
|
||||
return false unless sought_right
|
||||
|
||||
# Check the cache for the sought_right. If it exists in the cache its
|
||||
# state (true or false) will be returned. Otherwise we calculate the
|
||||
# state and cache it.
|
||||
Cache.fetch(permission_cache_key_for(user, session, sought_right)) do
|
||||
|
||||
# Loop through all the conditions until we find the first one that
|
||||
# grants us the sought_right.
|
||||
self.class.policy.conditions.each do |args|
|
||||
condition = args[0]
|
||||
condition_rights = args[1]
|
||||
|
||||
# We don't need to run the condition if the sought_right is not applicable
|
||||
# to that condition.
|
||||
next unless condition_rights.include?(sought_right)
|
||||
if check_condition?(condition, user, session)
|
||||
|
||||
# Since the condition is true we can loop through all the rights
|
||||
# that belong to it and cache them. This will short circut the above
|
||||
# Rails.cache.fetch for future checks that we won't have to do again.
|
||||
condition_rights.each do |condition_right|
|
||||
|
||||
# Skip the condition_right if its the one we are looking for.
|
||||
# The Rails.cache.fetch will take care of caching it for us.
|
||||
next if condition_right == sought_right
|
||||
|
||||
# Cache the condition_right since we already know they have access.
|
||||
Cache.write(permission_cache_key_for(user, session, condition_right), true)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
# Looks like we didn't find anything for this sought_right so it is
|
||||
# not granted to them.
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Internal: Gets the cache key for the user and right.
|
||||
#
|
||||
# user - The user to derive the cache key from.
|
||||
# session - The session to pull session specific key information from.
|
||||
# right - The right to derive the cache key from.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# permission_cache_key_for(user, :read)
|
||||
# # => '42/read'
|
||||
#
|
||||
# permission_cache_key_for(user, { :permissions_key => 'student' }, :read)
|
||||
# # => '42/read'
|
||||
#
|
||||
# Returns a string to use as a permissions cache key in the context of the
|
||||
# provided user and/or right.
|
||||
def permission_cache_key_for(user, session, right)
|
||||
# If you're going to add something to the user session that
|
||||
# affects permissions, you'd durn well better a :permissions_key
|
||||
# on the session as well
|
||||
if session && (session[:session_affects_permissions] || session[:permissions_key])
|
||||
session[:permissions_key] ||= session[:session_id]
|
||||
permissions_key = session[:permissions_key]
|
||||
end
|
||||
adheres_to_policy_cache_key(['permissions', self, user, permissions_key, right].compact)
|
||||
end
|
||||
|
||||
# Internal: Checks the users condition state.
|
||||
#
|
||||
# condition - The condition/policy block to call which determines the users
|
||||
# state. This is a callable proc with either a single argument
|
||||
# of a user or two arguments, a user and session.
|
||||
# user - The user passed to the condition to determine if they pass the
|
||||
# condition.
|
||||
# session - The session passed to the condition to determine if the user
|
||||
# passes the condition.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# check_condition?({ |user| true }, user)
|
||||
# # => true
|
||||
#
|
||||
# check_condition?({ |user, session| false }, user, session)
|
||||
# # => false
|
||||
#
|
||||
# Returns true or false on whether the user passes the condition.
|
||||
def check_condition?(condition, user, session)
|
||||
if condition.arity == 1
|
||||
instance_exec(user, &condition)
|
||||
elsif condition.arity == 2
|
||||
instance_exec(user, session, &condition)
|
||||
end
|
||||
end
|
||||
|
||||
# Internal: Generates a cache key from an array.
|
||||
#
|
||||
# some_array - The array used to generate the cache key.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# adheres_to_policy_cache_key([ 42, :read ])
|
||||
# # => '42/read'
|
||||
#
|
||||
# adheres_to_policy_cache_key([ 42, 'key', :read, :write ])
|
||||
# # => '42/key/read/write'
|
||||
#
|
||||
# Returns a string representing the cache key generated.
|
||||
def adheres_to_policy_cache_key(some_array)
|
||||
cache_key = some_array.instance_variable_get("@cache_key")
|
||||
cache_key = some_array.instance_variable_get("@cache_keys")
|
||||
return cache_key if cache_key
|
||||
|
||||
value = some_array.collect { |element| ActiveSupport::Cache.expand_cache_key(element) }.to_param
|
||||
some_array.instance_variable_set("@cache_key", value) unless some_array.frozen?
|
||||
value
|
||||
end
|
||||
|
||||
some_array.collect { |element| ActiveSupport::Cache.expand_cache_key(element) }.to_param
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2011 Instructure, Inc.
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
|
@ -16,7 +16,7 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
module AdheresToPolicy #:nodoc:
|
||||
module AdheresToPolicy
|
||||
class Policy
|
||||
attr_reader :conditions
|
||||
|
||||
|
@ -44,5 +44,9 @@ module AdheresToPolicy #:nodoc:
|
|||
@conditions.last.last.uniq!
|
||||
true
|
||||
end
|
||||
|
||||
def available_rights
|
||||
@all_rights ||= @conditions.map { |c| c.last }.flatten.compact.uniq
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,334 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2011 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe AdheresToPolicy::Policy, "set_policy" do
|
||||
|
||||
it "should take a block" do
|
||||
lambda {
|
||||
Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy { 1 + 1 }
|
||||
end
|
||||
}.should_not raise_error
|
||||
end
|
||||
|
||||
it "should allow multiple calls" do
|
||||
lambda {
|
||||
Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
3.times do
|
||||
set_policy { 1 + 1 }
|
||||
end
|
||||
end
|
||||
}.should_not raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe AdheresToPolicy::ClassMethods do
|
||||
before(:each) do
|
||||
@some_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
end
|
||||
end
|
||||
|
||||
it "should filter policy_block through a block filter with set_policy" do
|
||||
@some_class.should respond_to(:set_policy)
|
||||
lambda { @some_class.set_policy(1) }.should raise_error
|
||||
b = lambda { 1 }
|
||||
lambda { @some_class.set_policy(&b) }.should_not raise_error
|
||||
end
|
||||
|
||||
it "should use set_permissions as set_policy" do
|
||||
@some_class.should respond_to(:set_permissions)
|
||||
lambda { @some_class.set_permissions(1) }.should raise_error
|
||||
b = lambda { 1 }
|
||||
lambda { @some_class.set_permissions(&b) }.should_not raise_error
|
||||
end
|
||||
|
||||
it "should provide a Policy instance through policy" do
|
||||
@some_class.set_policy { 1 }
|
||||
@some_class.policy.should be_is_a(AdheresToPolicy::Policy)
|
||||
end
|
||||
|
||||
it "should continue to use the same Policy instance (an important check, since this is also a constructor)" do
|
||||
@some_class.set_policy { 1 }
|
||||
@some_class.policy.should eql(@some_class.policy)
|
||||
end
|
||||
|
||||
it "should apply all given policy blocks to the Policy instance" do
|
||||
@some_class.set_policy do
|
||||
given { |_| true }
|
||||
can :read
|
||||
end
|
||||
|
||||
@some_class.set_policy do
|
||||
given { |_| true }
|
||||
can :write
|
||||
end
|
||||
|
||||
some_class = @some_class.new
|
||||
some_class.check_policy(nil).should == [:read, :write]
|
||||
end
|
||||
end
|
||||
|
||||
describe AdheresToPolicy::InstanceMethods do
|
||||
before(:each) do
|
||||
@some_class = Class.new do
|
||||
attr_accessor :user
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy do
|
||||
given { |user| self.user == user }
|
||||
can :read
|
||||
end
|
||||
end
|
||||
|
||||
class User
|
||||
end
|
||||
end
|
||||
|
||||
it "should have setup a series of methods on the instance" do
|
||||
%w(check_policy grants_rights? has_rights?).each do |method|
|
||||
@some_class.new.should respond_to(method)
|
||||
end
|
||||
end
|
||||
|
||||
it "should be able to check a policy" do
|
||||
some_instance = @some_class.new
|
||||
some_instance.user = 1
|
||||
some_instance.check_policy(1).should eql([:read])
|
||||
end
|
||||
|
||||
it "should allow multiple forms of can statements" do
|
||||
actor_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy do
|
||||
given { |user| user == 1 }
|
||||
can :read and can :write
|
||||
|
||||
given { |user| user == 2 }
|
||||
can :update, :delete
|
||||
|
||||
given { |user| user == 3 }
|
||||
can [:manage, :set_permissions]
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.check_policy(1).should == [:read, :write]
|
||||
actor.check_policy(2).should == [:update, :delete]
|
||||
actor.check_policy(3).should == [:manage, :set_permissions]
|
||||
end
|
||||
|
||||
it "should execute all conditions when searching for all rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :write
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :update
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.check_policy(nil).should == [:read, :write, :update]
|
||||
actor.total.should == 3
|
||||
end
|
||||
|
||||
it "should skip duplicate conditions when searching for all rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read, :write
|
||||
|
||||
given { |_| raise "don't execute me" }
|
||||
can :write
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :update
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.check_policy(nil).should == [:read, :write, :update]
|
||||
actor.total.should == 2
|
||||
end
|
||||
|
||||
it "should only execute relevant conditions when searching for specific rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read
|
||||
|
||||
given { |_| raise "don't execute me" }
|
||||
can :write
|
||||
|
||||
given { |_| raise "me either" }
|
||||
can :update
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.check_policy(nil, nil, :read).should == [:read]
|
||||
actor.total.should == 1
|
||||
end
|
||||
|
||||
it "should skip duplicate conditions when searching for specific rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :write
|
||||
|
||||
given { |_| raise "me either" }
|
||||
can :read and can :write
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.check_policy(nil, nil, :read, :write).should == [:read, :write]
|
||||
actor.total.should == 2
|
||||
end
|
||||
|
||||
context "grants_right?" do
|
||||
before(:each) do
|
||||
@actor_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
set_policy do
|
||||
given { |actor| actor == "allowed actor" || actor.class.to_s == "User" }
|
||||
can :read
|
||||
|
||||
given { |actor| actor == "allowed actor" }
|
||||
can :read
|
||||
end
|
||||
|
||||
def is_a_context?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should check the policy" do
|
||||
non_context = @actor_class.new
|
||||
non_context.grants_right?("allowed actor", :read).should be_true
|
||||
non_context.grants_right?("allowed actor", :asdf).should be_false
|
||||
end
|
||||
|
||||
it "should return false if no specific ones are sought" do
|
||||
non_context = @actor_class.new
|
||||
non_context.grants_right?("allowed actor").should == false
|
||||
end
|
||||
|
||||
context "caching" do
|
||||
it "should cache for contexts" do
|
||||
user = User.new
|
||||
actor = @actor_class.new
|
||||
actor.stub(:is_a_context?).and_return(true)
|
||||
|
||||
Rails.cache.should_receive(:fetch).exactly(2).times.with { |p,| p =~ /context_permissions/ }.and_return([])
|
||||
actor.grants_rights?(user)
|
||||
# cache lookups for "nobody" as well
|
||||
actor.grants_rights?(nil)
|
||||
end
|
||||
|
||||
it "should not cache for contexts if session[:session_affects_permissions]" do
|
||||
actor = @actor_class.new
|
||||
actor.stub(:is_a_context?).and_return(true)
|
||||
|
||||
Rails.cache.should_receive(:read).never.with { |p,| p =~ /context_permissions/ }
|
||||
Rails.cache.stub(:read).with { |p,| p !~ /context_permissions/ }.and_return(nil)
|
||||
|
||||
actor.grants_rights?(User.new, {:session_affects_permissions => true})
|
||||
end
|
||||
|
||||
it "should not cache for non-contexts" do
|
||||
actor_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy {}
|
||||
|
||||
def is_a_context?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
Rails.cache.should_receive(:fetch).never
|
||||
|
||||
actor = actor_class.new
|
||||
actor_class.new.grants_rights?(actor)
|
||||
end
|
||||
|
||||
it "should not nil the session argument when not caching" do
|
||||
actor_class = Class.new do
|
||||
attr_reader :session
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy {
|
||||
given { |_, session| @session = session }
|
||||
can :read
|
||||
}
|
||||
|
||||
def is_a_context?;
|
||||
false;
|
||||
end
|
||||
end
|
||||
|
||||
Rails.cache.should_receive(:fetch).never
|
||||
|
||||
actor = actor_class.new
|
||||
actor.grants_rights?(actor, {})
|
||||
actor.session.should_not be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,80 @@
|
|||
#
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe AdheresToPolicy::Cache do
|
||||
def cached
|
||||
AdheresToPolicy::Cache.instance_variable_get(:@cache)
|
||||
end
|
||||
|
||||
context "#fetch" do
|
||||
it "tries to read the key value" do
|
||||
AdheresToPolicy::Cache.write(:key, 'value')
|
||||
expect(AdheresToPolicy::Cache).to_not receive(:write)
|
||||
value = AdheresToPolicy::Cache.fetch(:key){ 'new_value' }
|
||||
expect(value).to eq 'value'
|
||||
end
|
||||
|
||||
it "writes the key and value if it was not read" do
|
||||
expect(AdheresToPolicy::Cache).to receive(:write).with(:key, 'value')
|
||||
value = AdheresToPolicy::Cache.fetch(:key){ 'value' }
|
||||
expect(value).to eq 'value'
|
||||
end
|
||||
end
|
||||
|
||||
context "#write" do
|
||||
it "writes a value to the key provided" do
|
||||
expect(Rails.cache).to receive(:write).with(:key, 'value', anything).and_return('value')
|
||||
AdheresToPolicy::Cache.write(:key, 'value')
|
||||
expect(cached).to eq({ :key => 'value' })
|
||||
end
|
||||
end
|
||||
|
||||
context "#read" do
|
||||
before do
|
||||
AdheresToPolicy::Cache.write(:key, 'value')
|
||||
end
|
||||
|
||||
it "reads the provided key" do
|
||||
expect(AdheresToPolicy::Cache.read(:key)).to eq 'value'
|
||||
end
|
||||
|
||||
it "returns nil if the key does not exist" do
|
||||
expect(Rails.cache).to receive(:read).with(:key2)
|
||||
expect(AdheresToPolicy::Cache.read(:key2)).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
context "#clear" do
|
||||
before do
|
||||
AdheresToPolicy::Cache.write(:key1, 'value1')
|
||||
AdheresToPolicy::Cache.write(:key2, 'value2')
|
||||
end
|
||||
|
||||
it "clears all the cached objects" do
|
||||
AdheresToPolicy::Cache.clear
|
||||
expect(cached).to eq nil
|
||||
end
|
||||
|
||||
it "clears only the key provided" do
|
||||
AdheresToPolicy::Cache.clear(:key1)
|
||||
expect(cached).to eq({ :key2 => 'value2' })
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
#
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe AdheresToPolicy::ClassMethods do
|
||||
before(:each) do
|
||||
@some_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
end
|
||||
end
|
||||
|
||||
it "should filter policy_block through a block filter with set_policy" do
|
||||
@some_class.should respond_to(:set_policy)
|
||||
lambda { @some_class.set_policy(1) }.should raise_error
|
||||
b = lambda { 1 }
|
||||
lambda { @some_class.set_policy(&b) }.should_not raise_error
|
||||
end
|
||||
|
||||
it "should use set_permissions as set_policy" do
|
||||
@some_class.should respond_to(:set_permissions)
|
||||
lambda { @some_class.set_permissions(1) }.should raise_error
|
||||
b = lambda { 1 }
|
||||
lambda { @some_class.set_permissions(&b) }.should_not raise_error
|
||||
end
|
||||
|
||||
it "should provide a Policy instance through policy" do
|
||||
@some_class.set_policy { 1 }
|
||||
@some_class.policy.should be_is_a(AdheresToPolicy::Policy)
|
||||
end
|
||||
|
||||
it "should continue to use the same Policy instance (an important check, since this is also a constructor)" do
|
||||
@some_class.set_policy { 1 }
|
||||
@some_class.policy.should eql(@some_class.policy)
|
||||
end
|
||||
|
||||
it "should apply all given policy blocks to the Policy instance" do
|
||||
@some_class.set_policy do
|
||||
given { |_| true }
|
||||
can :read
|
||||
end
|
||||
|
||||
@some_class.set_policy do
|
||||
given { |_| true }
|
||||
can :write
|
||||
end
|
||||
|
||||
some_class = @some_class.new
|
||||
expect(some_class.grants_right?(nil, :read)).to eq true
|
||||
expect(some_class.grants_right?(nil, :write)).to eq true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,376 @@
|
|||
#
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe AdheresToPolicy::InstanceMethods do
|
||||
before(:each) do
|
||||
@some_class = Class.new do
|
||||
attr_accessor :user
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy do
|
||||
given { |user| self.user == user }
|
||||
can :read
|
||||
end
|
||||
end
|
||||
|
||||
class User
|
||||
end
|
||||
end
|
||||
|
||||
it "should have setup a series of methods on the instance" do
|
||||
%w(rights_status granted_rights grants_rights? grants_right?).each do |method|
|
||||
@some_class.new.should respond_to(method)
|
||||
end
|
||||
end
|
||||
|
||||
it "should be able to check a policy" do
|
||||
some_instance = @some_class.new
|
||||
some_instance.user = 1
|
||||
expect(some_instance.grants_right?(1, :read)).to eq true
|
||||
end
|
||||
|
||||
it "should allow multiple forms of can statements" do
|
||||
actor_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy do
|
||||
given { |user| user == 1 }
|
||||
can :read and can :write
|
||||
|
||||
given { |user| user == 2 }
|
||||
can :update, :delete
|
||||
|
||||
given { |user| user == 3 }
|
||||
can [:manage, :set_permissions]
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.rights_status(1, :read, :write).should == {:read => true, :write => true}
|
||||
actor.rights_status(2, :read, :update, :delete).should == {:read => false, :update => true, :delete => true}
|
||||
actor.rights_status(3, :read, :manage, :set_permissions).should == {:read => false, :manage => true, :set_permissions => true}
|
||||
|
||||
# Deprecated grants_rights?
|
||||
actor.grants_rights?(1).should == {:read => true, :write => true}
|
||||
actor.grants_rights?(1, :read, :write, :manage).should == {:read => true, :write => true, :manage => false}
|
||||
actor.grants_rights?(2, :read, :update, :delete).should == {:read => false, :update => true, :delete => true}
|
||||
actor.grants_rights?(3, :read, :manage, :set_permissions).should == {:read => false, :manage => true, :set_permissions => true}
|
||||
end
|
||||
|
||||
it "should execute all conditions when searching for all rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :write
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :update
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.rights_status(nil).should == {:read => true, :write => true, :update => true}
|
||||
actor.total.should == 3
|
||||
end
|
||||
|
||||
it "should skip duplicate conditions when searching for all rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read, :write
|
||||
|
||||
given { |_| raise "don't execute me" }
|
||||
can :write
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :update
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.rights_status(nil).should == {:read => true, :write => true, :update => true}
|
||||
actor.total.should == 2
|
||||
end
|
||||
|
||||
it "should only execute relevant conditions when searching for specific rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read
|
||||
|
||||
given { |_| raise "don't execute me" }
|
||||
can :write
|
||||
|
||||
given { |_| raise "me either" }
|
||||
can :update
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.rights_status(nil, :read).should == {:read => true}
|
||||
actor.total.should == 1
|
||||
end
|
||||
|
||||
it "should skip duplicate conditions when searching for specific rights" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |_| @total = @total + 1 }
|
||||
can :read
|
||||
|
||||
given { |_| @total = @total + 1 }
|
||||
can :write
|
||||
|
||||
given { |_| raise "me either" }
|
||||
can :read and can :write
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.rights_status(nil, :read, :write).should == {:read => true, :write => true}
|
||||
actor.total.should == 2
|
||||
end
|
||||
|
||||
context "clear_permissions_cache" do
|
||||
let (:sample_class) do
|
||||
Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
set_policy do
|
||||
given { |actor| actor == 1 }
|
||||
can :read
|
||||
|
||||
given { |actor| actor == 2 }
|
||||
can :read and can :write
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "clear the permissions cache" do
|
||||
expect(Rails.cache).to receive(:delete).with(/\/read$/)
|
||||
expect(Rails.cache).to receive(:delete).with(/\/write$/)
|
||||
|
||||
sample = sample_class.new
|
||||
expect(sample.grants_right?(1, :read)).to eq true
|
||||
sample.clear_permissions_cache(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "grants_any_right?" do
|
||||
let (:sample_class) do
|
||||
Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
set_policy do
|
||||
given { |actor| actor == 1 }
|
||||
can :read
|
||||
|
||||
given { |actor| actor == 2 }
|
||||
can :read and can :write
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should check the policy" do
|
||||
sample = sample_class.new
|
||||
expect(sample.grants_any_right?(1, :read, :write)).to eq true
|
||||
expect(sample.grants_any_right?(1, :asdf)).to eq false
|
||||
end
|
||||
|
||||
it "should return false if no specific ones are sought" do
|
||||
sample = sample_class.new
|
||||
sample.grants_any_right?(1).should == false
|
||||
end
|
||||
end
|
||||
|
||||
context "grants_all_rights?" do
|
||||
let (:sample_class) do
|
||||
Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
set_policy do
|
||||
given { |actor| actor == 1 }
|
||||
can :read
|
||||
|
||||
given { |actor| actor == 2 }
|
||||
can :read and can :write
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should check the policy" do
|
||||
sample = sample_class.new
|
||||
expect(sample.grants_all_rights?(1, :read, :write)).to eq false
|
||||
expect(sample.grants_all_rights?(2, :read, :write)).to eq true
|
||||
expect(sample.grants_all_rights?(3, :read, :asdf)).to eq false
|
||||
end
|
||||
|
||||
it "should return false if no specific ones are sought" do
|
||||
sample = sample_class.new
|
||||
sample.grants_all_rights?(1).should == false
|
||||
end
|
||||
end
|
||||
|
||||
context "check_condition?" do
|
||||
it "should run condition based on its arity" do
|
||||
actor_class = Class.new do
|
||||
attr_accessor :total
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
def initialize
|
||||
@total = 0
|
||||
end
|
||||
|
||||
set_policy do
|
||||
given { |arg1| @total = @total + arg1 }
|
||||
can :read
|
||||
|
||||
given { |arg1, arg2| @total = @total + arg1 + arg2[:count] }
|
||||
can :write
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.rights_status(1, { count: 2 }, :read, :write).should == {:read => true, :write => true}
|
||||
actor.total.should == 4
|
||||
end
|
||||
end
|
||||
|
||||
context "grants_right?" do
|
||||
before(:each) do
|
||||
@actor_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
set_policy do
|
||||
given { |actor| actor == "allowed actor" || actor.class.to_s == "User" }
|
||||
can :read
|
||||
|
||||
given { |actor| actor == "allowed actor" }
|
||||
can :read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should check the policy" do
|
||||
non_context = @actor_class.new
|
||||
expect(non_context.grants_right?("allowed actor", :read)).to eq true
|
||||
expect(non_context.grants_right?("allowed actor", :asdf)).to eq false
|
||||
end
|
||||
|
||||
it "should return false if no specific ones are sought" do
|
||||
non_context = @actor_class.new
|
||||
non_context.grants_right?("allowed actor").should == false
|
||||
end
|
||||
|
||||
it "should raise argument exception if anything other then one right is provided" do
|
||||
non_context = @actor_class.new
|
||||
expect(non_context.grants_right?("allowed actor", :read)).to eq true
|
||||
expect{
|
||||
non_context.grants_right?("allowed actor", :asdf, :read)
|
||||
}.to raise_exception ArgumentError
|
||||
end
|
||||
|
||||
context "caching" do
|
||||
it "should cache permissions" do
|
||||
user = User.new
|
||||
actor = @actor_class.new
|
||||
|
||||
expect(AdheresToPolicy::Cache).to receive(:fetch).twice.with(/permissions/).and_return([])
|
||||
actor.rights_status(user)
|
||||
# cache lookups for "nobody" as well
|
||||
actor.rights_status(nil)
|
||||
end
|
||||
|
||||
it "should not nil the session argument when not caching" do
|
||||
actor_class = Class.new do
|
||||
attr_reader :session
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy {
|
||||
given { |_, session| @session = session }
|
||||
can :read
|
||||
}
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
actor.rights_status(actor, {})
|
||||
actor.session.should_not be_nil
|
||||
end
|
||||
|
||||
it "should change cache key based on session[:permissions_key]" do
|
||||
session = {
|
||||
:session_affects_permissions => true,
|
||||
:permissions_key => 'permissions_key',
|
||||
:session_id => 'session_id'
|
||||
}
|
||||
actor_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy {
|
||||
given { |_| true }
|
||||
can :read
|
||||
}
|
||||
|
||||
def call_permission_cache_key_for(*args)
|
||||
permission_cache_key_for(*args)
|
||||
end
|
||||
end
|
||||
|
||||
actor = actor_class.new
|
||||
expect(actor.call_permission_cache_key_for(nil, session, :read)).to match(/\>\/permissions_key\/read$/)
|
||||
|
||||
session[:permissions_key] = nil
|
||||
expect(actor.call_permission_cache_key_for(nil, session, :read)).to match(/\>\/session_id\/read$/)
|
||||
|
||||
session[:session_affects_permissions] = false
|
||||
session.delete(:permissions_key)
|
||||
expect(actor.call_permission_cache_key_for(nil, session, :read)).to match(/\>\/read$/)
|
||||
|
||||
expect(actor.call_permission_cache_key_for(nil, nil, :read)).to match(/\>\/read$/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,60 @@
|
|||
#
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe AdheresToPolicy::Policy, "set_policy" do
|
||||
it "should take a block" do
|
||||
lambda {
|
||||
Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
set_policy { 1 + 1 }
|
||||
end
|
||||
}.should_not raise_error
|
||||
end
|
||||
|
||||
it "should allow multiple calls" do
|
||||
lambda {
|
||||
Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
3.times do
|
||||
set_policy { 1 + 1 }
|
||||
end
|
||||
end
|
||||
}.should_not raise_error
|
||||
end
|
||||
|
||||
context "available_rights" do
|
||||
it "should return all available rights" do
|
||||
example_class = Class.new do
|
||||
extend AdheresToPolicy::ClassMethods
|
||||
|
||||
set_policy {
|
||||
given { |_| true }
|
||||
can :read, nil
|
||||
|
||||
given { |_| true }
|
||||
can :write, :read
|
||||
}
|
||||
end
|
||||
|
||||
expect(example_class.policy.available_rights).to eq [:read, :write]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,12 +24,13 @@ RSpec.configure do |config|
|
|||
config.treat_symbols_as_metadata_keys_with_true_values = true
|
||||
config.run_all_when_everything_filtered = true
|
||||
config.filter_run :focus
|
||||
|
||||
# Run specs in random order to surface order dependencies. If you find an
|
||||
# order dependency and want to debug it, you can fix the order by providing
|
||||
# the seed, which is printed after each run.
|
||||
# --seed 1234
|
||||
config.order = 'random'
|
||||
|
||||
config.after(:each) do
|
||||
Rails.cache.clear
|
||||
# Clean up after ourselves since its a class instance variable
|
||||
AdheresToPolicy::Cache.instance_variable_set(:@cache, nil)
|
||||
end
|
||||
end
|
||||
|
||||
module Rails
|
||||
|
|
|
@ -3288,25 +3288,25 @@ describe Course do
|
|||
|
||||
it "should allow course-wide visibility regardless of membership given :manage_groups permission" do
|
||||
@course.groups_visible_to(@user).should be_empty
|
||||
@course.expects(:check_policy).with(@user).returns([:manage_groups])
|
||||
@course.expects(:grants_any_right?).returns(true)
|
||||
@course.groups_visible_to(@user).should == [@group]
|
||||
end
|
||||
|
||||
it "should allow course-wide visibility regardless of membership given :view_group_pages permission" do
|
||||
@course.groups_visible_to(@user).should be_empty
|
||||
@course.expects(:check_policy).with(@user).returns([:view_group_pages])
|
||||
@course.expects(:grants_any_right?).returns(true)
|
||||
@course.groups_visible_to(@user).should == [@group]
|
||||
end
|
||||
|
||||
it "should default to active groups only" do
|
||||
@course.expects(:check_policy).with(@user).returns([:manage_groups]).at_least_once
|
||||
@course.expects(:grants_any_right?).returns(true).at_least_once
|
||||
@course.groups_visible_to(@user).should == [@group]
|
||||
@group.destroy
|
||||
@course.reload.groups_visible_to(@user).should be_empty
|
||||
end
|
||||
|
||||
it "should allow overriding the scope" do
|
||||
@course.expects(:check_policy).with(@user).returns([:manage_groups]).at_least_once
|
||||
@course.expects(:grants_any_right?).returns(true).at_least_once
|
||||
@group.destroy
|
||||
@course.groups_visible_to(@user).should be_empty
|
||||
@course.groups_visible_to(@user, @course.groups).should == [@group]
|
||||
|
@ -3352,7 +3352,7 @@ describe Course do
|
|||
|
||||
it 'can be read by a prior user' do
|
||||
user.student_enrollments.create!(:workflow_state => 'completed', :course => @course)
|
||||
@course.check_policy(user).should == [:read, :read_outcomes, :read_grades, :read_forum]
|
||||
@course.check_policy(user).sort.should == [:read, :read_forum, :read_grades, :read_outcomes]
|
||||
end
|
||||
|
||||
it 'can have its forum read by an observer' do
|
||||
|
|
|
@ -409,6 +409,10 @@ end
|
|||
# so before(:all)'s don't get confused
|
||||
Account.clear_special_account_cache!
|
||||
Notification.after_create { Notification.reset_cache! }
|
||||
|
||||
# Since AdheresToPolicy::Cache uses an instance variable class cache lets clear
|
||||
# it so we start with a clean slate.
|
||||
AdheresToPolicy::Cache.clear
|
||||
end
|
||||
|
||||
def delete_fixtures!
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2011 Instructure, Inc.
|
||||
# Copyright (C) 2014 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
|
@ -31,7 +31,7 @@ describe "/gradebooks/gradebook2" do
|
|||
assigns[:gradebook_upload] = @course.build_gradebook_upload
|
||||
assigns[:body_classes] = []
|
||||
@course.expects(:allows_grade_publishing_by).with(@user).returns(course_allows)
|
||||
@course.expects(:grants_rights?).with(@user, {}, nil).returns(permissions_allow ? {:manage_grades=>true} : {}) if course_allows
|
||||
@course.expects(:grants_any_right?).returns(permissions_allow) if course_allows
|
||||
render "/gradebooks/gradebook2"
|
||||
response.should_not be_nil
|
||||
if course_allows && permissions_allow
|
||||
|
|
Loading…
Reference in New Issue