I've looked through the documentation and did some searching but i don't see an option for an omnipotent user (super user) level, or how one can be created.
Has anyone seen or created a why of doing this? im thinking it may be possible to tie into core authentication system but im not sure where to do the tie-in.
many thanks..
The only way to do this is to have your authorization checks return true for a user or role that has been designated a "super user." So, it would look like this:
def update?
*normal authorization logic* or is_superuser?
end
def edit?
*normal authorization logic* or is_superuser?
end
#etc...
private
def is_superuser?
# configure how to determine who the super users are and return true/false
end
You can define the is_superuser? private method in the ApplicationPolicy assuming that you inherit your class level policies from the Application Policy; otherwise, you will need to define it in each policy.
I've found a way a little DRYer, by using the inheritance of ApplicationPolicy. I alias access methods and I tie a superuser test before invoking any of them. If user is a super user, I simply return true. I do that at initialization before I need the instance method to be defined to be aliased.
ALIAS_PREFIX = '__original_'
def initialize(user, record)
#user = user
#record = record
[:index?,:show?,:create?,:new?, :update?, :edit?, :destroy?].each do |access_method|
alias_name = ALIAS_PREFIX+access_method.to_s
aliasing_original_method(access_method,alias_name)
self.class.send(:define_method, access_method) do |*args|
superuser? ? (return true) : send(alias_name, *args)
end
end
end
private
def superuser?
#whatever you want to define a super user
end
def aliasing_original_method(old_name, new_name)
self.class.send(:alias_method, new_name, old_name)
self.class.send(:private, new_name)
end
And in [AnyFile]Policy I do:
def initialize(user, record)
super(user, record)
end
This will ensure the true return for each method in sub-policy.
[UPDATE]
The first solution is a little messy, and my knowledge in ruby (and deadlines) doesn't permit me to push it farther. Anyway, I've found another way. As I was always switch-casing the roles of users, I've implemented in ApplicationPolicy a for_roles method.
def for_roles(*args,&block)
return true if superuser?
if args.include?(:all) || (#user.role_symbols & args).any?
block.call
else false
end
end
Then, in any policy, you can do for example
for_roles(:client_admin,:technician) do
#any rule computation, DB request you want
end
#or
for_roles(:all) do
#any rule computation, DB request you want
end
Related
I'm trying to make a concern that checks if a user is subscribed to an appropriate plan for my SaaS app.
Here's basically what I'm trying to do:
module SubscriptionControlled extend ActiveSupport::Concern
class_methods do
def requires_subscription_to(perm)
##perms = [perm]
end
end
included do
validate :check_subscription
end
def check_subscription
##perms.each do |perm|
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
This provides this api for a model:
class SomeModel < ApplicationModel
include SubscriptionControlled
requires_subscription_to :pro
end
The problem I'm having is that ##perms seems to be scoped to the CONCERN, rather than the MODEL. So this value is the same for all models. So whichever model is loaded last sets this value for all models.
eg: if loaded in this order:
Model1 -> sets ##perms to [:pro]
Model2 -> sets ##perms to [:business]
Both model 1 and model 2 will only require a subscription to :business
Is there a way of storing class-level variables in a concern that take effect on a per-model basis to accomplish this API?
I don't have a Ruby interpreter at hand right now but I'm fairly certain that using a single # in the class method should do the trick. Another thing that comes to mind is something along the lines of
included do
define_singleton_method :requires_subscription_to do |new_perm|
##perms ||= []
##perms << Array(new_perm)
end
end
Since that will create a new method every time the concern is included, it should work. I just seem to remember that methods defined like that are slightly slower - but since it will probably only be called during initialization, it shouldn't pose a problem in any case.
So I found the right way to do this using a ClassMethods module
module SubscriptionControlled extend ActiveSupport::Concern
module ClassMethods
#perms = []
def requires_subscription_to(perm)
#perms = [perm]
end
def perms
#perms
end
end
included do
validate :check_subscription
end
def check_subscription
self.class.perms.each do |perm|
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
this keeps the permissions scoped to the class, not the concern.
I think you're overcomplicating this. You don't need the check_subscription method at all and that method is why you're trying to make ##perms (or #perm) work.
validate is just a class method like any other and you can give validate block. You can use that block to capture the perm and do away with all the extra machinery:
module SubscriptionControlled extend ActiveSupport::Concern
module ClassMethods
def requires_subscription_to(perm)
validate do
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
end
I am created new rails application and I want to restrict user actions based on only one condition like record can be editable by owner(created_by) and sub-owner(Added by owner). I have models like App, User and controller like AppController. In AppController I have more than one actions like index, create, show, update, delete. I have one policy like AppPolicy. Here I need to create only one method to verify all actions but by default each action requires another method like action_name? in policy class.
Example
Existing code:
class AppPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope
end
end
def action1?
record.users.include? (user)
end
def action2?
record.users.include? (user)
end
def action3?
record.users.include? (user)
end
end
From above code we can see a same condition reside in all methods. I need to use only one method to verify action1, action2, action3. I don't know this is possible or not in Pundit.
I know this is an old question but I just had the same problem.
I can think about 2 solutions:
solution 1
When you know all the actions that could be called.
You can use define_method, like this
[:action1?, :action2?].each do |m|
define_method(m) { record.users.include? (user) }
end
solution 2
When you don't know all the actions. (this could be dangerous)
You can use a combination of method_missing and respond_to_missing. The latter is needed since pundit will call internally respond_to before calling the corresponding method of the policy.
Example:
def method_missing(m, *args, &block)
record.users.include? (user)
end
def respond_to_missing?(method_name, include_private = false)
true #Here it would be better to add some conditions
end
You can use cancan (or cancancan) gem rubygems link
You can create the ability configuration file with
rails g cancan:ability
The authorize! method in your controller will raise an exception if the user is not able to perform the given action, so call it on before_action callback.
Documentation here
Is it possible to DRY-up the following code:
def is_user?
is_role? ROLES[:user]
end
def is_mod?
is_role? ROLES[:mod]
end
def is_admin?
is_role? ROLES[:admin]
end
private
def is_role?(role)
self.roles & role == role
end
Into a single function, yet still have the ability to call the function names as currently (is_user?, is_mod?, etc)
UPDATE:
Using Aetherus' answer below I created the following for managing user roles (where a user can have multiple roles):
class User < ActiveRecord::Base
# Use bitwise values for more roles (double the previous values)
ROLES = { user: 1, dummy: 2, mod: 4, admin: 8 }
# Add the desired role
def add_role(role)
self.roles |= ROLES[role]
end
# eg: add_role :admin
# Removed the desired role
def remove_role(role)
self.roles &= ~ROLES[role]
end
# methods for each role (mod? admin? etc)
ROLES.keys.each do |role|
define_method("#{role}?") do
self.roles & ROLES[role] == ROLES[role]
end
end
end
You can define multiple methods with one single ruby code block.
%w(user mod admin).each do |role|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{role}?
role == '#{role}' && roles.include?('#{role}')
end
RUBY
end
Or a more clear way:
%w(user mod admin).each do |role|
define_method("#{role}?") do
self.role == role && roles.include?(role)
end
end
By the way, in ruby, the is_ prefix is not needed, since the trailing ? tells the programmers that method returns a true or a false.
You can go with method_missing.
The simplest possible solution would be something like:
class Roles
def method_missing(method_name, *args, &block)
if /^is_(?<role_name>\w+)\?$/ =~ method_name
is_role?(role_name.to_sym)
else
super
end
end
private
def is_role?(role_name)
# just for demo purposes
p "Checking #{role_name}"
end
end
roles = Roles.new
roles.is_user?
roles.is_mod?
roles.is_admin?
In method_missing I'm trying to catch any method that is not implemented (please note, I removed the proper methods is_user?, is_mod? and is_admin?), later, I'm checking if the name of method is of proper format with Regex (/^is_(?<role_name>\w+)\?$/), and if it is, I'm reusing captured role_name.
Slightly more restrictive method_missing.
Problem with this approach is, it will accept any method call, like let's say is_super_user?. In some cases this might be desirable, sometimes not. If you would like to restrict it only to the 3 type of users you've mentioned, you can change the Regex to:
/^is_(user|mod|admin)\?$/
One last thing. When implementing method_missing, you should also take care about respond_to_missing?, which is quite crucial when you would like to assert if the object responds to those magic methods:
class Roles
# ...
def respond_to_missing?(method_name, include_private = false)
/^is_(user|mod|admin)\?$/ =~ method_name
end
end
With this in place, you are able to do:
roles = Roles.new
roles.respond_to? :is_admin? # => true
roles.respond_to? :is_super_user? # => false
Read more here.
Hope that helps!
#Jacob, if you use rails 4, you can use AR#enum feature (http://api.rubyonrails.org/classes/ActiveRecord/Enum.html), no need to implement this by hands.
I have a Rails 4 app using Devise (the most recent) and am trying to create a random token for each user (like the ID, but longer, etc.) Using this answer I was able to come up with the follow code:
# app/models/model_name.rb
class ModelName < ActiveRecord::Base
include Tokenable
end
# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern
included do
before_create :generate_token
end
protected
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless self.class.exists?(token: random_token)
end
end
end
This code works fantastically for tokens that are unique for any given model. I.e. All Users will have unique tokens, and all Admins will have unique tokens. But an Admin may have the same token as a User – this behavior is unwanted.
Is there an elegant way, short of abstracting the token into its own model and using "has_one" relationships, to ensure that the token does not exist in all the models it is a part of?
(I guess I could hard code unless (User.exists? ... or Admin.exists? ... ) into the unless clause, though this seems bulky.)
Any thoughts or suggestions are appreciated! Thanks!
Rails 5 comes with a new feaeture has_secure_tokenis really easy to use:
# Schema: User(token:string, auth_token:string)
class User < ActiveRecord::Base
has_secure_token :auth_token
end
user = User.new
user.save
user.auth_token # => "pX27zsMN2ViQKta1bGfLmVJE"
user.regenerate_auth_token # => true
Since Rails 5 isn't already released, you can use the Backport has_secure_token gem
I would create a method that lists each of the Classes that are including my concern and then test against the token for each. Something like this:
# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern
included do
before_create :generate_token
end
protected
def included_classes
ActiveRecord::Base.descendants.select do |c|
c.included_modules.include(Concerns::Tokenable)}.map(&:name)
end
end
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless included_classes.map {|c| c.constantize.exists?(token: random_token) }.include?(true)
end
end
end
So include_classes is going to return an array of names as strings of each of the classes that include the Tokenable concern. And then in the loop within generate_token is going to check against each of these classes generating an array of true or false which then we just check if any are true with include?(true).
Here is were I found how to get included classes (first answer).
EDIT
In Rails 5 the included_classes looks like this (note the ApplicationRecord and not needing the Concerns::Tokenable):
def included_classes
ApplicationRecord.descendants.select do |c|
c.included_modules.include?(Tokenable)
end
end
Oddly enough, most of this works as it has been written, however I'm not sure how I can evaluate if the current_user has a badge, (all the relationships are proper, I am only having trouble with my methods in my class (which should partially be moved into a lib or something), regardless, the issue is specifically 1) checking if the current user has a record, and 2) if not create the corresponding new record.
If there is an easier or better way to do this, please share. The following is what I have:
# Recipe Controller
class RecipesController < ApplicationController
def create
# do something
if #recipe.save
current_user.check_if_badges_earned(current_user)
end
end
So as for this, it definitely seems messy, I'd like for it to be just check_if_badges_earned and not have to pass the current_user into the method, but may need to because it might not always be the current user initiating this method.
# User model
class User < ActiveRecord::Base
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1, user)
end
if user.recipes.count > 20
award_badge(2, user)
end
end
def award_badge(badge_id, user)
#see if user already has this badge, if not, give it to them!
unless user.badgings.any? { |b| b[:badge_id] == badge_id}
#badging = Badging.new(:badge_id => badge_id, :user_id => user)
#badging.save
end
end
end
So while the first method (check_if_badges_earned) seems to excucte fine and only give run award_badge() when the conditions are met, the issue happens in the award_badge() method itself the expression unless user.badgings.any? { |b| b[:badge_id] == badge_id} always evaluates as true, so the user is given the badge even if it already had the same one (by badge_id), secondly the issue is that it always saves the user_id as 1.
Any ideas on how to go about debugging this would be awesome!
Regardless of whether you need the current_user behavior above, award_badge should just be a regular instance method acting on self instead of acting on the passed user argument (same goes for check_if_badges_earned). In your award_badge method, try find_or_create_by_... instead of the logic you currently have. For example, try this:
class User < ActiveRecord::Base
# ...
def award_badge(badge_id)
badgings.find_or_create_by_badge_id(badge_id)
end
end
To access the current_user in your model classes, I sometimes like to use thread-local variables. It certainly blurs the separation of MVC, but sometimes this kind of coupling is just necessary in an application.
In your ApplicationController, store the current_user in a thread-local variable:
class ApplicationController < ActionController::Base
before_filter :set_thread_locals
private
# Store thread-local variables so models can access them (Hackish, but useful)
def set_thread_locals
Thread.current[:current_user] = current_user
end
end
Add a new class method to your ActiveRecord model to return the current_user (you could also extend ActiveRecord::Base to make this available to all models):
class User < ActiveRecord::Base
def self.current_user
Thread.current[:current_user]
end
end
Then, you'll be able to access the current user in the instance methods of your User model with self.class.current_user.
What you need to do first of all is make those methods class methods (call on self), which avoids needlessly passing the user reference.
Then, in your award_badge method, you should add the Badging to the user's list of Badgings, e.g.: user.badgings << Badging.new(:badge_id => badge_id)