Merge pull request #321 from warpc/270-user_control

[Refs #270] Admin user control (include ban, protect from mass assigment)
This commit is contained in:
Vladimir Sharshov 2012-03-22 08:41:07 -07:00
commit 09a3d46303
26 changed files with 280 additions and 156 deletions

View File

@ -622,6 +622,22 @@ div.rightlist textarea.resizable {
resize: both;
}
table.tablesorter.list-users th.th1 {
width: 150px;
}
table.tablesorter.list-users th.th2 {
width: 100px;
}
table.tablesorter.list-users th.th3 {
width: 200px;
}
table.tablesorter.list-users th.th4 {
width: 50px;
}
div.admin-role {
padding-right: 10px;
}

View File

@ -0,0 +1,70 @@
# -*- encoding : utf-8 -*-
class Admin::UsersController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource :except => :create
authorize_resource :only => :create
def index
@filter = params[:filter] || 'all'
end
def new
end
def create
@user = User.new params[:user]
@user.role = params[:role]
@user.uname = params[:uname]
if @user.save
flash[:notice] = t('flash.user.saved')
redirect_to users_path
else
flash[:error] = t('flash.user.save_error')
render :action => :new
end
end
def profile
end
def update
@user.role = params[:role]
if @user.update_without_password(params[:user])
if @user.avatar && params[:delete_avatar] == '1'
@user.avatar = nil
@user.save
end
flash[:notice] = t('flash.user.saved')
redirect_to users_path#edit_user_path(@user)
else
flash[:error] = t('flash.user.save_error')
flash[:warning] = @user.errors.full_messages.join('. ')
render(:action => :profile)
end
end
def destroy
@user.destroy
flash[:notice] = t("flash.user.destroyed")
redirect_to users_path
end
def list
colName = ['users.name', 'users.uname', 'users.email']
sort_col = params[:iSortCol_0] || 0
sort_dir = params[:sSortDir_0]=="asc" ? 'asc' : 'desc'
order = "#{colName[sort_col.to_i]} #{sort_dir}"
@users = @users.paginate(:page => (params[:iDisplayStart].to_i/params[:iDisplayLength].to_i).to_i + 1, :per_page => params[:iDisplayLength])
@total_users = @users.count
if !params[:sSearch].blank? && search = "%#{params[:sSearch]}%"
@users = @users.where('users.name ILIKE ? or users.uname ILIKE ? or users.email ILIKE ?', search, search, search)
end
@filter = params[:filter] || 'all'
@users = @users.send(@filter) if ['real', 'admin', 'banned'].include? @filter
@total_user = @users.count
@users = @users.order(order)
render :partial =>'users_ajax', :layout => false
end
end

View File

@ -12,7 +12,7 @@ class ApplicationController < ActionController::Base
helper_method :get_owner
rescue_from CanCan::AccessDenied do |exception|
redirect_to forbidden_url, :alert => t('flash.exception_message')#:alert => exception.message
redirect_to forbidden_url, :alert => t("flash.exception_message")
end
protected

View File

@ -2,57 +2,27 @@
class UsersController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource
load_and_authorize_resource :only => :show
before_filter :set_current_user, :only => [:profile, :update, :private]
autocomplete :user, :uname
def index
@user = User.scoped
if !params[:filter].blank? && !params[:filter][:email].blank?
@users = @users.where(:email => params[:filter][:email])
@email = params[:filter][:email]
end
@users = @users.paginate(:page => params[:user_page])
@action_url = users_path
end
def show
@groups = @user.groups.uniq
@platforms = @user.platforms.paginate(:page => params[:platform_page], :per_page => 10)
@projects = @user.projects.paginate(:page => params[:project_page], :per_page => 10)
end
def new
@user = User.new
end
def profile
@user ||= current_user
end
def create
@user = User.new params[:user]
if @user.save
flash[:notice] = t('flash.user.saved')
redirect_to users_path
else
flash[:error] = t('flash.user.save_error')
render :action => :new
end
end
def update
if params[:user][:role] && current_user.admin?
@user.role = params[:user][:role]
params[:user].delete(:role)
end
@user ||= current_user
if @user.update_without_password(params[:user])
if @user.avatar && params[:delete_avatar] == '1'
@user.avatar = nil
@user.save
end
flash[:notice] = t('flash.user.saved')
redirect_to @user == current_user ? edit_profile_path : edit_user_path(@user)
redirect_to edit_profile_path
else
flash[:error] = t('flash.user.save_error')
flash[:warning] = @user.errors.full_messages.join('. ')
@ -73,10 +43,10 @@ class UsersController < ApplicationController
end
end
def destroy
@user.destroy
flash[:notice] = t("flash.user.destroyed")
redirect_to users_path
protected
def set_current_user
@user = current_user
end
end

View File

@ -25,7 +25,7 @@ class Group < ActiveRecord::Base
attr_accessible :description
attr_readonly :own_projects_count
delegate :ssh_key, :email, :to => :owner
delegate :email, :to => :owner
after_create :add_owner_to_members

View File

@ -1,6 +1,6 @@
# -*- encoding : utf-8 -*-
class User < ActiveRecord::Base
ROLES = ['admin']
ROLES = ['', 'admin', 'banned']
LANGUAGES_FOR_SELECT = [['Russian', 'ru'], ['English', 'en']]
LANGUAGES = LANGUAGES_FOR_SELECT.map(&:last)
MAX_AVATAR_SIZE = 5.megabyte
@ -43,14 +43,16 @@ class User < ActiveRecord::Base
validates :role, :inclusion => {:in => ROLES}, :allow_blank => true
validates :language, :inclusion => {:in => LANGUAGES}, :allow_blank => true
attr_accessible :email, :password, :password_confirmation, :current_password, :remember_me, :login, :name, :ssh_key, :uname, :language,
attr_accessible :email, :password, :password_confirmation, :current_password, :remember_me, :login, :name, :language,
:site, :company, :professional_experience, :location, :avatar
attr_readonly :uname, :own_projects_count
attr_readonly :uname
attr_accessor :login
scope :search_order, order("CHAR_LENGTH(uname) ASC")
scope :search, lambda {|q| where("uname ILIKE ?", "%#{q}%")}
scope :banned, where(:role => 'banned')
scope :admin, where(:role => 'admin')
scope :real, where(:role => ['', nil])
after_create lambda { self.create_notifier }
@ -66,6 +68,10 @@ class User < ActiveRecord::Base
new_record?
end
def access_locked?
role == 'banned'
end
def fullname
return "#{uname} (#{name})"
end

View File

@ -0,0 +1,9 @@
- content_for :sidebar do
.bordered.nopadding
%h3= t("layout.users.filter_header")
%table
- t('layout.users.users_filter').each_key do |base|
%tr
%td.width18=radio_button_tag :myradio, base, @filter.to_sym == base, {:id => 'users_filter', :class => 'niceRadio', :name => 'filter'}
%td.width135=t("layout.users.users_filter.#{base}")
.both

View File

@ -0,0 +1,19 @@
{
"sEcho": <%=h params[:sEcho].to_i || -1 %>,
"iTotalRecords": <%= @total_users %>,
"iTotalDisplayRecords": <%= @total_user %>,
"aaData": [
<% @users.each do |user| %>
[
"<%= user.name %>",
"<%= user.uname %>",
"<%= user.email %>",
"<span style='<%=user.access_locked? ? 'background: #FEDEDE' : ''%>'><%= user.role %></span>",
"<%= j raw [(link_to t('layout.show'), user_path(user) if can? :read, user),
(link_to t('layout.edit'), edit_user_path(user) if can? :edit, user),
(link_to t('layout.delete'), delete_user_path(user), :method => :delete, :confirm => t('layout.users.confirm_delete') if can? :destroy, user)
].compact.join('&nbsp;|&nbsp;') %>"
]<%= user == @users.last ? '' : ',' %>
<% end %>
]
}

