Allow guests users to access project releases

This is step one of resolving
https://gitlab.com/gitlab-org/gitlab-ce/issues/56838.

Here is what changed:
- Revert the security fix from bdee9e8412.
- Do not leak repository information (tag name, commit) to guests in API
responses.
- Do not include links to source code in API responses for users that do
not have download_code access.
- Show Releases in sidebar for guests.
- Do not display links to source code under Assets for users that do not
have download_code access.

GET ':id/releases/:tag_name' still do not allow guests to access
releases. This is to prevent guessing tag existence.
This commit is contained in:
Krasimir Angelov 2019-05-03 13:29:20 +00:00 committed by Lin Jen-Shin
parent 9a9aa22352
commit 241ba4be79
18 changed files with 162 additions and 38 deletions

View file

@ -86,7 +86,7 @@ export default {
</div>
<div
v-if="assets.links.length || assets.sources.length"
v-if="assets.links.length || (assets.sources && assets.sources.length)"
class="card-text prepend-top-default"
>
<b>
@ -103,7 +103,7 @@ export default {
</li>
</ul>
<div v-if="assets.sources.length" class="dropdown">
<div v-if="assets.sources && assets.sources.length" class="dropdown">
<button
type="button"
class="btn btn-link"

View file

@ -318,8 +318,9 @@ module ProjectsHelper
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
if !project.empty_repo? && can?(current_user, :download_code, project)
nav_tabs << [:files, :commits, :network, :graphs, :forks, :releases]
unless project.empty_repo?
nav_tabs << [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project)
nav_tabs << :releases if can?(current_user, :read_release, project)
end
if project.repo_exists? && can?(current_user, :read_merge_request, project)

View file

@ -31,8 +31,11 @@ class Release < ApplicationRecord
actual_tag.nil?
end
def assets_count
links.count + sources.count
def assets_count(except: [])
links_count = links.count
sources_count = except.include?(:sources) ? 0 : sources.count
links_count + sources_count
end
def sources

View file

@ -186,6 +186,7 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics
enable :award_emoji
enable :read_pages_content
enable :read_release
end
# These abilities are not allowed to admins that are not members of the project,
@ -212,7 +213,6 @@ class ProjectPolicy < BasePolicy
enable :read_deployment
enable :read_merge_request
enable :read_sentry_issue
enable :read_release
enable :read_prometheus
end

View file

@ -0,0 +1,5 @@
---
title: Allow guests users to access project releases
merge_request: 27247
author:
type: changed

View file

@ -1156,22 +1156,33 @@ module API
end
end
class Release < TagRelease
class Release < Grape::Entity
expose :name
expose :tag, as: :tag_name, if: lambda { |_, _| can_download_code? }
expose :description
expose :description_html do |entity|
MarkupHelper.markdown_field(entity, :description)
end
expose :created_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit
expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? }
expose :assets do
expose :assets_count, as: :count
expose :sources, using: Entities::Releases::Source
expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources]
release.assets_count(except: assets_to_exclude)
end
expose :sources, using: Entities::Releases::Source, if: lambda { |_, _| can_download_code? }
expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted
end
end
private
def can_download_code?
Ability.allowed?(options[:current_user], :download_code, object.project)
end
end
class Tag < Grape::Entity

View file

@ -23,7 +23,7 @@ module API
get ':id/releases' do
releases = ::ReleasesFinder.new(user_project, current_user).execute
present paginate(releases), with: Entities::Release
present paginate(releases), with: Entities::Release, current_user: current_user
end
desc 'Get a single project release' do
@ -34,9 +34,9 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
end
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_read_release!
authorize_download_code!
present release, with: Entities::Release
present release, with: Entities::Release, current_user: current_user
end
desc 'Create a new release' do
@ -63,7 +63,7 @@ module API
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
end
@ -86,7 +86,7 @@ module API
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
end
@ -107,7 +107,7 @@ module API
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
end
@ -135,6 +135,10 @@ module API
authorize! :destroy_release, release
end
def authorize_download_code!
authorize! :download_code, release
end
def release
@release ||= user_project.releases.find_by_tag(params[:tag])
end

View file

@ -1,12 +1,33 @@
{
"type": "object",
"required" : [
"tag_name",
"description"
],
"properties" : {
"tag_name": { "type": ["string", "null"] },
"description": { "type": "string" }
"required": ["name", "tag_name", "commit"],
"properties": {
"name": { "type": "string" },
"tag_name": { "type": "string" },
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
"commit": {
"oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }]
},
"author": {
"oneOf": [{ "type": "null" }, { "$ref": "user/basic.json" }]
},
"assets": {
"required": ["count", "links", "sources"],
"properties": {
"count": { "type": "integer" },
"links": { "$ref": "../../release/links.json" },
"sources": {
"type": "array",
"items": {
"format": "zip",
"url": "string"
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View file

@ -0,0 +1,22 @@
{
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
"author": {
"oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
},
"assets": {
"required": ["count", "links"],
"properties": {
"count": { "type": "integer" },
"links": { "$ref": "../../../release/links.json" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View file

@ -0,0 +1,4 @@
{
"type": "array",
"items": { "$ref": "release_for_guest.json" }
}

View file

@ -0,0 +1,12 @@
{
"type": "object",
"required" : [
"tag_name",
"description"
],
"properties" : {
"tag_name": { "type": ["string", "null"] },
"description": { "type": "string" }
},
"additionalProperties": false
}

View file

@ -0,0 +1,4 @@
{
"type": "array",
"items": { "$ref": "release.json" }
}

View file

@ -14,7 +14,7 @@
"release": {
"oneOf": [
{ "type": "null" },
{ "$ref": "release.json" }
{ "$ref": "release/tag_release.json" }
]
}
},

View file

@ -49,6 +49,11 @@ RSpec.describe Release do
it 'counts the link as an asset' do
is_expected.to eq(1 + Releases::Source::FORMATS.count)
end
it "excludes sources count when asked" do
assets_count = release.assets_count(except: [:sources])
expect(assets_count).to eq(1)
end
end
end

View file

@ -17,7 +17,7 @@ describe ProjectPolicy do
read_project_for_iids read_issue_iid read_label
read_milestone read_project_snippet read_project_member read_note
create_project create_issue create_note upload_file create_merge_request_in
award_emoji
award_emoji read_release
]
end
@ -26,7 +26,7 @@ describe ProjectPolicy do
download_code fork_project create_project_snippet update_issue
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_sentry_issue read_release
read_merge_request download_wiki_code read_sentry_issue
]
end

View file

@ -52,7 +52,7 @@ describe API::Releases do
it 'matches response schema' do
get api("/projects/#{project.id}/releases", maintainer)
expect(response).to match_response_schema('releases')
expect(response).to match_response_schema('public_api/v4/releases')
end
end
@ -69,10 +69,25 @@ describe API::Releases do
end
context 'when user is a guest' do
it 'responds 403 Forbidden' do
let!(:release) do
create(:release,
project: project,
tag: 'v0.1',
author: maintainer,
created_at: 2.days.ago)
end
it 'responds 200 OK' do
get api("/projects/#{project.id}/releases", guest)
expect(response).to have_gitlab_http_status(:forbidden)
expect(response).to have_gitlab_http_status(:ok)
end
it "does not expose tag, commit and source code" do
get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/release/releases_for_guest')
expect(json_response[0]['assets']['count']).to eq(release.links.count)
end
context 'when project is public' do
@ -83,6 +98,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
it "exposes tag, commit and source code" do
get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/releases')
expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count)
end
end
end
@ -135,7 +157,7 @@ describe API::Releases do
it 'matches response schema' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to match_response_schema('release')
expect(response).to match_response_schema('public_api/v4/release')
end
it 'contains source information as assets' do
@ -225,6 +247,17 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
it "exposes tag and commit" do
create(:release,
project: project,
tag: 'v0.1',
author: maintainer,
created_at: 2.days.ago)
get api("/projects/#{project.id}/releases/v0.1", guest)
expect(response).to match_response_schema('public_api/v4/release')
end
end
end
end
@ -306,7 +339,7 @@ describe API::Releases do
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to match_response_schema('release')
expect(response).to match_response_schema('public_api/v4/release')
end
it 'does not create a new tag' do
@ -378,7 +411,7 @@ describe API::Releases do
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to match_response_schema('release')
expect(response).to match_response_schema('public_api/v4/release')
end
end
@ -532,7 +565,7 @@ describe API::Releases do
it 'matches response schema' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(response).to match_response_schema('release')
expect(response).to match_response_schema('public_api/v4/release')
end
context 'when user tries to update sha' do
@ -624,7 +657,7 @@ describe API::Releases do
it 'matches response schema' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to match_response_schema('release')
expect(response).to match_response_schema('public_api/v4/release')
end
context 'when there are no corresponding releases' do

View file

@ -378,7 +378,7 @@ describe API::Tags do
post api(route, user), params: { description: description }
expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/release')
expect(response).to match_response_schema('public_api/v4/release/tag_release')
expect(json_response['tag_name']).to eq(tag_name)
expect(json_response['description']).to eq(description)
end

View file

@ -24,8 +24,7 @@ RSpec.shared_context 'ProjectPolicy context' do
download_code fork_project create_project_snippet update_issue
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_sentry_issue read_release
read_prometheus
read_merge_request download_wiki_code read_sentry_issue read_prometheus
]
end