From 65fc96866782de44427e17c7f21964775e4dbd36 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu Date: Fri, 21 Jun 2024 23:56:38 +0800 Subject: [PATCH 1/2] Add REST API endpoints to manage uploads Allows project and group maintainers to list, download, and delete uploads Changelog: added --- app/finders/banzai/uploads_finder.rb | 2 + app/models/upload.rb | 13 +- app/policies/group_policy.rb | 1 + app/policies/project_policy.rb | 1 + doc/api/groups.md | 97 +++++++++++- doc/api/projects.md | 95 +++++++++++- doc/security/user_file_uploads.md | 3 + lib/api/entities/markdown_upload_admin.rb | 13 ++ lib/api/markdown_uploads.rb | 143 ++++++++++++++++++ .../entities/markdown_upload_admin_spec.rb | 26 ++++ spec/requests/api/markdown_uploads_spec.rb | 137 ++++++++++++++++- .../policies/group_policy_shared_context.rb | 2 +- .../policies/project_policy_shared_context.rb | 2 +- 13 files changed, 523 insertions(+), 12 deletions(-) create mode 100644 lib/api/entities/markdown_upload_admin.rb create mode 100644 spec/lib/api/entities/markdown_upload_admin_spec.rb diff --git a/app/finders/banzai/uploads_finder.rb b/app/finders/banzai/uploads_finder.rb index e05e426e27e9ef..28dc83b907c337 100644 --- a/app/finders/banzai/uploads_finder.rb +++ b/app/finders/banzai/uploads_finder.rb @@ -2,6 +2,8 @@ module Banzai class UploadsFinder + include FinderMethods + def initialize(parent:) @parent = parent end diff --git a/app/models/upload.rb b/app/models/upload.rb index bad9fb660af983..1ebcac75baaeba 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -20,6 +20,7 @@ class Upload < ApplicationRecord scope :for_model_type_and_id, ->(type, id) { where(model_type: type, model_id: id) } scope :for_uploader, ->(uploader_class) { where(uploader: uploader_class.to_s) } scope :order_by_created_at_desc, -> { reorder(created_at: :desc) } + scope :preload_uploaded_by_user, -> { preload(:uploaded_by_user) } before_save :calculate_checksum!, if: :foreground_checksummable? # as the FileUploader is not mounted, the default CarrierWave ActiveRecord @@ -98,7 +99,7 @@ def build_uploader(mounted_as = nil) # @return [GitlabUploader] one of the subclasses, defined at the model's uploader attribute def retrieve_uploader(mounted_as = nil) build_uploader(mounted_as).tap do |uploader| - uploader.retrieve_from_store!(identifier) + uploader.retrieve_from_store!(filename) end end @@ -124,7 +125,7 @@ def exist? def uploader_context { - identifier: identifier, + identifier: filename, secret: secret, uploaded_by_user_id: uploaded_by_user_id }.compact @@ -144,6 +145,10 @@ def needs_checksum? checksum.nil? && local? && exist? end + def filename + File.basename(path) + end + private def delete_file! @@ -166,10 +171,6 @@ def uploader_class Object.const_get(uploader, false) end - def identifier - File.basename(path) - end - def mount_point super&.to_sym end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index a9ad133c0234be..249c52bf330b9d 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -256,6 +256,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy enable :create_jira_connect_subscription enable :maintainer_access enable :read_upload + enable :admin_upload enable :destroy_upload enable :admin_push_rules end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 09312d4e883f95..c8659478d4b438 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -613,6 +613,7 @@ class ProjectPolicy < BasePolicy enable :admin_project_aws enable :admin_secure_files enable :read_upload + enable :admin_upload enable :destroy_upload enable :admin_incident_management_timeline_event_tag enable :stop_environment diff --git a/doc/api/groups.md b/doc/api/groups.md index 170e2f41d91f80..6d47b06dc1c95f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1701,6 +1701,99 @@ Example response: } ``` +## Markdown uploads + +Markdown uploads are files uploaded to a group that can be referenced in Markdown text in an epic or wiki page. + +### List uploads + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157066) in GitLab 17.2. + +Get all uploads of the group sorted by `created_at` in descending order. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +GET /groups/:id/uploads +``` + +| Attribute | Type | Required | Description | +|-----------|-------------------|----------|-------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/uploads" +``` + +Returned object: + +```json +[ + { + "id": 1, + "size": 1024, + "filename": "image.png", + "created_at":"2024-06-20T15:53:03.067Z", + "uploaded_by": { + "id": 18, + "name" : "Alexandra Bashirian", + "username" : "eileen.lowe" + } + }, + { + "id": 2, + "size": 512, + "filename": "other-image.png", + "created_at":"2024-06-19T15:53:03.067Z", + "uploaded_by": null + } +] +``` + +### Download an uploaded file + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157066) in GitLab 17.2. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +GET /groups/:id/uploads/:upload_id +``` + +| Attribute | Type | Required | Description | +|-------------|-------------------|----------|-------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). | +| `upload_id` | integer | Yes | The ID of the upload. | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/uploads/1" +``` + +### Delete an uploaded file + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157066) in GitLab 17.2. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +DELETE /groups/:id/uploads/:upload_id +``` + +| Attribute | Type | Required | Description | +|-------------|-------------------|----------|-------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). | +| `upload_id` | integer | Yes | The ID of the upload. | + +Example request: + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/uploads/1" +``` + ## Hooks DETAILS: @@ -2433,11 +2526,11 @@ DELETE /groups/:id/push_rule > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371117) in GitLab 17.2 [with a flag](../administration/feature_flags.md) named `group_agnostic_token_revocation`. Disabled by default. FLAG: -The availability of this feature is controlled by a feature flag. +The availability of this feature is controlled by a feature flag. For more information, see the history. Revoke a token, if it has access to the group or any of its subgroups -and projects. If the token is revoked, or was already revoked, its +and projects. If the token is revoked, or was already revoked, its details are returned in the response. The following criteria must be met: diff --git a/doc/api/projects.md b/doc/api/projects.md index 7de24c1817191f..e3cc29d84fe7d9 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2599,7 +2599,11 @@ POST /projects/:id/restore |-----------|-------------------|----------|-------------| | `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | -## Upload a file +## Markdown uploads + +Markdown uploads are files uploaded to a project that can be referenced in Markdown text in an issue, merge request, snippet, or wiki page. + +### Upload a file > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112450) in GitLab 15.10. Feature flag `enforce_max_attachment_size_upload_api` removed. @@ -2640,6 +2644,95 @@ The returned `url` is relative to the project path. The returned `full_path` is the absolute path to the file. In Markdown contexts, the link is expanded when the format in `markdown` is used. +### List uploads + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157066) in GitLab 17.2. + +Get all uploads of the project sorted by `created_at` in descending order. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +GET /projects/:id/uploads +``` + +| Attribute | Type | Required | Description | +|-----------|-------------------|----------|-------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/uploads" +``` + +Returned object: + +```json +[ + { + "id": 1, + "size": 1024, + "filename": "image.png", + "created_at":"2024-06-20T15:53:03.067Z", + "uploaded_by": { + "id": 18, + "name" : "Alexandra Bashirian", + "username" : "eileen.lowe" + } + }, + { + "id": 2, + "size": 512, + "filename": "other-image.png", + "created_at":"2024-06-19T15:53:03.067Z", + "uploaded_by": null + } +] +``` + +### Download an uploaded file + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157066) in GitLab 17.2. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +GET /projects/:id/uploads/:upload_id +``` + +| Attribute | Type | Required | Description | +|-------------|-------------------|----------|-------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | +| `upload_id` | integer | Yes | The ID of the upload. | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/uploads/1" +``` + +### Delete an uploaded file + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157066) in GitLab 17.2. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +DELETE /projects/:id/uploads/:upload_id +``` + +| Attribute | Type | Required | Description | +|-------------|-------------------|----------|-------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | +| `upload_id` | integer | Yes | The ID of the upload. | + +Example request: + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/uploads/1" +``` + ## Upload a project avatar Uploads an avatar to the specified project. diff --git a/doc/security/user_file_uploads.md b/doc/security/user_file_uploads.md index b2d6bc18fc0443..b70378e0acfb56 100644 --- a/doc/security/user_file_uploads.md +++ b/doc/security/user_file_uploads.md @@ -65,6 +65,7 @@ You cannot select this option for public projects. ## Delete uploaded files > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92791) in GitLab 15.3. +> - REST API [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157066) support in GitLab 17.2. You should delete an uploaded file when that file contains sensitive or confidential information. When you have deleted that file, users cannot access the file and the direct URL returns a 404 error. @@ -87,6 +88,8 @@ mutation{ Project members that do not have the Owner or Maintainer role cannot access this GraphQL endpoint. +You can also use the REST API for [projects](../api/projects.md#delete-an-uploaded-file) or [groups](../api/groups.md#delete-an-uploaded-file) to delete an uploaded file. +