Automatically set created_by on model when using devise - ruby-on-rails

I am trying to automatically set created_by in my models to currently authenticated user without explicitly passing down user as an argument. I am using devise for authentication and for setting user globally on each request I am using new rails api ActiveSupport::CurrentAttributes like so:
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
# config/initializers/warden.rb
Warden::Manager.after_set_user { |user, auth, opts| Current.user = user }
Then I have Owned concern
# app/models/concerns/owned.rb
module Owned
extend ActiveSupport::Concern
included do
belongs_to(
:created_by,
class_name: "User",
foreign_key: "created_by_id",
default: -> { Current.user }
)
end
end
which is included in models in which I want to automatically set created_by.
It works but recently I got bug where created_by was assigned to wrong person and I am wondering if it could be caused by code I described above because its not thread safe. I am unable to reproduce the bug again and I don't know how to test for such potential race conditions. So my questions are: Is my usage of CurrentAttributes correct and my code in general? Is there a better way how to automatically set created_by?

Related

Rails / Multi-Tenancy: Conditional default scope based on a different model's db value / global setting?

I've got a Rails application that is multi-tenant. Every model has an account_id, belongs to an account, and has a default scope to a current account id:
class Derp < ApplicationRecord
default_scope { where(account_id: Account.current_id) }
belongs_to :account
end
This works well and I've used this pattern in production in other apps (I understand that default scopes are frowned upon, but this is an accepted pattern. See: https://leanpub.com/multi-tenancy-rails).
Now here's the kicker - I have one client (and potentially more down the line, who knows), who wants to run the software on their own server. To solve this, I simply made a Server model with a type attribute:
class Server < ApplicationRecord
enum server_type: { multitenant: 0, standalone: 1 }
end
Now on my multi-tenant server instance, I simply make one Server record and set the server_type to 0, and on my standalone instance I set it to 1. Then I've got some helper methods in my application controller to help with this, namely:
class ApplicationController < ActionController::Base
around_action :scope_current_account
...
def server
#server ||= Server.first
end
def current_account
if server.standalone?
#current_account ||= Account.first
elsif server.first.multitenant?
#current_account ||= Account.find_by_subdomain(subdomain) if subdomain
end
end
def scope_current_account
Account.current_id = current_account.id
yield
rescue ActiveRecord::RecordNotFound
redirect_to not_found_path
ensure
Account.current_id = nil
end
end
This works, but I've got large record sets that I'm querying on this particular standalone client (70,000 records). I've got an index on the account_id, but it took my main customers table from 100ms to 400ms on my development machine.
Then I realized: standalone servers really don't need to concern themselves with the account id at all, especially if it is going to affect performance.
So really all I've got to do is make this line conditional:
default_scope { where(account_id: Account.current_id) }
I'd like to do something like this:
class Derp < ApplicationRecord
if Server.first.multitenant?
default_scope { where(account_id: Account.current_id) }
end
end
But obviously that syntax wrong. I've seen some other examples on Stack Overflow for conditional scopes, but none seem to work with a conditional statement based on a completely separate model. Is there a way to accomplish something like that in Ruby?
EDIT: Kicker here that I just realized is that this will only solve the speed issue for the one standalone server, and all the multi-tenant accounts will still have to deal with querying with the account_id. Maybe I should focus on that instead...
I would avoid using default_scope as I've been bitten by it in the past. In particular, I've had places in an application where I want to definitely have it scoped, and other places where I don't. The places where I want the scoping typically end up being controllers / background jobs and the places where I don't want / need it end up being the tests.
So with that in mind, I would opt for an explicit method in the controller, rather than an implicit scoping in the model:
Whereas you have:
class Derp < ApplicationRecord
if Server.first.multitenant?
default_scope { where(account_id: Account.current_id) }
end
end
I would have a method in the controller called something like account_derps:
def account_derps
Derp.for_account(current_account)
end
Then wherever I wanted to load just the derps for the given account I would use account_derps. I would then be free to use Derp to do an unscoped find if I ever needed to do that.
Best part about this method is you could chuck your Server.first.multitenant? logic here too.
You mention another problem here:
This works, but I've got large record sets that I'm querying on this particular standalone client (70,000 records). I've got an index on the account_id, but it took my main customers table from 100ms to 400ms on my development machine.
I think this is most likely due to a missing index. But I don't see the table schema here or the query so I don't know for certain. It could be that you're doing a where query on account_id and some other field, but you've only added the index to the account_id. If you're using PostgreSQL, then an EXPLAIN ANALYZE before the query will point you in the right direction. If you're not sure how to decipher its results (and sometimes they can be tricky to) then I would recommend using the wonderful pev (Postgres EXPLAIN Visualizer) which will point you at the slowest parts of your query in a graphical format.
Lastly, thanks for taking the time to read my book and to ask such a detailed question about a related topic on SO :)
Here's my solution:
First, abstract the account scoping stuff that any account scoped model will have to an abstract base class that inherits from ApplicationRecord:
class AccountScopedRecord < ApplicationRecord
self.abstract_class = true
default_scope { where(account_id: Account.current_id) }
belongs_to :account
end
Now any model can cleanly be account scoped like:
class Job < AccountScopedRecord
...
end
To solve the conditional, abstract that one step further into an ActiveRecord concern:
module AccountScoped
extend ActiveSupport::Concern
included do
default_scope { where(account_id: Account.current_id) }
belongs_to :account
end
end
Then the AccountScopedRecord can do:
class AccountScopedRecord < ApplicationRecord
self.abstract_class = true
if Server.first.multitenant?
send(:include, AccountScoped)
end
end
Now standalone accounts can ignore any account related stuff:
# Don't need this callback on standalone anymore
around_action :scope_current_account, if: multitenant?
# Method gets simplified
def current_account
#current_account ||= Account.find_by_subdomain(subdomain) if subdomain
end

