projects.rb 18.1 KB
Newer Older
1 2
# frozen_string_literal: true

3
require_dependency 'declarative_policy'
4

5
module API
Nihad Abbasov's avatar
Nihad Abbasov committed
6
  class Projects < Grape::API
Robert Schilling's avatar
Robert Schilling committed
7
    include PaginationParams
8
    include Helpers::CustomAttributes
9
    include Helpers::ProjectsHelpers
Robert Schilling's avatar
Robert Schilling committed
10

11
    before { authenticate_non_get! }
Nihad Abbasov's avatar
Nihad Abbasov committed
12

13 14 15 16 17
    helpers do
      params :optional_filter_params_ee do
        # EE::API::Projects would override this helper
      end

Tiago Botelho's avatar
Tiago Botelho committed
18 19 20 21
      params :optional_update_params_ee do
        # EE::API::Projects would override this helper
      end

22 23 24 25 26 27
      # EE::API::Projects would override this method
      def apply_filters(projects)
        projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
        projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
        projects = projects.with_statistics if params[:statistics]

28 29 30
        lang = params[:with_programming_language]
        projects = projects.with_programming_language(lang) if lang

31 32
        projects
      end
Tiago Botelho's avatar
Tiago Botelho committed
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

      def verify_update_project_attrs!(project, attrs)
      end
    end

    def self.update_params_at_least_one_of
      [
        :jobs_enabled,
        :resolve_outdated_diff_discussions,
        :ci_config_path,
        :container_registry_enabled,
        :default_branch,
        :description,
        :issues_enabled,
        :lfs_enabled,
        :merge_requests_enabled,
        :merge_method,
        :name,
        :only_allow_merge_if_all_discussions_are_resolved,
        :only_allow_merge_if_pipeline_succeeds,
        :path,
        :printing_merge_request_link_enabled,
        :public_builds,
        :request_access_enabled,
        :shared_runners_enabled,
        :snippets_enabled,
        :tag_list,
        :visibility,
        :wiki_enabled,
        :avatar
      ]
64 65
    end

Robert Schilling's avatar
Robert Schilling committed
66
    helpers do
67 68 69
      params :statistics_params do
        optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
      end
vanadium23's avatar
vanadium23 committed
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87

      params :collection_params do
        use :sort_params
        use :filter_params
        use :pagination

        optional :simple, type: Boolean, default: false,
                          desc: 'Return only the ID, URL, name, and path of each project'
      end

      params :sort_params do
        optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
                            default: 'created_at', desc: 'Return projects ordered by field'
        optional :sort, type: String, values: %w[asc desc], default: 'desc',
                        desc: 'Return projects sorted in ascending and descending order'
      end

      params :filter_params do
88
        optional :archived, type: Boolean, desc: 'Limit by archived status'
vanadium23's avatar
vanadium23 committed
89 90 91 92 93 94 95 96
        optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
                              desc: 'Limit by visibility'
        optional :search, type: String, desc: 'Return list of projects matching the search criteria'
        optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
        optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
        optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
        optional :with_issues_enabled, type: Boolean, default: false, desc: 'Limit by enabled issues feature'
        optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
97
        optional :with_programming_language, type: String, desc: 'Limit to repositories which use the given programming language'
98
        optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
99 100

        use :optional_filter_params_ee
vanadium23's avatar
vanadium23 committed
101 102 103 104 105 106 107
      end

      params :create_params do
        optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
        optional :import_url, type: String, desc: 'URL from which the project is imported'
      end

108 109 110 111 112
      def load_projects
        ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
      end

      def present_projects(projects, options = {})
vanadium23's avatar
vanadium23 committed
113
        projects = reorder_projects(projects)
114
        projects = apply_filters(projects)
115
        projects = paginate(projects)
116
        projects, options = with_custom_attributes(projects, options)
