I have User, Account, and Role models. A User can exist by itself. An Account must be tied to a User through a Role. An Account must have at least one Role record of type "Owner". I'm not sure how to test this in RSpec and FactoryGirl.
# user_id, account_id, role
Role < ActiveRecord::Base
belongs_to :user
belongs_to :account
end
User < ActiveRecord::Base
has_many :roles
has_many :accounts, through: roles
accepts_nested_properties_for :accounts
end
Account < ActiveRecord::Base
has_many :roles
has_many :users, through: roles
end
If a user is not signed in, the Accounts.new will display the User and Account forms. If a user is signed in, only the Account form will be displayed. The trouble is, I'm unsure what Role and Account will look like in RSpec when trying to test the associations.
This test fails (the array comes back empty):
describe Account do
let(:user) { FactoryGirl.create(:user) }
before { #account = user.accounts.build(title: "ACME Corporation", subdomain: "acme") }
subject { #account }
it { should respond_to(:users) }
its(:users) { should include(user) }
Then there is the added complication of testing when users are signed in, and when they are not. Any reference sample code I can see for a similar use case? I'm also not sure what to test for in roles_spec, and what belongs to accounts_spec/user_spec.
Factories:
FactoryGirl.define do
factory :user do
name "Mickey Mouse"
email "mickey#disney.com"
password "m1ckey"
password_confirmation "m1ckey"
end
factory :account do
title "ACME Corporation"
subdomain "acme"
end
factory :role do
user
account
end
end
You test objects that depend on other objects by using a mocking framework to "mock" the other objects. There are a number of mock frameworks that allow you to do this.
Related
I'm actually not sure if this is a Pundit or general permissions architectural problem, but I setup a simple Pundit policy to restrict the actions a member within a company can perform. Users are joined as a Member to a company in a has_many, through: relationship. The Member model has a role attribute of owner or user.
Given a User that is a member of a Store, how can I restrict the access in a controller for the User's association to the Store? Below is a Admin::MembersController where a store owner can invite other members. How can I restrict this to the given User in pundit through their member association to the store? The policy below doesn't work, returning an array of records. If I were to check against only the first record it works but I feel that is because of my limited understanding.
All of the tutorials and documentation I see online for CCC and Pundit
involve application-wide permissions. But I need more granular
control.
For example, my application has hundreds of companies. Each company
has a user who is an "owner" and they login each day to look at their
earnings information. That owner/user wants to invite Joe Smith to the
application so they can also look at the data and make changes. But
they don't want Joe Smith to be able to see certain types of data. So
we restrict Joe Smith's access to certain data for that company.
class Admin::MembersController < Admin::BaseController
def index
#company_members = current_company.members
authorize([:admin, #company_members])
end
end
Policy
class Admin::MemberPolicy < ApplicationPolicy
def index?
return [ record.user_id, record.store_id ].include? user.id
## this works return [ record.first.user_id, record.first.store_id ].include? user.id
end
end
User.rb
class User < ApplicationRecord
# Automatically remove the associated `members` join records
has_many :members, dependent: :destroy
has_many :stores, through: :members
end
Member.rb
class Member < ApplicationRecord
belongs_to :store
belongs_to :user
enum role: [ :owner, :user ]
end
Store.rb
class Store < ApplicationRecord
has_many :members
has_many :users, through: :members
end
I got some insight from the contributors on Pundit; the most reasonable way to go about it this is to use a domain object which represents the context that a user is in - there is information about this in the Readme (https://github.com/varvet/pundit#additional-context). The UserContext object will provide references to a user and organization.
class ApplicationController
include Pundit
def pundit_user
if session[:organization_id]
UserContext.new(current_user, Organization.find(session[:organization_id]))
else
UserContext.new(current_user)
end
end
end
class UserContext
attr_reader :user, :organization
def initialize(user, organization = nil)
#user = user
#organization = organization
end
end
I think what you are looking for is scopes in pundit. You want to restrict certain data access to members of store and show that data to owner of that store.
For that purpose you need to change your query according to the user role
Something like this:
class EarningPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
# here check for owner and show all data
if user.members.owner # here you query your memberships to find the owner role.
scope.all
else
# here show only data relevant to member
scope.where(published: true)
end
end
end
end
You can now use this class like this in your controller
def index
#earnings = earning_scope(Earning)
end
Hope it helps
I have the current modeling:
A House belongs_to User
A User has_one House
Which means that the House model has a reference to User. My Factory for User, looks like this:
FactoryGirl.define do
factory :user do
house
end
end
What this does is basically creating a User and a House that references that User.
Now, I have introduced a column in User called house_id and I want to be able to allow the Factory to work as it is, but also, fill the house_id with the House that was created. The reason why I am doing this is because I want to change the reference direction incrementally.
I have done it like this:
FactoryGirl.define do
factory :user do
house
house_id { House.find_by(user_id: id).id }
end
end
But I suspect there might be a better way to do this. Any thoughts?
First of all you have the wrong idea of how has_one and belongs_to relations work.
belongs_to sets up a 1-1 or 1-to-many relationship and places the foreign key column on this models table. In the case its houses.user_id which references users.id:
class User < ActiveRecord::Base
has_one :house
end
class House < ActiveRecord::Base
belongs_to :user
end
Secondly specs should not hinge on records being created sequentially. It will create brittle specs and ordering issues. Instead let factory girl do its job in creating objects for you.
FactoryGirl.define do
factory :user do
house
end
end
FactoryGirl.define do
factory :house do
user
end
end
And don't assume that records have a given id:
# bad
get :show, id: 1
# good
get :show, id: user.to_param
If you need to setup a record which is explicitly associated somewhere you can just pass a user or house option:
let(:user) { create(:user) }
let(:house) { create(:house, user: user) }
it 'should be associated with the correct user' do
expect(house.user).to eq user
end
I have a simple setup of Account and User :
class Account < ActiveRecord::Base
has_many :users
end
accounts
--------
id
name
...
.
class User < ActiveRecord::Base
belongs_to :account
end
users
-----
id
account_id
username
...
Now, how do I design that an Account has an "owner" (a user that has full privileges over the account). The options I can think of:
Add a boolean field on User called account_owner? (Does not feel right to me)
Add a field on accounts table called user_id, thus creating a kind of chicken-and-egg problem.
Something else? Roles?
Would something like this work? You'd need to add an owner_id foreign key to Account.
class Account < ActiveRecord::Base
belongs_to :owner, class_name: "User"
has_many :users
end
class User < ActiveRecord::Base
belongs_to :account
end
# Somewhere in your code...
account = Account.create(name: "Account 1")
owner = User.create(username: "tom")
account.users << owner
account.users << User.create(username: "sarah")
account.owner = owner
account.save
If it's possible that an account can have multiple owners you could add a role column to user.
#user.account == #account && #user.role == 'admin'
I have a number of users who have different roles in relation to a numbers of posts. These roles are owner, editor, viewer, none. Each user may only have one role for a post. I have represented this as a has many through relationship in rails as follows:
class User < ActiveRecord::Base
has_many :roles
has_many :posts, :through => :roles
end
class Post < ActiveRecord::Base
has_many :roles
has_many :users, through => :roles
end
class Role < ActiveRecord::Base
attr_accessor :role
belongs_to :users
belongs_to :posts
end
Where the role attribute is used to indicate which type of role the user has on the post.
When setting a new role I cannot simply use the << operator as it wont set the role attribute. What is the preferred way of handling this situation? How can I enforce that there is only one role per user / post combination and enforce this in my Role creation logic?
You can check in the creation of roles for the User , if he already has a role assigned in which case you can skip assigning this role.
unless user.roles.present?
user.roles.create
end
I understand that you want to make sure that no user will have more than one role for a certain post. If this is what you want to achieve then you just need to add uniquness validation to your Role mode
validates :user_id, uniqueness: {scope: :post_id, message: 'User can have one role per post'}
this will ensure that the combination of user_id and post_id will be unique, you can see more on rails guide on validation with scope.
I have User, Account, and Role models. Role stores the relationship type between Account and User.
I left attr_accessible in Role blank to prevent a mass assignment vulnerability (otherwise attackers could change the role type--owner, admin, etc...--, account or user ids).
But what if an admin wants to change a subscriber to a moderator? This would raise a mass assignment security exception:
user = User.find(params[:id])
role = user.roles.find_by_account_id(params[:account_id])
role.type = "admin"
How do I solve this? One way is to create a separate model to represent each role (owner, admin, moderator, subscriber) and use an STI type pattern. This lets me do:
user = User.find(params[:id])
user.moderatorship.build(account_id: params([:account_id])
Tedious! I would have to create Onwership, Moderatorship, Subscribership, etc..., and have them inherit from Role. If I want to stick to a single Role model, how can I have dynamic roles without exposing myself to mass assignment vulnerability?
Bonus question: Should I use a User has_many roles (user can have a single record for each role type) or has_one role (user can only have one role record, which must be toggled if their role changes) pattern?
class User < ActiveRecord::Base
attr_accessible :name, :email
has_many :accounts, through: roles
end
class Account < ActiveRecord::Base
attr_accessible :title
has_many :users, through: roles
end
class Role < ActiveRecord::Base
attr_accessible
belongs_to: :user
belongs_to: :account
end
You can use "as" with attr_accessible to have different assignment abilities. For instance,
attr_accessible :type, as: :admin
Then, when you do mass assignment, you can do something like
#role.update_attributes {type: :moderator}, as: :admin # Will update type
#role.update_attributes {type: :moderator} # Will not update type
The most flexible approach is to override mass_assignment_authorizer method in model class to change accessible attributes dynamically.
For example:
class Article < ActiveRecord::Base
attr_accessible :name, :content
attr_accessor :accessible
private
def mass_assignment_authorizer
super + (accessible || [])
end
end
Now you can use it this way:
#article.accessible = [:important] if admin?
This example are from RailsCast #237, where you can learn more information about this approach.
In addition, I want to recommend you CanCan gem which can help you handle with roles and abilities.