diff --git a/ee/lib/ee/api/entities/project.rb b/ee/lib/ee/api/entities/project.rb index f4e216cdbc470bdc54d22901c4040c4a1d233c8b..7220156f7729caa924cee299bba495995686e046 100644 --- a/ee/lib/ee/api/entities/project.rb +++ b/ee/lib/ee/api/entities/project.rb @@ -16,59 +16,49 @@ def preload_relation(projects_relation, options = {}) end prepended do - expose :issues_template, - if: ->(project, options) { - project.feature_available?(:issuable_default_templates) && - Ability.allowed?(options[:current_user], :read_issue, project) - } - - expose :merge_requests_template, - if: ->(project, options) { - project.feature_available?(:issuable_default_templates) && - Ability.allowed?(options[:current_user], :read_merge_request, project) - } - - with_options if: ->(project, options) { Ability.allowed?(options[:current_user], :admin_project, project) } do - expose :approvals_before_merge, if: ->(project, _) { project.feature_available?(:merge_request_approvers) } - expose :mirror, if: ->(project, _) { project.feature_available?(:repository_mirrors) } - expose :mirror_user_id, if: ->(project, _) { project.mirror? } - expose :mirror_trigger_builds, if: ->(project, _) { project.mirror? } - expose :only_mirror_protected_branches, if: ->(project, _) { project.mirror? } - expose :mirror_overwrites_diverged_branches, if: ->(project, _) { project.mirror? } - expose :external_authorization_classification_label, - if: ->(_, _) { License.feature_available?(:external_authorization_service_api_management) } - expose :marked_for_deletion_at, if: ->(project, _) { project.feature_available?(:adjourned_deletion_for_projects_and_groups) } - expose :marked_for_deletion_on, if: ->(project, _) { project.feature_available?(:adjourned_deletion_for_projects_and_groups) } do |project, _| - project.marked_for_deletion_at - end - - # Expose old field names with the new permissions methods to keep API compatible - # TODO: remove in API v5, replaced by *_access_level - expose :requirements_enabled do |project, options| - project.feature_available?(:requirements, options[:current_user]) - end - - expose :security_and_compliance_enabled do |project, options| - project.feature_available?(:security_and_compliance, options[:current_user]) - end - - expose :requirements_access_level do |project, _| - project_feature_string_access_level(project, :requirements) - end - - expose :compliance_frameworks do |project, _| - [project.compliance_framework_setting&.compliance_management_framework&.name].compact - end - - expose :merge_pipelines_enabled?, as: :merge_pipelines_enabled, if: ->(project, _) { project.feature_available?(:merge_pipelines) } - expose :merge_trains_enabled?, as: :merge_trains_enabled, if: ->(project, _) { project.feature_available?(:merge_pipelines) } - expose :merge_trains_skip_train_allowed?, as: :merge_trains_skip_train_allowed, if: ->(project, _) { project.feature_available?(:merge_pipelines) } - expose :only_allow_merge_if_all_status_checks_passed, if: ->(project, _) { project.feature_available?(:external_status_checks) } - expose :allow_pipeline_trigger_approve_deployment, documentation: { type: 'boolean' }, if: ->(project, _) { project.feature_available?(:protected_environments) } - expose :prevent_merge_without_jira_issue, if: ->(project, _) { project.feature_available?(:jira_issue_association_enforcement) } + expose :approvals_before_merge, if: ->(project, _) { project.feature_available?(:merge_request_approvers) } + expose :mirror, if: ->(project, _) { project.feature_available?(:repository_mirrors) } + expose :mirror_user_id, if: ->(project, _) { project.mirror? } + expose :mirror_trigger_builds, if: ->(project, _) { project.mirror? } + expose :only_mirror_protected_branches, if: ->(project, _) { project.mirror? } + expose :mirror_overwrites_diverged_branches, if: ->(project, _) { project.mirror? } + expose :external_authorization_classification_label, + if: ->(_, _) { License.feature_available?(:external_authorization_service_api_management) } + expose :marked_for_deletion_at, if: ->(project, _) { project.feature_available?(:adjourned_deletion_for_projects_and_groups) } + expose :marked_for_deletion_on, if: ->(project, _) { project.feature_available?(:adjourned_deletion_for_projects_and_groups) } do |project, _| + project.marked_for_deletion_at + end + # Expose old field names with the new permissions methods to keep API compatible + # TODO: remove in API v5, replaced by *_access_level + expose :requirements_enabled do |project, options| + project.feature_available?(:requirements, options[:current_user]) + end + expose(:requirements_access_level) { |project, _| project_feature_string_access_level(project, :requirements) } - expose :restrict_pipeline_cancellation_role, as: :ci_restrict_pipeline_cancellation_role, if: ->(project, _) { project.ci_cancellation_restriction.feature_available? } + expose :security_and_compliance_enabled do |project, options| + project.feature_available?(:security_and_compliance, options[:current_user]) + end + expose :compliance_frameworks do |project, _| + [project.compliance_framework_setting&.compliance_management_framework&.name].compact + end + expose :issues_template, if: ->(project, options) do + project.feature_available?(:issuable_default_templates) && + Ability.allowed?(options[:current_user], :read_issue, project) + end + expose :merge_requests_template, if: ->(project, options) do + project.feature_available?(:issuable_default_templates) && + Ability.allowed?(options[:current_user], :read_merge_request, project) end + expose :restrict_pipeline_cancellation_role, as: :ci_restrict_pipeline_cancellation_role, if: ->(project, options) { + project.ci_cancellation_restriction.feature_available? && + Ability.allowed?(options[:current_user], :admin_project, project) + } + expose :merge_pipelines_enabled?, as: :merge_pipelines_enabled, if: ->(project, _) { project.feature_available?(:merge_pipelines) } + expose :merge_trains_enabled?, as: :merge_trains_enabled, if: ->(project, _) { project.feature_available?(:merge_pipelines) } + expose :merge_trains_skip_train_allowed?, as: :merge_trains_skip_train_allowed, if: ->(project, _) { project.feature_available?(:merge_pipelines) } + expose :only_allow_merge_if_all_status_checks_passed, if: ->(project, _) { project.feature_available?(:external_status_checks) } + expose :allow_pipeline_trigger_approve_deployment, documentation: { type: 'boolean' }, if: ->(project, _) { project.feature_available?(:protected_environments) } + expose :prevent_merge_without_jira_issue, if: ->(project, _) { project.feature_available?(:jira_issue_association_enforcement) } end end end diff --git a/ee/spec/lib/ee/api/entities/project_spec.rb b/ee/spec/lib/ee/api/entities/project_spec.rb index 1fbd3544d8905f8468966c1a942eaf8ff4370462..047229f1edf0522292d90e2e62d5da4c4ebfba37 100644 --- a/ee/spec/lib/ee/api/entities/project_spec.rb +++ b/ee/spec/lib/ee/api/entities/project_spec.rb @@ -2,178 +2,68 @@ require 'spec_helper' -RSpec.describe EE::API::Entities::Project, feature_category: :shared do - let_it_be(:private_project) { create(:project, :private) } - let_it_be(:current_user) { create(:user) } +RSpec.describe ::EE::API::Entities::Project, feature_category: :shared do + let_it_be(:project) { create(:project) } - let(:project) { private_project } - let(:options) { { current_user: current_user, statistics: true } } - let(:entity) { ::API::Entities::Project.new(project, options) } + let(:options) { {} } + let(:developer) { create(:user, developer_of: project) } - subject(:json) { entity.as_json } - - before do - allow(Gitlab.config.registry).to receive(:enabled).and_return(true) - end - - context 'as a guest' do - before_all do - private_project.add_guest(current_user) - end - - it 'exposes the correct attributes' do - expect(json.keys).to contain_exactly( - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, - :created_at, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, :web_url, - :avatar_url, :star_count, :last_activity_at, :namespace, :container_registry_image_prefix, - :_links, :empty_repo, :archived, :visibility, :owner, :open_issues_count, - :description_html, :updated_at, :can_create_merge_request_in, :shared_with_groups, - :issues_template - ) - end - end - - context 'as a reporter' do - before_all do - private_project.add_reporter(current_user) - end - - it 'exposes the correct attributes' do - expect(json.keys).to contain_exactly( - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, - :created_at, :default_branch, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, - :web_url, :readme_url, :forks_count, :avatar_url, :star_count, :last_activity_at, - :namespace, :container_registry_image_prefix, :_links, :empty_repo, :archived, - :visibility, :owner, :open_issues_count, :description_html, :updated_at, - :can_create_merge_request_in, :statistics, :ci_config_path, - :shared_with_groups, :service_desk_address, :issues_template, :merge_requests_template - ) - end - end - - context 'as a developer' do - before_all do - private_project.add_developer(current_user) - end - - it 'exposes the correct attributes' do - expect(json.keys).to contain_exactly( - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, - :created_at, :default_branch, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, - :web_url, :readme_url, :forks_count, :avatar_url, :star_count, :last_activity_at, - :namespace, :container_registry_image_prefix, :_links, :empty_repo, :archived, - :visibility, :owner, :open_issues_count, :description_html, :updated_at, - :can_create_merge_request_in, :statistics, :ci_config_path, :shared_with_groups, - :service_desk_address, :issues_template, :merge_requests_template - ) - end + let(:entity) do + ::API::Entities::Project.new(project, options) end - context 'as a maintainer' do - before_all do - private_project.add_maintainer(current_user) - end - - it 'exposes the correct attributes' do - expect(json.keys).to contain_exactly( - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, :created_at, - :default_branch, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, :web_url, - :readme_url, :forks_count, :avatar_url, :star_count, :last_activity_at, :namespace, - :container_registry_image_prefix, :_links, :empty_repo, :archived, :visibility, - :owner, :open_issues_count, :description_html, :updated_at, :can_create_merge_request_in, - :statistics, :shared_with_groups, :service_desk_address, :emails_disabled, :emails_enabled, - :resolve_outdated_diff_discussions, :container_expiration_policy, :repository_object_format, - :shared_runners_enabled, :lfs_enabled, :creator_id, :import_url, :import_type, - :import_status, :import_error, :ci_config_path, :ci_default_git_depth, - :ci_forward_deployment_enabled, :ci_forward_deployment_rollback_allowed, - :ci_job_token_scope_enabled, :ci_separated_caches, - :ci_allow_fork_pipelines_to_run_in_parent_project, :build_git_strategy, - :keep_latest_artifact, :restrict_user_defined_variables, :runners_token, - :runner_token_expiration_interval, :group_runners_enabled, :auto_cancel_pending_pipelines, - :build_timeout, :auto_devops_enabled, :auto_devops_deploy_strategy, :public_jobs, - :only_allow_merge_if_pipeline_succeeds, :allow_merge_on_skipped_pipeline, - :request_access_enabled, :only_allow_merge_if_all_discussions_are_resolved, - :remove_source_branch_after_merge, :printing_merge_request_link_enabled, - :merge_method, :squash_option, :enforce_auth_checks_on_uploads, - :suggestion_commit_message, :merge_commit_template, :squash_commit_template, - :issue_branch_template, :warn_about_potentially_unwanted_characters, - :autoclose_referenced_issues, :packages_enabled, :service_desk_enabled, - :issues_enabled, :merge_requests_enabled, :wiki_enabled, :jobs_enabled, - :snippets_enabled, :container_registry_enabled, :issues_access_level, - :repository_access_level, :merge_requests_access_level, :forking_access_level, - :wiki_access_level, :builds_access_level, :snippets_access_level, :pages_access_level, - :analytics_access_level, :container_registry_access_level, - :security_and_compliance_access_level, :releases_access_level, :environments_access_level, - :feature_flags_access_level, :infrastructure_access_level, :monitor_access_level, - :model_experiments_access_level, :model_registry_access_level, :issues_template, - :merge_requests_template, :approvals_before_merge, :mirror, :requirements_enabled, - :security_and_compliance_enabled, :requirements_access_level, :compliance_frameworks - ) - end - end + subject { entity.as_json } context 'compliance_frameworks' do - let_it_be(:project_with_compliance) { create(:project, :with_sox_compliance_framework) } - context 'when project has a compliance framework' do - let(:project) { project_with_compliance } - - before_all do - project_with_compliance.add_maintainer(current_user) - end + let(:project) { create(:project, :with_sox_compliance_framework) } it 'is an array containing a single compliance framework' do - expect(json[:compliance_frameworks]).to contain_exactly('SOX') + expect(subject[:compliance_frameworks]).to contain_exactly('SOX') end end context 'when project has no compliance framework' do - let(:project) { private_project } - - before_all do - private_project.add_maintainer(current_user) - end + let(:project) { create(:project) } it 'is empty array when project has no compliance framework' do - expect(json[:compliance_frameworks]).to eq([]) + expect(subject[:compliance_frameworks]).to eq([]) end end end describe 'ci_restrict_pipeline_cancellation_role' do + let(:options) { { current_user: current_user } } + context 'when user has maintainer permission or above' do - before_all do - private_project.add_maintainer(current_user) - end + let(:current_user) { project.owner } - context 'when feature is available' do + context 'when available' do before do mock_available end - it { expect(json[:ci_restrict_pipeline_cancellation_role]).to eq 'developer' } + it { expect(subject[:ci_restrict_pipeline_cancellation_role]).to eq 'developer' } end - context 'when feature is not available' do - it { expect(json[:ci_restrict_pipeline_cancellation_role]).to be nil } + context 'when not available' do + it { expect(subject[:ci_restrict_pipeline_cancellation_role]).to be nil } end end context 'when user permission is below maintainer' do - before_all do - private_project.add_developer(current_user) - end + let(:current_user) { developer } - context 'when feature is available' do + context 'when available' do before do mock_available end - it { expect(json[:ci_restrict_pipeline_cancellation_role]).to be nil } + it { expect(subject[:ci_restrict_pipeline_cancellation_role]).to be nil } end - context 'when feature is not available' do - it { expect(json[:ci_restrict_pipeline_cancellation_role]).to be nil } + context 'when not available' do + it { expect(subject[:ci_restrict_pipeline_cancellation_role]).to be nil } end end diff --git a/ee/spec/requests/api/projects_spec.rb b/ee/spec/requests/api/projects_spec.rb index dee60f05701ab3adf16b5d03e3990598a4592720..6a11061f5d15c692701761af342e9cabf0bb1878 100644 --- a/ee/spec/requests/api/projects_spec.rb +++ b/ee/spec/requests/api/projects_spec.rb @@ -278,10 +278,6 @@ create(:project, :public, archived: true, marked_for_deletion_at: 1.day.ago, deleting_user: user) end - before do - project.add_maintainer(user) - end - describe 'marked_for_deletion_at attribute' do it 'exposed when the feature is available' do stub_licensed_features(adjourned_deletion_for_projects_and_groups: true) diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index a42574d3c925efdbebac50da5dd05e8203b2652d..50074ca10d957caacea729f4701ddc0220200c08 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -41,78 +41,87 @@ class Project < BasicProjectDetails end end + expose :packages_enabled, documentation: { type: 'boolean' } expose :empty_repo?, as: :empty_repo, documentation: { type: 'boolean' } expose :archived?, as: :archived, documentation: { type: 'boolean' } expose :visibility, documentation: { type: 'string', example: 'public' } expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } - expose :open_issues_count, documentation: { type: 'integer', example: 1 }, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } - expose :description_html, documentation: { type: 'string' } - expose :updated_at, documentation: { type: 'dateTime', example: '2020-05-07T04:27:17.016Z' } + expose :resolve_outdated_diff_discussions, documentation: { type: 'boolean' } + expose :container_expiration_policy, + using: Entities::ContainerExpirationPolicy, + if: ->(project, _) { project.container_expiration_policy } + expose :repository_object_format, documentation: { type: 'string', example: 'sha1' } + + # Expose old field names with the new permissions methods to keep API compatible + # TODO: remove in API v5, replaced by *_access_level + expose(:issues_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose(:merge_requests_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } + expose(:wiki_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:wiki, options[:current_user]) } + expose(:jobs_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:builds, options[:current_user]) } + expose(:snippets_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:snippets, options[:current_user]) } + expose(:container_registry_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:container_registry, options[:current_user]) } + expose :service_desk_enabled, documentation: { type: 'boolean' } + expose :service_desk_address, documentation: { type: 'string', example: 'address@example.com' }, if: ->(project, options) do + Ability.allowed?(options[:current_user], :admin_issue, project) + end expose(:can_create_merge_request_in, documentation: { type: 'boolean' }) do |project, options| Ability.allowed?(options[:current_user], :create_merge_request_in, project) end + expose(:issues_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :issues) } + expose(:repository_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :repository) } + expose(:merge_requests_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :merge_requests) } + expose(:forking_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :forking) } + expose(:wiki_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :wiki) } + expose(:builds_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :builds) } + expose(:snippets_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :snippets) } + expose(:pages_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :pages) } + expose(:analytics_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :analytics) } + expose(:container_registry_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :container_registry) } + expose(:security_and_compliance_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :security_and_compliance) } + expose(:releases_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :releases) } + expose(:environments_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :environments) } + expose(:feature_flags_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :feature_flags) } + expose(:infrastructure_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :infrastructure) } + expose(:monitor_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :monitor) } + expose(:model_experiments_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :model_experiments) } + expose(:model_registry_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :model_registry) } + + expose(:emails_disabled, documentation: { type: 'boolean' }) { |project, options| project.emails_disabled? } + expose :emails_enabled, documentation: { type: 'boolean' } + + expose :shared_runners_enabled, documentation: { type: 'boolean' } + expose :lfs_enabled?, as: :lfs_enabled, documentation: { type: 'boolean' } + expose :creator_id, documentation: { type: 'integer', example: 1 } expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project) end - - expose :statistics, using: 'API::Entities::ProjectStatistics', if: ->(project, options) { - options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) - } - - expose :ci_config_path, documentation: { type: 'string', example: '' }, if: ->(project, options) { - Ability.allowed?(options[:current_user], :read_code, project) - } - expose :mr_default_target_self, if: ->(project) { project.forked? }, documentation: { type: 'boolean' } - expose :shared_with_groups, documentation: { is_array: true } do |project, options| - user = options[:current_user] - - SharedGroupWithProject.represent(project.visible_group_links(for_user: user), options) + expose :import_url, documentation: { type: 'string', example: 'https://gitlab.com/gitlab/gitlab.git' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :admin_project, project) } do |project| + project[:import_url] end - - expose :service_desk_address, documentation: { type: 'string', example: 'address@example.com' }, if: ->(project, options) do - Ability.allowed?(options[:current_user], :admin_issue, project) + expose :import_type, documentation: { type: 'string', example: 'git' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :admin_project, project) } + expose :import_status, documentation: { type: 'string', example: 'none' } + expose :import_error, documentation: { type: 'string', example: 'Import error' }, if: lambda { |_project, options| options[:user_can_admin_project] } do |project| + project.import_state&.last_error end + expose :open_issues_count, documentation: { type: 'integer', example: 1 }, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose :description_html, documentation: { type: 'string' } + expose :updated_at, documentation: { type: 'dateTime', example: '2020-05-07T04:27:17.016Z' } - with_options if: ->(_, _) { user_can_admin_project? } do - expose :emails_disabled?, as: :emails_disabled, documentation: { type: 'boolean' } - expose :emails_enabled, documentation: { type: 'boolean' } - - expose :resolve_outdated_diff_discussions, documentation: { type: 'boolean' } - expose :container_expiration_policy, - using: Entities::ContainerExpirationPolicy, - if: ->(project, _) { project.container_expiration_policy } - expose :repository_object_format, documentation: { type: 'string', example: 'sha1' } - - expose :shared_runners_enabled, documentation: { type: 'boolean' } - expose :lfs_enabled?, as: :lfs_enabled, documentation: { type: 'boolean' } - expose :creator_id, documentation: { type: 'integer', example: 1 } - - expose :import_url, documentation: { type: 'string', example: 'https://gitlab.com/gitlab/gitlab.git' } do |project| - project[:import_url] - end - - expose :import_type, documentation: { type: 'string', example: 'git' } - expose :import_status, documentation: { type: 'string', example: 'none' } - expose :import_error, documentation: { type: 'string', example: 'Import error' } do |project| - project.import_state&.last_error - end - + with_options if: ->(_, _) { Ability.allowed?(options[:current_user], :admin_project, project) } do + # CI/CD Settings expose :ci_default_git_depth, documentation: { type: 'integer', example: 20 } expose :ci_forward_deployment_enabled, documentation: { type: 'boolean' } expose :ci_forward_deployment_rollback_allowed, documentation: { type: 'boolean' } - - expose :ci_outbound_job_token_scope_enabled?, as: :ci_job_token_scope_enabled, documentation: { type: 'boolean' } - + expose(:ci_job_token_scope_enabled, documentation: { type: 'boolean' }) { |p, _| p.ci_outbound_job_token_scope_enabled? } expose :ci_separated_caches, documentation: { type: 'boolean' } expose :ci_allow_fork_pipelines_to_run_in_parent_project, documentation: { type: 'boolean' } - expose :build_git_strategy, documentation: { type: 'string', example: 'fetch' } do |project| + expose :build_git_strategy, documentation: { type: 'string', example: 'fetch' } do |project, options| project.build_allow_git_fetch ? 'fetch' : 'clone' end - expose :keep_latest_artifacts_available?, as: :keep_latest_artifact, documentation: { type: 'boolean' } expose :restrict_user_defined_variables, documentation: { type: 'boolean' } expose :runners_token, documentation: { type: 'string', example: 'b8547b1dc37721d05889db52fa2f02' } @@ -121,132 +130,39 @@ class Project < BasicProjectDetails expose :auto_cancel_pending_pipelines, documentation: { type: 'string', example: 'enabled' } expose :build_timeout, documentation: { type: 'integer', example: 3600 } expose :auto_devops_enabled?, as: :auto_devops_enabled, documentation: { type: 'boolean' } - expose :auto_devops_deploy_strategy, documentation: { type: 'string', example: 'continuous' } do |project| + expose :auto_devops_deploy_strategy, documentation: { type: 'string', example: 'continuous' } do |project, options| project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy end + end - expose :public_builds, as: :public_jobs, documentation: { type: 'boolean' } - - expose :only_allow_merge_if_pipeline_succeeds, documentation: { type: 'boolean' } - expose :allow_merge_on_skipped_pipeline, documentation: { type: 'boolean' } - expose :request_access_enabled, documentation: { type: 'boolean' } - expose :only_allow_merge_if_all_discussions_are_resolved, documentation: { type: 'boolean' } - expose :remove_source_branch_after_merge, documentation: { type: 'boolean' } - expose :printing_merge_request_link_enabled, documentation: { type: 'boolean' } - expose :merge_method, documentation: { type: 'string', example: 'merge' } - expose :squash_option, documentation: { type: 'string', example: 'default_off' } - expose :enforce_auth_checks_on_uploads, documentation: { type: 'boolean' } - expose :suggestion_commit_message, documentation: { type: 'string', example: 'Suggestion message' } - expose :merge_commit_template, documentation: { type: 'string', example: '%(title)' } - expose :squash_commit_template, documentation: { type: 'string', example: '%(source_branch)' } - expose :issue_branch_template, documentation: { type: 'string', example: '%(title)' } - - expose :warn_about_potentially_unwanted_characters, documentation: { type: 'boolean' } - - expose :autoclose_referenced_issues, documentation: { type: 'boolean' } - - # Expose old field names with the new permissions methods to keep API compatible - # TODO: remove in API v5, replaced by *_access_level - expose :packages_enabled, documentation: { type: 'boolean' } - expose :service_desk_enabled, documentation: { type: 'boolean' } - - expose :issues_enabled, documentation: { type: 'boolean' } do |project, options| - project.feature_available?(:issues, options[:current_user]) - end - - expose :merge_requests_enabled, documentation: { type: 'boolean' } do |project, options| - project.feature_available?(:merge_requests, options[:current_user]) - end - - expose :wiki_enabled, documentation: { type: 'boolean' } do |project, options| - project.feature_available?(:wiki, options[:current_user]) - end - - expose :jobs_enabled, documentation: { type: 'boolean' } do |project, options| - project.feature_available?(:builds, options[:current_user]) - end - - expose :snippets_enabled, documentation: { type: 'boolean' } do |project, options| - project.feature_available?(:snippets, options[:current_user]) - end - - expose :container_registry_enabled, documentation: { type: 'boolean' } do |project, options| - project.feature_available?(:container_registry, options[:current_user]) - end - - # Visibility, project features, permissions settings - expose :issues_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :issues) - end - - expose :repository_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :repository) - end - - expose :merge_requests_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :merge_requests) - end - - expose :forking_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :forking) - end - - expose :wiki_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :wiki) - end - - expose :builds_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :builds) - end - - expose :snippets_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :snippets) - end - - expose :pages_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :pages) - end - - expose :analytics_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :analytics) - end - - expose :container_registry_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :container_registry) - end - - expose :security_and_compliance_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :security_and_compliance) - end - - expose :releases_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :releases) - end - - expose :environments_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :environments) - end - - expose :feature_flags_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :feature_flags) - end + expose :ci_config_path, documentation: { type: 'string', example: '' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :read_code, project) } + expose :public_builds, as: :public_jobs, documentation: { type: 'boolean' } - expose :infrastructure_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :infrastructure) - end + expose :shared_with_groups, documentation: { is_array: true } do |project, options| + user = options[:current_user] - expose :monitor_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :monitor) - end + SharedGroupWithProject.represent(project.visible_group_links(for_user: user), options) + end - expose :model_experiments_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :model_experiments) - end + expose :only_allow_merge_if_pipeline_succeeds, documentation: { type: 'boolean' } + expose :allow_merge_on_skipped_pipeline, documentation: { type: 'boolean' } + expose :request_access_enabled, documentation: { type: 'boolean' } + expose :only_allow_merge_if_all_discussions_are_resolved, documentation: { type: 'boolean' } + expose :remove_source_branch_after_merge, documentation: { type: 'boolean' } + expose :printing_merge_request_link_enabled, documentation: { type: 'boolean' } + expose :merge_method, documentation: { type: 'string', example: 'merge' } + expose :squash_option, documentation: { type: 'string', example: 'default_off' } + expose :enforce_auth_checks_on_uploads, documentation: { type: 'boolean' } + expose :suggestion_commit_message, documentation: { type: 'string', example: 'Suggestion message' } + expose :merge_commit_template, documentation: { type: 'string', example: '%(title)' } + expose :squash_commit_template, documentation: { type: 'string', example: '%(source_branch)' } + expose :issue_branch_template, documentation: { type: 'string', example: '%(title)' } + expose :statistics, using: 'API::Entities::ProjectStatistics', if: ->(project, options) { + options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) + } + expose :warn_about_potentially_unwanted_characters, documentation: { type: 'boolean' } - expose :model_registry_access_level, documentation: { type: 'string', example: 'enabled' } do |project| - project_feature_string_access_level(project, :model_registry) - end - end + expose :autoclose_referenced_issues, documentation: { type: 'boolean' } # rubocop: disable CodeReuse/ActiveRecord def self.preload_resource(project) @@ -284,10 +200,6 @@ def self.execute_batch_counting(projects_relation) def self.repositories_for_preload(projects_relation) super + projects_relation.map(&:forked_from_project).compact.map(&:repository) end - - def user_can_admin_project? - Ability.allowed?(options[:current_user], :admin_project, project) - end end end end diff --git a/spec/lib/api/entities/project_spec.rb b/spec/lib/api/entities/project_spec.rb index b2402234a4b42355ec49faab180cf071a3e72cd7..2c2cabba5e902b215ee676d98eb962ed5b7a0591 100644 --- a/spec/lib/api/entities/project_spec.rb +++ b/spec/lib/api/entities/project_spec.rb @@ -2,169 +2,94 @@ require 'spec_helper' -RSpec.describe ::API::Entities::Project, feature_category: :groups_and_projects do - let_it_be(:project) { create(:project, :private) } - let_it_be(:current_user) { create(:user) } +RSpec.describe ::API::Entities::Project do + let(:project) { create(:project, :public) } + let(:current_user) { create(:user) } + let(:options) { { current_user: current_user } } - let(:options) { { current_user: current_user, statistics: true } } - let(:entity) { described_class.new(project, options) } + let(:entity) do + described_class.new(project, options) + end subject(:json) { entity.as_json } - before do - allow(Gitlab.config.registry).to receive(:enabled).and_return(true) - end - - context 'as a guest' do - before_all do - project.add_guest(current_user) + context 'without project feature' do + before do + project.project_feature.destroy! + project.reload end - it 'exposes the correct attributes' do - expect(json.keys).to contain_exactly( - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, - :created_at, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, :web_url, - :avatar_url, :star_count, :last_activity_at, :namespace, :container_registry_image_prefix, - :_links, :empty_repo, :archived, :visibility, :owner, :open_issues_count, - :description_html, :updated_at, :can_create_merge_request_in, :shared_with_groups - ) + it 'returns a response' do + expect(json[:issues_access_level]).to be_nil + expect(json[:repository_access_level]).to be_nil + expect(json[:merge_requests_access_level]).to be_nil end end - context 'as a reporter' do - before_all do - project.add_reporter(current_user) + describe '.service_desk_address', feature_category: :service_desk do + before do + allow(project).to receive(:service_desk_enabled?).and_return(true) end - it 'exposes the correct attributes' do - expect(json.keys).to contain_exactly( - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, - :created_at, :default_branch, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, - :web_url, :readme_url, :forks_count, :avatar_url, :star_count, :last_activity_at, - :namespace, :container_registry_image_prefix, :_links, :empty_repo, :archived, - :visibility, :owner, :open_issues_count, :description_html, :updated_at, - :can_create_merge_request_in, :statistics, :ci_config_path, :shared_with_groups, :service_desk_address - ) - end - end + context 'when a user can admin issues' do + before do + project.add_reporter(current_user) + end - context 'as a developer' do - before_all do - project.add_developer(current_user) + it 'is present' do + expect(json[:service_desk_address]).to be_present + end end - it 'exposes the correct attributes' do - expect(json.keys).to contain_exactly( - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, - :created_at, :default_branch, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, - :web_url, :readme_url, :forks_count, :avatar_url, :star_count, :last_activity_at, - :namespace, :container_registry_image_prefix, :_links, :empty_repo, :archived, - :visibility, :owner, :open_issues_count, :description_html, :updated_at, - :can_create_merge_request_in, :statistics, :ci_config_path, :shared_with_groups, :service_desk_address - ) + context 'when a user can not admin project' do + it 'is empty' do + expect(json[:service_desk_address]).to be_nil + end end end - context 'as a maintainer' do - before_all do - project.add_maintainer(current_user) + describe '.shared_with_groups' do + let(:group) { create(:group, :private) } + + before do + project.project_group_links.create!(group: group) end - it 'exposes the correct attributes' do - expected_fields = [ - :id, :description, :name, :name_with_namespace, :path, :path_with_namespace, - :created_at, :default_branch, :tag_list, :topics, :ssh_url_to_repo, :http_url_to_repo, - :web_url, :readme_url, :forks_count, :avatar_url, :star_count, :last_activity_at, - :namespace, :container_registry_image_prefix, :_links, :empty_repo, :archived, - :visibility, :owner, :open_issues_count, :description_html, :updated_at, - :can_create_merge_request_in, :statistics, :ci_config_path, :shared_with_groups, :service_desk_address, - :emails_disabled, :emails_enabled, :resolve_outdated_diff_discussions, - :container_expiration_policy, :repository_object_format, :shared_runners_enabled, - :lfs_enabled, :creator_id, :import_url, :import_type, :import_status, - :import_error, :ci_default_git_depth, :ci_forward_deployment_enabled, - :ci_forward_deployment_rollback_allowed, :ci_job_token_scope_enabled, - :ci_separated_caches, :ci_allow_fork_pipelines_to_run_in_parent_project, :build_git_strategy, - :keep_latest_artifact, :restrict_user_defined_variables, :runners_token, - :runner_token_expiration_interval, :group_runners_enabled, :auto_cancel_pending_pipelines, - :build_timeout, :auto_devops_enabled, :auto_devops_deploy_strategy, :public_jobs, - :only_allow_merge_if_pipeline_succeeds, :allow_merge_on_skipped_pipeline, - :request_access_enabled, :only_allow_merge_if_all_discussions_are_resolved, - :remove_source_branch_after_merge, :printing_merge_request_link_enabled, - :merge_method, :squash_option, :enforce_auth_checks_on_uploads, - :suggestion_commit_message, :merge_commit_template, :squash_commit_template, - :issue_branch_template, :warn_about_potentially_unwanted_characters, - :autoclose_referenced_issues, :packages_enabled, :service_desk_enabled, :issues_enabled, - :merge_requests_enabled, :wiki_enabled, :jobs_enabled, :snippets_enabled, - :container_registry_enabled, :issues_access_level, :repository_access_level, - :merge_requests_access_level, :forking_access_level, :wiki_access_level, - :builds_access_level, :snippets_access_level, :pages_access_level, :analytics_access_level, - :container_registry_access_level, :security_and_compliance_access_level, - :releases_access_level, :environments_access_level, :feature_flags_access_level, - :infrastructure_access_level, :monitor_access_level, :model_experiments_access_level, - :model_registry_access_level - ] - - if Gitlab.ee? - expected_fields += [ - :requirements_enabled, :security_and_compliance_enabled, - :requirements_access_level, :compliance_frameworks - ] + context 'when the current user does not have access to the group' do + it 'is empty' do + expect(json[:shared_with_groups]).to be_empty end - - expect(json.keys).to match(expected_fields) end - context 'without project feature' do + context 'when the current user has access to the group' do before do - project.project_feature.destroy! - project.reload + group.add_guest(current_user) end - it 'returns nil for all features' do - expect(json[:issues_access_level]).to be_nil - expect(json[:repository_access_level]).to be_nil - expect(json[:merge_requests_access_level]).to be_nil - expect(json[:forking_access_level]).to be_nil - expect(json[:wiki_access_level]).to be_nil - expect(json[:builds_access_level]).to be_nil - expect(json[:snippets_access_level]).to be_nil - expect(json[:pages_access_level]).to be_nil - expect(json[:analytics_access_level]).to be_nil - expect(json[:container_registry_access_level]).to be_nil - expect(json[:security_and_compliance_access_level]).to be_nil - expect(json[:releases_access_level]).to be_nil - expect(json[:environments_access_level]).to be_nil - expect(json[:feature_flags_access_level]).to be_nil - expect(json[:infrastructure_access_level]).to be_nil - expect(json[:monitor_access_level]).to be_nil - expect(json[:model_experiments_access_level]).to be_nil - expect(json[:model_registry_access_level]).to be_nil + it 'contains information about the shared group' do + expect(json[:shared_with_groups]).to contain_exactly(include(group_id: group.id)) end end end - describe 'shared_with_groups' do - let_it_be(:group) { create(:group, :private) } - - subject(:shared_with_groups) { json[:shared_with_groups].as_json } - - before do - project.project_group_links.create!(group: group) - end + describe '.ci/cd settings' do + context 'when the user is not an admin' do + before do + project.add_reporter(current_user) + end - context 'when the current user does not have access to the group' do - it 'is empty' do - expect(shared_with_groups).to be_empty + it 'does not return ci settings' do + expect(json[:ci_default_git_depth]).to be_nil end end - context 'when the current user has access to the group' do - before_all do - group.add_guest(current_user) + context 'when the user has admin privileges' do + before do + project.add_maintainer(current_user) end - it 'contains information about the shared group' do - expect(shared_with_groups[0]['group_id']).to eq(group.id) + it 'returns ci settings' do + expect(json[:ci_default_git_depth]).to be_present end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 3a21c7f9ad475f037b1a968517c605bd676eb2d3..ed8eda967c47a63bc8dbb60f93b935875de6af0b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1735,6 +1735,14 @@ def request expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id) end + it 'includes container_registry_access_level' do + get api("/users/#{user4.id}/projects/", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('container_registry_access_level') + end + context 'filter by updated_at' do it 'returns only projects updated on the given timeframe' do get api("/users/#{user.id}/projects", user),