vanadium23's avatar
vanadium23 committed
117 118 119 120

        options = options.reverse_merge(
          with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
          statistics: params[:statistics],
J.D. Bean's avatar
J.D. Bean committed
121 122
          current_user: current_user,
          license: false
vanadium23's avatar
vanadium23 committed
123 124 125
        )
        options[:with] = Entities::BasicProjectDetails if params[:simple]

126
        present options[:with].prepare_relation(projects, options), options
vanadium23's avatar
vanadium23 committed
127
      end
128 129 130 131 132

      def translate_params_for_compatibility(params)
        params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled)
        params
      end
Robert Schilling's avatar
Robert Schilling committed
133
    end
Nihad Abbasov's avatar
Nihad Abbasov committed
134

135
    resource :users, requirements: API::USER_REQUIREMENTS do
vanadium23's avatar
vanadium23 committed
136 137 138 139 140 141 142
      desc 'Get a user projects' do
        success Entities::BasicProjectDetails
      end
      params do
        requires :user_id, type: String, desc: 'The ID or username of the user'
        use :collection_params
        use :statistics_params
143
        use :with_custom_attributes
144
      end
vanadium23's avatar
vanadium23 committed
145 146 147
      get ":user_id/projects" do
        user = find_user(params[:user_id])
        not_found!('User') unless user
148

vanadium23's avatar
vanadium23 committed
149 150
        params[:user] = user

151
        present_projects load_projects
vanadium23's avatar
vanadium23 committed
152 153 154 155
      end
    end

    resource :projects do
156 157
      include CustomAttributesEndpoints

158
      desc 'Get a list of visible projects for authenticated user' do
Robert Schilling's avatar
Robert Schilling committed
159 160 161
        success Entities::BasicProjectDetails
      end
      params do
Markus Koller's avatar
Markus Koller committed
162
        use :collection_params
163
        use :statistics_params
164
        use :with_custom_attributes
Robert Schilling's avatar
Robert Schilling committed
165
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
166
      get do
167
        present_projects load_projects
168 169
      end

Robert Schilling's avatar
Robert Schilling committed
170 171 172 173
      desc 'Create new project' do
        success Entities::Project
      end
      params do
174
        optional :name, type: String, desc: 'The name of the project'
Robert Schilling's avatar
Robert Schilling committed
175
        optional :path, type: String, desc: 'The path of the repository'
176
        at_least_one_of :name, :path
177
        use :optional_project_params
Robert Schilling's avatar
Robert Schilling committed
178 179
        use :create_params
      end
180
      post do
181
        attrs = declared_params(include_missing: false)
182
        attrs = translate_params_for_compatibility(attrs)
Robert Schilling's avatar
Robert Schilling committed
183 184 185 186
        project = ::Projects::CreateService.new(current_user, attrs).execute

        if project.saved?
          present project, with: Entities::Project,
187 188
                           user_can_admin_project: can?(current_user, :admin_project, project),
                           current_user: current_user
189
        else
Robert Schilling's avatar
Robert Schilling committed
190 191
          if project.errors[:limit_reached].present?
            error!(project.errors[:limit_reached], 403)
192
          end
193

Robert Schilling's avatar
Robert Schilling committed
194
          render_validation_error!(project)
195 196 197
        end
      end

Robert Schilling's avatar
Robert Schilling committed
198 199 200 201 202 203
      desc 'Create new project for a specified user. Only available to admin users.' do
        success Entities::Project
      end
      params do
        requires :name, type: String, desc: 'The name of the project'
        requires :user_id, type: Integer, desc: 'The ID of a user'
204
        optional :path, type: String, desc: 'The path of the repository'
Robert Schilling's avatar
Robert Schilling committed
205
        optional :default_branch, type: String, desc: 'The default branch of the project'
206
        use :optional_project_params
Robert Schilling's avatar
Robert Schilling committed
207 208
        use :create_params
      end
209
      # rubocop: disable CodeReuse/ActiveRecord
Angus MacArthur's avatar
Angus MacArthur committed
210 211
      post "user/:user_id" do
        authenticated_as_admin!