manage roles in rails

I want create roles in my project. Each user can be: admin, registered or demo. Each role see different things.
How can I do that? What is the best gem to do roles?
This is a example in 'bad programming" of what I want:
def index
if current_user.role[:name] == 'admin'
#installations = Installation.all
elsif current_user.role[:name] == 'registered'
#installations = current_user.installations
elsif current_user.role[:name] == 'demo'
#installations = current_user.installations.first
else
end
end
Some gems that might be interesting for you :
rolify
role_model
If you decide to implement it yourself, then within some page you might want to change the content, for that you might want to do something like this :
Add a role to the user model using a migration :
class AddRoleToUsers < ActiveRecord::Migration
def change
add_column :users, :role, :string, default: :demo
end
end
Then in your app you can use it as follows:
def index
case current_user.role
when :admin
#installations = Installation.all
when :registered
#installations = current_user.installations
else
#installations = current_user.installations.first
end
end
You can also simply create a boolean admin for instance.
What you might want to do also is create some methods in your model so that you can call current_user.admin? or current_user.registered? . You can do that by doing (if you chose to use a string to store the role):
class User < ActiveRecord::Base
def admin?
self.role == "admin"
end
def registered?
self.role == "registered"
end
end
One advantage I see of having a role stored in a string is that if you have 5 roles for instance then you do not have 4 booleans (as when you store admin in a boolean) but only one string. On the long run you might want to store actually a role_id instead of a string and have a separate role model.
An excellent alternative pointed out by Jorge de Los Santos (another answer) is to use enum :
class User < ActiveRecord::Base
enum role: [:demo, :admin, :registered]
end
It is an excellent alternative because it will automagically add the methods described above such as current_user.admin? without hard coding them.
With your roles, you might want to do some authorization (admins can have access to specific pages, demo users are restricted to only a subset of pages, etc.). For this, you can use the gem called cancancan. You can look at this railscast to learn more about it. Also, you can have some infos here : How to use cancancan? .
There are plenty of solutions available to you.
Starting by gems:
https://github.com/RolifyCommunity/rolify
https://github.com/martinrehfeld/role_model
By using Devise architecture (in case you use it):
https://github.com/plataformatec/devise/wiki/How-To:-Add-a-default-role-to-a-User
By using enums in rails 4:
class AddRolesToUser < ActiveRecord::Migration
#add_column 'role', :integer, default: 0 to the users table
end
class User < ActiveRecord::Base
enum role: [:demo, :admin, :registered]
end
That will enable role methods.
user = User.find(1)
user.role #:demo
user.admin? #false
user.registered? #false
And consequently:
if user.admin?
#somethig
elsif user.registered?
#another something
else
#another another something.
And last but not least, what you are searching is not the manage roles solution, is the manage permissions solutions:
https://github.com/ryanb/cancan
Add a boolean, :admin to your User model.
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, deafult: false
end
end
Create a method for a registered user to separate them from demo users, such as verifying their email, providing a home address and phone number, filling out a profile, etc. This is up to you though, first you need to decide how a registered and demo user should be different.
The CanCan gem adds authorization to your project, and is especially useful if you want to implement multiple roles with differing abilities. When used with an authentication system like devise, you get a full suite of capability for your site.
You're in full control of what roles you want to define and what abilities they have. CanCan manages tracking, assignment, and querying of roles, and then gets out of your way to let you build what you need.
You can find the CanCan gem in Github: https://github.com/ryanb/cancan
It's simple to use, and the documentation is straightforward and easy to follow.

