Commit 9a30b27b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '18583-implement-access-request-to-project-group-api' into 'master'

Add group members API endpoints to request access and approve requests

## What does this MR do?

Add group members API endpoints to request access and approve requests.

## Are there points in the code the reviewer needs to double check?

I chose to factorize the group/project members API as well as the new group/project access requests API.

I initially also created new services to centralize the permission checks related to actions on members, but this was out of scope and this MR is already quite big so I opened a temp MR at !5566 based on this one.

## Why was this MR needed?

To finish the "request access" feature.

## What are the relevant issue numbers?

Closes #18583.

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [x] API support added
- Tests
  - [x] Added for this feature/bug
  - [x] All builds are passing
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if you do - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)

See merge request !4833
parents af019d2f 115c00fd
......@@ -8,6 +8,7 @@ v 8.11.0 (unreleased)
- Improve diff performance by eliminating redundant checks for text blobs
- Convert switch icon into icon font (ClemMakesApps)
- API: Endpoints for enabling and disabling deploy keys
- API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
- Use long options for curl examples in documentation !5703 (winniehell)
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
......
......@@ -8,6 +8,7 @@ class ProjectMember < Member
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
validates_format_of :source_type, with: /\AProject\z/
validates :access_level, inclusion: { in: Gitlab::Access.values }
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
......
......@@ -999,6 +999,10 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
def add_user(user, access_level, current_user = nil)
team.add_user(user, access_level, current_user)
end
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
......
......@@ -2,8 +2,9 @@ module Members
class DestroyService < BaseService
attr_accessor :member, :current_user
def initialize(member, user)
@member, @current_user = member, user
def initialize(member, current_user)
@member = member
@current_user = current_user
end
def execute
......
......@@ -16,6 +16,8 @@ following locations:
- [Commits](commits.md)
- [Deploy Keys](deploy_keys.md)
- [Groups](groups.md)
- [Group Access Requests](access_requests.md)
- [Group Members](members.md)
- [Issues](issues.md)
- [Keys](keys.md)
- [Labels](labels.md)
......@@ -25,6 +27,8 @@ following locations:
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
- [Project Members](members.md)
- [Project Snippets](project_snippets.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
......@@ -154,7 +158,7 @@ be returned with status code `403`:
```json
{
"message": "403 Forbidden: Must be admin to use sudo"
"message": "403 Forbidden - Must be admin to use sudo"
}
```
......
# Group and project access requests
>**Note:** This feature was introduced in GitLab 8.11
**Valid access levels**
The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
```
10 => Guest access
20 => Reporter access
30 => Developer access
40 => Master access
50 => Owner access # Only valid for groups
```
## List access requests for a group or project
Gets a list of access requests viewable by the authenticated user.
Returns `200` if the request succeeds.
```
GET /groups/:id/access_requests
GET /projects/:id/access_requests
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
```
Example response:
```json
[
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"requested_at": "2012-10-22T14:13:35Z"
},
{
"id": 2,
"username": "john_doe",
"name": "John Doe",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"requested_at": "2012-10-22T14:13:35Z"
}
]
```
## Request access to a group or project
Requests access for the authenticated user to a group or project.
Returns `201` if the request succeeds.
```
POST /groups/:id/access_requests
POST /projects/:id/access_requests
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
```
Example response:
```json
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"requested_at": "2012-10-22T14:13:35Z"
}
```
## Approve an access request
Approves an access request for the given user.
Returns `201` if the request succeeds.
```
PUT /groups/:id/access_requests/:user_id/approve
PUT /projects/:id/access_requests/:user_id/approve
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the access requester |
| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
```
Example response:
```json
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 20
}
```
## Deny an access request
Denies an access request for the given user.
Returns `200` if the request succeeds.
```
DELETE /groups/:id/access_requests/:user_id
DELETE /projects/:id/access_requests/:user_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the access requester |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
```
......@@ -417,87 +417,7 @@ GET /groups?search=foobar
## Group members
**Group access levels**
The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
```
GUEST = 10
REPORTER = 20
DEVELOPER = 30
MASTER = 40
OWNER = 50
```
### List group members
Get a list of group members viewable by the authenticated user.
```
GET /groups/:id/members
```
```json
[
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
},
{
"id": 2,
"username": "john_doe",
"name": "John Doe",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
}
]
```
### Add group member
Adds a user to the list of group members.
```
POST /groups/:id/members
```
Parameters:
- `id` (required) - The ID or path of a group
- `user_id` (required) - The ID of a user to add
- `access_level` (required) - Project access level
### Edit group team member
Updates a group team member to a specified access level.
```
PUT /groups/:id/members/:user_id
```
Parameters:
- `id` (required) - The ID of a group
- `user_id` (required) - The ID of a group member
- `access_level` (required) - Project access level
### Remove user team member
Removes user from user team.
```
DELETE /groups/:id/members/:user_id
```
Parameters:
- `id` (required) - The ID or path of a user group
- `user_id` (required) - The ID of a group member
Please consult the [Group Members](members.md) documentation.
## Namespaces in groups
......
# Group and project members
**Valid access levels**
The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
```
10 => Guest access
20 => Reporter access
30 => Developer access
40 => Master access
50 => Owner access # Only valid for groups
```
## List all members of a group or project
Gets a list of group or project members viewable by the authenticated user.
Returns `200` if the request succeeds.
```
GET /groups/:id/members
GET /projects/:id/members
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
| `query` | string | no | A query string to search for members |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
```
Example response:
```json
[
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
},
{
"id": 2,
"username": "john_doe",
"name": "John Doe",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
}
]
```
## Get a member of a group or project
Gets a member of a group or project.
Returns `200` if the request succeeds.
```
GET /groups/:id/members/:user_id
GET /projects/:id/members/:user_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the member |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
```
Example response:
```json
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
}
```
## Add a member to a group or project
Adds a member to a group or project.
Returns `201` if the request succeeds.
```
POST /groups/:id/members
POST /projects/:id/members
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the new member |
| `access_level` | integer | yes | A valid access level |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
```
Example response:
```json
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
}
```
## Edit a member of a group or project
Updates a member of a group or project.
Returns `200` if the request succeeds.
```
PUT /groups/:id/members/:user_id
PUT /projects/:id/members/:user_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the member |
| `access_level` | integer | yes | A valid access level |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
```
Example response:
```json
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 40
}
```
## Remove a member from a group or project
Removes a user from a group or project.
Returns `200` if the request succeeds.
```
DELETE /groups/:id/members/:user_id
DELETE /projects/:id/members/:user_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the member |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
```
......@@ -858,95 +858,9 @@ Parameters:
In Markdown contexts, the link is automatically expanded when the format in `markdown` is used.
## Team members
## Project members
### List project team members
Get a list of a project's team members.
```
GET /projects/:id/members
```
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `query` (optional) - Query string to search for members
### Get project team member
Gets a project team member.
```
GET /projects/:id/members/:user_id
```
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `user_id` (required) - The ID of a user
```json
{
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z",
"access_level": 40
}
```
### Add project team member
Adds a user to a project team. This is an idempotent method and can be called multiple times
with the same parameters. Adding team membership to a user that is already a member does not
affect the existing membership.
```
POST /projects/:id/members
```
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `user_id` (required) - The ID of a user to add
- `access_level` (required) - Project access level
### Edit project team member
Updates a project team member to a specified access level.
```
PUT /projects/:id/members/:user_id
```
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `user_id` (required) - The ID of a team member
- `access_level` (required) - Project access level
### Remove project team member
Removes a user from a project team.
```
DELETE /projects/:id/members/:user_id
```
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `user_id` (required) - The ID of a team member
This method removes the project member if the user has the proper access rights to do so.
It returns a status code 403 if the member does not have the proper rights to perform this action.
In all other cases this method is idempotent and revoking team membership for a user who is not
currently a team member is considered success.
Please note that the returned JSON currently differs slightly. Thus you should not
rely on the returned JSON structure.
Please consult the [Project Members](members.md) documentation.
### Share project with group
......
module API
class AccessRequests < Grape::API
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
resource source_type.pluralize do
# Get a list of group/project access requests viewable by the authenticated user.
#
# Parameters:
# id (required) - The group/project ID
#
# Example Request:
# GET /groups/:id/access_requests
# GET /projects/:id/access_requests
get ":id/access_requests" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
access_requesters = paginate(source.requesters.includes(:user))
present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters
end
# Request access to the group/project
#
# Parameters:
# id (required) - The group/project ID
#
# Example Request:
# POST /groups/:id/access_requests
# POST /projects/:id/access_requests
post ":id/access_requests" do
source = find_source(source_type, params[:id])
access_requester = source.request_access(current_user)
if access_requester.persisted?
present access_requester.user, with: Entities::AccessRequester, access_requester: access_requester
else
render_validation_error!(access_requester)
end
end
# Approve a group/project access request
#
# Parameters:
# id (required) - The group/project ID
# user_id (required) - The user ID of the access requester
# access_level (optional) - Access level
#
# Example Request:
# PUT /groups/:id/access_requests/:user_id/approve
# PUT /projects/:id/access_requests/:user_id/approve
put ':id/access_requests/:user_id/approve' do
required_attributes! [:user_id]
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
member = source.requesters.find_by!(user_id: params[:user_id])
if params[:access_level]
member.update(access_level: params[:access_level])
end
member.accept_request
status :created
present member.user, with: Entities::Member, member: member
end
# Deny a group/project access request
#
# Parameters:
# id (required) - The group/project ID
# user_id (required) - The user ID of the access requester
#
# Example Request:
# DELETE /groups/:id/access_requests/:user_id
# DELETE /projects/:id/access_requests/:user_id
delete ":id/access_requests/:user_id" do
required_attributes! [:user_id]
source = find_source(source_type, params[:id])
access_requester = source.requesters.find_by!(user_id: params[:user_id])
::Members::DestroyService.new(access_requester, current_user).execute
end
end
end
end
end
......@@ -3,6 +3,10 @@ module API
include APIGuard
version 'v3', using: :path
rescue_from Gitlab::Access::AccessDeniedError do
rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
end
rescue_from ActiveRecord::RecordNotFound do
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
......@@ -32,6 +36,7 @@ module API
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
helpers ::API::Helpers
mount ::API::AccessRequests
mount ::API::AwardEmoji
mount ::API::Branches
mount ::API::Builds
......@@ -40,19 +45,18 @@ module API
mount ::API::DeployKeys
mount ::API::Environments
mount ::API::Files
mount ::API::GroupMembers
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
mount ::API::Keys
mount ::API::Labels
mount ::API::LicenseTemplates
mount ::API::Members
mount ::API::MergeRequests
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
mount ::API::ProjectHooks
mount ::API::ProjectMembers
mount ::API::ProjectSnippets
mount ::API::Projects
mount ::API::Repositories
......
......@@ -91,9 +91,17 @@ module API
end
end
class ProjectMember < UserBasic
class Member < UserBasic
expose :access_level do |user, options|
options[:project].project_members.find_by(user_id: user.id).access_level
member = options[:member] || options[:members].find { |m| m.user_id == user.id }
member.access_level
end
end
class AccessRequester < UserBasic
expose :requested_at do |user, options|
access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id }
access_requester.requested_at
end
end
......@@ -108,12 +116,6 @@ module API
expose :shared_projects, using: Entities::Project
end
class GroupMember < UserBasic
expose :access_level do |user, options|
options[:group].group_members.find_by(user_id: user.id).access_level
end
end
class RepoBranch < Grape::Entity
expose :name
......@@ -325,7 +327,7 @@ module API
expose :id, :path, :kind
end
class Member < Grape::Entity
class MemberAccess < Grape::Entity
expose :access_level
expose :notification_level do |member, options|
if member.notification_setting
......@@ -334,10 +336,10 @@ module API
end
end
class ProjectAccess < Member
class ProjectAccess < MemberAccess
end
class GroupAccess < Member
class GroupAccess < MemberAccess
end
class ProjectService < Grape::Entity
......
module API
class GroupMembers < Grape::API
before { authenticate! }
resource :groups do
# Get a list of group members viewable by the authenticated user.
#
# Example Request:
# GET /groups/:id/members
get ":id/members" do
group = find_group(params[:id])
users = group.users
present users, with: Entities::GroupMember, group: group
end
# Add a user to the list of group members
#
# Parameters:
# id (required) - group id
# user_id (required) - the users id
# access_level (required) - Project access level
# Example Request:
# POST /groups/:id/members
post ":id/members" do
group = find_group(params[:id])
authorize! :admin_group, group
required_attributes! [:user_id, :access_level]
unless validate_access_level?(params[:access_level])
render_api_error!("Wrong access level", 422)
end
if group.group_members.find_by(user_id: params[:user_id])
render_api_error!("Already exists", 409)
end
group.add_users([params[:user_id]], params[:access_level], current_user)
member = group.group_members.find_by(user_id: params[:user_id])
present member.user, with: Entities::GroupMember, group: group
end
# Update group member
#
# Parameters:
# id (required) - The ID of a group
# user_id (required) - The ID of a group member
# access_level (required) - Project access level
# Example Request:
# PUT /groups/:id/members/:user_id
put ':id/members/:user_id' do
group = find_group(params[:id])
authorize! :admin_group, group
required_attributes! [:access_level]
group_member = group.group_members.find_by(user_id: params[:user_id])
not_found!('User can not be found') if group_member.nil?
if group_member.update_attributes(access_level: params[:access_level])
@member = group_member.user
present @member, with: Entities::GroupMember, group: group
else
handle_member_errors group_member.errors
end
end
# Remove member.
#
# Parameters:
# id (required) - group id
# user_id (required) - the users id
#
# Example Request:
# DELETE /groups/:id/members/:user_id
delete ":id/members/:user_id" do
group = find_group(params[:id])
authorize! :admin_group, group
member = group.group_members.find_by(user_id: params[:user_id])
if member.nil?
render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}", 404)
else
member.destroy
end
end
end
end
end
......@@ -28,7 +28,7 @@ module API
# If the sudo is the current user do nothing
if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
@current_user = User.by_username_or_id(identifier)
not_found!("No user id or username for: #{identifier}") if @current_user.nil?
end
......@@ -49,16 +49,15 @@ module API
def user_project
@project ||= find_project(params[:id])
@project || not_found!("Project")
end
def find_project(id)
project = Project.find_with_namespace(id) || Project.find_by(id: id)
if project && can?(current_user, :read_project, project)
if can?(current_user, :read_project, project)
project
else
nil
not_found!('Project')
end
end
......@@ -89,11 +88,7 @@ module API
end
def find_group(id)
begin
group = Group.find(id)
rescue ActiveRecord::RecordNotFound
group = Group.find_by!(path: id)
end
group = Group.find_by(path: id) || Group.find_by(id: id)
if can?(current_user, :read_group, group)
group
......@@ -135,7 +130,7 @@ module API
end
def authorize!(action, subject)
forbidden! unless abilities.allowed?(current_user, action, subject)
forbidden! unless can?(current_user, action, subject)
end
def authorize_push_project
......@@ -197,10 +192,6 @@ module API
errors
end
def validate_access_level?(level)
Gitlab::Access.options_with_owner.values.include? level.to_i
end
# Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
# format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
#
......@@ -411,11 +402,6 @@ module API
File.read(Gitlab.config.gitlab_shell.secret_file).chomp
end
def handle_member_errors(errors)
error!(errors[:access_level], 422) if errors[:access_level].any?
not_found!(errors)
end
def send_git_blob(repository, blob)
env['api.format'] = :txt
content_type 'text/plain'
......
module API
module Helpers
module MembersHelpers
def find_source(source_type, id)
public_send("find_#{source_type}", id)
end
def authorize_admin_source!(source_type, source)
authorize! :"admin_#{source_type}", source
end
end
end
end
module API
class Members < Grape::API
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
resource source_type.pluralize do
# Get a list of group/project members viewable by the authenticated user.
#
# Parameters:
# id (required) - The group/project ID
# query - Query string
#
# Example Request:
# GET /groups/:id/members
# GET /projects/:id/members
get ":id/members" do
source = find_source(source_type, params[:id])
members = source.members.includes(:user)
members = members.joins(:user).merge(User.search(params[:query])) if params[:query]
members = paginate(members)
present members.map(&:user), with: Entities::Member, members: members
end
# Get a group/project member
#
# Parameters:
# id (required) - The group/project ID
# user_id (required) - The user ID of the member
#
# Example Request:
# GET /groups/:id/members/:user_id
# GET /projects/:id/members/:user_id
get ":id/members/:user_id" do
source = find_source(source_type, params[:id])
members = source.members
member = members.find_by!(user_id: params[:user_id])
present member.user, with: Entities::Member, member: member
end
# Add a new group/project member
#
# Parameters:
# id (required) - The group/project ID
# user_id (required) - The user ID of the new member
# access_level (required) - A valid access level
#
# Example Request:
# POST /groups/:id/members
# POST /projects/:id/members
post ":id/members" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
required_attributes! [:user_id, :access_level]
access_requester = source.requesters.find_by(user_id: params[:user_id])
if access_requester
# We pass current_user = access_requester so that the requester doesn't
# receive a "access denied" email
::Members::DestroyService.new(access_requester, access_requester.user).execute
end
member = source.members.find_by(user_id: params[:user_id])
# This is to ensure back-compatibility but 409 behavior should be used
# for both project and group members in 9.0!
conflict!('Member already exists') if source_type == 'group' && member
unless member
source.add_user(params[:user_id], params[:access_level], current_user)
member = source.members.find_by(user_id: params[:user_id])
end
if member
present member.user, with: Entities::Member, member: member
else
# Since `source.add_user` doesn't return a member object, we have to
# build a new one and populate its errors in order to render them.
member = source.members.build(attributes_for_keys([:user_id, :access_level]))
member.valid? # populate the errors
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
# Update a group/project member
#
# Parameters:
# id (required) - The group/project ID
# user_id (required) - The user ID of the member
# access_level (required) - A valid access level
#
# Example Request:
# PUT /groups/:id/members/:user_id
# PUT /projects/:id/members/:user_id
put ":id/members/:user_id" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
if member.update_attributes(access_level: params[:access_level])
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
# Remove a group/project member
#
# Parameters:
# id (required) - The group/project ID
# user_id (required) - The user ID of the member
#
# Example Request:
# DELETE /groups/:id/members/:user_id
# DELETE /projects/:id/members/:user_id
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
required_attributes! [:user_id]
# This is to ensure back-compatibility but find_by! should be used
# in that casse in 9.0!
member = source.members.find_by(user_id: params[:user_id])
# This is to ensure back-compatibility but this should be removed in
# favor of find_by! in 9.0!
not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
# This is to ensure back-compatibility but 204 behavior should be used
# for all DELETE endpoints in 9.0!
if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
::Members::DestroyService.new(member, current_user).execute
present member.user, with: Entities::Member, member: member
end
end
end
end
end
end
module API
# Projects members API
class ProjectMembers < Grape::API
before { authenticate! }
resource :projects do
# Get a project team members
#
# Parameters:
# id (required) - The ID of a project
# query - Query string
# Example Request:
# GET /projects/:id/members
get ":id/members" do
if params[:query].present?
@members = paginate user_project.users.where("username LIKE ?", "%#{params[:query]}%")
else
@members = paginate user_project.users
end
present @members, with: Entities::ProjectMember, project: user_project
end
# Get a project team members
#
# Parameters:
# id (required) - The ID of a project
# user_id (required) - The ID of a user
# Example Request:
# GET /projects/:id/members/:user_id
get ":id/members/:user_id" do
@member = user_project.users.find params[:user_id]
present @member, with: Entities::ProjectMember, project: user_project
end
# Add a new project team member
#
# Parameters:
# id (required) - The ID of a project
# user_id (required) - The ID of a user
# access_level (required) - Project access level
# Example Request:
# POST /projects/:id/members
post ":id/members" do
authorize! :admin_project, user_project
required_attributes! [:user_id, :access_level]
# either the user is already a team member or a new one
project_member = user_project.project_member(params[:user_id])
if project_member.nil?
project_member = user_project.project_members.new(
user_id: params[:user_id],
access_level: params[:access_level]
)
end
if project_member.save
@member = project_member.user
present @member, with: Entities::ProjectMember, project: user_project
else
handle_member_errors project_member.errors
end
end
# Update project team member
#
# Parameters:
# id (required) - The ID of a project
# user_id (required) - The ID of a team member
# access_level (required) - Project access level
# Example Request:
# PUT /projects/:id/members/:user_id
put ":id/members/:user_id" do
authorize! :admin_project, user_project
required_attributes! [:access_level]
project_member = user_project.project_members.find_by(user_id: params[:user_id])
not_found!("User can not be found") if project_member.nil?
if project_member.update_attributes(access_level: params[:access_level])
@member = project_member.user
present @member, with: Entities::ProjectMember, project: user_project
else
handle_member_errors project_member.errors
end
end
# Remove a team member from project
#
# Parameters:
# id (required) - The ID of a project
# user_id (required) - The ID of a team member
# Example Request:
# DELETE /projects/:id/members/:user_id
delete ":id/members/:user_id" do
project_member = user_project.project_members.find_by(user_id: params[:user_id])
unless current_user.can?(:admin_project, user_project) ||
current_user.can?(:destroy_project_member, project_member)
forbidden!
end
if project_member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
project_member.destroy
end
end
end
end
end
......@@ -10,7 +10,7 @@ describe Member, models: true do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
it_behaves_like 'an object with email-formated attributes', :invite_email do
subject { build(:project_member) }
......
......@@ -27,6 +27,7 @@ describe ProjectMember, models: true do
describe 'validations' do
it { is_expected.to allow_value('Project').for(:source_type) }
it { is_expected.not_to allow_value('project').for(:source_type) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
describe 'modules' do
......@@ -40,7 +41,7 @@ describe ProjectMember, models: true do
end
describe "#destroy" do
let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) }
let(:owner) { create(:project_member, access_level: ProjectMember::MASTER) }
let(:project) { owner.project }
let(:master) { create(:project_member, project: project) }
......
require 'spec_helper'
describe API::AccessRequests, api: true do
include ApiHelpers
let(:master) { create(:user) }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
let(:project) do
project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
project.team << [developer, :developer]
project.team << [master, :master]
project.request_access(access_requester)
project
end
let(:group) do
group = create(:group, :public)
group.add_developer(developer)
group.add_owner(master)
group.request_access(access_requester)
group
end
shared_examples 'GET /:sources/:id/access_requests' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
end
context 'when authenticated as a non-master/owner' do
%i[developer access_requester stranger].each do |type|
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
get api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
expect(response).to have_http_status(403)
end
end
end
end
context 'when authenticated as a master/owner' do
it 'returns access requesters' do
get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
end
end
end
end
shared_examples 'POST /:sources/:id/access_requests' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
end
context 'when authenticated as a member' do
%i[developer master].each do |type|
context "as a #{type}" do
it 'returns 400' do
expect do
user = public_send(type)
post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
expect(response).to have_http_status(400)
end.not_to change { source.requesters.count }
end
end
end
end
context 'when authenticated as an access requester' do
it 'returns 400' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester)
expect(response).to have_http_status(400)
end.not_to change { source.requesters.count }
end
end
context 'when authenticated as a stranger' do
it 'returns 201' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
expect(response).to have_http_status(201)
end.to change { source.requesters.count }.by(1)
# User attributes
expect(json_response['id']).to eq(stranger.id)
expect(json_response['name']).to eq(stranger.name)
expect(json_response['username']).to eq(stranger.username)
expect(json_response['state']).to eq(stranger.state)
expect(json_response['avatar_url']).to eq(stranger.avatar_url)
expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(stranger))
# Member attributes
expect(json_response['requested_at']).to be_present
end
end
end
end
shared_examples 'PUT /:sources/:id/access_requests/:user_id/approve' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", stranger) }
end
context 'when authenticated as a non-master/owner' do
%i[developer access_requester stranger].each do |type|
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user)
expect(response).to have_http_status(403)
end
end
end
end
context 'when authenticated as a master/owner' do
it 'returns 201' do
expect do
put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master),
access_level: Member::MASTER
expect(response).to have_http_status(201)
end.to change { source.members.count }.by(1)
# User attributes
expect(json_response['id']).to eq(access_requester.id)
expect(json_response['name']).to eq(access_requester.name)
expect(json_response['username']).to eq(access_requester.username)
expect(json_response['state']).to eq(access_requester.state)
expect(json_response['avatar_url']).to eq(access_requester.avatar_url)
expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester))
# Member attributes
expect(json_response['access_level']).to eq(Member::MASTER)
end
context 'user_id does not match an existing access requester' do
it 'returns 404' do
expect do
put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master)
expect(response).to have_http_status(404)
end.not_to change { source.members.count }
end
end
end
end
end
shared_examples 'DELETE /:sources/:id/access_requests/:user_id' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) }
end
context 'when authenticated as a non-master/owner' do
%i[developer stranger].each do |type|
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user)
expect(response).to have_http_status(403)
end
end
end
end
context 'when authenticated as the access requester' do
it 'returns 200' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
expect(response).to have_http_status(200)
end.to change { source.requesters.count }.by(-1)
end
end
context 'when authenticated as a master/owner' do
it 'returns 200' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
expect(response).to have_http_status(200)
end.to change { source.requesters.count }.by(-1)
end
context 'user_id does not match an existing access requester' do
it 'returns 404' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master)
expect(response).to have_http_status(404)
end.not_to change { source.requesters.count }
end
end
end
end
end
it_behaves_like 'GET /:sources/:id/access_requests', 'project' do
let(:source) { project }
end
it_behaves_like 'GET /:sources/:id/access_requests', 'group' do
let(:source) { group }
end
it_behaves_like 'POST /:sources/:id/access_requests', 'project' do
let(:source) { project }
end
it_behaves_like 'POST /:sources/:id/access_requests', 'group' do
let(:source) { group }
end
it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'project' do
let(:source) { project }
end
it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'group' do
let(:source) { group }
end
it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'project' do
let(:source) { project }
end
it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'group' do
let(:source) { group }
end
end
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:owner) { create(:user) }
let(:reporter) { create(:user) }
let(:developer) { create(:user) }
let(:master) { create(:user) }
let(:guest) { create(:user) }
let(:stranger) { create(:user) }
let!(:group_with_members) do
group = create(:group, :private)
group.add_users([reporter.id], GroupMember::REPORTER)
group.add_users([developer.id], GroupMember::DEVELOPER)
group.add_users([master.id], GroupMember::MASTER)
group.add_users([guest.id], GroupMember::GUEST)
group
end
let!(:group_no_members) { create(:group) }
before do
group_with_members.add_owner owner
group_no_members.add_owner owner
end
describe "GET /groups/:id/members" do
context "when authenticated as user that is part or the group" do
it "each user: returns an array of members groups of group3" do
[owner, master, developer, reporter, guest].each do |user|
get api("/groups/#{group_with_members.id}/members", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(5)
expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER)
expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER)
expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER)
expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER)
expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST)
end
end
it 'users not part of the group should get access error' do
get api("/groups/#{group_with_members.id}/members", stranger)
expect(response).to have_http_status(404)
end
end
end
describe "POST /groups/:id/members" do
context "when not a member of the group" do
it "does not add guest as member of group_no_members when adding being done by person outside the group" do
post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER
expect(response).to have_http_status(403)
end
end
context "when a member of the group" do
it "returns ok and add new member" do
new_user = create(:user)
expect do
post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER
end.to change { group_no_members.members.count }.by(1)
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(new_user.name)
expect(json_response['access_level']).to eq(GroupMember::MASTER)
end
it "does not allow guest to modify group members" do
new_user = create(:user)
expect do
post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER
end.not_to change { group_with_members.members.count }
expect(response).to have_http_status(403)
end
it "returns error if member already exists" do
post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER
expect(response).to have_http_status(409)
end
it "returns a 400 error when user id is not given" do
post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER
expect(response).to have_http_status(400)
end
it "returns a 400 error when access level is not given" do
post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id
expect(response).to have_http_status(400)
end
it "returns a 422 error when access level is not known" do
post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
expect(response).to have_http_status(422)
end
end
end
describe 'PUT /groups/:id/members/:user_id' do
context 'when not a member of the group' do
it 'returns a 409 error if the user is not a group member' do
put(
api("/groups/#{group_no_members.id}/members/#{developer.id}",
owner), access_level: GroupMember::MASTER
)
expect(response).to have_http_status(404)
end
end
context 'when a member of the group' do
it 'returns ok and update member access level' do
put(
api("/groups/#{group_with_members.id}/members/#{reporter.id}",
owner),
access_level: GroupMember::MASTER
)
expect(response).to have_http_status(200)
get api("/groups/#{group_with_members.id}/members", owner)
json_reporter = json_response.find do |e|
e['id'] == reporter.id
end
expect(json_reporter['access_level']).to eq(GroupMember::MASTER)
end
it 'does not allow guest to modify group members' do
put(
api("/groups/#{group_with_members.id}/members/#{developer.id}",
guest),
access_level: GroupMember::MASTER
)
expect(response).to have_http_status(403)
get api("/groups/#{group_with_members.id}/members", owner)
json_developer = json_response.find do |e|
e['id'] == developer.id
end
expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER)
end
it 'returns a 400 error when access level is not given' do
put(
api("/groups/#{group_with_members.id}/members/#{master.id}", owner)
)
expect(response).to have_http_status(400)
end
it 'returns a 422 error when access level is not known' do
put(
api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
access_level: 1234
)
expect(response).to have_http_status(422)
end
end
end
describe 'DELETE /groups/:id/members/:user_id' do
context 'when not a member of the group' do
it "does not delete guest's membership of group_with_members" do
random_user = create(:user)
delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
expect(response).to have_http_status(404)
end
end
context "when a member of the group" do
it "deletes guest's membership of group" do
expect do
delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
end.to change { group_with_members.members.count }.by(-1)
expect(response).to have_http_status(200)
end
it "returns a 404 error when user id is not known" do
delete api("/groups/#{group_with_members.id}/members/1328", owner)
expect(response).to have_http_status(404)
end
it "does not allow guest to modify group members" do
delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest)
expect(response).to have_http_status(403)
end
end
end
end
This diff is collapsed.
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let(:project_member) { create(:project_member, :master, user: user, project: project) }
let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
describe "GET /projects/:id/members" do
before { project_member }
before { project_member2 }
it "returns project team members" do
get api("/projects/#{project.id}/members", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.count).to eq(2)
expect(json_response.map { |u| u['username'] }).to include user.username
end
it "finds team members with query string" do
get api("/projects/#{project.id}/members", user), query: user.username
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['username']).to eq(user.username)
end
it "returns a 404 error if id not found" do
get api("/projects/9999/members", user)
expect(response).to have_http_status(404)
end
end
describe "GET /projects/:id/members/:user_id" do
before { project_member }
it "returns project team member" do
get api("/projects/#{project.id}/members/#{user.id}", user)
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(json_response['access_level']).to eq(ProjectMember::MASTER)
end
it "returns a 404 error if user id not found" do
get api("/projects/#{project.id}/members/1234", user)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/members" do
it "adds user to project team" do
expect do
post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
end.to change { ProjectMember.count }.by(1)
expect(response).to have_http_status(201)
expect(json_response['username']).to eq(user2.username)
expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
end
it "returns a 201 status if user is already project member" do
post api("/projects/#{project.id}/members", user),
user_id: user2.id,
access_level: ProjectMember::DEVELOPER
expect do
post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
end.not_to change { ProjectMember.count }
expect(response).to have_http_status(201)
expect(json_response['username']).to eq(user2.username)
expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
end
it "returns a 400 error when user id is not given" do
post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER
expect(response).to have_http_status(400)
end
it "returns a 400 error when access level is not given" do
post api("/projects/#{project.id}/members", user), user_id: user2.id
expect(response).to have_http_status(400)
end
it "returns a 422 error when access level is not known" do
post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
expect(response).to have_http_status(422)
end
end
describe "PUT /projects/:id/members/:user_id" do
before { project_member2 }
it "updates project team member" do
put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user3.username)
expect(json_response['access_level']).to eq(ProjectMember::MASTER)
end
it "returns a 404 error if user_id is not found" do
put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER
expect(response).to have_http_status(404)
end
it "returns a 400 error when access level is not given" do
put api("/projects/#{project.id}/members/#{user3.id}", user)
expect(response).to have_http_status(400)
end
it "returns a 422 error when access level is not known" do
put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
expect(response).to have_http_status(422)
end
end
describe "DELETE /projects/:id/members/:user_id" do
before do
project_member
project_member2
end
it "removes user from project team" do
expect do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
end.to change { ProjectMember.count }.by(-1)
end
it "returns 200 if team member is not part of a project" do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
expect do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
end.not_to change { ProjectMember.count }
expect(response).to have_http_status(200)
end
it "returns 200 if team member already removed" do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
delete api("/projects/#{project.id}/members/#{user3.id}", user)
expect(response).to have_http_status(200)
end
it "returns 200 OK when the user was not member" do
expect do
delete api("/projects/#{project.id}/members/1000000", user)
end.to change { ProjectMember.count }.by(0)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(1000000)
expect(json_response['message']).to eq('Access revoked')
end
context 'when the user is not an admin or owner' do
it 'can leave the project' do
expect do
delete api("/projects/#{project.id}/members/#{user3.id}", user3)
end.to change { ProjectMember.count }.by(-1)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(project_member2.id)
end
end
end
end
shared_examples 'a 404 response when source is private' do
before do
source.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
end
it 'returns 404' do
route
expect(response).to have_http_status(404)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment