Merge pull request #3084 from jmay/group-managers

table & model changes for group managers with permission to edit members
This commit is contained in:
Régis Hanol 2015-01-16 12:02:38 +01:00
commit 7a86abd105
11 changed files with 205 additions and 0 deletions

View File

@ -35,6 +35,38 @@ class GroupsController < ApplicationController
} }
end end
def add_members
guardian.ensure_can_edit!(the_group)
added_users = []
usernames = params.require(:usernames)
usernames.split(",").each do |username|
if user = User.find_by_username(username)
unless the_group.users.include?(user)
the_group.add(user)
added_users << user
end
end
end
# always succeeds, even if bogus usernames were provided
render_serialized(added_users, GroupUserSerializer)
end
def remove_member
guardian.ensure_can_edit!(the_group)
removed_users = []
username = params.require(:username)
if user = User.find_by_username(username)
the_group.remove(user)
removed_users << user
end
# always succeeds, even if user was not a member
render_serialized(removed_users, GroupUserSerializer)
end
private private
def find_group(param_name) def find_group(param_name)
@ -44,4 +76,8 @@ class GroupsController < ApplicationController
group group
end end
def the_group
@the_group ||= find_group(:group_id)
end
end end

View File

@ -7,6 +7,9 @@ class Group < ActiveRecord::Base
has_many :categories, through: :category_groups has_many :categories, through: :category_groups
has_many :users, through: :group_users has_many :users, through: :group_users
has_many :group_managers, dependent: :destroy
has_many :managers, through: :group_managers
after_save :destroy_deletions after_save :destroy_deletions
validate :name_format_validator validate :name_format_validator
@ -277,6 +280,10 @@ class Group < ActiveRecord::Base
self.group_users.where(user: user).each(&:destroy) self.group_users.where(user: user).each(&:destroy)
end end
def appoint_manager(user)
managers << user
end
protected protected
def name_format_validator def name_format_validator

View File

@ -0,0 +1,19 @@
class GroupManager < ActiveRecord::Base
belongs_to :group
belongs_to :manager, class_name: "User", foreign_key: :user_id
end
# == Schema Information
#
# Table name: group_managers
#
# id :integer not null, primary key
# group_id :integer not null
# user_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_group_managers_on_group_id_and_user_id (group_id,user_id) UNIQUE
#

View File

@ -52,6 +52,9 @@ class User < ActiveRecord::Base
has_many :groups, through: :group_users has_many :groups, through: :group_users
has_many :secure_categories, through: :groups, source: :categories has_many :secure_categories, through: :groups, source: :categories
has_many :group_managers, dependent: :destroy
has_many :managed_groups, through: :group_managers, source: :group
has_one :user_search_data, dependent: :destroy has_one :user_search_data, dependent: :destroy
has_one :api_key, dependent: :destroy has_one :api_key, dependent: :destroy

View File

@ -273,6 +273,9 @@ Discourse::Application.routes.draw do
get 'members' get 'members'
get 'posts' get 'posts'
get 'counts' get 'counts'
put "members" => "groups#add_members"
delete "members/:username" => "groups#remove_member"
end end
# In case people try the wrong URL # In case people try the wrong URL

View File

@ -0,0 +1,11 @@
class GroupManagers < ActiveRecord::Migration
def change
create_table :group_managers do |t|
t.integer :group_id, null: false
t.integer :user_id, null: false
t.timestamps
end
add_index :group_managers, [:group_id, :user_id], unique: true
end
end

View File

@ -4,6 +4,7 @@ require_dependency 'guardian/post_guardian'
require_dependency 'guardian/topic_guardian' require_dependency 'guardian/topic_guardian'
require_dependency 'guardian/user_guardian' require_dependency 'guardian/user_guardian'
require_dependency 'guardian/post_revision_guardian' require_dependency 'guardian/post_revision_guardian'
require_dependency 'guardian/group_guardian'
# The guardian is responsible for confirming access to various site resources and operations # The guardian is responsible for confirming access to various site resources and operations
class Guardian class Guardian
@ -13,6 +14,7 @@ class Guardian
include TopicGuardian include TopicGuardian
include UserGuardian include UserGuardian
include PostRevisionGuardian include PostRevisionGuardian
include GroupGuardian
class AnonymousUser class AnonymousUser
def blank?; true; end def blank?; true; end

