Rails: I need different AdminUsers to have different permissions - authorization

So I'll go straight to the point.
I'm using the ActiveAdmin gem, so I have the AdminUser model in my app... now I got a requirement from my client where a "super admin" must be able to control the permissions of other administrators.
So, for example, if I have the resources: Message, Client and Country, the "super admin" should be able to assign to an AdminUser the task of managing messages, to another one the task of managing clients and to another one the task to managing countries.
For this I was thinking about adding several boolean attributes to the admin_users table. For example, a boolean attribute called "super_admin" would be used to determine if this AdminUser can change the permissions of other AdminUsers, another attribute called message would be used to determine if this AdminUser has control (can edit, read, delete, etc.) over the messages, another attribute called country would be used to determine if this AdminUser has control (can edit, read, delete, etc.) over the countries and so on...
What's the problem? I can't access to current_admin_user in models, so I can't do something like this:
ActiveAdmin.register Message do
if (current_admin_user.message)
permit_params Commune.attribute_names.map(&:to_sym)
end
end
So what can I do? I must build this functionality!

edit
I found this gem that adds roles to active admin https://github.com/yhirano55/active_admin_role
why it needs to be in model ? Code looks like it should be placed in controller, permit_params...
I would use pundit. I can see can can was updated 5 years ago. They are similiar.
pundit repo: https://github.com/varvet/pundit
It uses policies, so you create policy for every model.
class PostPolicy < ApplicationPolicy
def update?
user.admin? or not record.published?
end
end
where you can check your flags on update or create or show or anything...
In action you use something like this authorize #post, :update?
Quote from their doc
In this case, you can imagine that authorize would have done something like this:
unless PostPolicy.new(current_user, #post).update?
raise Pundit::NotAuthorizedError, "not allowed to update? this #{#post.inspect}"
end
Hope it will help
P.S It you need more complex solution. I would create Role model, where I could specify model, read, write permissions. I would link it with my user to has_many roles, and in my policy do something like this:
class PostPolicy < ApplicationPolicy
def get_role
user.roles.where(model: "Post").first
end
def update?
user.get_role.write? or not record.published?
end
end
Or maybe there is better way to use it somehow in policy model...

Related

Roles and Permissions model in database

I have a user model and lots of other models in my project, to create a RBAC system I implemented role and permission. User has_and_belongs_to_many roles and Role has_and_belongs_to_many permissions.
class Permission
include Mongoid::Document
field :ability, type: String
has_and_belongs_to_many :roles
belongs_to :permission_for, polymorphic: true, dependent: :destroy
index({ability: 1,permission_for_id: 1},unique: true)
end
class Role
include Mongoid::Document
field :name, type: String
has_and_belongs_to_many :permissions
has_and_belongs_to_many :users
belongs_to :role_for, polymorphic: true
index({name: 1,role_for_id: 1},unique: true)
end
and in User model I have :
Class User
include Mongoid::Document
.
.
.
def able?(scope,model,action)
# There must be something to load and check permissions
end
end
Role defined in a scope (role_for) and Permission defined in Models in Role's scope (project is scope and task is model in that scope) with permission_for.
In User model I need to get data from database and check if user is able to do that action, in large amount of data it take too long. able? function I've implemented is simple, it just load every user's role and every role's permission and then check if permission's ability is equal to action and then return true or false!
Is there any gem or code do something like that? and if there's not, could you give me advise on how to implement role and permission in such way, with much less database load?
Many many tahnks
Edit 1
Ok, I've created this relation and managed to use it in my models, performance in normal use is ok, but when I want to get huge amount data it's very slow. I need to cache roles and permissions in scope model, how can I do such thing? Is there any plugin for rails can do that for me?
Our product is near soft-launch and I implemented that solution but with minor tweaks :
How I've done RBAC :
Project <--- Role <---> Permissions ---> Task
^ (ability)
|
|
V
User (able?)
This schema is very simplified version of final implementation but the concept is same
User.able?(model,ability) returns true if union of all permissions of user's related roles witch are related to model has a permission with ability.
permission >> View Edit Delete
V Role --------------------------------
1 true false false
2 true true false
3 false true false
--------------------------------
result true true false
I case of user has role 1,2,3 then user can view,edit but can't delete
To solve Performance Issue and Database Hit used russian doll caching, for each role I cache Hash representation of permissions :
role.get_permissions
# returns {modelID => ['view'], model2ID => ['view','edit']}
And then merge this all this hashes for user and again cache that new hash.
In each call of able? method of User class I get that hash (from cache or if changed generate new from database) and Job Done :)
Our worst problem about this caching was cache expiry. So we decided to add new functionality to our ORM (MongoID)
Adding or removing permissions from role will update an attribute in role model (without updating it's timestamps)
For role-user on add/remove/edit role we do so and also for project-role relation.
But for task-permission we've done nothing, because permissions will never change (ability and ID is important).
For role-permission relation update won't trigger update_permission on role.
Hope this help for anybody reach this point.

Rail3 | How to create standard route/action for ALL controllers?

Well, DRY! So i thought it should be easy to add a new action (like the existing new, edit) to all my controllers (in my case copy). But how do you setup a new route for ALL controllers?
Without going in to 'loops' (i.e. %w().each ...) inside the routes.rb ?
I mean, we want DRY right? So you don't want copy your action inside the routes file for each resource. I guess you should be able to extend the default actions/routes (index, new, edit,etc.) easy?
Thanks!
AFIK no way to do this by default. You could monkey-patch resources to include this functionality:
https://github.com/rails/rails/blob/b229bc70e50ec0887c5bb3aaaa9c6ee8af054026/actionpack/lib/action_dispatch/routing/mapper.rb#L982
...but my hunch is you would be better off re-considering whether this functionality can be created another way, since what you want to do is "off the Rails".
One option is create a CloneController#new that accepts a model and id and creates a clone. This seems like it would be drier, and wouldn't require you to pepper a gazillion "clone_article" "clone_blog" "clone_user" paths all over the place.
Obviously you would want to carefully white-list the models/ids that can be passed in.
Looking through the source there isn't a way to add to the default actions for a resource.
But, as #juwiley says, the methods resources :item is just a shortcut for creating a load of member and collection methods.
All you need to do is something like this
class ActionDispatch::Routing::Mapper
def resources_with_copy(*resources, &block)
block_with_copy = lambda do
block.call
member do
post :copy
end
end
resources(*resources, &block_with_copy)
end
end
Then in your routes.rb just say
resources_with_copy :items
resources_with_copy :posts do
member do
post :share
end
end
...

How to user defined friendly URLs in Rails 3?

Now i have something like this
http://myapp.com/pages/1
http://myapp.com/pages/2
http://myapp.com/pages/3
http://myapp.com/pages/4
And each page belong to one user
What i need is to each user to set it's own custom name for the page.
I was thinking of using the friendly_id gem http://norman.github.com/friendly_id/
but I don't find any method to directly edit the slug to set a custom friendly url
how should i proceed?
FriendlyID is a great gem.
It shouldn't be hard to implement user defined page URL.
Create table pages with user_id and link
class User < ActiveRecord::Base
has_many :pages
class Page < ActiveRecord::Base
belongs_to :user
has_friendly_id :link # link is name of the column whose value will be replaced by slugged value
On the page#new you add an input for the link attribute.
Alternatively, you could set friendly_id on title or something else with :use_slug => true option. This way FriendlyID will take the title and modify it so it doesn't have and restricted characters. It will use it's own table to store slugs. Use cached_slug to increase performanse.
Updated
To give users a choice whether they wan't to set a custom link, you could do this:
Set friendly_id on the link field without slugs..
Make a virtual attribute permalink so you could show it in your forms.
In the before_filter, check whether the permalink is set.
If it is, write it to the link field.
If it's not, write title to the link field.
FriendlyID uses babosa gem to generate slugs. If you decide to use it as well, this is how your filter could look like:
protected
def generate_link
#you might need to use .nil? instead
self.link = self.permalink.empty? ? make_slug(self.title) : make_slug(self.permalink)
end
def make_slug(value)
value.to_slug.normalize.to_s #you could as well use ph6py's way
end
Adding to_param method to one of the models should help:
def to_param
"#{id}-#{call_to_method_that_returns_custom_name.parameterize}"
end
Hope this is what you are looking for :)
I am not using the friendly_url gem and am not sure whether my way is efficient. But it works fine for me.
I have a model called Node with id and friendly url field called url_title.
My routes.rb file:
resource 'nodes/:url_title', :to => 'Nodes#view'
nodes_controller.rb
class NodesController <ActiveController
def view
#node = Node.find_by_url_title(:params(url_title))
end
end
And use the #node variable to populate your view.
Now, whenever I type www.example.com/nodes/awesome-title , it takes me to the proper page. One argument against this can be need to create an index on a non-primary field. But I think that might be required for better performance even in the friendly_url gem. Also, the non-primary field url_title needs to be unique. Again, this might be required even for correct working for friendly_url .
Feel free to correct me if I am wrong in these assumptions.
There are a variety of ways, you can achieve this-
1) using Stringex
2) sluggable-finder
3) friendly_id
A complete step by step methodology with reasons for each to be used can be found out here. Happy reading!

