Help for beginners? Nested resources, Authentication, and Authorization

In this thread, I hope to gather explanations from more experienced
Rails users than I on how to approach the topics of nested resources,
authentication, and authorization, areas that in my own experience as
a Rails newcomer and based on my findings in newsgroups, forums, and
articles, quickly present themselves as new users move beyond the
tutorials to their first real apps.

In the tutorials, the user first learns how to create a model and its
corresponding views and controllers. The user then learns how to make
another model, and associate it with the first. Let me start
attaching some names. We’ll have two models: Business and Customer.
And we’ll say there’s a many-many association (the details of which
don’t matter here and aren’t the goal of this post). Businesses have
many customers; customers have many businesses.

I’ve chosen these two because, though there is a nesting relationship
involved, the two are also distinct. In the guides.rubyonrails.org
tutorials, the nesting relationship between Posts and Comments is
different: whereas businesses and customers could very well stand
alone in a commerce site, it seems much less likely that comments
would be treated as a first class entity, since each comment is
attached to a post. (I’m not trying argue about the nature of
comments here, just trying to pick better models to exemplify the rest
of the post).

A new user following the tutorials as they build their app would
likely first build their Business MVC. They might then build their
Customer MVC, with or without addressing the association. Without the
association, the user will be able to go about creating businesses and
customers, with the usual CRUD and RESTful routing. Adding the
association brings in a new change: following the tutorial’s example,
nesting customers under businesses should leverage the
CustomersController. So, calls to /businesses/:business_id/customers/
should route to the CustomerController. The index method for the
CustomerController changes in kind. @customers = Customer.all is
replaced by something along the lines of @business = Business.find
(:business_id); @customers = @business.customers. With these changes,
the user can no longer list all of the customers; he/she can only list
all of a given business’s customers. We are now presented with the
first question:

  1. What is a/the good/best/recommended way to index a nestable but
    independent resource? In other words, what should be done to allow
    both the independent and nested views? Some possible solutions (for
    the case presented):
  • Have conditional behavior reliant on whether the :business_id
    parameter is present? (If so, get the nested customers, if not, get
    all)
  • Have different controllers for nested access and unnested access?
  • Have different routes and methods in the same controller for nested
    and unnested access?
  • Some other method?

And at this point, the user encounters a new set of issues. The
application will have users: businesses will access the app to see
their customers, and court new ones; customers will want to view their
favorite businesses, and look for others. [Let’s assume for the sake
of this post that we’re not making a social app where everyone can see
everyone else’s everything] - but businesses shouldn’t be able to see
other businesses’ customers, and customers shouldn’t able to see other
customers’ chosen businesses. If the user treads this path quickly,
(and so that I can skip ahead a few steps), they’ll add an
authentication plug-in and set up a bunch of callbacks, only to find
themselves needing an administrator role: to 1) view all records as
before, without any nesting, and to 2) act as if they were a given
business or customer.

For the benefit of any other beginners that have made it this far,
there are two separate pieces at play here: Authentication and
Authorization. Authentication handles identifying the user: which
business or which customer has logged in? Authorization handles
access: what pages is the user allowed to see?

Around this point, my foundations falter, but I’ll try to proceed so
as to reach what I believe are the next questions.

As part of setting up their authentication mechanism, the user most
likely will have added something like a :require_user callback (and
may have put it in the ApplicationController) as a :before_filter.
This callback in general sets @current_user; @current_user is then
used throughout the controller.

– I’d like to interject here with a particular question of my own
that is well reflected by the model choices here. Since ‘users’ can
refer to both businesses and customers here, how should authentication
be handled? Should there be one user login/session that associates
with a business or customer, and sets the appropriate
@current_business or @current_customer in the require_user callback?
Or, should there be separate business login/sessions and customer
login/sessions, with their own callbacks? –

Some new questions arise from the presence of @current_user.
Previously, the controller looked at the path parameters to determine,
for example, which business’ customers to index.

  1. How does routing and controller behavior change as a result of
    @current_foo ?
  • Should the RESTful, nested routing be maintained? e.g, /
    businesses/:business_id/customers
  • If so, how should :business_id be handled?
    — Should it be ignored, so that regardless of :business_id, we
    always use @current_business ?
    — Should it redirect to the URL corresponding to
    @current_business?
  • If not, should /businesses/:business_id/ be dropped entirely?
    — e.g, when logged in as a business, /customers presents
    @current_business.customers , as managed by the CustomersController
    — the /businesses/:business_id becomes implicit
    • and how should the logic be handled by the controller?
  • Some other change?
  1. How should the relationship between the :id from the path param and
    the :id from the session be enforced?
  • Should there be additional callbacks to @require_user, say
    @require_specified_user to compare the session and path param :ids?
  • How should a mismatch be handled?
    — Should an unauthorized message be displayed?
    — Should the page be redirected to the closest relevant authorized
    page?/
  • Some other method?

And that leads to what is, at least for me thus far, the last
question. Having reached this point, the user will have created a
Business model and a Customer model, and been able to view all of the
associated records. Then the user will have made associations,
allowing the indexing of all of a given business’ customers, and all
of a customer’s business. Hopefully, by adddressing question 1, the
user will still have a means of indexing all customers and all
businesses. The user will have also added one or more callbacks (a
monolithic :require_user ?, :require_business and :require_customer
that include the behavior of :require_user ?, :require_user that runs
before :require_business or :require_customer ?), and a mechanism for
enforcing the relationship between the session identification and the
path parameters (from question 3). At this point, the user, who is
now the Administrator, and neither a business nor a customer, wants to
be able to view all the records, and also to be able to view the
nested resources as seen by the ‘users.’

  1. How should authorization for the administrator be handled?
  • If the answer to question 1 was to create a separate controller,
    the answer is simple: a :require_admin callback for that controller.
  • If the answer to question 2 was to maintain RESTful nested routing,
    should the :require_user (or equivalent callback) have an :unless
    => :as_admin?
    — Thus falling back to the :business_id path parameter to identify
    the top level resource.
    — How would this be handled if the controller was changed to
    always use, e.g, @current_business, in the solution to question 2?
  • Some method that works with the implicit logic from dropping the
    nested routing as in question 2?

Hopefully this post captures some of the most common questions that
Rails newcomers have as they’re delving into their first real
projects. With Rails’ emphasis on on DRY, and proper separation of
controller, view, and model logic, I’m really interested to see what,
if any, are the common, clean solutions. If there are other common
questions that I’ve missed, please add them. To anyone who responds,
thank you in advance. I’m going to append one more question that is
less of a core issue, but has just caught my attention.

Thanks,
Andrew

APPENDIX

Having set the nested associations as described above, there are now
two ways to ‘show’ a customer: /customers/:id and /
businesses/:business_id/customers/:customer_id . The administrator
wants the businesses to see only a limited set of the customer’s
profile in the nested view; the customer, of course, can see all of
its own data.

A) What is a/the good/best/recommended solution to limit the business’
view?

  • Should there be two separate methods/views (e.g,
    CustomersController ‘show’ and ‘show_business’)?
  • Should the ‘show’ view have conditional logic that hides data based
    on the presence of @current_business?
  • Should the ‘show’ method render a different view for each case?