Skip to content
Snippets Groups Projects
Commit 3c2b4a1c authored by Ahmad Sherif's avatar Ahmad Sherif
Browse files

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
No related branches found
No related tags found
No related merge requests found
Showing
with 195 additions and 3 deletions
Loading
Loading
@@ -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)
 
Loading
Loading
# 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
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
 
Loading
Loading
Loading
Loading
@@ -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,
Loading
Loading
Loading
Loading
@@ -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"
 
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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 }
Loading
Loading
@@ -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
 
Loading
Loading
= 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"
Loading
Loading
@@ -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'
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
 
Loading
Loading
# 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
Loading
Loading
@@ -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"
Loading
Loading
@@ -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"
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
 
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment