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),