View File

@ -0,0 +1,11 @@
#mixin for all guardian methods dealing with group permissions
module GroupGuardian
# Edit authority for groups means membership changes only.
# Automatic groups are not represented in the GROUP_USERS
# table and thus do not allow membership changes.
def can_edit_group?(group)
(group.managers.include?(user) || is_admin?) && !group.automatic
end
end

View File

@ -84,4 +84,78 @@ describe GroupsController do
expect(members.map{ |m| m['username'] }).to eq(usernames[3..4]) expect(members.map{ |m| m['username'] }).to eq(usernames[3..4])
end end
end end
describe "membership edit permission" do
it "refuses membership changes to unauthorized users" do
Guardian.any_instance.stubs(:can_edit?).with(group).returns(false)
xhr :put, :add_members, group_id: group.name, usernames: "bob"
response.should be_forbidden
xhr :delete, :remove_member, group_id: group.name, username: "bob"
response.should be_forbidden
end
it "cannot add members to automatic groups" do
Guardian.any_instance.stubs(:is_admin?).returns(true)
auto_group = Fabricate(:group, name: "auto_group", automatic: true)
xhr :put, :add_members, group_id: group.name, usernames: "bob"
response.should be_forbidden
end
end
describe "membership edits" do
before do
@user1 = Fabricate(:user)
group.add(@user1)
group.reload
Guardian.any_instance.stubs(:can_edit?).with(group).returns(true)
end
it "can make incremental adds" do
user2 = Fabricate(:user)
xhr :put, :add_members, group_id: group.name, usernames: user2.username
response.should be_success
group.reload
group.users.count.should eq(2)
end
it "succeeds silently when adding non-existent users" do
xhr :put, :add_members, group_id: group.name, usernames: "nosuchperson"
response.should be_success
group.reload
group.users.count.should eq(1)
end
it "succeeds silently when adding duplicate users" do
xhr :put, :add_members, group_id: group.name, usernames: @user1.username
response.should be_success
group.reload
group.users.should eq([@user1])
end
it "can make incremental deletes" do
xhr :delete, :remove_member, group_id: group.name, username: @user1.username
response.should be_success
group.reload
group.users.count.should eq(0)
end
it "succeeds silently when removing non-members" do
user2 = Fabricate(:user)
xhr :delete, :remove_member, group_id: group.name, username: user2.username
response.should be_success
group.reload
group.users.count.should eq(1)
end
end
end end

View File

@ -204,4 +204,27 @@ describe Group do
expect(user.groups.map(&:name).sort).to eq ["trust_level_0"] expect(user.groups.map(&:name).sort).to eq ["trust_level_0"]
end end
context "group management" do
let(:group) {Fabricate(:group)}
it "by default has no managers" do
group.managers.should be_empty
end
it "multiple managers can be appointed" do
2.times do |i|
u = Fabricate(:user)
group.appoint_manager(u)
end
expect(group.managers.count).to eq(2)
end
it "manager has authority to edit membership" do
u = Fabricate(:user)
expect(Guardian.new(u).can_edit?(group)).to be_falsy
group.appoint_manager(u)
expect(Guardian.new(u).can_edit?(group)).to be_truthy
end
end
end end

View File

@ -1029,6 +1029,22 @@ describe User do
end end
end end
context "group management" do
let!(:user) { Fabricate(:user) }
it "by default has no managed groups" do
expect(user.managed_groups).to be_empty
end
it "can manage multiple groups" do
3.times do |i|
g = Fabricate(:group, name: "group_#{i}")
g.appoint_manager(user)
end
expect(user.managed_groups.count).to eq(3)
end
end
describe "should_be_redirected_to_top" do describe "should_be_redirected_to_top" do
let!(:user) { Fabricate(:user) } let!(:user) { Fabricate(:user) }