View File

@ -0,0 +1,34 @@
%h3.fix= t("layout.users.list_header")
- if can? :create, User.new
= link_to t("layout.users.new"), new_user_path, :class => 'button'
- columns = [{:type => 'html'}, {:type => 'html'}, {:type => 'html'}, {:type => 'html', :sortable => false, :searchable => false}, {:type => nil, :sortable => false, :searchable => false, :class => 'buttons'}]
= raw datatable(columns, {:sort_by => "[0, 'asc']", :processing => t("layout.processing"),
:pagination_labels => {:previous => t("datatables.previous_label"), :next => t("datatables.next_label")},
:empty_label => t("datatables.empty_label"),
:info_label => t("datatables.info_label"),
:info_empty_label => t("datatables.info_empty_label"),
:filtered_label => t("datatables.filtered_label"),
:table_dom_id => 'datatable',
:auto_width => 'false',
:ajax_source => "#{url_for :controller => 'admin/users', :action => :list}",
:additional_data => {:filter => "' + $('#users_filter[type=\"radio\"]:checked').val() + '"} })
%table#datatable.tablesorter.list-users{:cellspacing => 0, :cellpadding => 0}
%thead
%tr
%th.th1= t("activerecord.attributes.user.name")
%th.th2= t("activerecord.attributes.user.uname")
%th.th3= t("activerecord.attributes.user.email")
%th.th4= t("activerecord.attributes.user.role")
%th.last &nbsp;
%tbody
%br
= render :partial => 'admin/users/sidebar'
= render 'admin/submenu'
:javascript
$('#users_filter[type="radio"]').live('change', function(){
$('#datatable').dataTable().fnDraw();
});