Rails + Rolify: Singleton Pattern / Only one Role per User per Resource

I am currently setting up my role management in my Rails app with the Rolify gem - both with the latest versions.
In my case, a user can only have one role at the same time for one specific resource. This means, that before I do a
user.add_role :lead, #resource
I would like to delete all maybe already existing roles. Unfortunately something like
user.current_role.remove #resource
is not existing. I only could loop through all maybe existing roles, check if it is existing and delete it. This sounds ugly to me. Stuff like
user.roles = []
does not help either me because i want to delete all roles for a specific resource.
Is there any standard functionality in rolify to support something like this?
Thanks for your help in advance!
Callback methods to the rescue!
class User < ActiveRecord::Base
rolify before_add: :before_add_method
def before_add_method(role)
# do something before it gets added
end
end
I ended up wanting a more substantial solution which removed all kinds of roles from the resource. I made a gist out of it:
user.rb
class User < ApplicationRecord
rolify :strict => true, :before_add => :before_add_role
#Helper method to remove any existing role this user has for a resource
def remove_all_roles resource
# README: This syntax relies on changes on the following PR
# https://github.com/RolifyCommunity/rolify/pull/427
# Or include the source of this directly:
# gem 'rolify', :git => "git://github.com/Genkilabs/rolify.git"
remove_role nil, resource
end
protected
#ensure that we only have a single role per resource
def before_add_role(role)
if role.resource
Rails.logger.debug "User::before_add_role: Adding the role of #{role.name} for #{role.resource_type} #{role.resource_id} to user #{id}"
#remove any pre-existing role this user has to the resource
remove_all_roles role.resource
end
end
end

How to use the Devise gem in a Rails app where the "User" is split up between three models?

Say I have the following in a Rails 4 app:
class Person < ActiveRecord::Base
has_many :email_addresses, as: :emailable
has_one :user_account
end
class EmailAddress < ActiveRecord::Base
belongs_to :emailable, polymorphic: true
# There is an :address column
end
class UserAccount < ActiveRecord::Base
belongs_to :person
end
A person can have multiple email addresses. A person can also have a user account. (I've moved this into it's own model because not all people would be users.) Any of the person's email addresses could be used as the "username" when logging in.
Given this, I would like to use the Devise gem. I see you can specify the model to which the authentication is applied. User is widely used, but I would be using UserAccount. However, Devise then expects the email (username) field to be in this model.
When a person registers a user account, there would actually be three associated records created (Person, EmailAddress, and UserAccount). I can't figure out how to to get Devise to work with this setup. Any ideas?
One option would be, to delegate the email method from your UserAccount to your email model and override the finder def self.find_first_by_auth_conditions(warden_conditions) used by the devise login procedure. I found a pretty nice blog post that describes this in depth and another stackoverflow answer that has the same approach. There is also a section in the docs about how to confirm a devise account with multiple emails.
As your setup is a bit more complicated, you could also maybe use EmailAddress as your primary devise model and delegate the password methods to the UserAccount.
This would potentially be useful if you have to confirm every email address with confirmable and not only the user account. This setup would you from overriding that finder, but you may run into other issues with the delegated password as is never tried that before.
If you are using ActiveRecord,
First, add
attr_accessor :email
to user_account (i think this is the easiest way to deal with devise form)
next, you need to modify devise login procedure. Still, in your user_account, override the devise method such
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
if email = conditions.delete(:email)
where(conditions.to_h).includes(:email_addresses).where(email_addresses: {email: email}).first
else
where(conditions.to_h).first
end
end
you may also need to define following to get work the code above
class UserAccount < ActiveRecord::Base
belongs_to :person
has_many :email_addresses, through: :person
That should just work, i have tested this using active record but if you are using mongoid, probably the solution will be different.
Note that, i have modified the code from devise's How To: Allow users to sign in using their username or email address documentation to get the solution.

