summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/openapi/index.js46
-rw-r--r--app/assets/javascripts/lib/swagger.js43
-rw-r--r--app/controllers/projects/artifacts_controller.rb11
-rw-r--r--app/controllers/sandbox_controller.rb4
-rw-r--r--app/views/projects/artifacts/_file_navigation.html.haml12
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml8
-rw-r--r--app/views/projects/artifacts/external_file.html.haml15
-rw-r--r--app/views/projects/artifacts/file.html.haml14
-rw-r--r--app/views/sandbox/swagger.html.erb9
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/webpack.config.js1
-rw-r--r--lib/gitlab/content_security_policy/config_loader.rb2
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb37
-rw-r--r--spec/features/projects/artifacts/user_browses_artifacts_spec.rb11
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb8
-rw-r--r--spec/frontend/blob/openapi/index_spec.js21
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb4
19 files changed, 182 insertions, 75 deletions
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 44b75cc3e68..943001b7ec4 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -1,23 +1,29 @@
-import { SwaggerUIBundle } from 'swagger-ui-dist';
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
+import { setAttributes } from '~/lib/utils/dom_utils';
+import axios from '~/lib/utils/axios_utils';
-export default () => {
- const el = document.getElementById('js-openapi-viewer');
+const createSandbox = () => {
+ const iframeEl = document.createElement('iframe');
+ setAttributes(iframeEl, {
+ src: '/-/sandbox/swagger',
+ sandbox: 'allow-scripts',
+ frameBorder: 0,
+ width: '100%',
+ // The height will be adjusted dynamically.
+ // Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/377969
+ height: '1000',
+ });
+ return iframeEl;
+};
+
+export default async () => {
+ const wrapperEl = document.getElementById('js-openapi-viewer');
+ const sandboxEl = createSandbox();
+
+ const { data } = await axios.get(wrapperEl.dataset.endpoint);
+
+ wrapperEl.appendChild(sandboxEl);
- Promise.all([import(/* webpackChunkName: 'openapi' */ 'swagger-ui-dist/swagger-ui.css')])
- .then(() => {
- SwaggerUIBundle({
- url: el.dataset.endpoint,
- dom_id: '#js-openapi-viewer',
- deepLinking: true,
- displayOperationId: true,
- });
- })
- .catch((error) => {
- createAlert({
- message: __('Something went wrong while initializing the OpenAPI viewer'),
- });
- throw error;
- });
+ sandboxEl.addEventListener('load', () => {
+ sandboxEl.contentWindow.postMessage(data, '*');
+ });
};
diff --git a/app/assets/javascripts/lib/swagger.js b/app/assets/javascripts/lib/swagger.js
new file mode 100644
index 00000000000..ed646176604
--- /dev/null
+++ b/app/assets/javascripts/lib/swagger.js
@@ -0,0 +1,43 @@
+import { SwaggerUIBundle } from 'swagger-ui-dist';
+import { safeLoad } from 'js-yaml';
+import { isObject } from '~/lib/utils/type_utility';
+
+const renderSwaggerUI = (value) => {
+ /* SwaggerUIBundle accepts openapi definition
+ * in only JSON format, so we convert the YAML
+ * config to JSON if it's not JSON value
+ */
+ let spec = value;
+ if (!isObject(spec)) {
+ spec = safeLoad(spec, { json: true });
+ }
+
+ Promise.all([import(/* webpackChunkName: 'openapi' */ 'swagger-ui-dist/swagger-ui.css')])
+ .then(() => {
+ SwaggerUIBundle({
+ spec,
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ displayOperationId: true,
+ });
+ })
+ .catch((error) => {
+ throw error;
+ });
+};
+
+const addInitHook = () => {
+ window.addEventListener(
+ 'message',
+ (event) => {
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+ renderSwaggerUI(event.data);
+ },
+ false,
+ );
+};
+
+addInitHook();
+export default {};
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 997d321ac24..40e89a06b46 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -14,7 +14,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_destroy_artifacts!, only: [:destroy]
before_action :extract_ref_name_and_path
before_action :validate_artifacts!, except: [:index, :download, :raw, :destroy]
- before_action :entry, only: [:file]
+ before_action :entry, only: [:external_file, :file]
MAX_PER_PAGE = 20
@@ -58,12 +58,19 @@ class Projects::ArtifactsController < Projects::ApplicationController
render_404 unless @entry.exists?
end
+ # External files are redirected to Gitlab Pages and might have unsecure content
+ # To warn the user about the possible unsecure content, we show a warning page
+ # before redirecting the user.
+ def external_file
+ @blob = @entry.blob
+ end
+
def file
blob = @entry.blob
conditionally_expand_blob(blob)
if blob.external_link?(build)
- redirect_to blob.external_url(@project, build)
+ redirect_to external_file_project_job_artifacts_path(@project, @build, path: params[:path])
else
respond_to do |format|
format.html do
diff --git a/app/controllers/sandbox_controller.rb b/app/controllers/sandbox_controller.rb
index a48b2b8a314..dffe6797831 100644
--- a/app/controllers/sandbox_controller.rb
+++ b/app/controllers/sandbox_controller.rb
@@ -8,4 +8,8 @@ class SandboxController < ApplicationController # rubocop:disable Gitlab/Namespa
def mermaid
render layout: false
end
+
+ def swagger
+ render layout: false
+ end
end
diff --git a/app/views/projects/artifacts/_file_navigation.html.haml b/app/views/projects/artifacts/_file_navigation.html.haml
new file mode 100644
index 00000000000..e9109451a69
--- /dev/null
+++ b/app/views/projects/artifacts/_file_navigation.html.haml
@@ -0,0 +1,12 @@
+.nav-block
+ %ul.breadcrumb.repo-breadcrumb
+ %li.breadcrumb-item
+ = link_to _('Artifacts'), browse_project_job_artifacts_path(project, build)
+ - path_breadcrumbs do |title, breadcrumb|
+ - title = truncate(title, length: 40)
+ %li.breadcrumb-item
+ - if path == breadcrumb
+ = link_to file_project_job_artifacts_path(project, build, breadcrumb) do
+ %strong= title
+ - else
+ = link_to title, browse_project_job_artifacts_path(project, build, breadcrumb)
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index 03d35c1c989..e120975a8f9 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -1,13 +1,15 @@
- blob = file.blob
-- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
- external_link = blob.external_link?(@build)
+- if external_link
+ - path_to_file = external_file_project_job_artifacts_path(@project, @build, path: file.path)
+- else
+ - path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
%tr.tree-item.js-artifact-tree-row{ data: { link: path_to_file, external_link: "#{external_link}" } }
%td.tree-item-file-name
= tree_icon('file', blob.mode, blob.name)
- if external_link
- = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip str-truncated',
- target: '_blank', rel: 'noopener noreferrer', title: _('Opens in a new window') do
+ = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip str-truncated' do
%span>= blob.name
= sprite_icon('external-link', css_class: 'js-artifact-tree-external-icon')
- else
diff --git a/app/views/projects/artifacts/external_file.html.haml b/app/views/projects/artifacts/external_file.html.haml
new file mode 100644
index 00000000000..a014d134e31
--- /dev/null
+++ b/app/views/projects/artifacts/external_file.html.haml
@@ -0,0 +1,15 @@
+- page_title @path, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs')
+
+= render "projects/jobs/header"
+
+.tree-holder
+ = render 'projects/artifacts/file_navigation', project: @project, build: @build, path: @path
+
+ %h2= _("You are being redirected away from GitLab")
+ %p= _("This page is hosted on GitLab pages but contains user-generated content and may contain malicious code. Do not accept unless you trust the author and source.")
+
+ = link_to @blob.external_url(@project, @build),
+ @blob.external_url(@project, @build),
+ target: '_blank',
+ title: _('Opens in a new window'),
+ rel: 'noopener noreferrer'
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index e16e3ef266d..5b9e5ad584f 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -4,19 +4,7 @@
= render "projects/jobs/header"
.tree-holder
- .nav-block
- %ul.breadcrumb.repo-breadcrumb
- %li.breadcrumb-item
- = link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build)
- - path_breadcrumbs do |title, path|
- - title = truncate(title, length: 40)
- %li.breadcrumb-item
- - if path == @path
- = link_to file_project_job_artifacts_path(@project, @build, path) do
- %strong= title
- - else
- = link_to title, browse_project_job_artifacts_path(@project, @build, path)
-
+ = render 'projects/artifacts/file_navigation', project: @project, build: @build, path: @path
%article.file-holder
- blob = @entry.blob
diff --git a/app/views/sandbox/swagger.html.erb b/app/views/sandbox/swagger.html.erb
new file mode 100644
index 00000000000..ab3c36e5f4a
--- /dev/null
+++ b/app/views/sandbox/swagger.html.erb
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <%= webpack_bundle_tag("sandboxed_swagger") %>
+ </head>
+ <body>
+ <div id="swagger-ui"></div>
+ </body>
+</html>
diff --git a/config/routes.rb b/config/routes.rb
index 28c08e9bbe7..6cfbb969acb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -119,6 +119,7 @@ InitializerConnections.with_disabled_database_connections do
# sandbox
get '/sandbox/mermaid' => 'sandbox#mermaid'
+ get '/sandbox/swagger' => 'sandbox#swagger'
get '/whats_new' => 'whats_new#index'
diff --git a/config/routes/project.rb b/config/routes/project.rb
index cd9315ba2aa..0b0d370223c 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -86,6 +86,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
+ get :external_file, path: 'external_file/*path', format: false
get :raw, path: 'raw/*path', format: false
post :keep
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 05523952769..d79e6e12b39 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -165,6 +165,7 @@ function generateEntries() {
jira_connect_app: './jira_connect/subscriptions/index.js',
sandboxed_mermaid: './lib/mermaid.js',
redirect_listbox: './entrypoints/behaviors/redirect_listbox.js',
+ sandboxed_swagger: './lib/swagger.js',
};
return Object.assign(manualEntries, incrementalCompiler.filterEntryPoints(autoEntries));
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index 8648ffe5f49..f1faade250e 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -154,7 +154,7 @@ module Gitlab
# Using 'self' in the CSP introduces several CSP bypass opportunities
# for this reason we list the URLs where GitLab frames itself instead
def self.allow_framed_gitlab_paths(directives)
- ['/admin/', '/assets/', '/-/speedscope/index.html', '/-/sandbox/mermaid'].map do |path|
+ ['/admin/', '/assets/', '/-/speedscope/index.html', '/-/sandbox/'].map do |path|
append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path))
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 545a2ca4855..8160d48fd95 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -38137,9 +38137,6 @@ msgstr ""
msgid "Something went wrong while fetching the packages list."
msgstr ""
-msgid "Something went wrong while initializing the OpenAPI viewer"
-msgstr ""
-
msgid "Something went wrong while obtaining the Let's Encrypt certificate."
msgstr ""
@@ -41564,6 +41561,9 @@ msgstr ""
msgid "This only applies to repository indexing operations."
msgstr ""
+msgid "This page is hosted on GitLab pages but contains user-generated content and may contain malicious code. Do not accept unless you trust the author and source."
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
@@ -46074,6 +46074,9 @@ msgstr ""
msgid "You are attempting to update a file that has changed since you started editing it."
msgstr ""
+msgid "You are being redirected away from GitLab"
+msgstr ""
+
msgid "You are billed if you exceed this number. %{qsrOverageLinkStart}How does billing work?%{qsrOverageLinkEnd}"
msgstr ""
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 808e67eff3d..f79a2c6a6d0 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -262,6 +262,31 @@ RSpec.describe Projects::ArtifactsController do
end
end
+ describe 'GET external_file' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context 'when the file exists' do
+ it 'renders the file view' do
+ path = 'ci_artifacts.txt'
+
+ get :external_file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the file does not exist' do
+ it 'responds Not Found' do
+ get :external_file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'GET file' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
@@ -274,17 +299,11 @@ RSpec.describe Projects::ArtifactsController do
context 'when the file exists' do
it 'renders the file view' do
- get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt' }
+ path = 'ci_artifacts.txt'
- expect(response).to have_gitlab_http_status(:found)
- end
- end
-
- context 'when the file does not exist' do
- it 'responds Not Found' do
- get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown' }
+ get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
- expect(response).to be_not_found
+ expect(response).to redirect_to(external_file_project_job_artifacts_path(project, job, path: path))
end
end
end
diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
index 2d09f5a4263..c0d710fe186 100644
--- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
+++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
@@ -81,12 +81,11 @@ RSpec.describe "User browses artifacts" do
end
it "shows correct content" do
- link = first(".tree-item-file-external-link")
-
- expect(link[:target]).to eq("_blank")
- expect(link[:rel]).to include("noopener").and include("noreferrer")
- expect(page).to have_link("doc_sample.txt", href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path))
- .and have_selector(".js-artifact-tree-external-icon")
+ expect(page)
+ .to have_link(
+ "doc_sample.txt",
+ href: external_file_project_job_artifacts_path(project, job, path: txt_entry.blob.path)
+ ).and have_selector(".js-artifact-tree-external-icon")
page.within(".tree-table") do
expect(page).to have_content("..").and have_content("another-subdirectory")
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 93e5be18229..d679d1eeeb9 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -1001,11 +1001,9 @@ RSpec.describe 'File blob', :js do
wait_for_requests
end
- it 'removes `style`, `class`, and `data-*`` attributes from HTML' do
- expect(page).to have_css('h1', text: 'Swagger API documentation')
- expect(page).not_to have_css('.foo-bar')
- expect(page).not_to have_css('[style="background-color: red;"]')
- expect(page).not_to have_css('[data-foo-bar="baz"]')
+ it 'renders sandboxed iframe' do
+ expected = %(<iframe src="/-/sandbox/swagger" sandbox="allow-scripts" frameborder="0" width="100%" height="1000">)
+ expect(page.html).to include(expected)
end
end
end
diff --git a/spec/frontend/blob/openapi/index_spec.js b/spec/frontend/blob/openapi/index_spec.js
index 53220809f80..5884b27d951 100644
--- a/spec/frontend/blob/openapi/index_spec.js
+++ b/spec/frontend/blob/openapi/index_spec.js
@@ -1,28 +1,27 @@
-import { SwaggerUIBundle } from 'swagger-ui-dist';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import renderOpenApi from '~/blob/openapi';
-jest.mock('swagger-ui-dist');
-
describe('OpenAPI blob viewer', () => {
const id = 'js-openapi-viewer';
const mockEndpoint = 'some/endpoint';
+ let mock;
- beforeEach(() => {
+ beforeEach(async () => {
setHTMLFixture(`<div id="${id}" data-endpoint="${mockEndpoint}"></div>`);
- renderOpenApi();
+ mock = new MockAdapter(axios).onGet().reply(200);
+ await renderOpenApi();
});
afterEach(() => {
resetHTMLFixture();
+ mock.restore();
});
it('initializes SwaggerUI with the correct configuration', () => {
- expect(SwaggerUIBundle).toHaveBeenCalledWith({
- url: mockEndpoint,
- dom_id: `#${id}`,
- deepLinking: true,
- displayOperationId: true,
- });
+ expect(document.body.innerHTML).toContain(
+ '<iframe src="/-/sandbox/swagger" sandbox="allow-scripts" frameborder="0" width="100%" height="1000"></iframe>',
+ );
});
});
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index 616fe15c1a6..6b1d8d8d1af 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -85,7 +85,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://cdn.example.com")
expect(directives['font_src']).to eq("'self' https://cdn.example.com")
expect(directives['worker_src']).to eq('http://localhost/assets/ blob: data: https://cdn.example.com')
- expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/mermaid")
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/")
end
end
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds CUSTOMER_PORTAL_URL to CSP' do
- expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/mermaid #{customer_portal_url}")
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/ #{customer_portal_url}")
end
end