View File

@ -0,0 +1,10 @@
.block
.content
%h2.title= t("layout.users.new_header")
.inner
= form_for @user, :url => users_path, :html => { :class => :form } do |f|
= render :partial => "users/form", :locals => {:f => f}
- content_for :sidebar do
.bordered.nopadding
= render 'admin/submenu'

View File

@ -0,0 +1,16 @@
%h3.fix.bpadding10= @user.uname
= form_for @user, :url => update_user_path(@user), :html => { :class => :form } do |f|
= render :partial => "users/form", :locals => {:f => f}
.notify
%p= t('layout.users.public_data_edit_warning')
.notify
%p= t('layout.users.avatar_notice')
:javascript
$('article .right').addClass('middlepadding');
= render 'admin/submenu'
- content_for :sidebar do
.bordered.nopadding

View File

@ -7,15 +7,17 @@
.logo
/ Page
%article
- is_error = (flash.try(:first).try(:first) == :alert && flash.try(:first).try(:last) == t('devise.failure.invalid')) # Trash
- if flash.try(:first).try(:first) == :alert && [t('devise.failure.invalid'), t('devise.failure.locked')].include?(flash.try(:first).try(:last))
- error = flash.first.last
- error ||= false
- login = t('devise.sessions.login'); password = t('devise.sessions.password')
=hidden_field_tag :login_default, login
=hidden_field_tag :password_default, password
= form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { :class => "form login" }) do |f|
%h1= title t("layout.sessions.sign_in_header")
= f.text_field :login, :class => "registartion-input #{is_error ? "registartion-input-error" : ''}", :value => login
= f.text_field :login, :class => "registartion-input #{error ? "registartion-input-error" : ''}", :value => login
%br/
= f.password_field :password, :class => "registartion-input #{is_error ? "registartion-input-error" : ''}", :value => password
= f.password_field :password, :class => "registartion-input #{error ? "registartion-input-error" : ''}", :value => password
%br/
.registration
.remember
@ -30,4 +32,4 @@
.forgot
.password
%p= link_to t("layout.devise.shared_links.forgot_password"), new_password_path(resource_name)
=showDeviseHintError(:login_error, is_error ? t('devise.failure.invalid') : false)
=showDeviseHintError(:login_error, error)

View File

@ -18,4 +18,4 @@
.actions-bar.wat-cf
.actions= will_paginate
= render 'admins/submenu'
= render 'admin/submenu'

View File

@ -1,6 +1,6 @@
%ul
- (collection = t which_menu).each do |base, title|
- if can? :index, base.to_s.classify.constantize
%li= link_to title, send(:"#{base}_path"), :class => [].tap{|c| c << 'active' if params[:controller] == base.to_s}.join(' ')
%li= link_to title, send(:"#{base}_path"), :class => controller_name == base.to_s ? 'active' : ''
- if current_user.admin? && which_menu == 'top_menu'
%li= link_to 'Admin', users_path, :class => t('admins_menu').has_key?(params[:controller].to_sym) ? 'active' : ''
%li= link_to 'Admin', users_path, :class => t('admins_menu').has_key?(controller_name.to_sym) ? 'active' : ''

View File

@ -1,3 +1,23 @@
- if current_user.admin?
.leftlist
= f.label :role, t("activerecord.attributes.user.role"), :class => :label
.rightlist
= f.select :role, User::ROLES, {}, {:name => 'role'}
- if @user.new_record?
.leftlist
= f.label :uname, t("activerecord.attributes.user.uname")
.rightlist
= f.text_field :uname, :name => 'uname'
.leftlist
= f.label :password, t("activerecord.attributes.user.password")
.rightlist
= f.password_field :password
.both
.leftlist
= f.label :password_confirmation, t("activerecord.attributes.user.password_confirm")
.rightlist
= f.password_field :password_confirmation
.both
.leftlist
= f.label :name, t("activerecord.attributes.user.name")
.rightlist

View File

@ -5,7 +5,7 @@
.admin-preferences
%ul
- if can? :edit, @user
%li{:class => (act == :edit && :users == contr) ? 'active' : ''}
%li{:class => (act == :profile && :users == contr) ? 'active' : ''}
= link_to t("layout.users.profile"), @user == current_user ? edit_profile_path : edit_user_path(@user)
- if can? :private, @user
%li{:class => (act == :private && contr == :users) ? 'active' : ''}

View File

@ -1,39 +0,0 @@
.block
.secondary-navigation
%ul.wat-cf
%li.first.active= link_to t("layout.users.list"), users_path
%li= link_to t("layout.users.new"), new_user_path
%li= link_to t("layout.users.register_requests"), register_requests_path if can? :read, RegisterRequest
.content
%h2.title
= t("layout.users.list_header")
.inner
= render :partial => "users/filter", :action_url => @action_url
.inner
%table.table
%tr
%th.first ID
%th= t("activerecord.attributes.user.name")
%th= t("activerecord.attributes.user.uname")
%th= t("activerecord.attributes.user.email")
%th= t("activerecord.attributes.user.role")
%th.last &nbsp;
- @users.each do |user|
%tr{:class => cycle("odd", "even")}
%td= user.id
%td= link_to user.name, user_path(user)
%td= link_to user.uname, user_path(user)
%td= user.email
%td= user.role
%td.last
- if can? :read, user
= link_to t("layout.show"), user_path(user)
\|
- if can? :edit, user
#{link_to t("layout.edit"), edit_user_path(user)}
\|
- if can? :destroy, user
#{link_to t("layout.delete"), user_path(user), :method => :delete, :confirm => t("layout.users.confirm_delete")}
.actions-bar.wat-cf
.actions= will_paginate @users, :param_name => :user_page
= render 'admins/submenu'

View File

@ -1,12 +0,0 @@
.block
.secondary-navigation
%ul.wat-cf
%li.first= link_to t("layout.users.list"), users_path
%li.active= link_to t("layout.users.new"), new_user_path
.content
%h2.title= t("layout.users.new_header")
.inner
= form_for @user, :url => users_path, :html => { :class => :form } do |f|
= render :partial => "form", :locals => {:f => f}
- content_for :sidebar, render('sidebar')

View File

@ -1,12 +1,7 @@
%h3.fix.bpadding10= @user.uname
= form_for @user, :url => user_path(@user), :html => { :class => :form } do |f|
- if current_user.admin?
.leftlist
= f.label :role, t("activerecord.attributes.user.role"), :class => :label
.rightlist
= f.select :role, User::ROLES, :include_blank => true
= render :partial => "form", :locals => {:f => f}
= render :partial => "users/form", :locals => {:f => f}
.notify
%p= t('layout.users.public_data_edit_warning')

View File

@ -1,7 +1,7 @@
.left
= image_tag avatar_url(@user, :big)
%br
= link_to t("layout.users.settings"), edit_user_path(@user), :class => 'button width81' if can? :edit, @user
= link_to t("layout.users.settings"), current_user == @user ? edit_profile_path : edit_user_path(@user), :class => 'button width81' if can? :edit, @user
.left
%h3= @user.uname
= @user.name

View File

@ -25,6 +25,11 @@ en:
avatar_notice: Without uploaded avatar will be used avatar from gravar web service.
delete_avatar: Delete avatar
avatar_with_size: Avatar (less than %{max})
users_filter:
all: All
admin: Admins
real: Real
banned: Banned
activerecord:
attributes:

View File

@ -22,9 +22,14 @@ ru:
delete_header: Удалить аккаунт
delete_warning: Внимание! Удаленный аккаунт восстановлению не подлежит.
private_settings_header: Изменение пароля
avatar_notice: При отсутствии загруженной аватарки будет использована Ваша аватарка на сервисе gravatar.
avatar_notice: При отсутствии загруженного аватара будет использован Ваш аватар на сервисе gravatar.
delete_avatar: Удалить аватар
avatar_with_size: Аватар (менее %{max})
users_filter:
all: Все
admin: Админы
real: Обычные
banned: Забаненные
activerecord:
attributes:

View File