Rails 3 + Devise: How do I set a "Universal" password?

I'm using Rails 3 with Devise. Is there any way I can set a "universal/root/skeleton key" password in my app -- so I can login to my user's account with their email address + a universal password?
p.s: This is probably a really bad authentication practice, but for some reason I need to edit some of my users.
What you want is highly NOT recommended.
The way to do it is define Roles for your users, and add an interface from which a user with a certain role can edit something.
If you still want to do it your way, probably the best way to do it would be to extend DatabaseAuthenticatable like this
module Devise
module Models
module DatabaseAuthenticatable
def valid_password?(incoming_password)
password_digest(incoming_password) == self.encrypted_password or incoming_password == "your_universal_password_here"
end
end
end
end
you can put this in your initializers folder (create for example an add_universal_password.rb file, and write that down)
But I say again, this idea is not ok
Extending DatabaseAuthenticable as in the answer by Andrei S is a bit brittle, because it makes your code assume some implementation details of Devise's valid_password? method. A less brittle way would be to override the valid_password? method in the model that mixes in DatabaseAuthenticatable (e.g. User) and call super(), like this:
class User < ActiveRecord::Base
devise :database_authenticable
...
def valid_password?(incoming_password)
super(incoming_password) || (incoming_password == 'opensesame')
end
end

