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 | 55 | Parameters: |
| 56 | 56 | + `id` (required) - The ID of a group |
| 57 | 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 | 67 | expose :projects, using: Entities::Project |
| 68 | 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 | 76 | class RepoObject < Grape::Entity |
| 71 | 77 | expose :name, :commit |
| 72 | 78 | expose :protected do |repo, options| | ... | ... |
lib/api/groups.rb
| ... | ... | @@ -4,6 +4,20 @@ module API |
| 4 | 4 | before { authenticate! } |
| 5 | 5 | |
| 6 | 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 | 21 | # Get a groups list |
| 8 | 22 | # |
| 9 | 23 | # Example Request: |
| ... | ... | @@ -46,12 +60,8 @@ module API |
| 46 | 60 | # Example Request: |
| 47 | 61 | # GET /groups/:id |
| 48 | 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 | 65 | end |
| 56 | 66 | |
| 57 | 67 | # Transfer a project to the Group namespace |
| ... | ... | @@ -71,6 +81,58 @@ module API |
| 71 | 81 | not_found! |
| 72 | 82 | end |
| 73 | 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 | 136 | end |
| 75 | 137 | end |
| 76 | 138 | end | ... | ... |
spec/requests/api/groups_spec.rb
| ... | ... | @@ -3,11 +3,11 @@ require 'spec_helper' |
| 3 | 3 | describe API::API do |
| 4 | 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 | 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 | 12 | describe "GET /groups" do |
| 13 | 13 | context "when unauthenticated" do |
| ... | ... | @@ -52,7 +52,7 @@ describe API::API do |
| 52 | 52 | |
| 53 | 53 | it "should not return a group not attached to user1" do |
| 54 | 54 | get api("/groups/#{group2.id}", user1) |
| 55 | - response.status.should == 404 | |
| 55 | + response.status.should == 403 | |
| 56 | 56 | end |
| 57 | 57 | end |
| 58 | 58 | |
| ... | ... | @@ -90,7 +90,7 @@ describe API::API do |
| 90 | 90 | end |
| 91 | 91 | |
| 92 | 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 | 94 | response.status.should == 400 |
| 95 | 95 | end |
| 96 | 96 | |
| ... | ... | @@ -104,11 +104,10 @@ describe API::API do |
| 104 | 104 | describe "POST /groups/:id/projects/:project_id" do |
| 105 | 105 | let(:project) { create(:project) } |
| 106 | 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 | 109 | end |
| 110 | 110 | |
| 111 | - | |
| 112 | 111 | context "when authenticated as user" do |
| 113 | 112 | it "should not transfer project to group" do |
| 114 | 113 | post api("/groups/#{group1.id}/projects/#{project.id}", user2) |
| ... | ... | @@ -123,4 +122,109 @@ describe API::API do |
| 123 | 122 | end |
| 124 | 123 | end |
| 125 | 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 | 230 | end | ... | ... |