Commit 3c2b4a1c authored by Ahmad Sherif's avatar Ahmad Sherif

Enable serving static objects from an external storage

It consists of two parts:

1. Redirecting users to the configured external storage
1. Allowing the external storage to request the static object(s)
   on behalf of the user by means of specific tokens

Part of https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/6829
parent f7e7ee71
......@@ -2,10 +2,10 @@
# == SessionlessAuthentication
#
# Controller concern to handle PAT and RSS token authentication methods
# Controller concern to handle PAT, RSS, and static objects token authentication methods
#
module SessionlessAuthentication
# This filter handles personal access tokens, and atom requests with rss tokens
# This filter handles personal access tokens, atom requests with rss tokens, and static object tokens
def authenticate_sessionless_user!(request_format)
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format)
......
# frozen_string_literal: true
module StaticObjectExternalStorage
extend ActiveSupport::Concern
included do
include ApplicationHelper
end
def redirect_to_external_storage
return if external_storage_request?
redirect_to external_storage_url_or_path(request.fullpath, project)
end
def external_storage_request?
header_token = request.headers['X-Gitlab-External-Storage-Token']
return false unless header_token.present?
external_storage_token = Gitlab::CurrentSettings.static_objects_external_storage_auth_token
ActiveSupport::SecurityUtils.secure_compare(header_token, external_storage_token) ||
raise(Gitlab::Access::AccessDeniedError)
end
end
......@@ -46,6 +46,15 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_personal_access_tokens_path
end
def reset_static_object_token
Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_static_object_token!
end
redirect_to profile_personal_access_tokens_path,
notice: s_('Profiles|Static object token was successfully reset')
end
# rubocop: disable CodeReuse/ActiveRecord
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id)
......
......@@ -2,6 +2,9 @@
class Projects::RepositoriesController < Projects::ApplicationController
include ExtractsPath
include StaticObjectExternalStorage
prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) }
# Authorize
before_action :require_non_empty_project, except: :create
......@@ -9,6 +12,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
before_action :assign_append_sha, only: :archive
before_action :authorize_download_code!
before_action :authorize_admin_project!, only: :create
before_action :redirect_to_external_storage, only: :archive, if: :static_objects_external_storage_enabled?
def create
@project.create_repository
......
......@@ -169,6 +169,25 @@ module ApplicationHelper
Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
def static_objects_external_storage_enabled?
Gitlab::CurrentSettings.static_objects_external_storage_enabled?
end
def external_storage_url_or_path(path, project = @project)
return path unless static_objects_external_storage_enabled?
uri = URI(Gitlab::CurrentSettings.static_objects_external_storage_url)
path = URI(path) # `path` could have query parameters, so we need to split query and path apart
query = Rack::Utils.parse_nested_query(path.query)
query['token'] = current_user.static_object_token unless project.public?
uri.path = path.path
uri.query = query.to_query unless query.empty?
uri.to_s
end
def page_filter_path(options = {})
without = options.delete(:without)
......
......@@ -168,6 +168,8 @@ module ApplicationSettingsHelper
:asset_proxy_secret_key,
:asset_proxy_url,
:asset_proxy_whitelist,
:static_objects_external_storage_auth_token,
:static_objects_external_storage_url,
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
......
......@@ -8,6 +8,7 @@ class ApplicationSetting < ApplicationRecord
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token
belongs_to :instance_administration_project, class_name: "Project"
......@@ -211,6 +212,13 @@ class ApplicationSetting < ApplicationRecord
allow_blank: false,
if: :asset_proxy_enabled?
validates :static_objects_external_storage_url,
addressable_url: true, allow_blank: true
validates :static_objects_external_storage_auth_token,
presence: true,
if: :static_objects_external_storage_url?
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
......
......@@ -306,6 +306,10 @@ module ApplicationSettingImplementation
archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
end
def static_objects_external_storage_enabled?
static_objects_external_storage_url.present?
end
private
def array_to_string(arr)
......
......@@ -31,6 +31,7 @@ class User < ApplicationRecord
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
add_authentication_token_field :static_object_token
default_value_for :admin, false
default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
......@@ -1437,6 +1438,13 @@ class User < ApplicationRecord
ensure_feed_token!
end
# Each existing user needs to have a `static_object_token`.
# We do this on read since migrating all existing users is not a feasible
# solution.
def static_object_token
ensure_static_object_token!
end
def sync_attribute?(attribute)
return true if ldap_user? && attribute == :email
......
= form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-static-objects-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :static_objects_external_storage_url, class: 'label-bold' do
= _('External storage URL')
= f.text_field :static_objects_external_storage_url, class: 'form-control'
%span.form-text.text-muted#static_objects_external_storage_url_help_block
= _('URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...).')
.form-group
= f.label :static_objects_external_storage_auth_token, class: 'label-bold' do
= _('External storage authentication token')
= f.text_field :static_objects_external_storage_auth_token, class: 'form-control'
%span.form-text.text-muted#static_objects_external_storage_auth_token_help_block
= _('A secure token that identifies an external storage request.')
= f.submit _('Save changes'), class: "btn btn-success"
......@@ -34,3 +34,14 @@
= _('Configure automatic git checks and housekeeping on repositories.')
.settings-content
= render 'repository_check'
%section.settings.as-repository-static-objects.no-animate#js-repository-static-objects-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Repository static objects')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN).')
.settings-content
= render 'repository_static_objects'
......@@ -54,3 +54,23 @@
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
= reset_message.html_safe
- if static_objects_external_storage_enabled?
%hr
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
= s_('AccessTokens|Static object token')
%p
= s_('AccessTokens|Your static object token is used to authenticate you when repository static objects (e.g. archives, blobs, ...) are being served from an external storage.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8
= label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
= text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.form-text.text-muted
- reset_link = url_for [:reset, :static_object_token, :profile]
- reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
- reset_link_end = '</a>'.html_safe
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can access repository static objects as if they were you. You should %{reset_link_start}reset it%{reset_link_end} if that ever happens.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
= reset_message.html_safe
......@@ -2,4 +2,5 @@
.btn-group.ml-0.w-100
- formats.each do |(fmt, extra_class)|
= link_to fmt, project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
- archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
= link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
---
title: Enable serving static objects from an external storage
merge_request: 31025
author:
type: added
......@@ -8,6 +8,7 @@ resource :profile, only: [:show, :update] do
put :reset_incoming_email_token
put :reset_feed_token
put :reset_static_object_token
put :update_username
end
......
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddStaticObjectTokenToUsers < ActiveRecord::Migration[5.2]
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :users, :static_object_token, :string, limit: 255
end
def down
remove_column :users, :static_object_token
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddStaticObjectsExternalStorageColumnsToApplicationSettings < ActiveRecord::Migration[5.2]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :application_settings, :static_objects_external_storage_url, :string, limit: 255
add_column :application_settings, :static_objects_external_storage_auth_token, :string, limit: 255
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexToIndexOnStaticObjectToken < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :users, :static_object_token, unique: true
end
def down
remove_concurrent_index :users, :static_object_token
end
end
......@@ -284,6 +284,8 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.text "asset_proxy_whitelist"
t.text "encrypted_asset_proxy_secret_key"
t.string "encrypted_asset_proxy_secret_key_iv"
t.string "static_objects_external_storage_url", limit: 255
t.string "static_objects_external_storage_auth_token", limit: 255
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......@@ -3548,6 +3550,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.integer "bot_type", limit: 2
t.string "first_name", limit: 255
t.string "last_name", limit: 255
t.string "static_object_token", limit: 255
t.index ["accepted_term_id"], name: "index_users_on_accepted_term_id"
t.index ["admin"], name: "index_users_on_admin"
t.index ["bot_type"], name: "index_users_on_bot_type"
......@@ -3567,6 +3570,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.index ["state"], name: "index_users_on_state"
t.index ["state"], name: "index_users_on_state_and_internal", where: "(ghost IS NOT TRUE)"
t.index ["state"], name: "index_users_on_state_and_internal_ee", where: "((ghost IS NOT TRUE) AND (bot_type IS NULL))"
t.index ["static_object_token"], name: "index_users_on_static_object_token", unique: true
t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)"
t.index ["username"], name: "index_users_on_username"
t.index ["username"], name: "index_users_on_username_trigram", opclass: :gin_trgm_ops, using: :gin
......
......@@ -142,6 +142,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Repository storage types](repository_storage_types.md): Information about the different repository storage types.
- [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage.
- [Limit repository size](../user/admin_area/settings/account_and_limit_settings.md): Set a hard limit for your repositories' size. **(STARTER ONLY)**
- [Static objects external storage](static_objects_external_storage.md): Set external storage for static objects in a repository.
## Continuous Integration settings
......
# Static objects external storage
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31025) in GitLab 12.3.
GitLab can be configured to serve repository static objects (for example, archives) from an external
storage, such as a CDN.
## Configuring
To configure external storage for static objects:
1. Navigate to **Admin Area > Settings > Repository**.
1. Expand the **Repository static objects** section.
1. Enter the base URL and an arbitrary token.
The token is required to distinguish requests coming from the external storage, so users don't
circumvent the external storage and go for the application directly. The token is expected to be
set in the `X-Gitlab-External-Storage-Token` header in requests originating from the external
storage.
## Serving private static objects
GitLab will append a user-specific token for static object URLs that belong to private projects,
so an external storage can be authenticated on behalf of the user. When processing requests originating
from the external storage, GitLab will look for the token in the `token` query parameter or in
the `X-Gitlab-Static-Object-Token` header to check the user's ability to access the requested object.
## Requests flow example
The following example shows a sequence of requests and responses between the user,
GitLab, and the CDN:
```mermaid
sequenceDiagram
User->>GitLab: GET /project/-/archive/master.zip
GitLab->>User: 302 Found
Note over User,GitLab: Location: https://cdn.com/project/-/archive/master.zip?token=secure-user-token
User->>CDN: GET /project/-/archive/master.zip?token=secure-user-token
alt object not in cache
CDN->>GitLab: GET /project/-/archive/master.zip
Note over CDN,GitLab: X-Gitlab-External-Storage-Token: secure-cdn-token<br/>X-Gitlab-Static-Object-Token: secure-user-token
GitLab->>CDN: 200 OK
CDN->>User: master.zip
else object in cache
CDN->>GitLab: GET /project/-/archive/master.zip
Note over CDN,GitLab: X-Gitlab-External-Storage-Token: secure-cdn-token<br/>X-Gitlab-Static-Object-Token: secure-user-token<br/>If-None-Match: etag-value
GitLab->>CDN: 304 Not Modified
CDN->>User: master.zip
end
```
......@@ -24,7 +24,9 @@ module Gitlab
end
def find_sessionless_user(request_format)
find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format)
find_user_from_web_access_token(request_format) ||
find_user_from_feed_token(request_format) ||
find_user_from_static_object_token(request_format)
rescue Gitlab::Auth::AuthenticationError
nil
end
......
......@@ -28,6 +28,15 @@ module Gitlab
current_request.env['warden']&.authenticate if verified_request?
end
def find_user_from_static_object_token(request_format)
return unless valid_static_objects_format?(request_format)
token = current_request.params[:token].presence || current_request.headers['X-Gitlab-Static-Object-Token'].presence
return unless token
User.find_by_static_object_token(token) || raise(UnauthorizedError)
end
def find_user_from_feed_token(request_format)
return unless valid_rss_format?(request_format)
......@@ -154,6 +163,15 @@ module Gitlab
end
end
def valid_static_objects_format?(request_format)
case request_format
when :archive
archive_request?
else
false
end
end
def rss_request?
current_request.path.ends_with?('.atom') || current_request.format.atom?
end
......@@ -165,6 +183,10 @@ module Gitlab
def api_request?
current_request.path.starts_with?("/api/")
end
def archive_request?
current_request.path.include?('/-/archive/')
end
end
end
end
......@@ -517,6 +517,9 @@ msgstr ""
msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
msgstr ""
msgid "A secure token that identifies an external storage request."
msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
......@@ -568,6 +571,9 @@ msgstr ""
msgid "AccessTokens|Access Tokens"
msgstr ""
msgid "AccessTokens|Are you sure?"
msgstr ""
msgid "AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working."
msgstr ""
......@@ -586,6 +592,9 @@ msgstr ""
msgid "AccessTokens|It cannot be used to access any other data."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can access repository static objects as if they were you. You should %{reset_link_start}reset it%{reset_link_end} if that ever happens."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens."
msgstr ""
......@@ -595,6 +604,9 @@ msgstr ""
msgid "AccessTokens|Personal Access Tokens"
msgstr ""
msgid "AccessTokens|Static object token"
msgstr ""
msgid "AccessTokens|They are the only accepted password when you have Two-Factor Authentication (2FA) enabled."
msgstr ""
......@@ -610,6 +622,9 @@ msgstr ""
msgid "AccessTokens|Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses."
msgstr ""
msgid "AccessTokens|Your static object token is used to authenticate you when repository static objects (e.g. archives, blobs, ...) are being served from an external storage."
msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
......@@ -4852,6 +4867,12 @@ msgstr ""
msgid "External authorization request timeout"
msgstr ""
msgid "External storage URL"
msgstr ""
msgid "External storage authentication token"
msgstr ""
msgid "ExternalAuthorizationService|Classification label"
msgstr ""
......@@ -8668,6 +8689,9 @@ msgstr ""
msgid "Profiles|Some options are unavailable for LDAP accounts"
msgstr ""
msgid "Profiles|Static object token was successfully reset"
msgstr ""
msgid "Profiles|Tell us about yourself in fewer than 250 characters"
msgstr ""
......@@ -9693,6 +9717,9 @@ msgstr ""
msgid "Repository mirror"
msgstr ""
msgid "Repository static objects"
msgstr ""
msgid "Repository storage"
msgstr ""
......@@ -10244,6 +10271,9 @@ msgstr ""
msgid "September"
msgstr ""
msgid "Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN)."
msgstr ""
msgid "Server supports batch API only, please update your Git LFS client to version 1.0.1 and up."
msgstr ""
......@@ -12421,6 +12451,9 @@ msgstr ""
msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
msgstr ""
msgid "URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...)."
msgstr ""
msgid "Unable to apply suggestions to a deleted line."
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe StaticObjectExternalStorage do
controller(Projects::ApplicationController) do
include StaticObjectExternalStorage # rubocop:disable RSpec/DescribedClass
before_action :redirect_to_external_storage, if: :static_objects_external_storage_enabled?
def show
head :ok
end
end
let(:project) { create(:project, :public) }
let(:user) { create(:user, static_object_token: 'hunter1') }
before do
project.add_developer(user)
sign_in(user)
end
context 'when external storage is not configured' do
it 'calls the action normally' do
expect(Gitlab::CurrentSettings.static_objects_external_storage_url).to be_blank
do_request
expect(response).to have_gitlab_http_status(200)
end
end
context 'when external storage is configured' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_auth_token).and_return('letmein')
routes.draw { get '/:namespace_id/:id' => 'projects/application#show' }
end
context 'when external storage token is empty' do
let(:base_redirect_url) { "https://cdn.gitlab.com/#{project.namespace.to_param}/#{project.to_param}" }
context 'when project is public' do
it 'redirects to external storage URL without adding a token parameter' do
do_request
expect(response).to redirect_to(base_redirect_url)
end
end
context 'when project is not public' do
let(:project) { create(:project, :private) }
it 'redirects to external storage URL a token parameter added' do
do_request
expect(response).to redirect_to("#{base_redirect_url}?token=#{user.static_object_token}")
end
context 'when path includes extra parameters' do
it 'includes the parameters in the redirect URL' do
do_request(foo: 'bar')
expect(response.location).to eq("#{base_redirect_url}?foo=bar&token=#{user.static_object_token}")
end
end
end
end
context 'when external storage token is present' do
context 'when token is correct' do
it 'calls the action normally' do
request.headers['X-Gitlab-External-Storage-Token'] = 'letmein'
do_request
expect(response).to have_gitlab_http_status(200)
end
end
context 'when token is incorrect' do
it 'return 403' do
request.headers['X-Gitlab-External-Storage-Token'] = 'donotletmein'
do_request
expect(response).to have_gitlab_http_status(403)
end
end
end
end
def do_request(extra_params = {})
get :show, params: { namespace_id: project.namespace, id: project }.merge(extra_params)
end
end
......@@ -125,5 +125,59 @@ describe Projects::RepositoriesController do
end
end
end
context 'as a sessionless user' do
let(:user) { create(:user) }
before do
project.add_developer(user)
end
context 'when no token is provided' do
it 'redirects to sign in page' do
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
expect(response).to have_gitlab_http_status(302)
end
end
context 'when a token param is present' do
context 'when token is correct' do
it 'calls the action normally' do
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: user.static_object_token }, format: 'zip'
expect(response).to have_gitlab_http_status(200)
end
end
context 'when token is incorrect' do
it 'redirects to sign in page' do
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: 'foobar' }, format: 'zip'
expect(response).to have_gitlab_http_status(302)
end
end
end
context 'when a token header is present' do
context 'when token is correct' do
it 'calls the action normally' do
request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
expect(response).to have_gitlab_http_status(200)
end
end
context 'when token is incorrect' do
it 'redirects to sign in page' do
request.headers['X-Gitlab-Static-Object-Token'] = 'foobar'
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
expect(response).to have_gitlab_http_status(302)
end
end
end
end
end
end
......@@ -29,6 +29,11 @@ describe 'Download buttons in branches page' do
end
describe 'when checking branches' do
it_behaves_like 'archive download buttons' do
let(:ref) { 'binary-encoding' }
let(:path_to_visit) { project_branches_filtered_path(project, state: 'all', search: ref) }
end
context 'with artifacts' do
before do
visit project_branches_filtered_path(project, state: 'all', search: 'binary-encoding')
......
......@@ -24,11 +24,17 @@ describe 'Projects > Files > Download buttons in files tree' do
before do
sign_in(user)
project.add_developer(user)
end
visit project_tree_path(project, project.default_branch)
it_behaves_like 'archive download buttons' do
let(:path_to_visit) { project_tree_path(project, project.default_branch) }
end
context 'with artifacts' do
before do
visit project_tree_path(project, project.default_branch)
end
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
......
......@@ -29,6 +29,8 @@ describe 'Projects > Show > Download buttons' do
end
describe 'when checking project main page' do
it_behaves_like 'archive download buttons'
context 'with artifacts' do
before do
visit project_path(project)
......
......@@ -30,6 +30,11 @@ describe 'Download buttons in tags page' do
end
describe 'when checking tags' do
it_behaves_like 'archive download buttons' do
let(:path_to_visit) { project_tags_path(project) }
let(:ref) { tag }
end
context 'with artifacts' do
before do
visit project_tags_path(project)
......
......@@ -195,4 +195,41 @@ describe ApplicationHelper do
end
end
end
describe '#external_storage_url_or_path' do
let(:project) { create(:project) }
context 'when external storage is disabled' do
it 'returns the passed path' do
expect(helper.external_storage_url_or_path('/foo/bar', project)).to eq('/foo/bar')
end
end
context 'when external storage is enabled' do
let(:user) { create(:user, static_object_token: 'hunter1') }
before do
allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
allow(helper).to receive(:current_user).and_return(user)
end
it 'returns the external storage URL prepended to the path' do
expect(helper.external_storage_url_or_path('/foo/bar', project)).to eq("https://cdn.gitlab.com/foo/bar?token=#{user.static_object_token}")
end
it 'preserves the path query parameters' do
url = helper.external_storage_url_or_path('/foo/bar?unicode=1', project)
expect(url).to eq("https://cdn.gitlab.com/foo/bar?token=#{user.static_object_token}&unicode=1")
end
context 'when project is public' do
let(:project) { create(:project, :public) }
it 'returns does not append a token parameter' do
expect(helper.external_storage_url_or_path('/foo/bar', project)).to eq('https://cdn.gitlab.com/foo/bar')
end
end
end
end
end
......@@ -115,6 +115,60 @@ describe Gitlab::Auth::UserAuthFinders do
end
end
describe '#find_user_from_static_object_token' do
context 'when request format is archive' do
before do
env['SCRIPT_NAME'] = 'project/-/archive/master.zip'
end
context 'when token header param is present' do
context 'when token is correct' do
it 'returns the user' do
request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
expect(find_user_from_static_object_token(:archive)).to eq(user)
end
end
context 'when token is incorrect' do
it 'returns the user' do
request.headers['X-Gitlab-Static-Object-Token'] = 'foobar'
expect { find_user_from_static_object_token(:archive) }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
end
context 'when token query param is present' do
context 'when token is correct' do
it 'returns the user' do
set_param(:token, user.static_object_token)
expect(find_user_from_static_object_token(:archive)).to eq(user)
end
end
context 'when token is incorrect' do
it 'returns the user' do
set_param(:token, 'foobar')
expect { find_user_from_static_object_token(:archive) }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
end
end
context 'when request format is not archive' do
before do
env['script_name'] = 'url'
end
it 'returns nil' do
expect(find_user_from_static_object_token(:foo)).to be_nil
end
end
end
describe '#find_user_from_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
......
......@@ -48,6 +48,10 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:outbound_local_requests_whitelist) }
it { is_expected.to allow_value([]).for(:outbound_local_requests_whitelist) }
it { is_expected.to allow_value(nil).for(:static_objects_external_storage_url) }
it { is_expected.to allow_value(http).for(:static_objects_external_storage_url) }
it { is_expected.to allow_value(https).for(:static_objects_external_storage_url) }
context "when user accepted let's encrypt terms of service" do
before do
setting.update(lets_encrypt_terms_of_service_accepted: true)
......@@ -420,6 +424,16 @@ describe ApplicationSetting do
end
end
end
context 'static objects external storage' do
context 'when URL is set' do
before do
subject.static_objects_external_storage_url = http
end
it { is_expected.not_to allow_value(nil).for(:static_objects_external_storage_auth_token) }
end
end
end
context 'restrict creating duplicates' do
......
......@@ -945,6 +945,16 @@ describe User do
end
end
describe 'static object token' do
it 'ensures a static object token on read' do
user = create(:user, static_object_token: nil)
static_object_token = user.static_object_token
expect(static_object_token).not_to be_blank
expect(user.reload.static_object_token).to eq static_object_token
end
end
describe '#recently_sent_password_reset?' do
it 'is false when reset_password_sent_at is nil' do
user = build_stubbed(:user, reset_password_sent_at: nil)
......
# frozen_string_literal: true
shared_examples 'archive download buttons' do
let(:formats) { %w(zip tar.gz tar.bz2 tar) }
let(:path_to_visit) { project_path(project) }
let(:ref) { project.default_branch }
context 'when static objects external storage is enabled' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
visit path_to_visit
end
context 'private project' do
it 'shows archive download buttons with external storage URL prepended and user token appended to their href' do
formats.each do |format|
path = archive_path(project, ref, format)
uri = URI('https://cdn.gitlab.com')
uri.path = path
uri.query = "token=#{user.static_object_token}"
expect(page).to have_link format, href: uri.to_s
end
end
end
context 'public project' do
let(:project) { create(:project, :repository, :public) }
it 'shows archive download buttons with external storage URL prepended to their href' do
formats.each do |format|
path = archive_path(project, ref, format)
uri = URI('https://cdn.gitlab.com')
uri.path = path
expect(page).to have_link format, href: uri.to_s
end
end
end
end
context 'when static objects external storage is disabled' do
before do
visit path_to_visit
end
it 'shows default archive download buttons' do
formats.each do |format|
path = archive_path(project, ref, format)
expect(page).to have_link format, href: path
end
end
end
def archive_path(project, ref, format)
project_archive_path(project, id: "#{ref}/#{project.path}-#{ref}", path: nil, format: format)
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