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 | ... | ... |