Annotations: The Language Shoehorn?
A while back, before they had such a wide variety of shoe sizes to fit everybody’s foot we had this tool called a shoehorn. The shoehorn was there to help force your foot into a shoe that no longer fit. Well, it’s purpose was to keep the heel of the shoe from collapsing—but back in the day we used it to get a little more life out of a shoe that was too small. Buying a new pair of shoes was out of the question, because it was a choice between spending the $30 on shoes or some other need that we had. Times have changed, and so has my budget.
Background
Software languages have undergone many different fundamental shifts in thought over the years as well. Originally we had machine language and its symbolic cousin Assembly Language. Originally they had little structure and GOTOs (AKA JMP instructions) were written to specific addresses. Some natural evolutions happened as processors became more complex and subroutines were born, enabling some of the more Calculus like breakdown of applications. The first thought shift was Structured Programming which replaced hard addresses with symbols and registers with variables, and enabled an arbitrary number of variables—despite the limitations of the processor underneath. Then came Procedural Programming with C and its collection of similar languages. You’d think that Object Oriented Programming came next, but that was just one of several other shifts in thought. You also had declarative languages, logic languages, etc. For a longer breakdown you can visit Wikipedia’s Programming paradigm page. Each programming train of thought lends itself to a particular set of problems.
The ultimate sophistication is simplicity. Leonardo da Vinci
There are many aspects to simplicity, and it’s easy for me to throw this term around and add the disclaimer about how it’s easier said than done. Instead, let’s explore what makes software simple. What goes into this word simplicity that I keep harping on?
- Economy of concepts. There should be no more than one or two fundamental design principles that are used consistently, and without exception. If there are two principles, the choice between them also has to be consistently applied and clear when to use which principle.
- Consistency. As soon as you add one exception to any rule, you’ve just made it a lot more difficult to understand. For you math happy folks think of the complexity in terms of Re+1 where R is the number of rules and e is the number of exceptions (in total). Smaller numbers are more simple and easier to understand. For example, if you have 9 rules and rule 1 has 2 exceptions and rule 3 has 4 exceptions the complexity is 96+1 or 4,782,969. That’s bad.
- Predictability. Every action that you can make has to have a clear and predictable outcome. To understand the impact of unpredictability, treat the math for predictability like the math for consistency. For everything that can go wrong, you’ve just made the system that much harder to comprehend.
- Self evident. Based on the consistently applied principles and the predictability of actions, the course of action for the user to take should be self evident. In short, once you’ve learned how things work you shouldn’t need anyone to hold your hand when you have to make a change.
Now you get an idea of what I mean by simplicity, and how it is actually hard to pull off for anything but simplistic systems. I mean if you only have two rules and two exceptions you still have a relatively simple system. Of course, once you start adding more rules and actions the complexity increases exponentially.
Annotations Are Mixing Concepts
I’ll be the first to say that Annotations are not completely useless, and they can help address some otherwise potentially sticky problems. However, as soon as you use Annotations to perform a programming task you are using declarative programming. Java and C# are supposed to be Object Oriented Programming Languages. Build languages like ANT and Make are declarative languages. You declare some tasks and their dependencies and allow the build process to sort it all out. Java wasn’t designed like that, but because there were some aspects of programming that are too hard to do in Java’s version of OOP we introduce annotations.
Annotations provide a way to mix concepts in a declarative fashion.
All the Annotation does is mark something in the class with some sort of tag. In essence it’s like a marker interface for the class, method, attribute, or parameter. The most innocuous use of Annotations is simply to mark a method for documentation purposes. It doesn’t take up compiled class file space or runtime memory, just gets added to the documentation. That’s a reasonable and predictable use for annotations.
The tricky part is when we change what the application does when we use annotations. For example, marking methods as a web service endpoint changes how the method is used. Marking a method as a test method affects how that method is called. Or in the case of Java ActionPack, marking a method with @Filter has more side effects. That method will now never be used for an Action, and the filter will be called before/after all the actions that it applies to. I recently added a convenience method so that you can query whether an action is marked with an annotation or not.
I used this solution so that I could mark all my AJAX support methods with the @AJAXSupport annotation. Why would I want to do that? Two reasons: for documentation purposes, and so that I don’t have the Single Sign On (SSO) service bring me back to an AJAX support page. The system would automatically set a callback URL in a filter applied for every action in the system. If someone called an AJAX support action that returns a partial page result, and then clicks on the SSO link the return URL from the SSO service would be for the partial page result. I had to do something (other than manually listing the actions and testing each one of them) to prevent that from happening. So I added an annotation so that I could check if the action has that annotation or not.
When I explained that to someone on the team, they responded with “I would never have guessed that”. Of course they wanted the equivalent of a modal dialog box on the web—which I believe is bad even in desktop application design. Another complaint was that I was mixing application logic with display logic. To be fair, it is navigation logic which is sometimes considered application logic and sometimes considered display logic in different situations. However, from a pure “separation of concerns” principle I was now mixing concerns in my code.
Does that mean that the solution was bad? It works, and it was the simplest thing to maintain and perform. It does not mean it is the most elegant solution or the purely simple solution. For one thing, I’m mixing declarative logic with object oriented logic. Another problem is that I’m mixing navigation logic with application logic. To some extent you can’t get around that, but that can be a symptom of design smell as well.
So am I saying that Annotations are evil, and because they are mixing concerns and programming paradigms we should avoid them at all costs? No. I’m saying that they trade code complexity for conceptual complexity. The complexity is not really gone from the system, it’s just that it has been moved to a different form of complexity. You might be able to grasp each concept in isolation, but mixing the two will probably make it hard to maintain in the future.
Part of the reason that Ruby on Rails works so well is that they did keep the concepts as simple as they could. There are relatively few rules to learn, and the rules are applied consistently. It was also built on a language that was flexible enough to keep everything object oriented, so there is no need to resort to the declarative annotation model.
Justification for On Complexity
I chose the formula because as soon as you introduce one exception, the human mind has to sort out whether or not that exception applies to this rule. Even if there is one exception to a list of 10 rules, we have to recall whether the rule we are using has the exception or not, and if it does whether the exception is in effect. A list of rules with no exception still has the complexity of deciding which rule (or set of rules) applies so it has a base exponent of 1. You add one exception, and you’ve squared your complexity. For example a set of ten rules with no exceptions has a complexity score of 10, and as soon as you add one exception it becomes 100.
The complexity values have no units and no real meaning other than to provide a relative indication of what you are getting yourself into. It helps you understand the number of decisions the brain has to sort through before it is satisfied that it can predict that you are doing the right thing. Sometimes a little change can provide a huge impact on the relative complexity. The idea is to keep the numbers as small as you can.
Example of Simplicity in Design
It’s all well and good to preach simplicity, but if you don’t provide practical examples then no-one can get it. I’m going to highlight one section of Java ActionPack to show simplicity in action. The first mistake that people make when talking about simplicity is confusing it with simplisticness.
Simplicity is the ultimate sophistication. Leonardo DaVinci
So what does it mean to use simplicity in code? First, simplicity has to do with keeping the responsibilities of your code small and focused . You should resist adding responsibilities to a bit of code unless there is no better place to put it. The org.dhaven.actionpack.Route class in Java ActionPack takes a URL pattern, and set of default values and populates request attributes with the substitution values. That’s it. Nothing else. It’s the controller code that reacts to the special attributes “controller” and “action” to find the controller and action to execute. The org.dhaven.actionpack.Routes class will allow you to register a series of Route objects to evaluate in order. It also provides a method to iterate through the Routes and return when the first match has been found. Pretty sweet, huh? Two classes that have a relationship, but their responsibilities are very small.
Nice overview, but can we look at things in more detail? Sure thing. I wanted to make sure it was easy to programatically set up the Routes you wanted your application to use. If you didn’t want a routes file hanging around, or you wanted to embed this framework someplace I hadn’t expected, it shouldn’t be hard to do. I.e., I shouldn’t have to try and set up an object tree like the Apache Digester based projects. When you try to start from the configuration file format, instead of how you should add new routes to the system it gets really ugly really fast. So I wanted to make it simple , or easy to do. My solution? Why not simply call Routes.connect() ? If you were to set up the routes manually, it would look like this:
public static void setUpMyRoutes() {
Routes.connect("/{controller}/{action}/{id}");
Routes.connect("/{controller}/{action}");
Map<String,String> initParams = new HashMap<String,String>();
initParams.put("controller", "home");
initParams.put("action", "index");
Routes.connect("/{controller}", initParams);
Routes.connect("", initParams);
}
Notice that I’m only passing strings into the Routes object? In turn, the Routes object doesn’t do anything with these strings other than creating Route objects with the parameters passed in to the constructor. It’s the Route object itself that knows what to do with the provided strings. The initParams map is also a simple map, which provides default values. The default values are overridden by the values interpreted by the URL. What I haven’t done, and I probably ought to do is to make it so that if defaults are present, the same Route can match partial URLs like the Ruby on Rails version of the functionality can. Perhaps that is a future enhancement. Remember, it is only the Route object I have to alter to make that possible. Setting up the Routes is as simple as it gets.
So what if I don’t want to worry about setting up the routing myself? Well, why not create a route parser? That’s exactly what we did. The org.dhaven.actionpack.RouteMapParser handles parsing a very simple route file format that isn’t XML. In fact, it’s similar to the Ruby format (with minor differences). I didn’t want a full on Ruby parser, just a convenient format. If you call org.dhaven.actionpack.RouteMapParser.parseFile() while passing in an InputStream or Reader, you can have it parse the file for you. The parser is very simple as well. First, all routes are completely defined on one line. Second, blank lines and lines that start with the ’#’ character are ignored. Why the ’#’ character? Because it is used as the comment marker in Properties files, shell scripts, and Ruby files. It should be reasonable to assume people know that is a comment. So what does a line look like? Well, let’s do the same thing with our file that we did in the method above.
"/{controller}/{action}/{id}"
"/{controller}/{action}"
"/{controller}", action => "index"
"", controller => "home", action => "index"
The first part of the line is the URL in quotes. This is the “route” that is being connected through the Routes class. All elements of a route are separated by a comma. That means the route and all default request attributes are separated by commas. The key/value of each map entry is separated by the ’=>’ combination. That separator is a convention used in Ruby and Javascript, but it looks right. No quotes for the key, and the element is surrounded by quotes. There really isn’t much of a reason for this convention other than I thought it looked easy on the eyes. With everything in quotes it got tough to keep straight what were the keys and what were the values. Now, all the key/value pairs are strings. It’s up to you to perform the conversions to other types in the controller if you want. The reason for that has to do with simplicity of design, and not getting the whole route map process too convoluted.
The RouteMapParser takes care of calling the Routes.connect() method which in turn takes care of creating the Route and putting it in our list of routes to match against. Each responsibility is clearly mapped to one class. If someone else wanted a different format for setting this up, there would be nothing to stop them from doing it. It also makes understanding each class much easier. The RouteMapParser only has the responsibility of converting a file to a set of routes. The Routes object only has the responsibility of managing the list of routes and testing a request against that list. The Route object has the responsibility of trying to match the URL, and then populating the attributes if there is a match. The Route object may be expanded in the future to take the parameters and generate a URL from them (as Ruby On Rails currently does), but that is a future enhancement.
The bottom line is that the responsibilities are finite and related to each other. More importantly, the concerns of configuring the system are kept separate from the code that does the work. The number of objects needed to do simple things is very small, and the interactions are well defined and easy to understand. The simplicity allows the objects to be used in ways I may have not foreseen, but that doesn’t really matter. What matters is that the sophisticated uses I haven’t foreseen are because of the simplicity of the classes governing that responsibility.
Make Learning Your Software Fun
I have to say that both academia and corporations suffer from taking themselves too seriously. Sure there is a certain amount of propriety that you have to have with your clients, but people like having fun. They want to enjoy themselves. So why is it that science becomes such a fun-sucker? I mean you would think that having subjects like “funology” would be a great topic, but the scientific approach makes everyone stuffy. Then you visit the campus of MIT or something like that and people have robot powered couches and other cool stuff. Why? Because it’s fun!
If you play video games, you’ll invariably have to learn something before you really get into the game. The rules of fighting, how to get around, etc. Some games are better than others about teaching you what to do without making it feel like a classroom setting. Ok, press the ‘X’ button two times really fast… Thankfully the education levels are usually pretty quick. It’s a pity you can’t say “I already played the game, can’t I skip the class?”.

