1

I've sucessfully rolled my own roles and permissions based authorisation system:

  • Roles and Permissions are both models in the database that have a many-to-many relationship through another table.
  • Users belong_to a role and have_many permissions through that role
  • Admin users are able to create new roles and define which permissions go with which roles.
  • Controllers and Views check the current user has particular permissions before acting or rendering certain things.

It all works wonderfully. Here's a key part of the code which gets included into application_controller,rb:

def permitted_action (permission_names)
  # if the users permissions do not instersect with those given then redirect to root
  redirect_to(root_url) if (permission_names & current_user.permissions.map(&:name)).empty?
end

and here's a controller validating user permissions:

before_action only: [:new, :create] do
  permitted_action ['create_user'] 
end

My problem is that permissions are DB rows with unique string names, and I use the names when I want to identify the permissions. So I seed the DB with all the permissions that I need, and then when a View or Controller needs to check the permission I need to get the permission name right at that point. If I check for permission "add_user" but in the db the permission name is "add_users" it goes wrong.

Then in testing I have to put all the permissions in again as fixtures, and get all the names right again.

When I add functionality that requires more permissions I have to add these permissions in with the same string in at least 3 different places. That seems like the wrong thing to me.

Ideally the controllers would define what the permissions are that they use, and the db/seeds.rb file would pick up those and seed the db, and the test fixtures would also pick them up, but I'm not sure if this is possible.

How should I structure this?

Edit:

I think what would help me is a fixtures parser that feeds the db/seeds.rb file. Taking something like this from permissions.yml:

archive_tally:
  name: archive_tally
  description: Archive or make active any tally
  category: 3

And spitting out something like this:

archive_tally = Permission.find_or_initiate_by_name("archive_tally")
archive_tally.description = "Archive or make active any tally"
archive_tally.category = 3
archive_tally.save!

I don't think this sort of thing exists yet.

Toby 1 Kenobi
  • 4,717
  • 2
  • 29
  • 43
  • I think what I need to do is have specially formatted comments in the controllers that declare the permissions they are using, then have a rake task that scans the controllers for these declarations then populates db/seed.rb with the permissions that aren't already there. Another rake task would do the same to populate test fixtures. However since I don't know the first thing about editing rake files and don't have time to learn, I'll just have to be satisfied puting the seeds and fixtures in manually. – Toby 1 Kenobi Jul 12 '15 at 15:58
  • I've just noticed the accepted answer to [this question](http://stackoverflow.com/questions/761123/what-is-the-best-way-to-seed-a-database-in-rails) should get me much of the way there. – Toby 1 Kenobi Jul 13 '15 at 05:32

3 Answers3

1

You can actually cut this down to just being the user table (or roles too depending on how many there are) by adding fields like is_admin or is_general_user to group your users into permissions groups. Then the permissions are just a matter of methods on the User Model, for example:

def can_create_new_roles?
  self.is_admin
end

So now you can just do

before_action only: [:create, :new] do
  redirect_to root_path unless current_user.can_create_new_roles?
end

Which reads much nicer. Plus since this all happens on the User model vs. on its own DB table it would make testing for all of these UserPermissions much easier.

Zubatman
  • 1,235
  • 3
  • 17
  • 34
  • 1
    This solution wouldn't allow admin users to create and edit roles within the app. I've already implemented that functionality and don't want to lose it. – Toby 1 Kenobi Jul 11 '15 at 17:13
  • Also I've already got the functionality of `current_user.can_permission_name?` using `method_missing` as demonstrated by [Ernie Miller](http://erniemiller.org/2008/09/30/easy-role-based-authorization/) I'm using that sort of method call in the views. – Toby 1 Kenobi Jul 11 '15 at 17:15
  • But you're right that using those methods reads much nicer than my setup so I'll put them in the controllers too. – Toby 1 Kenobi Jul 13 '15 at 05:33
0

This is what I've come up with and it satisfies me, though it's not the ideal solution that I was hunting for in the question:

It turns out that I want the production database to be seeded with fixtures that are a subset of the fixtures I want appearing in the testing database.

  1. I've made a directory db/seed_fixtures that includes files such as roles.yml and permissions.yml. In these files I've put the fixtures I want to seed the production database with.

  2. In tests/fixtures/roles.yml I've included the other fixtures with this line at the top of the file: <%= ERB.new(IO.read(Rails.root.join "db/seed_fixtures/roles.yml")).result %> Same deal with permissions.yml and other YAML files this applies to. I put any testing-only fixtures in there under that include line.

  3. In db/seeds.rb I generate seeding code from the fixtures for production like this: ActiveRecord::Fixtures.create_fixtures("#{Rails.root}/db/seed_fixtures", "roles")

Now I write all my fixtures once each, some for use as seeds in the production database, others are only for testing. To load the seeds in to the database I run rake db:seed (which I do on the production server) and to load all the testing fixtures in the database I run rake db:fixtures:load

Update:

I've just started using Cucumber and I've realised that I can use the code in step 3 above in a Cucumber Step to get the seed data into Cucumber's test database:

Given(/^seed data is loaded into the database$/) do
  ActiveRecord::FixtureSet.create_fixtures("#{Rails.root}/db/seed_fixtures", "roles")
  _(Role.count).wont_equal 0
end

Cucumber also informed me that ActiveRecord::Fixtures is deprecated in favour of ActiveRecord::FixtureSet

Toby 1 Kenobi
  • 4,717
  • 2
  • 29
  • 43
  • I've just realised that `ActiveRecord::FixtureSet.create_fixtures` deletes data from the database before adding it. That's different to how `db/seeds.rb` usually behaves. This means you have to use it with caution, especially on the production server. – Toby 1 Kenobi Aug 07 '15 at 10:15
  • I've unaccepted this answer because I found out that the create_fixtures method deletes existing data which is not normal behaviour for db/seeds.rb and might cause all sorts of problems when used in a production database. – Toby 1 Kenobi Aug 08 '15 at 16:24
0

I've come up with a completely different solution: I've created a fixture parser.

Most of my first solution still applies (see my other answer) but now, instead of step 3 in that solution, I've made a parser that reads the fixtures that I want to use for seeds and adds them to the database, checking for whether they are already there or not. I run the parser from db/seeds.rb

I've put the code for the parser up as a gist on github

Toby 1 Kenobi
  • 4,717
  • 2
  • 29
  • 43
  • I'd be surprised if something like this doesn't already exist. It seems like others would want this sort of functionality - but maybe not if everyone is moving away from fixtures towards factory_girl. I'd be interested to see any other existing solutions that do this sort of thing. – Toby 1 Kenobi Aug 09 '15 at 18:59