From fc863cddef96c536f21f9d6fc32f16a736714fef Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 23 Dec 2007 00:32:08 +0000 Subject: [PATCH] roles and the ability to log in with a user name git-svn-id: http://svn.barleysodas.com/barleysodas/trunk@60 0f7b21a7-9e3a-4941-bbeb-ce5c7c368fa7 --- app/controllers/application.rb | 71 ++++++++++ app/controllers/peoples_controller.rb | 9 ++ app/controllers/sessions_controller.rb | 41 ++++++ app/helpers/roles_helper.rb | 15 ++ app/helpers/sessions_helper.rb | 2 + app/models/people.rb | 11 +- app/models/permission.rb | 18 +++ app/models/role.rb | 39 ++++++ app/views/layouts/application.rhtml | 8 +- app/views/roles/_permission.rhtml | 1 + app/views/roles/_role_form.rhtml | 14 ++ app/views/roles/edit.rhtml | 13 ++ app/views/roles/index.rhtml | 13 ++ app/views/roles/new.rhtml | 8 ++ app/views/roles/show.rhtml | 15 ++ app/views/sessions/new.rhtml | 6 + app/views/shared/unauthorized.rhtml | 1 + config/routes.rb | 5 +- db/migrate/007_create_peoples.rb | 1 + db/migrate/008_create_roles.rb | 24 ++++ db/migrate/009_create_permissions.rb | 20 +++ generate_permissions | 43 ++++++ lib/tasks/extract_permissions.rake | 13 ++ lib/tasks/load_permissions.rake | 8 ++ public/images/process-stop.png | Bin 0 -> 820 bytes public/javascripts/application.js | 17 ++- test/fixtures/peoples.yml | 1 + test/fixtures/permissions.yml | 145 ++++++++++++++++++++ test/fixtures/roles.yml | 9 ++ test/functional/peoples_controller_test.rb | 2 +- test/functional/roles_controller_test.rb | 57 ++++++++ test/functional/sessions_controller_test.rb | 18 +++ test/unit/permission_test.rb | 10 ++ test/unit/role_test.rb | 10 ++ 34 files changed, 653 insertions(+), 15 deletions(-) create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/helpers/roles_helper.rb create mode 100644 app/helpers/sessions_helper.rb create mode 100644 app/models/permission.rb create mode 100644 app/models/role.rb create mode 100644 app/views/roles/_permission.rhtml create mode 100644 app/views/roles/_role_form.rhtml create mode 100644 app/views/roles/edit.rhtml create mode 100644 app/views/roles/index.rhtml create mode 100644 app/views/roles/new.rhtml create mode 100644 app/views/roles/show.rhtml create mode 100644 app/views/sessions/new.rhtml create mode 100644 app/views/shared/unauthorized.rhtml create mode 100644 db/migrate/008_create_roles.rb create mode 100644 db/migrate/009_create_permissions.rb create mode 100644 generate_permissions create mode 100644 lib/tasks/extract_permissions.rake create mode 100644 lib/tasks/load_permissions.rake create mode 100644 public/images/process-stop.png create mode 100644 test/fixtures/permissions.yml create mode 100644 test/fixtures/roles.yml create mode 100644 test/functional/roles_controller_test.rb create mode 100644 test/functional/sessions_controller_test.rb create mode 100644 test/unit/permission_test.rb create mode 100644 test/unit/role_test.rb 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 0000000000000000000000000000000000000000..ab6808fba55428710250c72b2569ca5288cd6df2 GIT binary patch literal 820 zcmV-41Izr0P)Mc{ zK~y-)ZIioCQ(+i~pL6L2ZH>mr;3bB}Kr47@YPbohHi(*-khrlBSo{xMIye|<92`s> zj0 zfhzpM@ALIWhKAhp&ub29c4n916Y9^A&F#&y1u(Xj-$dR3#d>J@)tT!?^ zxB;)Of@N(J5UR>rI!z!F(O2i@jx3c*X9mOJ=UQhb^X7PCW6$}40e3bY$EvSKkHsLF zL}s%av!6J@-n)0HBoY{3zj8br){Ru^=#NAqsP(AolnVvazCP5J7L<+- z`2HPr|9>&Rx53dc9cF)8t|?gl%I3HXy4zQg`&b zxfypTgs$s291eB|gVcpW$SN$#WyIXvUjZpZ4OUxP@ZP$G)9YnD9%prQ6sO07@7_H; z?d_Pk9LBeA$dwfUbSdSAnqeR;i@LTpTvd2+co^F>(SQT}{nVX0#rlH>plR6EqS^&1 zyDP=w;XqrPR!Sr&y?#yQ=T8=H-lUw#P+nXl_v#gEv$G^`-$v0ie11Rar6v2v@^W5@ zfm4IGCmR~NTZ2JYDjsL8Sgb8;>r)&K8k(9&7YgEII=x^+r#1#60#9C-Mn2yDwjT2}2(B$L*s yY<9+mt|%;SyT+{|0(y3`SA}UC&P8E)GxrC_O;YyPhW8Wz0000 { :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