Commit ede81780 authored by Gabriel Mazetto's avatar Gabriel Mazetto

Merge branch 'permanent-link-to-latest-release' into 'master'

Resolve "Permanent link to latest version of a release"

See merge request gitlab-org/gitlab!78679
parents fdf4b090 4305f93e
......@@ -7,6 +7,7 @@ class Projects::ReleasesController < Projects::ApplicationController
before_action :authorize_read_release!
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new
before_action :validate_suffix_path, :fetch_latest_tag, only: :latest_permalink
before_action only: :index do
push_frontend_feature_flag(:releases_index_apollo_client, project, default_enabled: :yaml)
end
......@@ -26,10 +27,24 @@ class Projects::ReleasesController < Projects::ApplicationController
redirect_to link.url
end
def latest_permalink
unless @latest_tag.present?
return render_404
end
query_parameters_except_order_by = request.query_parameters.except(:order_by)
redirect_url = project_release_url(@project, @latest_tag)
redirect_url += "/#{params[:suffix_path]}" if params[:suffix_path]
redirect_url += "?#{query_parameters_except_order_by.compact.to_param}" if query_parameters_except_order_by.present?
redirect_to redirect_url
end
private
def releases
ReleasesFinder.new(@project, current_user).execute
def releases(params = {})
ReleasesFinder.new(@project, current_user, params).execute
end
def authorize_update_release!
......@@ -51,4 +66,18 @@ class Projects::ReleasesController < Projects::ApplicationController
def sanitized_tag_name
CGI.unescape(params[:tag])
end
# Default order_by is 'released_at', which is set in ReleasesFinder.
# Also if the passed order_by is invalid, we reject and default to 'released_at'.
def fetch_latest_tag
allowed_values = ['released_at']
params.reject! { |key, value| key.to_sym == :order_by && allowed_values.any?(value) }
@latest_tag = releases(order_by: params[:order_by]).first&.tag
end
def validate_suffix_path
Gitlab::Utils.check_path_traversal!(params[:suffix_path]) if params[:suffix_path]
end
end
......@@ -241,6 +241,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
get 'releases/permalink/latest(/)(*suffix_path)', to: 'releases#latest_permalink', as: :latest_release_permalink, format: false
resources :logs, only: [:index] do
collection do
get :k8s
......
......@@ -222,6 +222,166 @@ RSpec.describe Projects::ReleasesController do
end
end
describe 'GET #latest_permalink' do
# Uses default order_by=released_at parameter.
subject do
get :latest_permalink, params: { namespace_id: project.namespace, project_id: project }
end
before do
sign_in(user)
end
let(:release) { create(:release, project: project) }
let(:tag) { CGI.escape(release.tag) }
context 'when user is a guest' do
let(:project) { private_project }
let(:user) { guest }
it 'proceeds with the redirect' do
subject
expect(response).to have_gitlab_http_status(:redirect)
end
end
context 'when user is an external user for the project' do
let(:project) { private_project }
let(:user) { create(:user) }
it 'behaves like not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when there are no releases for the project' do
let(:project) { create(:project, :repository, :public) }
let(:user) { developer }
before do
project.releases.destroy_all # rubocop: disable Cop/DestroyAll
end
it 'behaves like not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'multiple releases' do
let(:user) { developer }
it 'redirects to the latest release' do
create(:release, project: project, released_at: 1.day.ago)
latest_release = create(:release, project: project, released_at: Time.current)
subject
expect(response).to redirect_to("#{project_releases_path(project)}/#{latest_release.tag}")
end
end
context 'suffix path redirection' do
let(:user) { developer }
let(:suffix_path) { 'downloads/zips/helm-hello-world.zip' }
let!(:latest_release) { create(:release, project: project, released_at: Time.current) }
subject do
get :latest_permalink, params: {
namespace_id: project.namespace,
project_id: project,
suffix_path: suffix_path
}
end
it 'redirects to the latest release with suffix path and format' do
subject
expect(response).to redirect_to(
"#{project_releases_path(project)}/#{latest_release.tag}/#{suffix_path}")
end
context 'suffix path abuse' do
let(:suffix_path) { 'downloads/zips/../../../../../../../robots.txt'}
it 'raises attack error' do
expect do
subject
end.to raise_error(Gitlab::Utils::PathTraversalAttackError)
end
end
context 'url parameters' do
let(:suffix_path) { 'downloads/zips/helm-hello-world.zip' }
subject do
get :latest_permalink, params: {
namespace_id: project.namespace,
project_id: project,
suffix_path: suffix_path,
order_by: 'released_at',
param_1: 1,
param_2: 2
}
end
it 'carries over query parameters without order_by parameter in the redirect' do
subject
expect(response).to redirect_to(
"#{project_releases_path(project)}/#{latest_release.tag}/#{suffix_path}?param_1=1&param_2=2")
end
end
end
context 'order_by parameter' do
let!(:latest_release) { create(:release, project: project, released_at: Time.current) }
shared_examples_for 'redirects to latest release ordered by using released_at' do
it do
subject
expect(response).to redirect_to("#{project_releases_path(project)}/#{latest_release.tag}")
end
end
before do
create(:release, project: project, released_at: 1.day.ago)
create(:release, project: project, released_at: 2.days.ago)
end
context 'invalid parameter' do
let(:user) { developer }
subject do
get :latest_permalink, params: {
namespace_id: project.namespace,
project_id: project,
order_by: 'unsupported'
}
end
it_behaves_like 'redirects to latest release ordered by using released_at'
end
context 'valid parameter' do
subject do
get :latest_permalink, params: {
namespace_id: project.namespace,
project_id: project,
order_by: 'released_at'
}
end
it_behaves_like 'redirects to latest release ordered by using released_at'
end
end
end
# `GET #downloads` is addressed in spec/requests/projects/releases_controller_spec.rb
private
......
......@@ -680,6 +680,32 @@ RSpec.describe 'project routing' do
end
end
describe Projects::ReleasesController, 'routing' do
it 'to #latest_permalink with a valid permalink path' do
expect(get('/gitlab/gitlabhq/-/releases/permalink/latest/downloads/release-binary.zip')).to route_to(
'projects/releases#latest_permalink',
namespace_id: 'gitlab',
project_id: 'gitlabhq',
suffix_path: 'downloads/release-binary.zip'
)
expect(get('/gitlab/gitlabhq/-/releases/permalink/latest')).to route_to(
'projects/releases#latest_permalink',
namespace_id: 'gitlab',
project_id: 'gitlabhq'
)
end
it 'to #show for the release with tag named permalink' do
expect(get('/gitlab/gitlabhq/-/releases/permalink')).to route_to(
'projects/releases#show',
namespace_id: 'gitlab',
project_id: 'gitlabhq',
tag: 'permalink'
)
end
end
describe Projects::Registry::TagsController, 'routing' do
describe '#destroy' do
it 'correctly routes to a destroy action' do
......
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