BigW Consortium Gitlab

projects.rb 16.6 KB
Newer Older
1
require_dependency 'declarative_policy'
2

3
module API
4 5
  # Projects API
  class Projects < Grape::API
6 7
    include PaginationParams

8
    before { authenticate_non_get! }
9

10
    helpers do
11
      params :optional_params_ce do
12
        optional :description, type: String, desc: 'The description of the project'
13
        optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`'
14 15 16
        optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
        optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
        optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
17
        optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled'
18 19 20 21
        optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
        optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
        optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
        optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
22
        optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
23 24
        optional :public_builds, type: Boolean, desc: 'Perform public builds'
        optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
25
        optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
26
        optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
27
        optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
28
        optional :avatar, type: File, desc: 'Avatar image for project'
29
        optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
30
      end
31 32 33 34

      params :optional_params do
        use :optional_params_ce
      end
35 36 37 38

      params :statistics_params do
        optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
      end
vanadium23 committed
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

      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
        optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
        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'
      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

      def present_projects(options = {})
        projects = ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
        projects = reorder_projects(projects)
        projects = projects.with_statistics if params[:statistics]
        projects = projects.with_issues_enabled if params[:with_issues_enabled]
        projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]

80 81 82 83 84 85
        if current_user
          projects = projects.includes(:route, :taggings, namespace: :route)
          project_members = current_user.project_members
          group_members = current_user.group_members
        end

vanadium23 committed
86 87 88
        options = options.reverse_merge(
          with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
          statistics: params[:statistics],
89 90
          project_members: project_members,
          group_members: group_members,
vanadium23 committed
91 92 93 94 95 96
          current_user: current_user
        )
        options[:with] = Entities::BasicProjectDetails if params[:simple]

        present paginate(projects), options
      end
97
    end
98

vanadium23 committed
99 100 101 102 103 104 105 106
    resource :users, requirements: { user_id: %r{[^/]+} } do
      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
107
      end
vanadium23 committed
108 109 110
      get ":user_id/projects" do
        user = find_user(params[:user_id])
        not_found!('User') unless user
111

vanadium23 committed
112 113 114 115 116 117 118
        params[:user] = user

        present_projects
      end
    end

    resource :projects do
119
      desc 'Get a list of visible projects for authenticated user' do
120 121 122
        success Entities::BasicProjectDetails
      end
      params do
123
        use :collection_params
124
        use :statistics_params
125
      end
126
      get do
127
        present_projects
128 129
      end

130 131 132 133
      desc 'Create new project' do
        success Entities::Project
      end
      params do
134
        optional :name, type: String, desc: 'The name of the project'
135
        optional :path, type: String, desc: 'The path of the repository'
136
        at_least_one_of :name, :path
137 138 139
        use :optional_params
        use :create_params
      end
140
      post do
141
        attrs = declared_params(include_missing: false)
142
        attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
143 144 145 146 147
        project = ::Projects::CreateService.new(current_user, attrs).execute

        if project.saved?
          present project, with: Entities::Project,
                           user_can_admin_project: can?(current_user, :admin_project, project)
148
        else
149 150
          if project.errors[:limit_reached].present?
            error!(project.errors[:limit_reached], 403)
151
          end
152
          render_validation_error!(project)
153 154 155
        end
      end

156 157 158 159 160 161
      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'
162
        optional :path, type: String, desc: 'The path of the repository'
163 164 165 166
        optional :default_branch, type: String, desc: 'The default branch of the project'
        use :optional_params
        use :create_params
      end
Angus MacArthur committed
167 168
      post "user/:user_id" do
        authenticated_as_admin!
169 170 171
        user = User.find_by(id: params.delete(:user_id))
        not_found!('User') unless user

172
        attrs = declared_params(include_missing: false)
173 174 175 176 177
        project = ::Projects::CreateService.new(user, attrs).execute

        if project.saved?
          present project, with: Entities::Project,
                           user_can_admin_project: can?(current_user, :admin_project, project)
Angus MacArthur committed
178
        else
179
          render_validation_error!(project)
Angus MacArthur committed
180 181
        end
      end
182
    end
Angus MacArthur committed
183

184 185 186
    params do
      requires :id, type: String, desc: 'The ID of a project'
    end
187
    resource :projects, requirements: { id: %r{[^/]+} } do
188 189 190
      desc 'Get a single project' do
        success Entities::ProjectWithAccess
      end
191 192 193
      params do
        use :statistics_params
      end
194
      get ":id" do
195
        entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
196
        present user_project, with: entity, current_user: current_user,
197
                              user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
198 199 200 201 202 203 204 205
      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
206
      post ':id/fork' do
207 208
        fork_params = declared_params(include_missing: false)
        namespace_id = fork_params[:namespace]
209

210
        if namespace_id.present?
211 212 213 214 215
          fork_params[:namespace] = if namespace_id =~ /^\d+$/
                                      Namespace.find_by(id: namespace_id)
                                    else
                                      Namespace.find_by_path_or_name(namespace_id)
                                    end
216

217
          unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
218 219
            not_found!('Target Namespace')
          end
220
        end
221

222
        forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
223

224 225
        if forked_project.errors.any?
          conflict!(forked_project.errors.messages)
226
        else
227 228
          present forked_project, with: Entities::Project,
                                  user_can_admin_project: can?(current_user, :admin_project, forked_project)
229
        end
230 231
      end

232 233 234 235
      desc 'Update an existing project' do
        success Entities::Project
      end
      params do
236 237 238
        # CE
        at_least_one_of_ce =
          [
239
            :jobs_enabled,
240 241 242 243 244 245 246 247 248 249
            :container_registry_enabled,
            :default_branch,
            :description,
            :issues_enabled,
            :lfs_enabled,
            :merge_requests_enabled,
            :name,
            :only_allow_merge_if_all_discussions_are_resolved,
            :only_allow_merge_if_pipeline_succeeds,
            :path,
250
            :printing_merge_request_link_enabled,
251 252 253 254
            :public_builds,
            :request_access_enabled,
            :shared_runners_enabled,
            :snippets_enabled,
255
            :tag_list,
256
            :visibility,
257
            :wiki_enabled
258
          ]
259 260 261
        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'
262

263
        use :optional_params
264
        at_least_one_of(*at_least_one_of_ce)
265
      end
266 267
      put ':id' do
        authorize_admin_project
268
        attrs = declared_params(include_missing: false)
269
        authorize! :rename_project, user_project if attrs[:name].present?
270
        authorize! :change_visibility_level, user_project if attrs[:visibility].present?
271

272
        attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
273

274
        result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
275

276
        if result[:status] == :success
277 278
          present user_project, with: Entities::Project,
                                user_can_admin_project: can?(current_user, :admin_project, user_project)
279 280
        else
          render_validation_error!(user_project)
281 282 283
        end
      end

284 285 286
      desc 'Archive a project' do
        success Entities::Project
      end
287
      post ':id/archive' do
288 289 290 291
        authorize!(:archive_project, user_project)

        user_project.archive!

292
        present user_project, with: Entities::Project
293 294
      end

295 296 297
      desc 'Unarchive a project' do
        success Entities::Project
      end
298
      post ':id/unarchive' do
299 300 301 302
        authorize!(:archive_project, user_project)

        user_project.unarchive!

303
        present user_project, with: Entities::Project
304 305
      end

306 307 308
      desc 'Star a project' do
        success Entities::Project
      end
309
      post ':id/star' do
310 311 312
        if current_user.starred?(user_project)
          not_modified!
        else
313 314
          current_user.toggle_star(user_project)
          user_project.reload
315

316 317 318 319
          present user_project, with: Entities::Project
        end
      end

320 321 322
      desc 'Unstar a project' do
        success Entities::Project
      end
323
      post ':id/unstar' do
324 325 326
        if current_user.starred?(user_project)
          current_user.toggle_star(user_project)
          user_project.reload
327

328 329 330 331 332 333
          present user_project, with: Entities::Project
        else
          not_modified!
        end
      end

334
      desc 'Remove a project'
335 336
      delete ":id" do
        authorize! :remove_project, user_project
Stan Hu committed
337
        ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
338 339

        accepted!
340
      end
Angus MacArthur committed
341

342 343 344 345
      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
346 347
      post ":id/fork/:forked_from_id" do
        authenticated_as_admin!
348

349
        forked_from_project = find_project!(params[:forked_from_id])
350 351 352 353
        not_found!("Source Project") unless forked_from_project

        if user_project.forked_from_project.nil?
          user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
354 355

          ::Projects::ForksCountService.new(forked_from_project).refresh_cache
356
        else
357
          render_api_error!("Project already forked", 409)
358 359 360
        end
      end

361
      desc 'Remove a forked_from relationship'
362
      delete ":id/fork" do
363
        authorize! :remove_fork_project, user_project
364

365
        if user_project.forked?
Dmitriy Zaporozhets committed
366
          status 204
367
          user_project.forked_project_link.destroy
368 369
        else
          not_modified!
370 371
        end
      end
372

373 374 375 376 377 378 379 380
      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'
        requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level'
        optional :expires_at, type: Date, desc: 'Share expiration date'
      end
381 382
      post ":id/share" do
        authorize! :admin_project, user_project
383
        group = Group.find_by_id(params[:group_id])
384 385 386 387

        unless group && can?(current_user, :read_group, group)
          not_found!('Group')
        end
388 389 390 391 392

        unless user_project.allowed_to_share_with_group?
          return render_api_error!("The project sharing with group is disabled", 400)
        end

393
        link = user_project.project_group_links.new(declared_params(include_missing: false))
394

395 396 397 398 399 400 401
        if link.save
          present link, with: Entities::ProjectGroupLink
        else
          render_api_error!(link.errors.full_messages.first, 409)
        end
      end

402 403
      params do
        requires :group_id, type: Integer, desc: 'The ID of the group'
404
      end
405 406 407 408 409
      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
410

Dmitriy Zaporozhets committed
411
        status 204
412 413
        link.destroy
      end
414

415 416 417 418
      desc 'Upload a file'
      params do
        requires :file, type: File, desc: 'The file to be uploaded'
      end
419
      post ":id/uploads" do
420
        UploadService.new(user_project, params[:file]).execute
421
      end
422

423 424 425 426 427 428
      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
429
      end
430
      get ':id/users' do
431
        users = DeclarativePolicy.subject_scope { user_project.team.users }
432 433 434
        users = users.search(params[:search]) if params[:search].present?

        present paginate(users), with: Entities::UserBasic
435
      end
436 437 438 439 440 441 442 443 444 445 446 447

      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
448
      end
449 450 451
    end
  end
end