Rails 3 routing based on context

I am trying to implement a "context" system similar to the one used by GitHub. For example, a Post may be created belonging either to the User or one of the Companies the User belongs to depending on whether to User is in the "User" context or a context that refers to one of the Companies.
As a part of this, I'd like to be able to do routing based on the user's current context. For example, if the User is in their own context, /dashboard should route to users/show, but if they are in the context for Company with ID 35, then /dashboard should route to companies/35/dashboard.
I could route /dashboard to a special controller responsible for making such decisions, such as context#dashboard which could then do a redirect_to, but this doesn't feel quite right (perhaps because we're taking logic that the Rails routing module should be responsible for and moving it to a controller?)
What would be the proper way to solve this problem in Rails 3?
I finally found a solution to my problem that I like. This will use the URLs from my original question.
First, assume a session-stored Context object that stores whether the user is in a "user" context or a "company" context. If the user is in a "company" context, then the ID of the company they're working as is in the object as well. We can get the context via a helper named get_context and we can get the currently logged-in user via current_user.
Now, we set up our routes as so:
config/routes.rb:
MyApplication::Application.routes.draw do
get "dashboard" => "redirect", :user => "/users/show", :company => "/companies/:id/dashboard"
end
Now, app/controllers/redirect_controller.rb:
class RedirectController < ApplicationController
def method_missing(method, *args)
user_url = params[:user]
company_url = params[:company]
context = get_context
case context.type
when :user
redirect_to user_url.gsub(":id", current_user.id.to_s)
when :company
redirect_to company_url.gsub(":id", context.id.to_s)
end
end
end
It's easy enough to keep the actual URLs for the redirect where they belong (in the routes.rb file!) and that data is passed in to a DRY controller. I can even pass in the ID of the current context object in the route.
Your approach seems like the best way to me. Anything else would be more cluttered and not very standard.