diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index e7f79a0d359a3b48995cfa051143027128fb2131..8762aad42254e1649afd1b5794452b54624c6922 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -27,6 +27,7 @@ Example response: ```json [ { + "id": 42, "tag_name":"v0.2", "description":"## CHANGELOG\r\n\r\n- Escape label and milestone titles to prevent XSS in GFM autocomplete. !2740\r\n- Prevent private snippets from being embeddable.\r\n- Add subresources removal to member destroy service.", "name":"Awesome app v0.2 beta", @@ -93,6 +94,7 @@ Example response: } }, { + "id": 43, "tag_name":"v0.1", "description":"## CHANGELOG\r\n\r\n-Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516", "name":"Awesome app v0.1 alpha", @@ -154,6 +156,9 @@ Example response: Get a Release for the given tag. +CAUTION: **Warning:** +This endpoint has been deprecated in Gitlab 11.11 and will be removed in 12.0. Switch to using `GET /projects/:id/releases/:release_id` instead. + ``` GET /projects/:id/releases/:tag_name ``` @@ -173,6 +178,87 @@ Example response: ```json { + "id": 42, + "tag_name":"v0.1", + "description":"## CHANGELOG\r\n\r\n- Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516", + "name":"Awesome app v0.1 alpha", + "description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eRemove limit of 100 when searching repository code. !8671\u003c/li\u003e\n\u003cli\u003eShow error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\u003c/li\u003e\n\u003cli\u003eFix a bug where internal email pattern wasn't respected. !22516\u003c/li\u003e\n\u003c/ul\u003e", + "created_at":"2019-01-03T01:55:18.203Z", + "author":{ + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "web_url":"http://localhost:3000/root" + }, + "commit":{ + "id":"f8d3d94cbd347e924aa7b715845e439d00e80ca4", + "short_id":"f8d3d94c", + "title":"Initial commit", + "created_at":"2019-01-03T01:53:28.000Z", + "parent_ids":[ + + ], + "message":"Initial commit", + "author_name":"Administrator", + "author_email":"admin@example.com", + "authored_date":"2019-01-03T01:53:28.000Z", + "committer_name":"Administrator", + "committer_email":"admin@example.com", + "committed_date":"2019-01-03T01:53:28.000Z" + }, + "assets":{ + "count":4, + "sources":[ + { + "format":"zip", + "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" + }, + { + "format":"tar.gz", + "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" + }, + { + "format":"tar.bz2", + "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" + }, + { + "format":"tar", + "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" + } + ], + "links":[ + + ] + } +} +``` + +## Get single release + +Get a specific release for given id. + +``` +GET /projects/:id/releases/:release_id +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | --------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | +| `release_id` | integer/string | yes | The release id. | + +Example request: + +```sh +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/123" +``` + +Example response: + +```json +{ + "id": 42, "tag_name":"v0.1", "description":"## CHANGELOG\r\n\r\n- Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516", "name":"Awesome app v0.1 alpha", @@ -260,6 +346,7 @@ Example response: ```json { + "id": 42, "tag_name":"v0.3", "description":"Super nice release", "name":"New release", @@ -346,6 +433,7 @@ Example response: ```json { + "id": 42, "tag_name":"v0.1", "description":"## CHANGELOG\r\n\r\n- Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516", "name":"new name", @@ -425,6 +513,7 @@ Example response: ```json { + "id": 42, "tag_name":"v0.1", "description":"## CHANGELOG\r\n\r\n- Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516", "name":"new name", diff --git a/doc/api/tags.md b/doc/api/tags.md index 3177fec618f916f172abd42295ae3ed32fabfbdc..645e338e736f509b2d64d6b6fe8919caf3c13ff3 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -41,6 +41,7 @@ Parameters: "committed_date": "2012-05-28T04:42:42-07:00" }, "release": { + "id": 42, "tag_name": "1.0.0", "description": "Amazing release. Wow" }, @@ -133,6 +134,7 @@ Parameters: "committed_date": "2012-05-28T04:42:42-07:00" }, "release": { + "id": 42, "tag_name": "1.0.0", "description": "Amazing release. Wow" }, @@ -191,6 +193,7 @@ Response: ```json { + "id": 42, "tag_name": "1.0.0", "description": "Amazing release. Wow" } @@ -223,6 +226,7 @@ Response: ```json { + "id": 42, "tag_name": "1.0.0", "description": "Amazing release. Wow" } diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e25401b62604aa2785cf4f6240d0b6518b6dbae8..c493c45a874cb536983fd1cdb5debb4ec7de9855 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1130,6 +1130,7 @@ def self.exposed_attributes # deprecated old Release representation class TagRelease < Grape::Entity + expose :id expose :tag, as: :tag_name expose :description end @@ -1149,6 +1150,7 @@ class Source < Grape::Entity end class Release < Grape::Entity + expose :id expose :name expose :tag, as: :tag_name, if: -> (release, _) { can_download_code?(release.project) } expose :description diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 6b17f4317db1272106eb81093fe297440cdca5e5..94f73bb156b647930031f3f93167896e09598589 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -27,16 +27,28 @@ class Releases < Grape::API end desc 'Get a single project release' do - detail 'This feature was introduced in GitLab 11.7.' + detail 'Finding release by tag name has been deprecated in Gitlab 11.11 and will be removed in 12.0. Switch to using `GET /projects/:id/releases/:release_id` instead.' success Entities::Release end params do 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_download_code! - - present release, with: Entities::Release, current_user: current_user + # Deprecate `:id/releases/:tag_name` in GitLab 11.11. + # We want to introduce `:id/releases/:release_id` which is basicaly the same route, + # so for the time being we need to make this endpoint support both. + # If release with such tag exists behave like the old endpoint, + # otherwise act like looking up release by id. + # Cleanup once we remove `:id/releases/:tag_name` in GitLab 12.0. + if release_by_tag + authorize! :download_code, release_by_tag + present release_by_tag, with: Entities::Release, current_user: current_user + elsif release_by_id + authorize! :read_release, release_by_id + present release_by_id, with: Entities::Release, current_user: current_user + else + forbidden! + end end desc 'Create a new release' do @@ -123,10 +135,6 @@ def authorize_read_releases! authorize! :read_release, user_project end - def authorize_read_release! - authorize! :read_release, release - end - def authorize_update_release! authorize! :update_release, release end @@ -135,13 +143,17 @@ def authorize_destroy_release! 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 + + def release_by_tag + release + end + + def release_by_id + @release_by_id ||= user_project.releases.find_by_id(params[:tag]) + end end end end diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json index 6ea0781c1edef1362ac4a5f77ca2790f64267af6..a6ab0f647bd6560cd07d8f47a826e3b248f473e4 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release.json +++ b/spec/fixtures/api/schemas/public_api/v4/release.json @@ -1,7 +1,8 @@ { "type": "object", - "required": ["name", "tag_name", "commit"], + "required": ["id", "name", "tag_name", "commit"], "properties": { + "id": { "type": "integer" }, "name": { "type": "string" }, "tag_name": { "type": "string" }, "description": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json index e78398ad1d5231335451c3a2fb8ff20765c6be33..54cd83cb68c3a546143f452f55b9f13b3fcee9ff 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json +++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json @@ -1,7 +1,8 @@ { "type": "object", - "required": ["name"], + "required": ["id", "name"], "properties": { + "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": "string" }, "description_html": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json index 6612c2a9911b852e1ccbcbac862ab1577990dc2e..635b4323458dbcc4ae8e7b8d52ca31db682305e0 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json +++ b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json @@ -1,10 +1,12 @@ { "type": "object", "required" : [ + "id", "tag_name", "description" ], "properties" : { + "id": { "type": "integer" }, "tag_name": { "type": ["string", "null"] }, "description": { "type": "string" } }, diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 8603fa2a73dafe0098d958bfcd62662b4c23fb94..5efd480868f9c38694a1d5a5b6a9d5ec65d241b3 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -291,6 +291,169 @@ end end + describe 'GET /projects/:id/releases/:id' do + context 'when there is a release' do + let!(:release) do + create(:release, + project: project, + tag: 'v0.1', + sha: commit.id, + author: maintainer, + description: 'This is v0.1') + end + + it 'returns 200 HTTP status' do + get api("/projects/#{project.id}/releases/#{release.id}", maintainer) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns a release entry' do + get api("/projects/#{project.id}/releases/#{release.id}", maintainer) + + expect(json_response['tag_name']).to eq(release.tag) + expect(json_response['description']).to eq('This is v0.1') + expect(json_response['author']['name']).to eq(maintainer.name) + expect(json_response['commit']['id']).to eq(commit.id) + expect(json_response['assets']['count']).to eq(4) + end + + it 'matches response schema' do + get api("/projects/#{project.id}/releases/#{release.id}", maintainer) + + expect(response).to match_response_schema('public_api/v4/release') + end + + it 'contains source information as assets' do + get api("/projects/#{project.id}/releases/#{release.id}", maintainer) + + expect(json_response['assets']['sources'].map { |h| h['format'] }) + .to match_array(release.sources.map(&:format)) + expect(json_response['assets']['sources'].map { |h| h['url'] }) + .to match_array(release.sources.map(&:url)) + end + + context "when release description contains confidential issue's link" do + let(:confidential_issue) do + create(:issue, + :confidential, + project: project, + title: 'A vulnerability') + end + + let!(:release) do + create(:release, + project: project, + tag: 'v0.1', + sha: commit.id, + author: maintainer, + description: "This is confidential #{confidential_issue.to_reference}") + end + + it "does not expose confidential issue's title" do + get api("/projects/#{project.id}/releases/#{release.id}", maintainer) + + expect(json_response['description_html']).to include(confidential_issue.to_reference) + expect(json_response['description_html']).not_to include('A vulnerability') + end + end + + context 'when release has link asset' do + let!(:link) do + create(:release_link, + release: release, + name: 'release-18.04.dmg', + url: url) + end + + let(:url) { 'https://my-external-hosting.example.com/scrambled-url/app.zip' } + + it 'contains link information as assets' do + get api("/projects/#{project.id}/releases/#{release.id}", maintainer) + + expect(json_response['assets']['links'].count).to eq(1) + expect(json_response['assets']['links'].first['id']).to eq(link.id) + expect(json_response['assets']['links'].first['name']) + .to eq('release-18.04.dmg') + expect(json_response['assets']['links'].first['url']) + .to eq('https://my-external-hosting.example.com/scrambled-url/app.zip') + expect(json_response['assets']['links'].first['external']) + .to be_truthy + end + + context 'when link is internal' do + let(:url) do + "#{project.web_url}/-/jobs/artifacts/v11.6.0-rc4/download?" \ + "job=rspec-mysql+41%2F50" + end + + it 'has external false' do + get api("/projects/#{project.id}/releases/#{release.id}", maintainer) + + expect(json_response['assets']['links'].first['external']) + .to be_falsy + end + end + end + + context 'when user is a guest' do + it 'responds 200 OK' do + get api("/projects/#{project.id}/releases/#{release.id}", guest) + + 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/#{release.id}", guest) + + expect(response).to match_response_schema('public_api/v4/release/release_for_guest') + expect(json_response['assets']['count']).to eq(release.links.count) + end + + context 'when project is public' do + let(:project) { create(:project, :repository, :public) } + + it 'responds 200 OK' do + get api("/projects/#{project.id}/releases/#{release.id}", guest) + + 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/#{release.id}", guest) + + expect(response).to match_response_schema('public_api/v4/release') + end + end + end + end + + context 'when user is not a project member' do + let!(:release) { create(:release, tag: 'v0.1', project: project) } + + it 'cannot find the project' do + get api("/projects/#{project.id}/releases/#{release.id}", non_project_member) + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'when project is public' do + let(:project) { create(:project, :repository, :public) } + + it 'allows the request' do + get api("/projects/#{project.id}/releases/#{release.id}", non_project_member) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end + describe 'POST /projects/:id/releases' do let(:params) do {