diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index f6d7af0b5..9a51833a6 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -35,6 +35,38 @@ class GroupsController < ApplicationController
     }
   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
 
     def find_group(param_name)
@@ -44,4 +76,8 @@ class GroupsController < ApplicationController
       group
     end
 
+    def the_group
+      @the_group ||= find_group(:group_id)
+    end
+
 end
diff --git a/app/models/group.rb b/app/models/group.rb
index 1d9ba3721..d9985367d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -7,6 +7,9 @@ class Group < ActiveRecord::Base
   has_many :categories, through: :category_groups
   has_many :users, through: :group_users
 
+  has_many :group_managers, dependent: :destroy
+  has_many :managers, through: :group_managers
+
   after_save :destroy_deletions
 
   validate :name_format_validator
@@ -277,6 +280,10 @@ class Group < ActiveRecord::Base
     self.group_users.where(user: user).each(&:destroy)
   end
 
+  def appoint_manager(user)
+    managers << user
+  end
+
   protected
 
     def name_format_validator
diff --git a/app/models/group_manager.rb b/app/models/group_manager.rb
new file mode 100644
index 000000000..a30d523f5
--- /dev/null
+++ b/app/models/group_manager.rb
@@ -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
+#
diff --git a/app/models/user.rb b/app/models/user.rb
index 0f6c35b1f..f661b825f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -52,6 +52,9 @@ class User < ActiveRecord::Base
   has_many :groups, through: :group_users
   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 :api_key, dependent: :destroy
 
diff --git a/config/routes.rb b/config/routes.rb
index d7453e878..70d26b189 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -273,6 +273,9 @@ Discourse::Application.routes.draw do
     get 'members'
     get 'posts'
     get 'counts'
+
+    put "members" => "groups#add_members"
+    delete "members/:username" => "groups#remove_member"
   end
 
   # In case people try the wrong URL
diff --git a/db/migrate/20150108221703_group_managers.rb b/db/migrate/20150108221703_group_managers.rb
new file mode 100644
index 000000000..9c6e356ff
--- /dev/null
+++ b/db/migrate/20150108221703_group_managers.rb
@@ -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
diff --git a/lib/guardian.rb b/lib/guardian.rb
index f2e452335..d644e5485 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -4,6 +4,7 @@ require_dependency 'guardian/post_guardian'
 require_dependency 'guardian/topic_guardian'
 require_dependency 'guardian/user_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
 class Guardian
@@ -13,6 +14,7 @@ class Guardian
   include TopicGuardian
   include UserGuardian
   include PostRevisionGuardian
+  include GroupGuardian
 
   class AnonymousUser
     def blank?; true; end
diff --git a/lib/guardian/group_guardian.rb b/lib/guardian/group_guardian.rb
new file mode 100644
index 000000000..308d95b2b
--- /dev/null
+++ b/lib/guardian/group_guardian.rb
@@ -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
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index aca580b9c..3973449cb 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -84,4 +84,78 @@ describe GroupsController do
       expect(members.map{ |m| m['username'] }).to eq(usernames[3..4])
     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
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 1337be073..52a9a74f8 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -204,4 +204,27 @@ describe Group do
     expect(user.groups.map(&:name).sort).to eq ["trust_level_0"]
   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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 74afa1583..96aa8286c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1029,6 +1029,22 @@ describe User do
     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
     let!(:user) { Fabricate(:user) }