@ -5,14 +5,19 @@ Rosa::Application.routes.draw do
devise_scope :user do
get '/users/auth/:provider' => 'users/omniauth_callbacks#passthru'
get '/user' => 'users#profile', :as => 'edit_profile'
put '/user' => 'users#update', :as => 'update_profile'
get '/users/:id/edit' => 'users#profile', :as => 'edit_user'
put '/users/:id/edit' => 'users#update', :as => 'update_user'
get '/user' => 'users#profile', :as => :edit_profile
put '/user' => 'users#update', :as => :update_profile
get '/users' => 'admin/users#index', :as => :users
get '/users/new' => 'admin/users#new', :as => :new_user
get '/users/list' => 'admin/users#list', :as => :users_list
post '/users' => 'admin/users#create', :as => :create_user
get '/users/:id/edit' => 'admin/users#profile', :as => :edit_user
put '/users/:id/edit' => 'admin/users#update', :as => :update_user
delete '/users/:id/delete' => 'admin/users#destroy', :as => :delete_user
end
devise_for :users, :controllers => {:omniauth_callbacks => 'users/omniauth_callbacks'}
resources :users do
resources :users, :only => [:show, :profile, :update] do
resources :groups, :only => [:new, :create, :index]
collection do
resources :register_requests, :only => [:index, :new, :create, :show_message, :approve, :reject] do
@ -27,6 +32,7 @@ Rosa::Application.routes.draw do
namespace :settings do
resource :notifier, :only => [:show, :update]
end
resources :platforms, :only => [:new, :create]
end
match 'users/:id/settings/private' => 'users#private', :as => :user_private_settings, :via => :get
match 'users/:id/settings/private' => 'users#private', :as => :user_private_settings, :via => :put
@ -182,14 +188,14 @@ Rosa::Application.routes.draw do
delete :remove
end
end
end
resources :users, :groups do
resources :platforms, :only => [:new, :create]
# resources :repositories, :only => [:new, :create]
end
# resources :users, :groups do
# resources :platforms, :only => [:new, :create]
# resources :repositories, :only => [:new, :create]
# end
resources :activity_feeds, :only => [:index]
resources :search, :only => [:index]

View File

@ -12,7 +12,7 @@ module Grack
return render_not_found if project.blank?
return ::Rack::Auth::Basic.new(@app) do |u, p|
user = User.find_for_database_authentication(:login => u) and user.valid_password?(p) and
user = User.find_for_database_authentication(:login => u) and !user.access_locked? and user.valid_password?(p) and
ability = ::Ability.new(user) and ability.can?(action, project) # project.members.include?(user)
end.call(env) unless project.public? and read? # need auth
end

View File

@ -1,52 +1,30 @@
# -*- encoding : utf-8 -*-
require 'spec_helper'
shared_examples_for 'user with users list viewer rights' do
it 'should be able to perform index action' do
get :index
response.should render_template(:index)
end
it 'should assigns 5 users without filter params' do
get :index
assigns[:users].count.should == 5
end
it 'should find one user' do
get :index, :filter => {:email => "user1@nonexistanceserver.com"}
assigns[:users].size == 1
end
it 'should find user with searchable email' do
get :index, :filter => {:email => "user1@nonexistanceserver.com"}
assigns[:users].first.email.should == "user1@nonexistanceserver.com"
end
end
describe UsersController do
before(:each) do
stub_rsync_methods
@simple_user = Factory(:user)
@other_user = Factory(:user)
@admin = Factory(:admin)
%w[user1 user2 user3].each do |uname|
Factory(:user, :uname => uname, :email => "#{ uname }@nonexistanceserver.com")
end
end
context 'for global admin' do
before(:each) do
set_session_for(@admin)
end
it_should_behave_like 'user with users list viewer rights'
@update_params = {:email => 'new_email@test.com'}
end
context 'for guest' do
it 'should not be able to perform index action' do
get :index
it 'should not be able to view profile' do
get :profile
response.should redirect_to(new_user_session_path)
end
it 'should not be able to update other profile' do
get :update, {:id => @other_user.id}.merge(@update_params)
response.should redirect_to(new_user_session_path)
@other_user.reload.email.should_not == @update_params[:email]
end
end
context 'for simple user' do
@ -54,9 +32,23 @@ describe UsersController do
set_session_for(@simple_user)
end
it 'should not be able to perform index action' do
get :index
response.should redirect_to(forbidden_path)
it 'should be able to view profile' do
get :profile
response.code.should eq('200')
end
context 'with mass assignment' do
it 'should not be able to update uname' do
@simple_user.should_not allow_mass_assignment_of :uname
end
it 'should not be able to update role' do
@simple_user.should_not allow_mass_assignment_of :role
end
it 'should not be able to update other user' do
@simple_user.should_not allow_mass_assignment_of :id
end
end
end
end