What is the general approach for recording records by topics that the user can “read”?

I am using Ruby on Rails 3.2.2, and I would like to know what a general approach is when it should be checked if the user has the appropriate permissions to "read" the entries in the "list" of entries. That is, at this time I have the following:

class Article < ActiveRecord::Base def readable_by_user?(user) # Implementation of multiple authorization checks that are not easy to # translate into an SQL query (at database level, it executes a bunch of # "separate" / "different" SQL queries). ... # return 'true' or 'false' end end 

Using the code above, I can perform authorization checks for a single article object:

 @article.readable_by_user?(@current_user) 

However, when I would like to do (usually in my index action controller) something like the following, getting exactly 10 objects

 Article.readable_by_user(@current_user).search(...).paginate(..., :per_page => 10) 

I still need to perform authorization for each object. So, what can I do to perform authorization checks in this “list” of entries (an array of Article objects) in a smart / performance way? That is, for example, should you load Article.all (perhaps sort them by the created data, limiting the SQL query to 10 records, ...), and then iterate over each of these objects to perform authorization checks? or should I do something else (perhaps with a SQL query trick, some Ruby on Rails tool, or something else)?

UPDATED after @ Matzi's answer

I tried to get articles read by the user "manually", for example, using the find_each method:

 # Note: This method is intended to be used as a "scope" method # # Article.readable_by_user(@current_user).search(...).paginate(..., :per_page => 10) # def self.readable_by_user(user, n = 10) readable_article_ids = [] Article.find_each(:batch_size => 1000) do |article| readable_article_ids << article.id if article.readable_by_user?(user) # Breaks the block when 10 articles have passed the readable authorization # check. break if readable_article_ids.size == n end where("articles.id IN (?)", readable_article_ids) end 

At this time, the above code is the most “compromise” compromise I can think of, even if it has some error: it “limits” the number of restored objects to a given number of records with a given id (10 default records in the above example) ; in fact, it doesn’t “really” retrieve all the objects that the user reads, because when you try to extend the scope of ActiveRecord::Relation "where" / "with which the readable_by_user method is used (for example, when you also search for articles using title adding another sentence of the SQL query), it would limit the entries to those where("articles.id IN (?)", readable_article_ids) (that is, "limited" / "limited" the number of restored and readable objects to the first 10 and all others articles read by the user will be ignored when searching by title ). The solution to the problem that the readable_by_user method worked correctly with additional methods of the area was to break block in such a way as to load all readable articles, but this is not suitable for performance when there are a lot of records (maybe another the solution may be to store the entire id article read by the user somewhere, but I think this is not a simple / easy solution to solve the problem).

So, is there a way to do what I would like to do in execution and "really" correctly (perhaps by changing the above approach in general)?

+2
source share
5 answers

It depends on your readable_by_user function. If this is easy to translate into SQL, then this is the way forward. If this is more complicated than this, you will most likely have to do a manual check.

UPDATE: To clarify the meaning of creating an SQL query for a readable list, I will give an example. Assume that the readability of an article for a given user depends on the following:

  • Custom Article ( SELECT a.user == ? FROM Articles a WHERE a.id = ? )
  • The article is open to all ( SELECT a.state == 0 FROM Articles a WHERE a.user = ? )
  • The user is a member of a group that has access to articles

SQL:

 SELECT max(g.rights) > 64 FROM Groups g JOIN Groups_users gu on g.id = ug.group_id WHERE gu.id = ? 
  • User is assigned to this article.

SQL:

 SELECT 1 FROM Articles_users au WHERE au.article_id = ? AND au.user_id = ? 

They can be summarized in the following query:

 def articles_for_user(user) Articles.find_by_sql([" SELECT a.* FROM Articles a LEFT OUTER JOIN Articles_users au on au.article_id = a.id and au.user_id = ? WHERE a.user_id = ? OR au.user_id = ? OR 64 <= (SELECT max(g.rights) FROM Groups g JOIN Groups_users gu on g.id = ug.group_id WHERE gu.id = ?) ", user.id, user.id, user.id, user.id]) end 

This is a confident complex request, but the most effective solution. The database should do the database stuff, if you use only SQL queries and some logic to evaluate your readable_bu_user , then you can translate it into one clean SQL query.

+3
source

I think you can look for declarative_authorization gem . With the with_permissions_to method with_permissions_to you can easily perform such database queries. For example: Article.with_permissions_to(:read).limit(10).offset(20)

+1
source

cancan gem has this functionality. In fact, starting with version 1.4, it will automatically expand your queries to return objects available to the current user.

See this page for more information: https://github.com/ryanb/cancan/wiki/Fetching-Records

0
source

I had the same problem in the system that I am currently working on.

The most efficient way I've found is to implement a batch job that pre-computes the authorization status of each record. I went with something like accessible_by_companies and saved an array with all the corporate codes that could access these records, but you could also work with accessible_by_users if this is your case.

In the show action, I recount the list of authorized companies for the record, use it to verify authorization, and save it again.

I used ElasticSearch to store pre-calculated values ​​and all the data needed to complete queries and lists. The database only applies when viewing a record or in a batch job. There's a big performance boost on this approach, try it.

0
source

For best performance, I would suggest keeping a list of articles read by the user in the session - the user will not change during the session, and you can consider the update frequency and / or conditions separately. Assuming that your Article.list () can be filtered using identifiers, all you need to do is pass the list of user-readable identifiers in the Articles.list () function. Refresh the list of readable lists: you should really update it relatively rarely - no more than once per search, you do not want to update the complete list on each page load for the simple reason that new results can be displayed on the pages, scrolls anyway.

0
source

All Articles