Commit 201158f1dee15accf6abbd7ad5a50af023ba5d23
Exists in
master
and in
4 other branches
Merge pull request #4990 from karlhungus/feature_group_membership_api
Add group membership api
Showing
4 changed files
with
249 additions
and
15 deletions
Show diff stats
doc/api/groups.md
| @@ -55,3 +55,65 @@ POST /groups/:id/projects/:project_id | @@ -55,3 +55,65 @@ POST /groups/:id/projects/:project_id | ||
| 55 | Parameters: | 55 | Parameters: |
| 56 | + `id` (required) - The ID of a group | 56 | + `id` (required) - The ID of a group |
| 57 | + `project_id (required) - The ID of a project | 57 | + `project_id (required) - The ID of a project |
| 58 | + | ||
| 59 | + | ||
| 60 | +## Group members | ||
| 61 | + | ||
| 62 | +### List group members | ||
| 63 | + | ||
| 64 | +Get a list of group members viewable by the authenticated user. | ||
| 65 | + | ||
| 66 | +``` | ||
| 67 | +GET /groups/:id/members | ||
| 68 | +``` | ||
| 69 | + | ||
| 70 | +```json | ||
| 71 | +[ | ||
| 72 | + { | ||
| 73 | + id: 1, | ||
| 74 | + username: "raymond_smith", | ||
| 75 | + email: "ray@smith.org", | ||
| 76 | + name: "Raymond Smith", | ||
| 77 | + state: "active", | ||
| 78 | + created_at: "2012-10-22T14:13:35Z", | ||
| 79 | + access_level: 30 | ||
| 80 | + }, | ||
| 81 | + { | ||
| 82 | + id: 2, | ||
| 83 | + username: "john_doe", | ||
| 84 | + email: "joh@doe.org", | ||
| 85 | + name: "John Doe", | ||
| 86 | + state: "active", | ||
| 87 | + created_at: "2012-10-22T14:13:35Z", | ||
| 88 | + access_level: 30 | ||
| 89 | + } | ||
| 90 | +] | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +### Add group member | ||
| 94 | + | ||
| 95 | +Adds a user to the list of group members. | ||
| 96 | + | ||
| 97 | +``` | ||
| 98 | +POST /groups/:id/members | ||
| 99 | +``` | ||
| 100 | + | ||
| 101 | +Parameters: | ||
| 102 | + | ||
| 103 | ++ `id` (required) - The ID of a group | ||
| 104 | ++ `user_id` (required) - The ID of a user to add | ||
| 105 | ++ `access_level` (required) - Project access level | ||
| 106 | + | ||
| 107 | + | ||
| 108 | +### Remove user team member | ||
| 109 | + | ||
| 110 | +Removes user from user team. | ||
| 111 | + | ||
| 112 | +``` | ||
| 113 | +DELETE /groups/:id/members/:user_id | ||
| 114 | +``` | ||
| 115 | + | ||
| 116 | +Parameters: | ||
| 117 | + | ||
| 118 | ++ `id` (required) - The ID of a user group | ||
| 119 | ++ `user_id` (required) - The ID of a group member |
lib/api/entities.rb
| @@ -67,6 +67,12 @@ module API | @@ -67,6 +67,12 @@ module API | ||
| 67 | expose :projects, using: Entities::Project | 67 | expose :projects, using: Entities::Project |
| 68 | end | 68 | end |
| 69 | 69 | ||
| 70 | + class GroupMember < UserBasic | ||
| 71 | + expose :group_access, as: :access_level do |user, options| | ||
| 72 | + options[:group].users_groups.find_by_user_id(user.id).group_access | ||
| 73 | + end | ||
| 74 | + end | ||
| 75 | + | ||
| 70 | class RepoObject < Grape::Entity | 76 | class RepoObject < Grape::Entity |
| 71 | expose :name, :commit | 77 | expose :name, :commit |
| 72 | expose :protected do |repo, options| | 78 | expose :protected do |repo, options| |
lib/api/groups.rb
| @@ -4,6 +4,20 @@ module API | @@ -4,6 +4,20 @@ module API | ||
| 4 | before { authenticate! } | 4 | before { authenticate! } |
| 5 | 5 | ||
| 6 | resource :groups do | 6 | resource :groups do |
| 7 | + helpers do | ||
| 8 | + def find_group(id) | ||
| 9 | + group = Group.find(id) | ||
| 10 | + if current_user.admin or current_user.groups.include? group | ||
| 11 | + group | ||
| 12 | + else | ||
| 13 | + render_api_error!("403 Forbidden - #{current_user.username} lacks sufficient access to #{group.name}", 403) | ||
| 14 | + end | ||
| 15 | + end | ||
| 16 | + def validate_access_level?(level) | ||
| 17 | + Gitlab::Access.options_with_owner.values.include? level.to_i | ||
| 18 | + end | ||
| 19 | + end | ||
| 20 | + | ||
| 7 | # Get a groups list | 21 | # Get a groups list |
| 8 | # | 22 | # |
| 9 | # Example Request: | 23 | # Example Request: |
| @@ -46,12 +60,8 @@ module API | @@ -46,12 +60,8 @@ module API | ||
| 46 | # Example Request: | 60 | # Example Request: |
| 47 | # GET /groups/:id | 61 | # GET /groups/:id |
| 48 | get ":id" do | 62 | get ":id" do |
| 49 | - @group = Group.find(params[:id]) | ||
| 50 | - if current_user.admin or current_user.groups.include? @group | ||
| 51 | - present @group, with: Entities::GroupDetail | ||
| 52 | - else | ||
| 53 | - not_found! | ||
| 54 | - end | 63 | + group = find_group(params[:id]) |
| 64 | + present group, with: Entities::GroupDetail | ||
| 55 | end | 65 | end |
| 56 | 66 | ||
| 57 | # Transfer a project to the Group namespace | 67 | # Transfer a project to the Group namespace |
| @@ -71,6 +81,58 @@ module API | @@ -71,6 +81,58 @@ module API | ||
| 71 | not_found! | 81 | not_found! |
| 72 | end | 82 | end |
| 73 | end | 83 | end |
| 84 | + | ||
| 85 | + # Get a list of group members viewable by the authenticated user. | ||
| 86 | + # | ||
| 87 | + # Example Request: | ||
| 88 | + # GET /groups/:id/members | ||
| 89 | + get ":id/members" do | ||
| 90 | + group = find_group(params[:id]) | ||
| 91 | + members = group.users_groups | ||
| 92 | + users = (paginate members).collect(&:user) | ||
| 93 | + present users, with: Entities::GroupMember, group: group | ||
| 94 | + end | ||
| 95 | + | ||
| 96 | + # Add a user to the list of group members | ||
| 97 | + # | ||
| 98 | + # Parameters: | ||
| 99 | + # id (required) - group id | ||
| 100 | + # user_id (required) - the users id | ||
| 101 | + # access_level (required) - Project access level | ||
| 102 | + # Example Request: | ||
| 103 | + # POST /groups/:id/members | ||
| 104 | + post ":id/members" do | ||
| 105 | + required_attributes! [:user_id, :access_level] | ||
| 106 | + unless validate_access_level?(params[:access_level]) | ||
| 107 | + render_api_error!("Wrong access level", 422) | ||
| 108 | + end | ||
| 109 | + group = find_group(params[:id]) | ||
| 110 | + if group.users_groups.find_by_user_id(params[:user_id]) | ||
| 111 | + render_api_error!("Already exists", 409) | ||
| 112 | + end | ||
| 113 | + group.add_users([params[:user_id]], params[:access_level]) | ||
| 114 | + member = group.users_groups.find_by_user_id(params[:user_id]) | ||
| 115 | + present member.user, with: Entities::GroupMember, group: group | ||
| 116 | + end | ||
| 117 | + | ||
| 118 | + # Remove member. | ||
| 119 | + # | ||
| 120 | + # Parameters: | ||
| 121 | + # id (required) - group id | ||
| 122 | + # user_id (required) - the users id | ||
| 123 | + # | ||
| 124 | + # Example Request: | ||
| 125 | + # DELETE /groups/:id/members/:user_id | ||
| 126 | + delete ":id/members/:user_id" do | ||
| 127 | + group = find_group(params[:id]) | ||
| 128 | + member = group.users_groups.find_by_user_id(params[:user_id]) | ||
| 129 | + if member.nil? | ||
| 130 | + render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}",404) | ||
| 131 | + else | ||
| 132 | + member.destroy | ||
| 133 | + end | ||
| 134 | + end | ||
| 135 | + | ||
| 74 | end | 136 | end |
| 75 | end | 137 | end |
| 76 | end | 138 | end |
spec/requests/api/groups_spec.rb
| @@ -3,11 +3,11 @@ require 'spec_helper' | @@ -3,11 +3,11 @@ require 'spec_helper' | ||
| 3 | describe API::API do | 3 | describe API::API do |
| 4 | include ApiHelpers | 4 | include ApiHelpers |
| 5 | 5 | ||
| 6 | - let(:user1) { create(:user) } | ||
| 7 | - let(:user2) { create(:user) } | 6 | + let(:user1) { create(:user) } |
| 7 | + let(:user2) { create(:user) } | ||
| 8 | let(:admin) { create(:admin) } | 8 | let(:admin) { create(:admin) } |
| 9 | - let!(:group1) { create(:group, owner: user1) } | ||
| 10 | - let!(:group2) { create(:group, owner: user2) } | 9 | + let!(:group1) { create(:group, owner: user1) } |
| 10 | + let!(:group2) { create(:group, owner: user2) } | ||
| 11 | 11 | ||
| 12 | describe "GET /groups" do | 12 | describe "GET /groups" do |
| 13 | context "when unauthenticated" do | 13 | context "when unauthenticated" do |
| @@ -52,7 +52,7 @@ describe API::API do | @@ -52,7 +52,7 @@ describe API::API do | ||
| 52 | 52 | ||
| 53 | it "should not return a group not attached to user1" do | 53 | it "should not return a group not attached to user1" do |
| 54 | get api("/groups/#{group2.id}", user1) | 54 | get api("/groups/#{group2.id}", user1) |
| 55 | - response.status.should == 404 | 55 | + response.status.should == 403 |
| 56 | end | 56 | end |
| 57 | end | 57 | end |
| 58 | 58 | ||
| @@ -90,7 +90,7 @@ describe API::API do | @@ -90,7 +90,7 @@ describe API::API do | ||
| 90 | end | 90 | end |
| 91 | 91 | ||
| 92 | it "should return 400 bad request error if name not given" do | 92 | it "should return 400 bad request error if name not given" do |
| 93 | - post api("/groups", admin), { path: group2.path } | 93 | + post api("/groups", admin), {path: group2.path} |
| 94 | response.status.should == 400 | 94 | response.status.should == 400 |
| 95 | end | 95 | end |
| 96 | 96 | ||
| @@ -104,11 +104,10 @@ describe API::API do | @@ -104,11 +104,10 @@ describe API::API do | ||
| 104 | describe "POST /groups/:id/projects/:project_id" do | 104 | describe "POST /groups/:id/projects/:project_id" do |
| 105 | let(:project) { create(:project) } | 105 | let(:project) { create(:project) } |
| 106 | before(:each) do | 106 | before(:each) do |
| 107 | - project.stub!(:transfer).and_return(true) | ||
| 108 | - Project.stub(:find).and_return(project) | 107 | + project.stub!(:transfer).and_return(true) |
| 108 | + Project.stub(:find).and_return(project) | ||
| 109 | end | 109 | end |
| 110 | 110 | ||
| 111 | - | ||
| 112 | context "when authenticated as user" do | 111 | context "when authenticated as user" do |
| 113 | it "should not transfer project to group" do | 112 | it "should not transfer project to group" do |
| 114 | post api("/groups/#{group1.id}/projects/#{project.id}", user2) | 113 | post api("/groups/#{group1.id}/projects/#{project.id}", user2) |
| @@ -123,4 +122,109 @@ describe API::API do | @@ -123,4 +122,109 @@ describe API::API do | ||
| 123 | end | 122 | end |
| 124 | end | 123 | end |
| 125 | end | 124 | end |
| 125 | + | ||
| 126 | + describe "members" do | ||
| 127 | + let(:owner) { create(:user) } | ||
| 128 | + let(:reporter) { create(:user) } | ||
| 129 | + let(:developer) { create(:user) } | ||
| 130 | + let(:master) { create(:user) } | ||
| 131 | + let(:guest) { create(:user) } | ||
| 132 | + let!(:group_with_members) do | ||
| 133 | + group = create(:group, owner: owner) | ||
| 134 | + group.add_users([reporter.id], UsersGroup::REPORTER) | ||
| 135 | + group.add_users([developer.id], UsersGroup::DEVELOPER) | ||
| 136 | + group.add_users([master.id], UsersGroup::MASTER) | ||
| 137 | + group.add_users([guest.id], UsersGroup::GUEST) | ||
| 138 | + group | ||
| 139 | + end | ||
| 140 | + let!(:group_no_members) { create(:group, owner: owner) } | ||
| 141 | + | ||
| 142 | + describe "GET /groups/:id/members" do | ||
| 143 | + context "when authenticated as user that is part or the group" do | ||
| 144 | + it "each user: should return an array of members groups of group3" do | ||
| 145 | + [owner, master, developer, reporter, guest].each do |user| | ||
| 146 | + get api("/groups/#{group_with_members.id}/members", user) | ||
| 147 | + response.status.should == 200 | ||
| 148 | + json_response.should be_an Array | ||
| 149 | + json_response.size.should == 5 | ||
| 150 | + json_response.find { |e| e['id']==owner.id }['access_level'].should == UsersGroup::OWNER | ||
| 151 | + json_response.find { |e| e['id']==reporter.id }['access_level'].should == UsersGroup::REPORTER | ||
| 152 | + json_response.find { |e| e['id']==developer.id }['access_level'].should == UsersGroup::DEVELOPER | ||
| 153 | + json_response.find { |e| e['id']==master.id }['access_level'].should == UsersGroup::MASTER | ||
| 154 | + json_response.find { |e| e['id']==guest.id }['access_level'].should == UsersGroup::GUEST | ||
| 155 | + end | ||
| 156 | + end | ||
| 157 | + | ||
| 158 | + it "users not part of the group should get access error" do | ||
| 159 | + get api("/groups/#{group_with_members.id}/members", user1) | ||
| 160 | + response.status.should == 403 | ||
| 161 | + end | ||
| 162 | + end | ||
| 163 | + end | ||
| 164 | + | ||
| 165 | + describe "POST /groups/:id/members" do | ||
| 166 | + context "when not a member of the group" do | ||
| 167 | + it "should not add guest as member of group_no_members when adding being done by person outside the group" do | ||
| 168 | + post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: UsersGroup::MASTER | ||
| 169 | + response.status.should == 403 | ||
| 170 | + end | ||
| 171 | + end | ||
| 172 | + | ||
| 173 | + context "when a member of the group" do | ||
| 174 | + it "should return ok and add new member" do | ||
| 175 | + count_before=group_no_members.users_groups.count | ||
| 176 | + new_user = create(:user) | ||
| 177 | + post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: UsersGroup::MASTER | ||
| 178 | + response.status.should == 201 | ||
| 179 | + json_response['name'].should == new_user.name | ||
| 180 | + json_response['access_level'].should == UsersGroup::MASTER | ||
| 181 | + group_no_members.users_groups.count.should == count_before + 1 | ||
| 182 | + end | ||
| 183 | + | ||
| 184 | + it "should return error if member already exists" do | ||
| 185 | + post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: UsersGroup::MASTER | ||
| 186 | + response.status.should == 409 | ||
| 187 | + end | ||
| 188 | + | ||
| 189 | + it "should return a 400 error when user id is not given" do | ||
| 190 | + post api("/groups/#{group_no_members.id}/members", owner), access_level: UsersGroup::MASTER | ||
| 191 | + response.status.should == 400 | ||
| 192 | + end | ||
| 193 | + | ||
| 194 | + it "should return a 400 error when access level is not given" do | ||
| 195 | + post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id | ||
| 196 | + response.status.should == 400 | ||
| 197 | + end | ||
| 198 | + | ||
| 199 | + it "should return a 422 error when access level is not known" do | ||
| 200 | + post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234 | ||
| 201 | + response.status.should == 422 | ||
| 202 | + end | ||
| 203 | + end | ||
| 204 | + end | ||
| 205 | + | ||
| 206 | + describe "DELETE /groups/:id/members/:user_id" do | ||
| 207 | + context "when not a member of the group" do | ||
| 208 | + it "should not delete guest's membership of group_with_members" do | ||
| 209 | + random_user = create(:user) | ||
| 210 | + delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) | ||
| 211 | + response.status.should == 403 | ||
| 212 | + end | ||
| 213 | + end | ||
| 214 | + | ||
| 215 | + context "when a member of the group" do | ||
| 216 | + it "should delete guest's membership of group" do | ||
| 217 | + count_before=group_with_members.users_groups.count | ||
| 218 | + delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) | ||
| 219 | + response.status.should == 200 | ||
| 220 | + group_with_members.users_groups.count.should == count_before - 1 | ||
| 221 | + end | ||
| 222 | + | ||
| 223 | + it "should return a 404 error when user id is not known" do | ||
| 224 | + delete api("/groups/#{group_with_members.id}/members/1328", owner) | ||
| 225 | + response.status.should == 404 | ||
| 226 | + end | ||
| 227 | + end | ||
| 228 | + end | ||
| 229 | + end | ||
| 126 | end | 230 | end |