![]() |
is a freelance Ruby developer/consultant. He specializes in developing web applications using the Ruby on Rails framework. Gavin is a BDD (behavior driven development) enthusiast, which means the code he writes caters precisely to his clients specifications and business goals. |
If your application is open to the public, where people can freely create a user account and start adding content or posting comments or forum entries, it's important that you maintain some control over what they can and cannot do.
Leaving usernames like "admin", "webmaster" or your domain name up for grabs could be pretty damaging, especially if they are misused by a malicious user. Imagine if a member of the public was free to leave offensive comments using your name or "admin".
By taking a few basic precautions, you can prevent this sort of abuse without having to continuously monitor your app. Here are a couple of ideas to get you started.
The simplest option is to use validates_exclusion_of to check if a new user's username appears in a list of prohibited usernames:
class User < ActiveRecord::Base
validates_exclusion_of :username,
:in => %w{ admin webmaster mydomain.com },
:on => :create,
:message => "is not available"
end
If the user's username appears in the array of names passed to the :in option, then the record is invalid. The error message here reads simply "is not available". You could go with something more authoritarian like "is restricted!" but that's not really necessary.
One problem with this approach is that validates_exclusion_of is case-sensitive. If a user signs up with the username "Admin" or "ADMIN", then their username will not be invalid. You could get around this with a before_validation callback to format the username before it's validated. But that means the original case will not be preserved.
class User < ActiveRecord::Base
before_validation :downcase_username
validates_exclusion_of :username,
:in => %w{ admin administrator superuser mydomain.com },
:on => :create,
:message => "is not available"
private
def downcase_username
self.username.downcase!
end
end
Another problem with this approach is that validates_exclusion_of only checks for exact values so "admin1" or "admin:" would be considered valid even though they are still misleading.
For a more effective validation, you can write your own custom method and call it using validate or validate_on_create.
class User < ActiveRecord::Base
validate_on_create :check_username_is_allowed
private
def check_username_is_allowed
%w{ admin webmaster mydomain }.each do |restricted_value|
errors.add(:username, "is not available") if username =~ /#{restricted_value}/i unless errors.on(:username)
end
end
end
In this example, the private method check_username_is_allowed will be called before any new user records are created. The user's username is scanned for each of the restricted values and if the restricted value appears anyplace within the username an error message "is not available" is added. This means we can now restrict usernames like "admin1" and "administrator" as well as "Admin:" and "Administrator". You'll notice there's an 'i' after the regular expression. This ensures the regexp will be case insensitive. You'll also notice the 'unless' clause. This will ensure an error will only be added if there are not already errors on the username; No point in bombarding the user with several error messages on the same attribute.
We can expand this a little further by moving the array of restricted values to a constant:
class User < ActiveRecord::Base
RESTRICTED_NAMES = %w{
admin
webmaster
mydomain.com
superuser
}
validate_on_create :check_username_is_allowed
private
def check_username_is_allowed
RESTRICTED_NAMES.each do |restricted|
errors.add(:username, "is not available") if username =~ /#{restricted}/i unless errors.on(:username)
end
end
end
Once you start thinking of the reserved or offensive words you want to prohibit you'll probably surprise yourself with how many you can think of. Moving these values to an constant will keep your check_username_is_allowed method small and legible regardless of how many restricted names you add to the list.
Finally, you may decide to extend this idea across more than one model. For example, you may also have a comment model that you want to add the same sort of validation to. In this case, to keep your models DRY, you should move this code to a module and include it where required.
# in lib/username_validations
module UsernameValidations
# this method is invoked when the module is included into another module or class
def self.included(my_class)
my_class.send(:validate_on_create, :check_username_is_allowed)
end
RESTRICTED_NAMES = %w{
admin
webmaster
mydomain.com
superuser
}
private
def check_username_is_allowed
RESTRICTED_NAMES.each do |restricted|
errors.add(:username, "is not available") if username =~ /#{restricted}/i unless errors.on(:username)
end
end
end
and, in your models:
class User < ActiveRecord::Base
include UsernameValidations
end
class Comment < ActiveRecord::Base
include UsernameValidations
end
By writing some custom validations like this you can prevent people from adding inappropriate content to your site, saving you from having to filter through every new post/comment etc. checking them. You can also employ the same method to help prevent spam on your site. Just add the usual words, viagra, weight-loss etc. to the list of restricted values.