diff options
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 |