Robert Schilling's avatar
Robert Schilling committed
212 213 214
        user = User.find_by(id: params.delete(:user_id))
        not_found!('User') unless user

215
        attrs = declared_params(include_missing: false)
216
        attrs = translate_params_for_compatibility(attrs)
Robert Schilling's avatar
Robert Schilling committed
217 218 219 220
        project = ::Projects::CreateService.new(user, attrs).execute

        if project.saved?
          present project, with: Entities::Project,
221 222
                           user_can_admin_project: can?(current_user, :admin_project, project),
                           current_user: current_user
Angus MacArthur's avatar
Angus MacArthur committed
223
        else
Robert Schilling's avatar
Robert Schilling committed
224
          render_validation_error!(project)
Angus MacArthur's avatar
Angus MacArthur committed
225 226
        end
      end
227
      # rubocop: enable CodeReuse/ActiveRecord
Robert Schilling's avatar
Robert Schilling committed
228
    end
Angus MacArthur's avatar
Angus MacArthur committed
229

Robert Schilling's avatar
Robert Schilling committed
230 231 232
    params do
      requires :id, type: String, desc: 'The ID of a project'
    end
233
    resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
Robert Schilling's avatar
Robert Schilling committed
234 235 236
      desc 'Get a single project' do
        success Entities::ProjectWithAccess
      end
237 238
      params do
        use :statistics_params
239
        use :with_custom_attributes
J.D. Bean's avatar
J.D. Bean committed
240 241 242

        optional :license, type: Boolean, default: false,
                           desc: 'Include project license data'
243
      end
Robert Schilling's avatar
Robert Schilling committed
244
      get ":id" do
245 246 247 248
        options = {
          with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
          current_user: current_user,
          user_can_admin_project: can?(current_user, :admin_project, user_project),
J.D. Bean's avatar
J.D. Bean committed
249 250
          statistics: params[:statistics],
          license: params[:license]
251 252 253 254 255
        }

        project, options = with_custom_attributes(user_project, options)

        present project, options
Robert Schilling's avatar
Robert Schilling committed
256 257 258 259 260 261 262 263
      end

      desc 'Fork new project for the current user or provided namespace.' do
        success Entities::Project
      end
      params do
        optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
      end
264
      post ':id/fork' do
265 266
        Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42284')

Robert Schilling's avatar
Robert Schilling committed
267 268
        fork_params = declared_params(include_missing: false)
        namespace_id = fork_params[:namespace]
269

270
        if namespace_id.present?
271
          fork_params[:namespace] = find_namespace(namespace_id)
272

Robert Schilling's avatar
Robert Schilling committed
273
          unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
274 275
            not_found!('Target Namespace')
          end
276
        end
277

Robert Schilling's avatar
Robert Schilling committed
278
        forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
279

Robert Schilling's avatar
Robert Schilling committed
280 281
        if forked_project.errors.any?
          conflict!(forked_project.errors.messages)
282
        else
Robert Schilling's avatar
Robert Schilling committed
283
          present forked_project, with: Entities::Project,
284 285
                                  user_can_admin_project: can?(current_user, :admin_project, forked_project),
                                  current_user: current_user
286
        end
287 288
      end

289 290 291 292 293
      desc 'List forks of this project' do
        success Entities::Project
      end
      params do
        use :collection_params
294
        use :with_custom_attributes
295 296 297 298 299 300 301
      end
      get ':id/forks' do
        forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute

        present_projects forks
      end

302 303 304 305 306 307
      desc 'Check pages access of this project'
      get ':id/pages_access' do
        authorize! :read_pages_content, user_project unless user_project.public_pages?
        status 200
      end

Robert Schilling's avatar
Robert Schilling committed
308 309 310 311 312 313 314
      desc 'Update an existing project' do
        success Entities::Project
      end
      params do
        optional :name, type: String, desc: 'The name of the project'
        optional :default_branch, type: String, desc: 'The default branch of the project'
        optional :path, type: String, desc: 'The path of the repository'
315

316
        use :optional_project_params