If there is one thing that video games can teach us “serious” programmers (you can take yourself too seriously) is that if you properly reward your users for good actions, then they will keep doing it. Whether it was the intention or not, the Ruby on Rails framework teaches us how the writer of the application prefers to do web applications. The naming scheme for tables and the wiring of controllers, helpers, views, and models into one application all have their rewards. The fundamental reward for us developers is that if we do it the blessed way, we have less work to do with configuring and wiring things together. Even the code generators teach by example how to do things like how to do CRUD actions (the scaffolding feature). It now becomes fun to write web applications because we are rewarded for most things. Our imagination will help us overcome those obstacles that are beyond the basics we learned from the framework.
There is nothing that says we can’t use the same model for our applications. If our users learn how to use the app to make their lives better in simple, yet profound ways, they will want to become power users. If the users never get over the “I suck” threshold, even after examples and handholding, you will lose them. Making learning to be a fun activity doesn’t require a hippie teacher standing in front of a classroom with a guitar singing the times tables to the tune of “Gilligan’s Isle”. It just means that you make your users feel empowered, cool, or at the very least useful.
Getting Typo 4.1.1 Installed on a Shared Host
Every shared host has their own procedures for getting a Rails app up and running. The instructions that TextDrive has for installing a Rails application does a great job for them. However there are a few gotchas that you need to be aware of. First and foremost the installed version of Rails at TextDrive does not match what Typo 4.1.1 needs. Not to mention that the quotas installed for users are pretty limiting for RAM usage. You can't run gem update or install because it exceeds the memory allowed for a user (you can run gem list --local, but that's about it). That means you can't use the preferred method for Typo installation! Such a pain, but all is not lost.
Preparing the Typo Package
Typo needs Rails version 1.2.2 to function properly. It needs at least 1.2.0, but I did find issues with 1.2.3 and I didn't have time to figure out what they where. So, how do we publish an application that needs a different version of Rails than our host provides? Thankfully the Rails folks already have a solution for that, which was designed to let you play with "EdgeRails" (or the latest trunk). First, let's make sure you have the appropriate version of Rails installed on your local machine:
gem uninstall rails gem install rails --version '= 1.2.2'
Now that the right version of rails is installed on your machine, we have to install it into the typo distribution. Download the tarball from the Typo website, and unpack it on your machine:
tar xzf typo-4.1.1.tgz
With the command line, navigate into the distribution directory. The following command is probably a good idea no matter where you are publishing--particularly on a shared host. You don't have control over when your ISP decides to upgrade or change things. We are going to "Freeze" the version of rails to the gems you just installed on your machine.
rake rails:freeze:gems
Now you can repack the tar file. Go ahead and delete the old tarball before you pack it. The Rails app will have paths that are too long for the "old" style tar so you can't use the 'o' option. The following worked well:
tar czf typo-4.1.1.tgz typo-4.1.1
All you have to do at this point is upload the tarball to your account on your ISP. Unpack the tarball
Setting up the Database
The next step is to get the database up and running. Do whatever you need with your ISP to create a database for typo, and run the appropriate SQL script provided in the typo/db directory to set up the database schema. Next you'll have to configure the database.yml. The Typo team was good enough to provide an example configuration file. You just need to edit it to connect to your ISP's database instance. Just for safe keeping, do change the database name to the same thing for the development, test, and production configurations.
As a way to test the database connection, and to ensure everything is configured properly, you'll need to run the migrate task.
RAILS_ENV=production rake db:migrate
There's just a couple things left to do now.
Configure The Application
We need to make things easier on ourselves, so whether your ISP lets you set environment variables for running tasks or not, you'll want to uncomment the line to force the rails environment to production in the config/environment.rb file:
ENV['RAILS_ENV'] = 'production'
Also, because shared hosting is usually pretty limiting on your memory allocations, you'll want to disable any plugins that generate thumbnails. Find the line where the config/environment.rb file specifies plugins and edit it to look like this:
config.plugins = [ 'localization',
'typo_textfilter_tmcode' ]
Finally, most ISP instructions include changing the public/dispatch.* files so that the hashbang line has an explicit reference to your ISP's ruby interpreter. There's no guarantee that the env will find the ruby interpreter, and it removes one thing the machine has to do to get the app running.
Assuming the rest of the instructions from your ISP worked to get the app visible to the world, you will be able to go into the blog, create your username/password and manage the blog. I highly recommend keeping the sidebar simple. Only use static text, archives, syndication, and tags (and categories if you want) plugins when you have to share your host. The Magnolia and Flickr plugins generate thumbnails which look nice, but you'll run out of memory quick and the fastcgi process will die.
The Master of My Domain
I’ve grown tired of being at the mercy of someone else’s opinion of blog hosting. I’ve grown tired of JRoller, and having my identity and my content out of my control. I’ve finally got my new blog software hosted on my own ISP. If there is something wrong I’m the one to blame. The good news is that I’m not going to have a service one day and then have it revoked the next.
I’ll post the details of how I got Typo working on TextDrive, because it was an ordeal. I’ll just do it in an article of its own. So why Typo and not one of the other blog applications? Partly, because I wanted to do something with Ruby on Rails but mostly because it was the most complete blogging tool I could find for the platform. I needed RSS feeds, XMLRPC support for blog posting clients, tagging, AJAX support, etc. Typo had all that out of the box.
All that remains is to properly customize my theme. I’m taking the opportunity to change everything. The blog is now “Software Artisan” to reflect the evolution of how I approach software projects. It will still be the eclectic view into my life, but instead of neatly compartmentalizing everything, I’m tagging it. I hope to get some really cool stuff here in the future. I’m debating whether I should just make a clean break from my past, or if I should attempt to import my old articles. I may import as many as fit on my old feed, and let the rest simply disappear. It’s a new blog, with a new history. It’s my content.
Yoroshiku Onegai Shimasu (Please be kind to me)
Oportunity? Ruby still playing catch up in one area
Let me say right off the bat that I am a Ruby fan, and I love Ruby on Rails. In so many ways the API just feels right for the type of applications I write. However, there is one area that Ruby and Rails is still playing catch up in, and it has to do with the relative size of the community. Let‘s face it, for all its benefits, Ruby on Rails is still a small player in the world of web application development and it enjoys a very enthusiastic, yet small, community of developers. In a way it is my continuation on the rants on M$ development. The main problem is that there still isn‘t enough people who like to share how they did things. I don‘t believe that it has to do with the same motivation as Micro$oft, because both Ruby and Ruby on Rails are open source efforts and don‘t seek monetary reward. I‘m sure they wouldn‘t refuse it if it was offered though….
In a way I believe that because there is this general feeling of the fact that everything is “so easy a caveman can do it” (apologies to any cavemen reading this). Why should I write about something that everyone and their brother can do? To answer that question I have to ask a counter-question, why is my article on role based authorization my most popular article I‘ve ever written? The problem with Ruby is that there are so many ways to do something that sometimes you get stuck trying to figure out what is the best way for your project. I applaud the efforts for writing plugins and “acts_as” filters, but how do we use them or write them?
I‘m not saying that there is no documentation, because the people who maintain Ruby on Rails have done a wonderful job in this area. The problem is that it is only them writing any decent documentation. I‘m glad that they set up a Wiki, as it contains some key information such as how to connect to SQL Server (not that I would ever recommend using that) or deploy on different servers. The problem is that it suffers the same fate of most Wikis (with Wikipedia the shining exception), disorganized mess with articals of varying quality and completeness. Ironically, some of the better introductory guides or howtos come from curious Java guys.
Perhaps if there was some outlet to stroke people‘s ego or let them become de facto gurus in a publication the world may change a bit. So far the only thing that resembles a regular (English) publication in the Ruby world I am aware of is the Ruby on Rails Podcast. As great as that is, there is still a dearth of spreading knowlege about how to solve some pretty cool problems. As a result, it‘s easy to get stuck.
I would seriously consider setting up some sort of publication, even if it is strictly online, if I could be reasonably sure that I could have recurring articles of decent quality. Perhaps you can consider this a feeler for the level of interest here. I can‘t give remuneration for article submissions at this time, but I would seriously like to do something like this. It would be even better if the host site was implemented in Rails. If you are interested, post a comment here. If I get underwelming silence then the idea fades away. If I get an overwelming response, then I would move forward. I would have a need for recurring articles as well as one-off how-tos and reviews.
Role Based Authorization in Ruby on Rails
I'm rereleasing all the articles on D-Haven so that when I upgrade the site they have a home.
...
Ruby on Rails is a great web framework, and it does simplify many aspects of writing a dynamic web application. While rails does have a login generator, it does not provide support for role based authorization. This article details how to extend the login to include role based authorization.
Role based authorization provides protection in the sense of enabling users who are trusted at different levels access to what they need at their trust level. For example, you may have an article submission system with all users able to add comments, some users able to submit and edit articles, and administrators able to delete articles. This level of access control enables the system to protect itself from accidental abuse.
Step 1: Enable logins
You will need to install the "login" generator for rails. Just follow the instructions on the LoginGenerator page. At that point, you just need to generate the login with a command similar to this:
$ script/generate login User
Next, set up your database with a table for the logins. The table name needs to be "users", with the set of columns listed below.
- id, integer, auto-increment, and primary key
- login, varchar(20+)
- password, char(40)
Note, you can add additional columns if you want to track more information about the user. I like to have a "Name" and an "Email" column in adition to the required columns. That way I can have a better login message than "Welcome bloritsch" and I can also add a process to reset passwords if the user forgot it. That's a subject for another time.
Another additional thing you should do at this time is to change the "salt" value. The login generator allows an application to salt the passwords so that it is not as trivial as finding the SHA1 hash of the password itself. You'll see a line with @@salt = 'change-me'. All you have to do is to replace the contents of the string with your new salt value.
Step 2: User specific authorization
In my article submission scheme, I wanted to make it that the user who submitted an article is the only one who can edit it. Call it an experiment in ownership. I very well may relax that requirement and make it a role based authorization. However, this is the first step in showing how authorization could work.
To enable user specific authorization, you have to associate a user with a controlled item. We will assume you have a model named "Article" which maps to the database table named "Articles". To make the association, you need to add in the reference from Articles to User. You do that by adding the "user_id" column to the "Articles" table. Next, modify your Article model which should be in app/models/Article.rb. Add the line "belongs_to :user" to the top of the class. You should also add to the "validates_presence_of" list the parameter ":user". The articles class will look something like this:
class Article < ActiveRecord::Base validates_presence_of :title, :user belongs_to :user end
Since it is very inconvenient for your user to have to explicitly add themselves to an article we need to alter the controller that manages the articles to automatically associate the logged in user to the article. If you generated the scaffolding already for your articles, this is fairly simple to do. We need to ensure the user is logged in, and then we need to alter the "create" method to make the association.
- Add the line
before_filter :login_required, :except => [:index, :list, :show]to the top of the class. This will ensure the user is logged in for any action that would alter the data in the articles without forcing them to be logged in to see the articles. - Add the line
@article.user = @session[:user]directly after the line@article = Article.new(params[:article])that was generated from the scaffolding code.
The resulting changes to the Article's Controller should look similar to the following code snippet:
class ArticlesController < ApplicationController
before_filter :login_required, :except => [:index, :list, :show]
# ... skip other generated code ...
def create
@article = Article.new(params[:article])
@article.user = @session[:user]
if @article.save
flash[:notice] = 'Article was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
# ... skip other generated code ...
end
So far, we have a user automatically associated with new articles. So far so good, but we aren't really protecting against just anyone altering my article. To do that we need to alter the "edit" method to validate the logged in user is the same as what is already associated with an article. To do that we need to add conditional to the code that is already there. For brevity's sake let's take a look at what it should look like:
def edit
@article = Article.find(params[:id])
if @article.user != @session[:user]
flash[:notice] = 'You cannot edit an article you didn\'t upload.'
redirect_to :action => 'show', :id => @article
end
end
What we did here was add an if statement that checks to see if the logged in user matches the article's author. If not, the controller flashes the notice "You cannot edit an article you didn't upload." and redirects the user back to showing the article. If you don't like the flash notice approach (it can get lost at the top of a long article) you can adapt the code to your purposes. So far we are performing a certain level of control over the process. What we are trying to do, of course, is to provide a way to protect against spammers or people with nothing better to do than to destroy your hard work.
Step 3: Adding Roles to the Mix
Now that we have authorization happening and some level of user control, we want to have more granular support so that you have to be an "Admin" role to delete an article. Just to make things interesting we will allow the "Admin" role to alter any article just in case it is largely OK, but the original author had some objectionable language in there.
We need to generate a model for "Role" and two tables in the database. We need two tables to provide for a many to many relationship between users and roles. Any role can belong to many users, and any user can have many roles. The database tables should look like this:
Table: Roles- id, int, auto-increment, primary key
- role, varchar(20), not null
- user_id, int, primary key
- role_id, int, primary key
To allow rails to understand the way they relate to each other in the database, we need to add the directives to associate them. We do this with the line has_and_belongs_to_many and then we cross reference the users with roles and roles with users. It should look something like this:
class Role < ActiveRecord::Base has_and_belongs_to_many :users #add validation for good measure validates_presence_of :role validates_uniqueness_of :role endModel: User
class User < ActiveRecord::Base has_and_belongs_to_many :roles # ... skip remaining code ... end
That's all well and good, but as it stands right now we have to do some fancy checking to see if your user has a particlur role. After all we need to look up the Role object we need, and then see if the array of roles for the User object includes that role. That's alot to ask for the consumer of the system. So lets make it a little easier.
First, let's make it easier to find the Role object that we need. We will assume that we look up a role by the name of the role instead of needing to know what the id is. It makes the code more understandable. To do that we will add a new method to the Role's model. The easiest way to do that is to create a static method overriding the bracket operator like this:
def self.[] (role)
find_first(["role = ?", role])
end
That code allows us to find any role with a very simple call: Role["Admin"]. But wouldn't it be nicer to be a little more Rubyesque in our API and us a symbol for the role? It's really easy to modify the above method to use symbols so that the call looks like Role[:Admin]. Just change the find_first call like this:
find_first(["role = ?", role.id2name])
Now we just want to find out if our user has a particular role. To do that we need a new method in our User model to check that fact. To keep things easy to use we will call it "is_in_role?" The method should look like this:
def is_in_role?(role)
if role.nil?
return false;
end
return roles.include?(Role[role])
end
That way we guard against an empty role and we can validate if the role exists. Instead of asking the developer to constantly find the proper role himself, we include that logic in the method. That way we can validate if the user is in a particular role like this: @user.is_in_role? (:Admin).
Using the Role Checking in Code
Now that we have effectivley added the role checking code we can start using it. Let's start by extending the user specific checking for editing an article to allow anyone with the role "Admin" to edit the article. This is a relatively simple thing to do. First, we need a role with the "Admin" role defined. Next we need a user with the "Admin" privilege associated. At this point you'll have to add those directly to the database.
Blocking actions in the controller
Now, let's take a second look at the "edit" method in the articles controller. All we need to do is add an additional clause to the if statement to check to see if the role is not in "Admin". The altered if statement should look like this:
if @article.user != @session[:user] and not @session[:user].is_in_role? (:Admin)
Hiding parts of the view based on role
Let's say that we want to suppress the "Edit" link in the show.rhtml view for the article if the user does not have the "Edit" role. To do that we need to perform our check in the show.rhtml file. If you recall, anyone can view an article without being logged in, so we do need to verify if the user is logged in before we check their role. I did the check like this:
<% if not @user.nil? and @user.is_in_role? (:Edit) %> <%= link_to 'Edit', :action => 'edit', :id => @article %> | <% end %> <%= link_to 'Back', :action => 'list' %>
To enable @user to work, you'll have to add the line @user = session[:user] to the "show" method in the articles generator. It does make it a little easier to handle.
Protecting known URLs
The last thing we need to do is protect against just anyone deleting any article. If they recognize your app as a Rails based app, they will know that the "destroy" method will delete the article. Its best to protect any action before we take it. To do that we need to alter the articles controller again, this time with the "destroy" method. Something that the scaffold generator doesn't do is provide any flash notices whether a delete was successful or not. We are going to fix this oversight at the same time. The method should look like this when it is done:
def destroy
if session[:user].is_in_role? (:Admin)
Article.find(params[:id]).destroy
flash[:notice] = 'Article was successfully deleted.'
else
flash[:notice] = 'Only an administrator can delete the article.'
end
redirect_to :action => 'list'
end
What we just did was to validate if the user is in the "Admin" role, and if they were we destroyed the article and flashed the success message. If they were not an Admin, they are notified that they can't delete the article.
We're Done
We have a thing of beauty. We can provide fine grained control, and we have a very flexible role based system we can extend. What we didn't do was user administration or adding a way to manage who gets assigned what roles. That's handled manually in the database. Its up to you to write the rest of that. The good news is that you have the ability to make your administration maintained by only authorized users.
