Commit 1addbab9 authored by Erick Bajao's avatar Erick Bajao Committed by Shinya Maeda

Support PAT authentication for visual reviews

This makes visual reviews work with private/internal projects.
This also has added benefit of using the user's GitLab username
and email.
parent b979c9b3
...@@ -188,9 +188,6 @@ With Visual Reviews, you can provide a feedback form to your Review Apps so ...@@ -188,9 +188,6 @@ With Visual Reviews, you can provide a feedback form to your Review Apps so
that reviewers can post comments directly from the app back to the merge request that reviewers can post comments directly from the app back to the merge request
that spawned the Review App. that spawned the Review App.
NOTE: **Note:** Visual Reviews currently only work for public projects. Support for private
and internal projects [is planned](https://gitlab.com/gitlab-org/gitlab/-/issues/42750).
### Configuring Visual Reviews ### Configuring Visual Reviews
Ensure that the `anonymous_visual_review_feedback` feature flag is enabled. Ensure that the `anonymous_visual_review_feedback` feature flag is enabled.
...@@ -218,6 +215,7 @@ looks like: ...@@ -218,6 +215,7 @@ looks like:
data-merge-request-id='1' data-merge-request-id='1'
data-mr-url='https://gitlab.example.com' data-mr-url='https://gitlab.example.com'
data-project-path='sarah/review-app-tester' data-project-path='sarah/review-app-tester'
data-require-auth='true'
id='review-app-toolbar-script' id='review-app-toolbar-script'
src='https://gitlab.example.com/assets/webpack/visual_review_toolbar.js'> src='https://gitlab.example.com/assets/webpack/visual_review_toolbar.js'>
</script> </script>
...@@ -235,6 +233,7 @@ to replace those values at runtime when each review app is created: ...@@ -235,6 +233,7 @@ to replace those values at runtime when each review app is created:
- `data-mr-url` is the URL of the GitLab instance and will be the same for all - `data-mr-url` is the URL of the GitLab instance and will be the same for all
review apps. review apps.
- `data-project-path` is the project's path, which can be found by `CI_PROJECT_PATH`. - `data-project-path` is the project's path, which can be found by `CI_PROJECT_PATH`.
- `data-require-auth` is optional for public projects but required for [private and internal ones](#visual-reviews-in-private-or-internal-projects). If this is set to `true`, the user will be required to enter their [personal access token](../../user/profile/personal_access_tokens.md) instead of their name and email.
- `id` is always `review-app-toolbar-script`, you don't need to change that. - `id` is always `review-app-toolbar-script`, you don't need to change that.
- `src` is the source of the review toolbar script, which resides in the - `src` is the source of the review toolbar script, which resides in the
respective GitLab instance and will be the same for all review apps. respective GitLab instance and will be the same for all review apps.
...@@ -272,6 +271,15 @@ can supply the ID by either:​​ ...@@ -272,6 +271,15 @@ can supply the ID by either:​​
- Dynamically adding the `data-merge-request-id` value during the build of the app. - Dynamically adding the `data-merge-request-id` value during the build of the app.
- Supplying it manually through the visual review form in the app. - Supplying it manually through the visual review form in the app.
### Visual Reviews in private or internal projects
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/42750#note_317271120) in GitLab 12.10.
To enable visual reviews for private and internal projects, set the
[`data-require-auth` variable](#configuring-visual-reviews) to `true`. When enabled,
the user must enter a [personal access token](../../user/profile/personal_access_tokens.md)
with `read_api` scope before submitting feedback.
### Using Visual Reviews ### Using Visual Reviews
After Visual Reviews has been [enabled](#configuring-visual-reviews) for the After Visual Reviews has been [enabled](#configuring-visual-reviews) for the
...@@ -285,7 +293,7 @@ To use the feedback form: ...@@ -285,7 +293,7 @@ To use the feedback form:
1. Make a comment on the visual review. You can make use of all the 1. Make a comment on the visual review. You can make use of all the
[Markdown annotations](../../user/markdown.md) that are also available in [Markdown annotations](../../user/markdown.md) that are also available in
merge request comments. merge request comments.
1. Submit your feedback anonymously or add your name. 1. If `data-require-auth` is `true`, you must enter your [personal access token](../../user/profile/personal_access_tokens.md). Otherwise, you must enter your name, and optionally, your email.
1. Finally, click **Send feedback**. 1. Finally, click **Send feedback**.
After you make and submit a comment in the visual review box, it will appear After you make and submit a comment in the visual review box, it will appear
......
# frozen_string_literal: true
module Notes
class CreateVisualReviewService < CreateService
def initialize(merge_request, current_user, body:, position: nil)
super(
merge_request.project,
User.visual_review_bot,
{
note: note_body(current_user, body),
position: position,
type: 'DiscussionNote',
noteable_type: 'MergeRequest',
noteable_id: merge_request.id
}
)
end
private
def note_body(user, body)
if user && body.present?
"**Feedback from @#{user.username} (#{user.email})**\n\n#{body}"
else
body
end
end
end
end
---
title: Support visual reviews on private and internal projects through PAT authentication
merge_request: 29336
author:
type: fixed
...@@ -36,15 +36,16 @@ module API ...@@ -36,15 +36,16 @@ module API
forbidden!('Anonymous visual review feedback is disabled') forbidden!('Anonymous visual review feedback is disabled')
end end
merge_request = find_merge_request_without_permissions_check(params[:merge_request_iid]) merge_request = find_merge_request(params[:merge_request_iid])
note = create_visual_review_note(merge_request, { note = ::Notes::CreateVisualReviewService.new(
note: params[:body], merge_request,
type: 'DiscussionNote', current_user,
noteable_type: 'MergeRequest', {
position: params[:position], body: params[:body],
noteable_id: merge_request.id position: params[:position]
}) }
).execute
if note.valid? if note.valid?
present note.discussion, with: Entities::Discussion present note.discussion, with: Entities::Discussion
......
...@@ -23,22 +23,14 @@ module EE ...@@ -23,22 +23,14 @@ module EE
end end
end end
# Used only for anonymous Visual Review Tools feedback # This is mainly used finding the target MR of the Visual Review note.
def find_merge_request_without_permissions_check(noteable_id) # If current_user is nil (PAT is not passed), only public merge requests can be found
params = finder_params_by_noteable_type_and_id(::MergeRequest, noteable_id) # If current_user is present (PAT is passed), private projects can be found as long as user is a project member.
# If current_user is present (PAT is passed), internal projects can be found by any authenticated user.
::NotesFinder.new(current_user, params).target || not_found!(noteable_type) def find_merge_request(merge_request_iid)
end params = finder_params_by_noteable_type_and_id(::MergeRequest, merge_request_iid)
def create_visual_review_note(noteable, opts) ::NotesFinder.new(current_user, params).target || not_found!(::MergeRequest)
unless ::Feature.enabled?(:anonymous_visual_review_feedback)
forbidden!('Anonymous visual review feedback is disabled')
end
parent = noteable_parent(noteable)
project = parent if parent.is_a?(Project)
::Notes::CreateService.new(project, ::User.visual_review_bot, opts).execute
end end
end end
end end
......
...@@ -3,19 +3,39 @@ ...@@ -3,19 +3,39 @@
require 'spec_helper' require 'spec_helper'
describe API::VisualReviewDiscussions do describe API::VisualReviewDiscussions do
let(:user) { create(:user) } shared_examples_for 'accepting request without authentication' do
let!(:project) { create(:project, :public, :repository, namespace: user.namespace) } let(:request) do
post api("/projects/#{project_id}/merge_requests/#{merge_request.iid}/visual_review_discussions"), params: note_params
context 'when sending merge request feedback from a visual review app without authentication' do
let!(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, author: user)
end end
it_behaves_like 'handling merge request feedback'
end
shared_examples_for 'accepting request with authentication' do
let(:token) { create(:personal_access_token) }
let(:user) { token.user }
let(:request) do let(:request) do
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/visual_review_discussions"), params: note_params post api("/projects/#{project_id}/merge_requests/#{merge_request.iid}/visual_review_discussions"), params: note_params, headers: { 'Private-Token' => token.token }
end
before do
create(:project_member,
user: user,
project: project,
access_level: ProjectMember::DEVELOPER)
end
it_behaves_like 'handling merge request feedback', :with_auth
end
shared_examples_for 'handling merge request feedback' do |with_auth|
let!(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project)
end end
let(:note_params) { { body: 'hi!' } } let(:project_id) { project.id }
let(:note_params) { { body: 'hi!', created_at: 2.weeks.ago } }
let(:response_note) { json_response['notes'].first } let(:response_note) { json_response['notes'].first }
it 'creates a new note' do it 'creates a new note' do
...@@ -53,8 +73,14 @@ describe API::VisualReviewDiscussions do ...@@ -53,8 +73,14 @@ describe API::VisualReviewDiscussions do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end end
it 'returns the persisted note body' do if with_auth
expect(response_note['body']).to eq('hi!') it 'returns the persisted note body including user details' do
expect(response_note['body']).to eq("**Feedback from @#{user.username} (#{user.email})**\n\nhi!")
end
else
it 'returns the persisted note body' do
expect(response_note['body']).to eq('hi!')
end
end end
it 'returns the name of the Visual Review Bot assigned as the author' do it 'returns the name of the Visual Review Bot assigned as the author' do
...@@ -64,6 +90,10 @@ describe API::VisualReviewDiscussions do ...@@ -64,6 +90,10 @@ describe API::VisualReviewDiscussions do
it 'returns the id of the merge request as the parent noteable_id' do it 'returns the id of the merge request as the parent noteable_id' do
expect(response_note['noteable_id']).to eq(merge_request.id) expect(response_note['noteable_id']).to eq(merge_request.id)
end end
it 'returns a current time stamp instead of the provided one' do
expect(Time.parse(response_note['created_at']) > 1.day.ago).to eq(true)
end
end end
context 'with no message body' do context 'with no message body' do
...@@ -76,10 +106,26 @@ describe API::VisualReviewDiscussions do ...@@ -76,10 +106,26 @@ describe API::VisualReviewDiscussions do
end end
end end
context 'with an invalid project ID' do
let(:project_id) { project.id + 1 }
it 'does not create a new note' do
expect { request }.not_to change(Note, :count)
end
describe 'the API response' do
it 'responds with a status 404' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with an invalid merge request IID' do context 'with an invalid merge request IID' do
let(:merge_request) { double(iid: 546574823564) } let(:merge_request) { double(iid: 546574823564) }
it 'creates a new note' do it 'does not create a new note' do
expect { request }.not_to change(Note, :count) expect { request }.not_to change(Note, :count)
end end
...@@ -115,43 +161,77 @@ describe API::VisualReviewDiscussions do ...@@ -115,43 +161,77 @@ describe API::VisualReviewDiscussions do
end end
end end
end end
end
shared_examples_for 'rejecting request without authentication' do
let!(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project)
end
context 'when an admin or owner makes an authenticated request' do let(:project_id) { project.id }
let(:request) do let(:note_params) { { body: 'hi!', created_at: 2.weeks.ago } }
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/visual_review_discussions", project.owner), params: note_params
let(:request) do
post api("/projects/#{project_id}/merge_requests/#{merge_request.iid}/visual_review_discussions"), params: note_params
end
it 'returns a 404 project not found' do
expect { request }.not_to change(merge_request.notes, :count)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when project is public' do
let!(:project) { create(:project, :public, :repository) }
it_behaves_like 'accepting request without authentication'
it_behaves_like 'accepting request with authentication'
end
context 'when project is private' do
let!(:project) { create(:project, :private, :repository) }
it_behaves_like 'accepting request with authentication'
it_behaves_like 'rejecting request without authentication'
context 'and authenticated user has no project access' do
let!(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project)
end end
let(:token) { create(:personal_access_token) }
let(:user) { token.user }
let(:project_id) { project.id }
let(:note_params) { { body: 'hi!', created_at: 2.weeks.ago } } let(:note_params) { { body: 'hi!', created_at: 2.weeks.ago } }
it 'creates a new note' do let(:request) do
expect { request }.to change(merge_request.notes, :count).by(1) post api("/projects/#{project_id}/merge_requests/#{merge_request.iid}/visual_review_discussions"), params: note_params, headers: { 'Private-Token' => token.token }
end end
describe 'the API response' do it 'returns a 404 project not found' do
before do expect { request }.not_to change(merge_request.notes, :count)
request
end
it 'responds with a status 201 Created' do expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:created) end
end end
end
it 'returns the persisted note body' do context 'when project is internal' do
expect(response_note['body']).to eq('hi!') let!(:project) { create(:project, :internal, :repository) }
end
it 'returns the name of the Visual Review Bot assigned as the author' do it_behaves_like 'accepting request with authentication'
expect(response_note['author']['username']).to eq(User.visual_review_bot.username) it_behaves_like 'rejecting request without authentication'
end
it 'returns the id of the merge request as the parent noteable_id' do context 'and authenticated user has no project access' do
expect(response_note['noteable_id']).to eq(merge_request.id) let(:token) { create(:personal_access_token) }
end let(:user) { token.user }
it 'returns a current time stamp instead of the provided one' do let(:request) do
expect(Time.parse(response_note['created_at']) > 1.day.ago).to eq(true) post api("/projects/#{project_id}/merge_requests/#{merge_request.iid}/visual_review_discussions"), params: note_params, headers: { 'Private-Token' => token.token }
end
end end
it_behaves_like 'handling merge request feedback', :with_auth
end end
end end
end end
...@@ -806,10 +806,10 @@ ...@@ -806,10 +806,10 @@
vue-loader "^15.4.2" vue-loader "^15.4.2"
vue-runtime-helpers "^1.1.2" vue-runtime-helpers "^1.1.2"
"@gitlab/visual-review-tools@1.5.1": "@gitlab/visual-review-tools@1.6.1":
version "1.5.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.5.1.tgz#2552927cd7a376f1f06ef3293a69fe2ffcdddb52" resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.6.1.tgz#0d8f3ff9f51b05f7c80b9a107727703d48997e4e"
integrity sha512-8d6xgK4TsLA5gucd78jzaMyginAMJ8cbu/6ghUGws84zzAEsyJsMTstyt/fA5l4toQXVxtOh90BvDzwxSjZ6hQ== integrity sha512-vY8K1igwZFoEOmU0h4E7XTLlilsQ4ylPr27O01UsSe6ZTKi6oEMREsRAEpNIUgRlxUARCsf+Opp4pgSFzFkFcw==
"@gitlab/vue-toasted@^1.3.0": "@gitlab/vue-toasted@^1.3.0":
version "1.3.0" version "1.3.0"
......
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