diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 6728b6b..35f6983 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -1,6 +1,8 @@ class ApplicationController < ActionController::Base session :session_key => '_barleysodas_session_id' append_before_filter :block_prefetching_links + append_before_filter :authorized? + helper_method :logged_in? ## # Ensures that the request was made using an Ajax request. @@ -10,6 +12,53 @@ class ApplicationController < ActionController::Base true end + ## + # Determines if the user is logged in. + # + def logged_in? + return !session[:people_title].nil? + end + + ## + # Saves the request uri in the session for later redirect after a login. + # + def save_request_url + session[:request_url] = request.request_uri + end + + ## + # Checks to see if the currently requested uri is the same as the uri saved + # in the session. + # + def already_saved_request_url + return true if session[:request_url] and + session[:request_url] == request.request_uri + false + end + + ## + # Determines if a user can access an action. + # + def authorized? + return true if has_permission_for_action? + respond_to do |format| + format.html { + # prevent double-redirects to the login page if for some reason it is + # not allowed + unless logged_in? and !already_saved_request_url + save_request_url + redirect_to new_session_path + return + end + @content_title = 'Forbidden' + @secondary_title = '' + @hide_sidebar = true + render :template => 'shared/unauthorized' + } + format.xml { render :nothing => true, :status => 403 } + end + end + ## # Sane error and missing document messages. # @@ -71,4 +120,26 @@ class ApplicationController < ActionController::Base return false end end + + ## + # Finds a People Permission models and determines if the People has access + # to a particular aspect of the system. Also finds the Guest user and checks + # for the Guest Role. + # + def has_permission_for_action? + role = nil + if logged_in? + role = People.find_by_title(session[:people_title]).role rescue nil + end + logger.debug("role is #{role.inspect}") + role ||= Role.base_role + while role + return true if role.permissions.detect do |p| + p.controller.to_s == params[:controller].to_s and + p.action.to_s == params[:action].to_s + end + role = role.parent + end + false + end end diff --git a/app/controllers/peoples_controller.rb b/app/controllers/peoples_controller.rb index cb37c4b..6b51c7a 100644 --- a/app/controllers/peoples_controller.rb +++ b/app/controllers/peoples_controller.rb @@ -41,6 +41,7 @@ class PeoplesController < ApplicationController # POST /peoples.xml def create @people = People.new(params[:people]) + set_people_role @page = Page.new(params[:page]) @people.page = @page respond_to do |format| @@ -60,6 +61,7 @@ class PeoplesController < ApplicationController # PUT /peoples/1.xml def update @people.attributes = params[:people] + set_people_role @page.attributes = params[:page] respond_to do |format| if @people.update_attributes(params[:people]) @@ -91,4 +93,11 @@ class PeoplesController < ApplicationController raise ActiveRecord::RecordNotFound.new if @people.nil? @page = @people.page end + + def set_people_role + # set checks here for valid role assignment + if params[:people] and params[:people][:role_id] + @people.role_id = params[:people][:role_id] + end + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..f79c539 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,41 @@ +class SessionsController < ApplicationController + def new + @content_title = 'Log In' + @secondary_title = '' + end + + def create + @people = People.find_by_title(params[:login]) rescue nil + if @people + session[:people_title] = @people.title + respond_to do |format| + format.html { + flash[:info] = "Welcome, #{@people.title}" + if session[:request_url] + t_url = session[:request_url] + session[:request_url] = nil + redirect_to t_url + else + redirect_to '/' + end + } + format.xml { head :ok } + end + else + respond_to do |format| + format.html { + @content_title = 'Log In' + @secondary_title = '' + flash.now[:error] = 'Login failed, try again.' + render :action => 'new' + } + format.xml { render :xml => @beer.errors.to_xml, :status => 400 } + end + end + end + + def destroy + reset_session + redirect_to '/' + end +end diff --git a/app/helpers/roles_helper.rb b/app/helpers/roles_helper.rb new file mode 100644 index 0000000..1166685 --- /dev/null +++ b/app/helpers/roles_helper.rb @@ -0,0 +1,15 @@ +module RolesHelper + def new_role_link + link_to 'New Role', new_role_path, { :title => 'Create a new role' } + end + + def show_role_link(role) + link_to role.name, role_path(role.code), + { :title => role.name } + end + + def edit_role_link(role) + link_to 'Edit Role', edit_role_path(role.code), + { :title => "Edit #{role.name}" } + end +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 0000000..309f8b2 --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/app/models/people.rb b/app/models/people.rb index d455d7d..a5597af 100644 --- a/app/models/people.rb +++ b/app/models/people.rb @@ -5,16 +5,11 @@ class People < ActiveRecord::Base has_one_tuxwiki_page :owner_class => 'People' belongs_to :role attr_protected :role_id - validates_presence_of :role_id - - before_create :set_base_role - - protected ## - # Sets the Role to the top level model. + # Finds the Guest user for the system. # - def set_base_role - self.role = Role.base_role + def self.guest_user + self.find_by_title('Guest') rescue nil end end diff --git a/app/models/permission.rb b/app/models/permission.rb new file mode 100644 index 0000000..bd3c890 --- /dev/null +++ b/app/models/permission.rb @@ -0,0 +1,18 @@ +## +# Models the ability to perform an action in the system. +# +class Permission < ActiveRecord::Base + has_and_belongs_to_many :roles + validates_presence_of :controller, :action + + def to_s # :nodoc: + "#{controller} :: #{action}" + end + + ## + # Helper to find the necessary models for a form edit. + # + def self.find_for_form + self.find(:all, :order => "controller ASC, action ASC") + end +end diff --git a/app/models/role.rb b/app/models/role.rb new file mode 100644 index 0000000..48a38db --- /dev/null +++ b/app/models/role.rb @@ -0,0 +1,39 @@ +## +# This model is a grouping of Permission models associated to a particular +# People. +# +class Role < ActiveRecord::Base + has_many :peoples + belongs_to :parent, :foreign_key => 'parent_id', :class_name => 'Role' + validates_presence_of :code, :name + validates_uniqueness_of :code + validates_format_of :code, :with => /^([A-Za-z0-9])+$/, + :message => 'may only contain letters and numbers' + has_and_belongs_to_many :permissions + + ## + # Ensures that the Role does not have a parent of itself. + # + def validate + if !self.new_record? and self.parent_id == id + self.errors.add(:parent, 'cannot be self') + end + return false if self.errors.size > 0 + true + end + + ## + # Returns a Role found by +code+ if the method is missing. + # + def self.method_missing(method_name, *args) + return self.find_by_code($1) if method_name.to_s =~ /^(.+)_role$/ + super + end + + ## + # Returns a select-box compatible array. + # + def self.for_select + self.find(:all).collect { |x| [ x.name, x.id.to_s ] } + end +end diff --git a/app/views/layouts/application.rhtml b/app/views/layouts/application.rhtml index 57c1529..f591c84 100644 --- a/app/views/layouts/application.rhtml +++ b/app/views/layouts/application.rhtml @@ -8,6 +8,9 @@ + @@ -27,6 +30,7 @@
<%= yield %>
+<% unless @hide_sidebar -%> +<% end -%>
diff --git a/app/views/roles/_permission.rhtml b/app/views/roles/_permission.rhtml new file mode 100644 index 0000000..3c2c729 --- /dev/null +++ b/app/views/roles/_permission.rhtml @@ -0,0 +1 @@ +
  • <%= check_box_tag 'role[permission_ids][]', permission.id, @role.permissions.include?(permission) -%> <%= permission -%>
  • \ No newline at end of file diff --git a/app/views/roles/_role_form.rhtml b/app/views/roles/_role_form.rhtml new file mode 100644 index 0000000..99af109 --- /dev/null +++ b/app/views/roles/_role_form.rhtml @@ -0,0 +1,14 @@ +

    + <%= text_field :role, :name %> +

    +

    + <%= text_field :role, :code %> +

    +

    + <%= select :role, :parent_id, Role.for_select, { :include_blank => true, :selected => @role.parent_id.to_s } %> +

    +

    Permissions

    +

    <%= link_to_function "Check All", "set_all_checkboxes('role_form', 'role[permission_ids][]', true);" -%> <%= link_to_function "Uncheck All", "set_all_checkboxes('role_form', 'role[permission_ids][]', false);" -%> +

    \ No newline at end of file diff --git a/app/views/roles/edit.rhtml b/app/views/roles/edit.rhtml new file mode 100644 index 0000000..11a5bd7 --- /dev/null +++ b/app/views/roles/edit.rhtml @@ -0,0 +1,13 @@ +<%= error_messages_for :role %> + +<% form_for(:role, :url => role_path(@role.code), :html => { :id => 'role_form', :method => :put }) do |f| %> + <%= render :partial => 'role_form' %> +

    + <%= submit_tag "Update" %> +

    +<% end %> + +<% content_for :sidebar do -%> + <%= new_role_link -%>
    + <%= show_role_link(@role) -%>
    +<% end -%> \ No newline at end of file diff --git a/app/views/roles/index.rhtml b/app/views/roles/index.rhtml new file mode 100644 index 0000000..3ba92af --- /dev/null +++ b/app/views/roles/index.rhtml @@ -0,0 +1,13 @@ + + +<% content_for :sidebar do -%> + <%= new_role_link -%>
    +<% end -%> \ No newline at end of file diff --git a/app/views/roles/new.rhtml b/app/views/roles/new.rhtml new file mode 100644 index 0000000..3960df5 --- /dev/null +++ b/app/views/roles/new.rhtml @@ -0,0 +1,8 @@ +<%= error_messages_for :role %> + +<% form_for(:role, :url => roles_path, :html => { :id => 'role_form' }) do |f| %> + <%= render :partial => 'role_form' %> +

    + <%= submit_tag "Create" %> +

    +<% end %> diff --git a/app/views/roles/show.rhtml b/app/views/roles/show.rhtml new file mode 100644 index 0000000..30a2de3 --- /dev/null +++ b/app/views/roles/show.rhtml @@ -0,0 +1,15 @@ +

    <%= @role.name -%>

    + + + +<% content_for :sidebar do -%> + <%= new_role_link -%>
    + <%= edit_role_link(@role) -%>
    + <%= link_to 'Destroy Role', role_path(@role.code), :confirm => 'Are you sure?', :method => :delete -%>
    +<% end -%> \ No newline at end of file diff --git a/app/views/sessions/new.rhtml b/app/views/sessions/new.rhtml new file mode 100644 index 0000000..73c4243 --- /dev/null +++ b/app/views/sessions/new.rhtml @@ -0,0 +1,6 @@ +<% form_for(:session, @people, :url => sessions_path, :html => { :method => :post }) do |f| -%> +

    + <%= text_field_tag 'login' -%> +

    + <%= submit_tag 'Login' %> +<% end -%> \ No newline at end of file diff --git a/app/views/shared/unauthorized.rhtml b/app/views/shared/unauthorized.rhtml new file mode 100644 index 0000000..bde3874 --- /dev/null +++ b/app/views/shared/unauthorized.rhtml @@ -0,0 +1 @@ +
    <%= image_tag 'process-stop.png' -%> Sorry, you do not have permission to perform this action.
    \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 5c7745f..79db361 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,6 @@ ActionController::Routing::Routes.draw do |map| - map.resources :roles - - map.resources :beers, :breweries, :pages, :discussions, :peoples + map.resources :beers, :breweries, :pages, :discussions, :peoples, :roles, + :sessions map.connect ':controller/:action/:id.:format' map.connect ':controller/:action/:id' diff --git a/db/migrate/007_create_peoples.rb b/db/migrate/007_create_peoples.rb index 5593e85..a5fda73 100644 --- a/db/migrate/007_create_peoples.rb +++ b/db/migrate/007_create_peoples.rb @@ -3,6 +3,7 @@ class CreatePeoples < ActiveRecord::Migration create_table :peoples do |t| t.column :title, :string end + People.create :title => 'Guest', :page => Page.new end def self.down diff --git a/db/migrate/008_create_roles.rb b/db/migrate/008_create_roles.rb new file mode 100644 index 0000000..6b69578 --- /dev/null +++ b/db/migrate/008_create_roles.rb @@ -0,0 +1,24 @@ +class CreateRoles < ActiveRecord::Migration + def self.up + create_table :roles do |t| + t.column :name, :string + t.column :code, :string + t.column :parent_id, :integer + end + add_index :roles, :parent_id + add_index :roles, :code + add_column :peoples, :role_id, :integer + add_index :peoples, :role_id + br = Role.create :code => 'base', :name => 'Base Role' + ar = Role.create :code => 'admin', :name => 'Administrative Role', + :parent_id => br.id + g = People.guest_user + g.role = br + g.save + end + + def self.down + drop_table :roles + remove_column :peoples, :role_id + end +end diff --git a/db/migrate/009_create_permissions.rb b/db/migrate/009_create_permissions.rb new file mode 100644 index 0000000..4eac761 --- /dev/null +++ b/db/migrate/009_create_permissions.rb @@ -0,0 +1,20 @@ +class CreatePermissions < ActiveRecord::Migration + def self.up + create_table :permissions do |t| + t.column :controller, :string + t.column :action, :string + t.column :http_method, :string + end + create_table :permissions_roles, :id => false do |t| + t.column :permission_id, :integer + t.column :role_id, :integer + end + add_index :permissions_roles, :permission_id + add_index :permissions_roles, :role_id + end + + def self.down + drop_table :permissions + drop_table :permissions_roles + end +end diff --git a/generate_permissions b/generate_permissions new file mode 100644 index 0000000..9471bf2 --- /dev/null +++ b/generate_permissions @@ -0,0 +1,43 @@ +Permission.destroy_all + +base_actions = ApplicationController.action_methods +# i should probably figure out all of the children of ApplicationController +# rather than defining them here. +controllers = [ AutocompleteController, SessionsController, PagesController, + PeoplesController, BeersController, BreweriesController, RolesController, + DiscussionsController ] +controllers.each do |c| + actions = c.action_methods - base_actions + cname = c.controller_name + actions.each { |a| Permission.create(:controller => cname, :action => a) } +end + +r = Role.base_role +Permission.find(:all, + :conditions => [ 'controller = ?', 'autocomplete' ]).each do |p| + r.permissions << p +end + +Permission.find(:all, + :conditions => [ 'controller = ?', 'sessions' ]).each do |p| + r.permissions << p +end + +Permission.find(:all, + :conditions => [ 'controller = ?', 'pages' ]).each do |p| + next if [ 'new', 'create', 'edit', 'update', 'destroy' ].include?(p.action) + r.permissions << p +end + +r2 = Role.admin_role +Permission.find(:all).each do |p| + r2.permissions << p unless r.permissions.include?(p) +end + +p = People.new :title => 'penguincoder' +page = Page.new +p.page = page +p.role = r2 +p.save + +puts "All permissions created" diff --git a/lib/tasks/extract_permissions.rake b/lib/tasks/extract_permissions.rake new file mode 100644 index 0000000..0216ab1 --- /dev/null +++ b/lib/tasks/extract_permissions.rake @@ -0,0 +1,13 @@ +namespace :barleysodas do + desc "Saves permission models to the test fixture file" + task :extract_permissions => :environment do + i = "000" + File.open("#{RAILS_ROOT}/test/fixtures/permissions.yml", 'w') do |file| + p = Permission.find(:all) + file.write p.inject({}) { |hash, record| + hash["permissions_#{i.succ!}"] = record.attributes.reject { |key, val| key == "id" } + hash + }.to_yaml + end + end +end diff --git a/lib/tasks/load_permissions.rake b/lib/tasks/load_permissions.rake new file mode 100644 index 0000000..04c9b55 --- /dev/null +++ b/lib/tasks/load_permissions.rake @@ -0,0 +1,8 @@ +namespace :barleysodas do + desc "Loads permission models from the test fixture file" + task :load_permissions => :environment do + YAML::load_file("#{RAILS_ROOT}/test/fixtures/permissions.yml").each do |k,p| + Permission.create(p) + end + end +end diff --git a/public/images/process-stop.png b/public/images/process-stop.png new file mode 100644 index 0000000..ab6808f Binary files /dev/null and b/public/images/process-stop.png differ diff --git a/public/javascripts/application.js b/public/javascripts/application.js index fe45776..867ece8 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -1,2 +1,15 @@ -// Place your application-specific JavaScript functions and classes here -// This file is automatically included by javascript_include_tag :defaults +function set_all_checkboxes(form_name, field_name, check_value) +{ + if(!document.forms[form_name]) + return; + var objCheckBoxes = document.forms[form_name].elements[field_name]; + if(!objCheckBoxes) + return; + var countCheckBoxes = objCheckBoxes.length; + if(!countCheckBoxes) + objCheckBoxes.checked = check_value; + else + // set the check value for all check boxes + for(var i = 0; i < countCheckBoxes; i++) + objCheckBoxes[i].checked = check_value; +} diff --git a/test/fixtures/peoples.yml b/test/fixtures/peoples.yml index 7b2da6c..3c74a4d 100644 --- a/test/fixtures/peoples.yml +++ b/test/fixtures/peoples.yml @@ -2,3 +2,4 @@ one: id: 1 title: penguin coder + role_id: 1 diff --git a/test/fixtures/permissions.yml b/test/fixtures/permissions.yml new file mode 100644 index 0000000..5740e36 --- /dev/null +++ b/test/fixtures/permissions.yml @@ -0,0 +1,145 @@ +--- +permissions_030: + http_method: + action: destroy + controller: peoples +permissions_019: + http_method: + action: new + controller: pages +permissions_008: + http_method: + action: update + controller: breweries +permissions_031: + http_method: + action: new + controller: roles +permissions_020: + http_method: + action: update + controller: pages +permissions_009: + http_method: + action: create + controller: breweries +permissions_032: + http_method: + action: update + controller: roles +permissions_021: + http_method: + action: create + controller: pages +permissions_010: + http_method: + action: edit + controller: breweries +permissions_033: + http_method: + action: create + controller: roles +permissions_022: + http_method: + action: edit + controller: pages +permissions_011: + http_method: + action: index + controller: breweries +permissions_034: + http_method: + action: edit + controller: roles +permissions_023: + http_method: + action: index + controller: pages +permissions_012: + http_method: + action: destroy + controller: breweries +permissions_001: + http_method: + action: new + controller: beers +permissions_035: + http_method: + action: index + controller: roles +permissions_024: + http_method: + action: destroy + controller: pages +permissions_013: + http_method: + action: new + controller: discussions +permissions_002: + http_method: + action: update + controller: beers +permissions_036: + http_method: + action: destroy + controller: roles +permissions_025: + http_method: + action: new + controller: peoples +permissions_014: + http_method: + action: update + controller: discussions +permissions_003: + http_method: + action: create + controller: beers +permissions_026: + http_method: + action: update + controller: peoples +permissions_015: + http_method: + action: create + controller: discussions +permissions_004: + http_method: + action: edit + controller: beers +permissions_027: + http_method: + action: create + controller: peoples +permissions_016: + http_method: + action: edit + controller: discussions +permissions_005: + http_method: + action: index + controller: beers +permissions_028: + http_method: + action: edit + controller: peoples +permissions_017: + http_method: + action: index + controller: discussions +permissions_006: + http_method: + action: destroy + controller: beers +permissions_029: + http_method: + action: index + controller: peoples +permissions_018: + http_method: + action: destroy + controller: discussions +permissions_007: + http_method: + action: new + controller: breweries diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml new file mode 100644 index 0000000..35ddab7 --- /dev/null +++ b/test/fixtures/roles.yml @@ -0,0 +1,9 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 + code: base + name: Base Role +two: + id: 2 + code: admin + name: Administrative Role diff --git a/test/functional/peoples_controller_test.rb b/test/functional/peoples_controller_test.rb index 0cf8f0f..20b41ba 100644 --- a/test/functional/peoples_controller_test.rb +++ b/test/functional/peoples_controller_test.rb @@ -26,7 +26,7 @@ class PeoplesControllerTest < Test::Unit::TestCase def test_should_create_people old_count = People.count - post :create, :people => { :title => '1' } + post :create, :people => { :title => 'mypeople', :role_id => 1 } assert_equal old_count+1, People.count assert_redirected_to people_path(assigns(:people).page.title_for_url) diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb new file mode 100644 index 0000000..d6f680a --- /dev/null +++ b/test/functional/roles_controller_test.rb @@ -0,0 +1,57 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'roles_controller' + +# Re-raise errors caught by the controller. +class RolesController; def rescue_action(e) raise e end; end + +class RolesControllerTest < Test::Unit::TestCase + fixtures :roles + + def setup + @controller = RolesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index + assert_response :success + assert assigns(:roles) + end + + def test_should_get_new + get :new + assert_response :success + end + + def test_should_create_role + old_count = Role.count + post :create, :role => { :code => 'test', :name => 'test role' } + assert_equal old_count+1, Role.count + + assert_redirected_to role_path(assigns(:role).code) + end + + def test_should_show_role + get :show, :id => 'base' + assert_response :success + end + + def test_should_get_edit + get :edit, :id => 'base' + assert_response :success + end + + def test_should_update_role + put :update, :id => 'base', :role => { :name => 'base role new!' } + assert_redirected_to role_path(assigns(:role).code) + end + + def test_should_destroy_role + old_count = Role.count + delete :destroy, :id => 'base' + assert_equal old_count-1, Role.count + + assert_redirected_to roles_path + end +end diff --git a/test/functional/sessions_controller_test.rb b/test/functional/sessions_controller_test.rb new file mode 100644 index 0000000..00743f8 --- /dev/null +++ b/test/functional/sessions_controller_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'sessions_controller' + +# Re-raise errors caught by the controller. +class SessionsController; def rescue_action(e) raise e end; end + +class SessionsControllerTest < Test::Unit::TestCase + def setup + @controller = SessionsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/permission_test.rb b/test/unit/permission_test.rb new file mode 100644 index 0000000..cab1ace --- /dev/null +++ b/test/unit/permission_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PermissionTest < Test::Unit::TestCase + fixtures :permissions + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb new file mode 100644 index 0000000..05d6652 --- /dev/null +++ b/test/unit/role_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class RoleTest < Test::Unit::TestCase + fixtures :roles + + # Replace this with your real tests. + def test_truth + assert true + end +end