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:
Nick Cloward 2014-05-02 10:35:29 -06:00 committed by Cody Cutrer
parent 7f3df8adfb
commit e690395a9c
22 changed files with 1086 additions and 481 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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
# 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 = (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)
sought_rights.inject({}) do |h, r|
granted = check_right?(user, session, r)
h[r] = granted if granted || !all_rights
h
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
# 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
# user [, session], [, sought_right]
# 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)
# 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
def permission_cache_key_for(user)
adheres_to_policy_cache_key(['context_permissions', self, user])
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!

View File

@ -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