Tiago Botelho's avatar
Tiago Botelho committed
317 318

        at_least_one_of(*::API::Projects.update_params_at_least_one_of)
Robert Schilling's avatar
Robert Schilling committed
319
      end
320 321
      put ':id' do
        authorize_admin_project
322
        attrs = declared_params(include_missing: false)
323
        authorize! :rename_project, user_project if attrs[:name].present?
324
        authorize! :change_visibility_level, user_project if attrs[:visibility].present?
325

326
        attrs = translate_params_for_compatibility(attrs)
327

Tiago Botelho's avatar
Tiago Botelho committed
328 329
        verify_update_project_attrs!(user_project, attrs)

330
        result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
331

332
        if result[:status] == :success
333
          present user_project, with: Entities::Project,
334 335
                                user_can_admin_project: can?(current_user, :admin_project, user_project),
                                current_user: current_user
336 337
        else
          render_validation_error!(user_project)
338 339 340
        end
      end

Robert Schilling's avatar
Robert Schilling committed
341 342 343
      desc 'Archive a project' do
        success Entities::Project
      end
344
      post ':id/archive' do
345 346
        authorize!(:archive_project, user_project)

347
        ::Projects::UpdateService.new(user_project, current_user, archived: true).execute
348

349
        present user_project, with: Entities::Project, current_user: current_user
350 351
      end

Robert Schilling's avatar
Robert Schilling committed
352 353 354
      desc 'Unarchive a project' do
        success Entities::Project
      end
355
      post ':id/unarchive' do
356 357
        authorize!(:archive_project, user_project)

358
        ::Projects::UpdateService.new(@project, current_user, archived: false).execute
359

360
        present user_project, with: Entities::Project, current_user: current_user
361 362
      end

Robert Schilling's avatar
Robert Schilling committed
363 364 365
      desc 'Star a project' do
        success Entities::Project
      end
366
      post ':id/star' do
367 368 369
        if current_user.starred?(user_project)
          not_modified!
        else
370 371
          current_user.toggle_star(user_project)
          user_project.reload
372

373
          present user_project, with: Entities::Project, current_user: current_user
374 375 376
        end
      end

Robert Schilling's avatar
Robert Schilling committed
377 378 379
      desc 'Unstar a project' do
        success Entities::Project
      end
380
      post ':id/unstar' do
381 382 383
        if current_user.starred?(user_project)
          current_user.toggle_star(user_project)
          user_project.reload
384

385
          present user_project, with: Entities::Project, current_user: current_user
386 387 388 389 390
        else
          not_modified!
        end
      end

391 392 393 394 395
      desc 'Get languages in project repository'
      get ':id/languages' do
        user_project.repository.languages.map { |language| language.values_at(:label, :value) }.to_h
      end

Robert Schilling's avatar
Robert Schilling committed
396
      desc 'Remove a project'
397 398
      delete ":id" do
        authorize! :remove_project, user_project
399

400 401 402
        destroy_conditionally!(user_project) do
          ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
        end
403 404

        accepted!
405
      end
Angus MacArthur's avatar
Angus MacArthur committed
406

Robert Schilling's avatar
Robert Schilling committed
407 408 409 410
      desc 'Mark this project as forked from another'
      params do
        requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from'
      end
411
      post ":id/fork/:forked_from_id" do
412
        authorize! :admin_project, user_project
Robert Schilling's avatar
Robert Schilling committed
413

414
        fork_from_project = find_project!(params[:forked_from_id])
Robert Schilling's avatar
Robert Schilling committed
415

416
        not_found!("Source Project") unless fork_from_project
417

418 419 420
        result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project)

        if result
421
          present user_project.reload, with: Entities::Project, current_user: current_user
422
        else
423
          render_api_error!("Project already forked", 409) if user_project.forked?
424 425 426
        end
      end

Robert Schilling's avatar
Robert Schilling committed
427
      desc 'Remove a forked_from relationship'
428
      delete ":id/fork" do
