I've been trying to implement an authorization layer on top of ActiveRecord. Let me explain how that is supposed to work.
Databases
Consider a database table Invoices with the following fields
InvoiceId
CustomerId
... other fields
There will be an auxiliary table InvoicePrivileges with the fields
ObjectId (referring to an invoice id)
SubjectId (referring to a customer id in this case)
Subject type (to handle multiple kinds of users - customer, admin, operator, etc)
Read (boolean)
Write (boolean)
Authorization checks
To be able to read an invoice, the entity attempting to read the row or set of rows must have a set of entries in the InvoicePrivileges table (where InvoicePrivileges.object_id refers to an InvoiceId) with InvoicePrivileges.read = true.
Example, a query to fetch a bunch of invoices from the DB
SELECT invoice.*
FROM Invoices invoice
LEFT JOIN InvoicePrivileges privilege
ON invoice.invoice_id = privilege.object_id
AND privilege.subject_id = <user_id>
AND privilege.subject_type = <user_type>
WHERE privilege.read = TRUE;
The same condition applies when trying to update an invoice, except the last WHERE condition becomes WHERE privilege.write = true.
Implementation
I can use the Arel library to create these constraints with ease. However, where do I implement these methods in such a way that all ActiveRecord save and update actions include these constraints?
I don't mind writing a bit of code to enable this. I'm looking for pointers as to how best to go about it.
Create a gem and require it after ActiveRecord gem. In your gem open up ActiveRecord classes and override the interested methods.
module ActiveRecord
class Base
def save(*)
if(authorised?)
super
else
raise NotAuthorisedError.new("Your meaningful message!")
end
end
end
end
The original code from Rails source is in Persistence class. All persistence methods usually use create_or_update method internally. So, you might consider overriding that method instead. But, you need to look through the code and see if that's a good idea.
Related
We need to allow users to customize their entities like products... so my intention was to have a product table and a custom_product table with just the information the users are allowed to change.
When a client goes to the product I want to merge the information, means I want to merge the two tables - the custom overwrites the default Products table.
I know that in mysql there exists a ifnull(a.title, b.title) way but I was wondering if there is any nice and efficient way to solve this in Rails 4 with Active Record. Assume that the products and custom products table have just 2 columns, ID and TITLE
I think you can convert both objects to JSON and then handle their params as a hash, using the merge method:
class Product
end
class Customization
belongs_to :product
end
a = Product.find(...)
b = a.customization
c = JSON(a.to_json).merge(JSON(b.to_json).reject!{|k,v| v.nil?})
Therefore c will contain all params from Product eventually overridden by those in Customization which are not nil.
If you still want to use a Product object with hybrid values (taken from Customization) you can try this:
a.attributes = a.attributes.merge(b.attributes.reject!{|k,v| v.nil?})
In this case a will still be a Product instance. I would recommend to keep the same attributes in both models when doing this.
I have a model User thats has_many user_entitlements. I want to get all of the users that have a preview attribute and no user entitlements.
My code is currently:
User.where("preview is not null").keep_if {|user| user.user_entitlements.empty?}
This is iterating through all of the users and seeing if they have any user entitlements.
Is there a way I could do this in SQL to make it more efficient and avoid having to go through each user?
You can use Arel to build a query leveraging NOT EXISTS.
user_arel = User.arel_table
ue_arel = UserEntitlement.arel_table
User.where("preview is not null").where(
UserEntitlement.where(ue_arel[:user_id].eq(user_arel[:id])).exists.not
)
I am making an assumption on your schema that UserEntitlement contains a user_id column.
Even simpler, you can use includes to force a LEFT JOIN, then check for NULL:
User.includes(:user_entitlements).
where("users.preview IS NOT NULL AND user_entitlements.id IS NULL")
I am creating an auction house similar to eBay. My question is with my bids resource. It will keep track of all of the bids that are placed. However I MUST ensure that every new bid on an item, is higher than the current bid, otherwise that would defeat the purpose of an auction.
The way I currently want to do this is to create a transaction inside the bids controller, that would check whether or not the amount being bid for an item, is greater than the max of the other bids for the same item.
def create
bid.transaction
#bid = Bid.new(params[:bid])
#bid.user = current_user
#bid.item = current_item
# DO STUFF TO CHECK ITS GREATER THAN MAX OF BIDS FOR CURRENT_ITEM
# ON ERROR, ROLLBACK TRANSACTION AND THROW ERROR
end
end
I would definitely handle this in the model. Create a custom validation such as:
#Bid Model
validate :bid_is_higher_than_current_highest
private
def bid_is_higher_than_current_highest
bid > item.current_highest_bid
end
Something like that.
First off make sure you're using the InnoDB table type as MyISAM does not support transactions.
But you're basically right, you just want to run a query inside your transaction to determine the maximum bid:
max_bid_amount = Bid.where(:id => item.id).maximum(:bid_amount)
if #bid.bid_amount > max_bid_amount
# proceed with bid placement
else
# do something, return some flag, nil or whatever
end
Then check the value and proceed accordingly. I would think you would not want to raise an exception for flow control purposes.
I also recommend that you move this logic outside of the controller and either into the Model or some other management layer, maybe have a generic Ruby class like BidManager which contains the flow logic for bid placement.
We have the need to clone a complex data structure from one org to another. This contains a series of custom SObjects, including parents and children.
The flow would be the following. On origin org, we just JSON.serialize the list of SObjects we want to send. Then, on target org, we can JSON.deserialize that list of objects. So far so good.
The problem is that we cannot insert those SObjects directly, since they contain the origin org's IDs and Salesforce won't let us insert objects that already have Ids.
The solution we found is to manually insert the object hierarchy, maintaining a map of originId > targetId and fixing the relationships manually. However, we wonder if Salesforce provides an easier way to do such a thing, or someone knows a better way to do it.
Is there an embedded way in Salesforce to do this? Or are we stuck into a tedious manual process?
List.deepClone() call with preserveIds = false might deal with one problem, then:
Consider using upsert operation to build the relationships for you.
Upsert not only can prevent duplicates but also maintain hierarchies.
You'll need an external Id field on the parent, not on the children though.
/* Prerequisites to run this example succesfully:
- having a field Account_Number__c that will be marked as ext. id (you can't mark the standard one sadly)
- having an account in the DB with such value (but the point of the example is to NOT query for it's Id)
*/
Account parent = new Account(Account_Number__c = 'A364325');
Contact c = new Contact(LastName = 'Test', Account = parent);
upsert c;
System.debug(c);
System.debug([SELECT AccountId, Account.Account_Number__c FROM Contact WHERE Id = :c.Id]);
If you're not sure whether it will work for you - play with Data Loader's upsert function, might help to understand.
If you have more than 2 level hierarchy on the same sObject type I think you'd still have to upsert them in correct order though (or use Database.upsert version and keep on rerunning it for failed ones).
I'm learning sqlalchemy and not sure if I grasp it fully yet(I'm more used to writing queries by hand but I like the idea of abstracting the queries and getting objects). I'm going through the tutorial and trying to apply it to my code and ran into this part when defining a model:
def __repr__(self):
return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)
Its useful because I can just search for a username and get only the info about the user that I want but is there a way to either have multiple of these type of views that I can call? or am I using it wrong and should be writing a specific query for getting different data for different views?
Some context to why I'm asking my site has different templates, and most pages will just need the usersname, first/last name but some pages will require things like twitter or Facebook urls(also fields in the model).
First of all, __repr__ is not a view, so if you have a simple model User with defined columns, and you query for a User, all the columns will get loaded from the database, and not only those used in __repr__.
Lets take model Book (from the example refered to later) as a basis:
class Book(Base):
book_id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
summary = Column(String(2000))
excerpt = Column(Text)
photo = Column(Binary)
The first option to skip loading some columns is to use Deferred Column Loading:
class Book(Base):
# ...
excerpt = deferred(Column(Text))
photo = deferred(Column(Binary))
In this case when you execute query session.query(Book).get(1), the photo and excerpt columns will not be loaded until accessed from the code, at which point another query against the database will be executed to load the missing data.
But if you know before you query for the Book that you need the column photo immediately, you can still override the deferred behavior with undefer option: query = session.query(Book).options(undefer('photo')).get(1).
Basically, the suggestion here is to defer all the columns (in your case: except username, password etc) and in each use case (view) override with undefer those you know you need for that particular view. Please also see the group parameter of deferred, so that you can group the attributes by use case (view).
Another way would be to query only some columns, but in this case you are getting the tuple instance instead of the model instance (in your case User), so it is potentially OK for form filling, but not so good for model validation: session.query(Book.id, Book.title).all()