Rails - Best-Practice: How to create dependent has_one relations

What's the best practice to create has_one relations?
For example, if I have a user model, and it must have a profile...
How could I accomplish that?
One solution would be:
# user.rb
class User << ActiveRecord::Base
after_create :set_default_association
def set_default_association
self.create_profile
end
end
But that doesn't seem very clean... Any suggestions?
Best practice to create has_one relation is to use the ActiveRecord callback before_create rather than after_create. Or use an even earlier callback and deal with the issues (if any) of the child not passing its own validation step.
Because:
with good coding, you have the opportunity for the child record's validations to be shown to the user if the validations fail
it's cleaner and explicitly supported by ActiveRecord -- AR automagically fills in the foreign key in the child record after it saves the parent record (on create). AR then saves the child record as part of creating the parent record.
How to do it:
# in your User model...
has_one :profile
before_create :build_default_profile
private
def build_default_profile
# build default profile instance. Will use default params.
# The foreign key to the owning User model is set automatically
build_profile
true # Always return true in callbacks as the normal 'continue' state
# Assumes that the default_profile can **always** be created.
# or
# Check the validation of the profile. If it is not valid, then
# return false from the callback. Best to use a before_validation
# if doing this. View code should check the errors of the child.
# Or add the child's errors to the User model's error array of the :base
# error item
end
Your solution is definitely a decent way to do it (at least until you outgrow it), but you can simplify it:
# user.rb
class User < ActiveRecord::Base
has_one :profile
after_create :create_profile
end
If this is a new association in an existing large database, I'll manage the transition like this:
class User < ActiveRecord::Base
has_one :profile
before_create :build_associations
def profile
super || build_profile(avatar: "anon.jpg")
end
private
def build_associations
profile || true
end
end
so that existing user records gain a profile when asked for it and new ones are created with it. This also places the default attributes in one place and works correctly with accepts_nested_attributes_for in Rails 4 onwards.
Probably not the cleanest solution, but we already had a database with half a million records, some of which already had the 'Profile' model created, and some of which didn't. We went with this approach, which guarantees a Profile model is present at any point, without needing to go through and retroactively generate all the Profile models.
alias_method :db_profile, :profile
def profile
self.profile = Profile.create(:user => self) if self.db_profile.nil?
self.db_profile
end
Here's how I do it. Not sure how standard this is, but it works very well and its lazy in that it doesn't create extra overhead unless it's necessary to build the new association (I'm happy to be corrected on this):
def profile_with_auto_build
build_profile unless profile_without_auto_build
profile_without_auto_build
end
alias_method_chain :profile, :auto_build
This also means that the association is there as soon as you need it. I guess the alternative is to hook into after_initialize but this seems to add quite a bit of overhead as it's run every time an object is initialized and there may be times where you don't care to access the association. It seems like a waste to check for its existence.
There is a gem for this:
https://github.com/jqr/has_one_autocreate
Looks like it is a bit old now. (not work with rails3)
I had an issue with this and accepts_nested_attributes_for because if nested attributes were passed in, the associated model was created there. I ended up doing
after_create :ensure_profile_exists
has_one :profile
accepts_nested_attributes_for :profile
def ensure_profile_exists
profile || create_profile
end
If you need the has_one association to exist before saving the object (when testing, for instance), you should use the after_initialize callback instead. Here is how it could be applied to your use case:
class User < ActiveRecord::Base
has_one :profile
after_initialize :build_profile, unless: :profile
end

Resources