429
        authorize! :remove_fork_project, user_project
Robert Schilling's avatar
Robert Schilling committed
430

431 432
        result = destroy_conditionally!(user_project) do
          ::Projects::UnlinkForkService.new(user_project, current_user).execute
433
        end
434 435

        result ? status(204) : not_modified!
436
      end
Douwe Maan's avatar
Douwe Maan committed
437

Robert Schilling's avatar
Robert Schilling committed
438 439 440 441 442
      desc 'Share the project with a group' do
        success Entities::ProjectGroupLink
      end
      params do
        requires :group_id, type: Integer, desc: 'The ID of a group'
443
        requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level'
Robert Schilling's avatar
Robert Schilling committed
444 445
        optional :expires_at, type: Date, desc: 'Share expiration date'
      end
446 447
      post ":id/share" do
        authorize! :admin_project, user_project
Robert Schilling's avatar
Robert Schilling committed
448
        group = Group.find_by_id(params[:group_id])
449

450
        unless user_project.allowed_to_share_with_group?
451
          break render_api_error!("The project sharing with group is disabled", 400)
452 453
        end

454 455
        result = ::Projects::GroupLinks::CreateService.new(user_project, current_user, declared_params(include_missing: false))
          .execute(group)
456

457 458
        if result[:status] == :success
          present result[:link], with: Entities::ProjectGroupLink
459
        else
460
          render_api_error!(result[:message], result[:http_status])
461 462 463
        end
      end

464 465
      params do
        requires :group_id, type: Integer, desc: 'The ID of the group'
Douwe Maan's avatar
Douwe Maan committed
466
      end
467
      # rubocop: disable CodeReuse/ActiveRecord
468 469 470 471 472
      delete ":id/share/:group_id" do
        authorize! :admin_project, user_project

        link = user_project.project_group_links.find_by(group_id: params[:group_id])
        not_found!('Group Link') unless link
Douwe Maan's avatar
Douwe Maan committed
473

474
        destroy_conditionally!(link)
475
      end
476
      # rubocop: enable CodeReuse/ActiveRecord
477

Robert Schilling's avatar
Robert Schilling committed
478 479 480 481
      desc 'Upload a file'
      params do
        requires :file, type: File, desc: 'The file to be uploaded'
      end
Douwe Maan's avatar
Douwe Maan committed
482
      post ":id/uploads" do
483
        UploadService.new(user_project, params[:file]).execute.to_h
484
      end
485

Robert Schilling's avatar
Robert Schilling committed
486 487 488 489 490 491
      desc 'Get the users list of a project' do
        success Entities::UserBasic
      end
      params do
        optional :search, type: String, desc: 'Return list of users matching the search criteria'
        use :pagination
492
      end
493
      get ':id/users' do
494
        users = DeclarativePolicy.subject_scope { user_project.team.users }
Robert Schilling's avatar
Robert Schilling committed
495 496 497
        users = users.search(params[:search]) if params[:search].present?

        present paginate(users), with: Entities::UserBasic
498
      end
499 500 501 502 503 504 505 506 507 508 509 510

      desc 'Start the housekeeping task for a project' do
        detail 'This feature was introduced in GitLab 9.0.'
      end
      post ':id/housekeeping' do
        authorize_admin_project

        begin
          ::Projects::HousekeepingService.new(user_project).execute
        rescue ::Projects::HousekeepingService::LeaseTaken => error
          conflict!(error.message)
        end
511
      end
512 513 514 515 516 517 518 519 520 521 522 523

      desc 'Transfer a project to a new namespace'
      params do
        requires :namespace, type: String, desc: 'The ID or path of the new namespace'
      end
      put ":id/transfer" do
        authorize! :change_namespace, user_project

        namespace = find_namespace!(params[:namespace])
        result = ::Projects::TransferService.new(user_project, current_user).execute(namespace)

        if result
524
          present user_project, with: Entities::Project, current_user: current_user
525 526 527 528
        else
          render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400)
        end
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
529 530 531
    end
  end
end