Commit bd4c83b8 authored by Doug Stull's avatar Doug Stull Committed by Kerri Miller

Allow Invitations API to receive user_ids

- enhance to allow eventual deprecation of members API use in
  invite modal

Changelog: added
parent 683c0926
...@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Invitations API **(FREE)** # Invitations API **(FREE)**
Use the Invitations API to send email to users you want to join a group or project, and to list pending Use the Invitations API to invite or add users to a group or project, and to list pending
invitations. invitations.
## Valid access levels ## Valid access levels
...@@ -26,9 +26,9 @@ WARNING: ...@@ -26,9 +26,9 @@ WARNING:
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299), Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299),
projects in personal namespaces don't show owner (`50`) permission. projects in personal namespaces don't show owner (`50`) permission.
## Invite by email to group or project ## Add a member to a group or project
Invites a new user by email to join a group or project. Adds a new member. You can specify a user ID or invite a user by email.
```plaintext ```plaintext
POST /groups/:id/invitations POST /groups/:id/invitations
...@@ -38,7 +38,8 @@ POST /projects/:id/invitations ...@@ -38,7 +38,8 @@ POST /projects/:id/invitations
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](index.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](index.md#namespaced-path-encoding) owned by the authenticated user |
| `email` | string | yes | The email of the new member or multiple emails separated by commas | | `email` | string | yes (if `user_id` isn't provided) | The email of the new member or multiple emails separated by commas. |
| `user_id` | integer/string | yes (if `email` isn't provided) | The ID of the new member or multiple IDs separated by commas. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350999) in GitLab 14.10. |
| `access_level` | integer | yes | A valid access level | | `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). | | `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
...@@ -47,9 +48,9 @@ POST /projects/:id/invitations ...@@ -47,9 +48,9 @@ POST /projects/:id/invitations
```shell ```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--data "email=test@example.com&access_level=30" "https://gitlab.example.com/api/v4/groups/:id/invitations" --data "email=test@example.com&user_id=1&access_level=30" "https://gitlab.example.com/api/v4/groups/:id/invitations"
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--data "email=test@example.com&access_level=30" "https://gitlab.example.com/api/v4/projects/:id/invitations" --data "email=test@example.com&user_id=1&access_level=30" "https://gitlab.example.com/api/v4/projects/:id/invitations"
``` ```
Example responses: Example responses:
...@@ -67,7 +68,8 @@ When there was any error sending the email: ...@@ -67,7 +68,8 @@ When there was any error sending the email:
"status": "error", "status": "error",
"message": { "message": {
"test@example.com": "Invite email has already been taken", "test@example.com": "Invite email has already been taken",
"test2@example.com": "User already exists in source" "test2@example.com": "User already exists in source",
"test_username": "Access level is not included in the list"
} }
} }
``` ```
...@@ -77,7 +79,7 @@ When there was any error sending the email: ...@@ -77,7 +79,7 @@ When there was any error sending the email:
Gets a list of invited group or project members viewable by the authenticated user. Gets a list of invited group or project members viewable by the authenticated user.
Returns invitations to direct members only, and not through inherited ancestors' groups. Returns invitations to direct members only, and not through inherited ancestors' groups.
This function takes pagination parameters `page` and `per_page` to restrict the list of users. This function takes pagination parameters `page` and `per_page` to restrict the list of members.
```plaintext ```plaintext
GET /groups/:id/invitations GET /groups/:id/invitations
......
...@@ -20,8 +20,9 @@ module API ...@@ -20,8 +20,9 @@ module API
success Entities::Invitation success Entities::Invitation
end end
params do params do
requires :email, types: [String, Array[String]], email_or_email_list: true, desc: 'The email address to invite, or multiple emails separated by comma'
requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)' requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :email, types: [String, Array[String]], email_or_email_list: true, desc: 'The email address to invite, or multiple emails separated by comma'
optional :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api' optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api'
optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do' optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do'
...@@ -30,11 +31,14 @@ module API ...@@ -30,11 +31,14 @@ module API
post ":id/invitations" do post ":id/invitations" do
::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/354016') ::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/354016')
params[:source] = find_source(source_type, params[:id]) bad_request!('Must provide either email or user_id as a parameter') if params[:email].blank? && params[:user_id].blank?
authorize_admin_source!(source_type, params[:source]) source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
create_service_params = params.except(:user_id).merge({ user_ids: params[:user_id], source: source })
::Members::InviteService.new(current_user, params).execute ::Members::InviteService.new(current_user, create_service_params).execute
end end
desc 'Get a list of group or project invitations viewable by the authenticated user' do desc 'Get a list of group or project invitations viewable by the authenticated user' do
......
...@@ -100,8 +100,6 @@ module API ...@@ -100,8 +100,6 @@ module API
end end
post ":id/members" do post ":id/members" do
::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/333434')
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source) authorize_admin_source!(source_type, source)
......
...@@ -69,7 +69,7 @@ RSpec.describe API::Invitations do ...@@ -69,7 +69,7 @@ RSpec.describe API::Invitations do
end end
end end
it 'invites a new member' do it 'adds a new member by email' do
expect do expect do
post invitations_url(source, maintainer), post invitations_url(source, maintainer),
params: { email: email, access_level: Member::DEVELOPER } params: { email: email, access_level: Member::DEVELOPER }
...@@ -78,6 +78,24 @@ RSpec.describe API::Invitations do ...@@ -78,6 +78,24 @@ RSpec.describe API::Invitations do
end.to change { source.members.invite.count }.by(1) end.to change { source.members.invite.count }.by(1)
end end
it 'adds a new member by user_id' do
expect do
post invitations_url(source, maintainer),
params: { user_id: stranger.id, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
end.to change { source.members.non_invite.count }.by(1)
end
it 'adds new members with email and user_id' do
expect do
post invitations_url(source, maintainer),
params: { email: email, user_id: stranger.id, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
end.to change { source.members.invite.count }.by(1).and change { source.members.non_invite.count }.by(1)
end
it 'invites a list of new email addresses' do it 'invites a list of new email addresses' do
expect do expect do
email_list = [email, email2].join(',') email_list = [email, email2].join(',')
...@@ -88,6 +106,19 @@ RSpec.describe API::Invitations do ...@@ -88,6 +106,19 @@ RSpec.describe API::Invitations do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end.to change { source.members.invite.count }.by(2) end.to change { source.members.invite.count }.by(2)
end end
it 'invites a list of new email addresses and user ids' do
expect do
stranger2 = create(:user)
email_list = [email, email2].join(',')
user_id_list = "#{stranger.id},#{stranger2.id}"
post invitations_url(source, maintainer),
params: { email: email_list, user_id: user_id_list, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
end.to change { source.members.invite.count }.by(2).and change { source.members.non_invite.count }.by(2)
end
end end
context 'access levels' do context 'access levels' do
...@@ -235,27 +266,36 @@ RSpec.describe API::Invitations do ...@@ -235,27 +266,36 @@ RSpec.describe API::Invitations do
expect(json_response['message'][developer.email]).to eq("User already exists in source") expect(json_response['message'][developer.email]).to eq("User already exists in source")
end end
it 'returns 404 when the email is not valid' do it 'returns 400 when the invite params of email and user_id are not sent' do
post invitations_url(source, maintainer),
params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('400 Bad request - Must provide either email or user_id as a parameter')
end
it 'returns 400 when the email is blank' do
post invitations_url(source, maintainer), post invitations_url(source, maintainer),
params: { email: '', access_level: Member::MAINTAINER } params: { email: '', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Invites cannot be blank') expect(json_response['message']).to eq('400 Bad request - Must provide either email or user_id as a parameter')
end end
it 'returns 404 when the email list is not a valid format' do it 'returns 400 when the user_id is blank' do
post invitations_url(source, maintainer), post invitations_url(source, maintainer),
params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER } params: { user_id: '', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('email contains an invalid email address') expect(json_response['message']).to eq('400 Bad request - Must provide either email or user_id as a parameter')
end end
it 'returns 400 when email is not given' do it 'returns 400 when the email list is not a valid format' do
post invitations_url(source, maintainer), post invitations_url(source, maintainer),
params: { access_level: Member::MAINTAINER } params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('email contains an invalid email address')
end end
it 'returns 400 when access_level is not given' do it 'returns 400 when access_level is not given' do
......
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