Commit da1e555b authored by Nathan Friend's avatar Nathan Friend Committed by charlie ablett

Update permissions of Release GraphQL data

This commit updates the availability of some Release-related data to
make it consistent with the current REST API.
parent c30c7a21
...@@ -251,7 +251,8 @@ module Types ...@@ -251,7 +251,8 @@ module Types
null: true, null: true,
description: 'A single release of the project', description: 'A single release of the project',
resolver: Resolvers::ReleasesResolver.single, resolver: Resolvers::ReleasesResolver.single,
feature_flag: :graphql_release_data feature_flag: :graphql_release_data,
authorize: :download_code
field :container_expiration_policy, field :container_expiration_policy,
Types::ContainerExpirationPolicyType, Types::ContainerExpirationPolicyType,
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Types module Types
class ReleaseAssetsType < BaseObject class ReleaseAssetsType < BaseObject
graphql_name 'ReleaseAssets' graphql_name 'ReleaseAssets'
description 'A container for all assets associated with a release'
authorize :read_release authorize :read_release
...@@ -10,7 +11,7 @@ module Types ...@@ -10,7 +11,7 @@ module Types
present_using ReleasePresenter present_using ReleasePresenter
field :assets_count, GraphQL::INT_TYPE, null: true, field :count, GraphQL::INT_TYPE, null: true, method: :assets_count,
description: 'Number of assets of the release' description: 'Number of assets of the release'
field :links, Types::ReleaseLinkType.connection_type, null: true, field :links, Types::ReleaseLinkType.connection_type, null: true,
description: 'Asset links of the release' description: 'Asset links of the release'
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Types module Types
class ReleaseLinkType < BaseObject class ReleaseLinkType < BaseObject
graphql_name 'ReleaseLink' graphql_name 'ReleaseLink'
description 'Represents an asset link associated with a release'
authorize :read_release authorize :read_release
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
module Types module Types
class ReleaseSourceType < BaseObject class ReleaseSourceType < BaseObject
graphql_name 'ReleaseSource' graphql_name 'ReleaseSource'
description 'Represents the source code attached to a release in a particular format'
authorize :read_release_sources authorize :download_code
field :format, GraphQL::STRING_TYPE, null: true, field :format, GraphQL::STRING_TYPE, null: true,
description: 'Format of the source' description: 'Format of the source'
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Types module Types
class ReleaseType < BaseObject class ReleaseType < BaseObject
graphql_name 'Release' graphql_name 'Release'
description 'Represents a release'
authorize :read_release authorize :read_release
...@@ -10,10 +11,12 @@ module Types ...@@ -10,10 +11,12 @@ module Types
present_using ReleasePresenter present_using ReleasePresenter
field :tag_name, GraphQL::STRING_TYPE, null: false, method: :tag, field :tag_name, GraphQL::STRING_TYPE, null: true, method: :tag,
description: 'Name of the tag associated with the release' description: 'Name of the tag associated with the release',
authorize: :download_code
field :tag_path, GraphQL::STRING_TYPE, null: true, field :tag_path, GraphQL::STRING_TYPE, null: true,
description: 'Relative web path to the tag associated with the release' description: 'Relative web path to the tag associated with the release',
authorize: :download_code
field :description, GraphQL::STRING_TYPE, null: true, field :description, GraphQL::STRING_TYPE, null: true,
description: 'Description (also known as "release notes") of the release' description: 'Description (also known as "release notes") of the release'
markdown_field :description_html, null: true markdown_field :description_html, null: true
...@@ -39,8 +42,7 @@ module Types ...@@ -39,8 +42,7 @@ module Types
field :commit, Types::CommitType, null: true, field :commit, Types::CommitType, null: true,
complexity: 10, calls_gitaly: true, complexity: 10, calls_gitaly: true,
description: 'The commit associated with the release', description: 'The commit associated with the release'
authorize: :reporter_access
def commit def commit
return if release.sha.nil? return if release.sha.nil?
......
...@@ -3,11 +3,5 @@ ...@@ -3,11 +3,5 @@
module Releases module Releases
class SourcePolicy < BasePolicy class SourcePolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
rule { can?(:public_access) | can?(:reporter_access) }.policy do
enable :read_release_sources
end
rule { ~can?(:read_release) }.prevent :read_release_sources
end end
end end
...@@ -5,7 +5,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated ...@@ -5,7 +5,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
presents :release presents :release
delegate :project, :tag, :assets_count, to: :release delegate :project, :tag, to: :release
def commit_path def commit_path
return unless release.commit && can_download_code? return unless release.commit && can_download_code?
...@@ -43,6 +43,18 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated ...@@ -43,6 +43,18 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
edit_project_release_url(project, release) edit_project_release_url(project, release)
end end
def assets_count
if can_download_code?
release.assets_count
else
release.assets_count(except: [:sources])
end
end
def name
can_download_code? ? release.name : "Release-#{release.id}"
end
private private
def can_download_code? def can_download_code?
......
...@@ -10097,6 +10097,9 @@ enum RegistryState { ...@@ -10097,6 +10097,9 @@ enum RegistryState {
SYNCED SYNCED
} }
"""
Represents a release
"""
type Release { type Release {
""" """
Assets of the release Assets of the release
...@@ -10191,7 +10194,7 @@ type Release { ...@@ -10191,7 +10194,7 @@ type Release {
""" """
Name of the tag associated with the release Name of the tag associated with the release
""" """
tagName: String! tagName: String
""" """
Relative web path to the tag associated with the release Relative web path to the tag associated with the release
...@@ -10199,11 +10202,14 @@ type Release { ...@@ -10199,11 +10202,14 @@ type Release {
tagPath: String tagPath: String
} }
"""
A container for all assets associated with a release
"""
type ReleaseAssets { type ReleaseAssets {
""" """
Number of assets of the release Number of assets of the release
""" """
assetsCount: Int count: Int
""" """
Asset links of the release Asset links of the release
...@@ -10351,6 +10357,9 @@ type ReleaseEvidenceEdge { ...@@ -10351,6 +10357,9 @@ type ReleaseEvidenceEdge {
node: ReleaseEvidence node: ReleaseEvidence
} }
"""
Represents an asset link associated with a release
"""
type ReleaseLink { type ReleaseLink {
""" """
Indicates the link points to an external resource Indicates the link points to an external resource
...@@ -10438,6 +10447,9 @@ enum ReleaseLinkType { ...@@ -10438,6 +10447,9 @@ enum ReleaseLinkType {
RUNBOOK RUNBOOK
} }
"""
Represents the source code attached to a release in a particular format
"""
type ReleaseSource { type ReleaseSource {
""" """
Format of the source Format of the source
......
...@@ -29574,7 +29574,7 @@ ...@@ -29574,7 +29574,7 @@
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Release", "name": "Release",
"description": null, "description": "Represents a release",
"fields": [ "fields": [
{ {
"name": "assets", "name": "assets",
...@@ -29801,13 +29801,9 @@ ...@@ -29801,13 +29801,9 @@
], ],
"type": { "type": {
"kind": "NON_NULL", "kind": "SCALAR",
"name": null, "name": "String",
"ofType": { "ofType": null
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
...@@ -29837,10 +29833,10 @@ ...@@ -29837,10 +29833,10 @@
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ReleaseAssets", "name": "ReleaseAssets",
"description": null, "description": "A container for all assets associated with a release",
"fields": [ "fields": [
{ {
"name": "assetsCount", "name": "count",
"description": "Number of assets of the release", "description": "Number of assets of the release",
"args": [ "args": [
...@@ -30267,7 +30263,7 @@ ...@@ -30267,7 +30263,7 @@
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ReleaseLink", "name": "ReleaseLink",
"description": null, "description": "Represents an asset link associated with a release",
"fields": [ "fields": [
{ {
"name": "external", "name": "external",
...@@ -30501,7 +30497,7 @@ ...@@ -30501,7 +30497,7 @@
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ReleaseSource", "name": "ReleaseSource",
"description": null, "description": "Represents the source code attached to a release in a particular format",
"fields": [ "fields": [
{ {
"name": "format", "name": "format",
...@@ -1400,6 +1400,8 @@ Represents a Project Member ...@@ -1400,6 +1400,8 @@ Represents a Project Member
## Release ## Release
Represents a release
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `assets` | ReleaseAssets | Assets of the release | | `assets` | ReleaseAssets | Assets of the release |
...@@ -1410,14 +1412,16 @@ Represents a Project Member ...@@ -1410,14 +1412,16 @@ Represents a Project Member
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `name` | String | Name of the release | | `name` | String | Name of the release |
| `releasedAt` | Time | Timestamp of when the release was released | | `releasedAt` | Time | Timestamp of when the release was released |
| `tagName` | String! | Name of the tag associated with the release | | `tagName` | String | Name of the tag associated with the release |
| `tagPath` | String | Relative web path to the tag associated with the release | | `tagPath` | String | Relative web path to the tag associated with the release |
## ReleaseAssets ## ReleaseAssets
A container for all assets associated with a release
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `assetsCount` | Int | Number of assets of the release | | `count` | Int | Number of assets of the release |
## ReleaseEvidence ## ReleaseEvidence
...@@ -1432,6 +1436,8 @@ Evidence for a release ...@@ -1432,6 +1436,8 @@ Evidence for a release
## ReleaseLink ## ReleaseLink
Represents an asset link associated with a release
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `external` | Boolean | Indicates the link points to an external resource | | `external` | Boolean | Indicates the link points to an external resource |
...@@ -1442,6 +1448,8 @@ Evidence for a release ...@@ -1442,6 +1448,8 @@ Evidence for a release
## ReleaseSource ## ReleaseSource
Represents the source code attached to a release in a particular format
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `format` | String | Format of the source | | `format` | String | Format of the source |
......
...@@ -5,9 +5,7 @@ module API ...@@ -5,9 +5,7 @@ module API
class Release < Grape::Entity class Release < Grape::Entity
include ::API::Helpers::Presentable include ::API::Helpers::Presentable
expose :name do |release, _| expose :name
can_download_code? ? release.name : "Release-#{release.id}"
end
expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? } expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? }
expose :description expose :description
expose :description_html do |entity| expose :description_html do |entity|
...@@ -23,10 +21,7 @@ module API ...@@ -23,10 +21,7 @@ module API
expose :tag_path, expose_nil: false expose :tag_path, expose_nil: false
expose :assets do expose :assets do
expose :assets_count, as: :count do |release, _| expose :assets_count, as: :count
assets_to_exclude = can_download_code? ? [] : [:sources]
release.assets_count(except: assets_to_exclude)
end
expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? } expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? }
expose :links, using: Entities::Releases::Link do |release, options| expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted release.links.sorted
......
...@@ -7,7 +7,7 @@ describe GitlabSchema.types['ReleaseAssets'] do ...@@ -7,7 +7,7 @@ describe GitlabSchema.types['ReleaseAssets'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
assets_count links sources count links sources
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe GitlabSchema.types['ReleaseSource'] do describe GitlabSchema.types['ReleaseSource'] do
it { expect(described_class).to require_graphql_authorizations(:read_release_sources) } it { expect(described_class).to require_graphql_authorizations(:download_code) }
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
......
...@@ -44,6 +44,5 @@ describe GitlabSchema.types['Release'] do ...@@ -44,6 +44,5 @@ describe GitlabSchema.types['Release'] do
subject { described_class.fields['commit'] } subject { described_class.fields['commit'] }
it { is_expected.to have_graphql_type(Types::CommitType) } it { is_expected.to have_graphql_type(Types::CommitType) }
it { is_expected.to require_graphql_authorizations(:reporter_access) }
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Releases::SourcePolicy do
using RSpec::Parameterized::TableSyntax
let(:policy) { described_class.new(user, source) }
let_it_be(:public_user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let(:release) { create(:release, project: project) }
let(:source) { release.sources.first }
shared_examples 'source code access' do
it "allows access a release's source code" do
expect(policy).to be_allowed(:read_release_sources)
end
end
shared_examples 'no source code access' do
it "does not allow access a release's source code" do
expect(policy).to be_disallowed(:read_release_sources)
end
end
context 'a private project' do
let_it_be(:project) { create(:project, :private) }
context 'accessed by a public user' do
let(:user) { public_user }
it_behaves_like 'no source code access'
end
context 'accessed by a user with Guest permissions' do
let(:user) { guest }
before do
project.add_guest(user)
end
it_behaves_like 'no source code access'
end
context 'accessed by a user with Reporter permissions' do
let(:user) { reporter }
before do
project.add_reporter(user)
end
it_behaves_like 'source code access'
end
end
context 'a public project' do
let_it_be(:project) { create(:project, :public) }
context 'accessed by a public user' do
let(:user) { public_user }
it_behaves_like 'source code access'
end
context 'accessed by a user with Guest permissions' do
let(:user) { guest }
before do
project.add_guest(user)
end
it_behaves_like 'source code access'
end
context 'accessed by a user with Reporter permissions' do
let(:user) { reporter }
before do
project.add_reporter(user)
end
it_behaves_like 'source code access'
end
end
end
...@@ -112,4 +112,36 @@ describe ReleasePresenter do ...@@ -112,4 +112,36 @@ describe ReleasePresenter do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
end end
describe '#assets_count' do
subject { presenter.assets_count }
it 'returns the number of assets associated to the release' do
is_expected.to be release.assets_count
end
context 'when a user is not allowed to download release sources' do
let(:presenter) { described_class.new(release, current_user: guest) }
it 'returns the number of all non-source assets associated to the release' do
is_expected.to be release.assets_count(except: [:sources])
end
end
end
describe '#name' do
subject { presenter.name }
it 'returns the release name' do
is_expected.to eq release.name
end
context "when a user is not allowed to access any repository information" do
let(:presenter) { described_class.new(release, current_user: guest) }
it 'returns a replacement name to avoid potentially leaking tag information' do
is_expected.to eq "Release-#{release.id}"
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe 'Query.project(fullPath).releases()' do
include GraphqlHelpers
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:stranger) { create(:user) }
let(:query) do
graphql_query_for(:project, { fullPath: project.full_path },
%{
releases {
nodes {
tagName
tagPath
name
commit {
sha
}
assets {
count
sources {
nodes {
url
}
}
}
evidences {
nodes {
sha
}
}
}
}
})
end
let(:post_query) { post_graphql(query, current_user: current_user) }
let(:data) { graphql_data.dig('project', 'releases', 'nodes', 0) }
shared_examples 'full access to all repository-related fields' do
describe 'repository-related fields' do
before do
post_query
end
it 'returns data for fields that are protected in private projects' do
expected_sources = release.sources.map do |s|
{ 'url' => s.url }
end
expected_evidences = release.evidences.map do |e|
{ 'sha' => e.sha }
end
expect(data).to eq({
'tagName' => release.tag,
'tagPath' => project_tag_path(project, release.tag),
'name' => release.name,
'commit' => {
'sha' => release.commit.sha
},
'assets' => {
'count' => release.assets_count,
'sources' => {
'nodes' => expected_sources
}
},
'evidences' => {
'nodes' => expected_evidences
}
})
end
end
end
shared_examples 'no access to any repository-related fields' do
describe 'repository-related fields' do
before do
post_query
end
it 'does not return data for fields that expose repository information' do
expect(data).to eq({
'tagName' => nil,
'tagPath' => nil,
'name' => "Release-#{release.id}",
'commit' => nil,
'assets' => {
'count' => release.assets_count(except: [:sources]),
'sources' => {
'nodes' => []
}
},
'evidences' => {
'nodes' => []
}
})
end
end
end
describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do
context 'when the project is private' do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:release) { create(:release, :with_evidence, project: project) }
before_all do
project.add_guest(guest)
project.add_reporter(reporter)
end
context 'when the user has Guest permissions' do
let(:current_user) { guest }
it_behaves_like 'no access to any repository-related fields'
end
context 'when the user has Reporter permissions' do
let(:current_user) { reporter }
it_behaves_like 'full access to all repository-related fields'
end
end
context 'when the project is public' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:release) { create(:release, :with_evidence, project: project) }
before_all do
project.add_guest(guest)
project.add_reporter(reporter)
end
context 'when the user is not logged in' do
let(:current_user) { stranger }
it_behaves_like 'full access to all repository-related fields'
end
context 'when the user has Guest permissions' do
let(:current_user) { guest }
it_behaves_like 'full access to all repository-related fields'
end
end
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