diff --git a/app/services/pages/update_service.rb b/app/services/pages/update_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..f27b1c29442fd503ac299d9974b740aea1897e91 --- /dev/null +++ b/app/services/pages/update_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Pages + class UpdateService < BaseService + include Gitlab::Allowable + + def execute + unless can_update_page_settings? + return ServiceResponse.error(message: _('The current user is not authorized to update the page settings'), + reason: :forbidden) + end + + Project.transaction do + update_pages_unique_domain_enabled! + update_pages_https_only! + end + + ServiceResponse.success(payload: { project: project }) + end + + private + + def update_pages_unique_domain_enabled! + return unless params.key?(:pages_unique_domain_enabled) + + project.project_setting.update!(pages_unique_domain_enabled: params[:pages_unique_domain_enabled]) + end + + def update_pages_https_only! + return unless params.key?(:pages_https_only) + + project.update!(pages_https_only: params[:pages_https_only]) + end + + def can_update_page_settings? + current_user&.can_read_all_resources? && can?(current_user, :update_pages, project) + end + end +end diff --git a/doc/api/pages.md b/doc/api/pages.md index 7d8f7e99e5431d150e5bebf8368dd2529f45ef71..5060670dc806608cef2edac51248bf0837710b6f 100644 --- a/doc/api/pages.md +++ b/doc/api/pages.md @@ -34,7 +34,7 @@ DELETE /projects/:id/pages curl --request 'DELETE' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/2/pages" ``` -## Get pages settings for a project +## Get Pages settings for a project > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436932) in GitLab 16.8. @@ -59,7 +59,7 @@ response attributes: | Attribute | Type | Description | | ----------------------------------------- | ---------- | ----------------------- | -| `url` | string | URL to access this project pages. | +| `url` | string | URL to access this project's Pages. | | `is_unique_domain_enabled` | boolean | If [unique domain](../user/project/pages/introduction.md) is enabled. | | `force_https` | boolean | `true` if the project is set to force HTTPS. | | `deployments[]` | array | List of current active deployments. | @@ -100,3 +100,73 @@ Example response: ] } ``` + +## Update Pages settings for a project + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147227) in GitLab 16.11. + +Prerequisites: + +- You must have administrator access to the instance. + +Update Pages settings for the project. + +```plaintext +PATCH /projects/:id/pages +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +| --------------------------------| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | +| `pages_unique_domain_enabled` | boolean | No | Whether to use unique domain | +| `pages_https_only` | boolean | No | Whether to force HTTPs | + +If successful, returns [`200`](rest/index.md#status-codes) and the following +response attributes: + +| Attribute | Type | Description | +| ----------------------------------------- | ---------- | ----------------------- | +| `url` | string | URL to access this project's Pages. | +| `is_unique_domain_enabled` | boolean | If [unique domain](../user/project/pages/introduction.md) is enabled. | +| `force_https` | boolean | `true` if the project is set to force HTTPS. | +| `deployments[]` | array | List of current active deployments. | + +| `deployments[]` attribute | Type | Description | +| ----------------------------------------- | ---------- | ----------------------- | +| `created_at` | date | Date deployment was created. | +| `url` | string | URL for this deployment. | +| `path_prefix` | string | Path prefix of this deployment when using [multiple deployments](../user/project/pages/index.md#create-multiple-deployments). | +| `root_directory` | string | Root directory. | + +Example request: + +```shell +curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" --url "https://gitlab.example.com/api/v4/projects/:id/pages" \ +--form 'pages_unique_domain_enabled=true' --form 'pages_https_only=true' +``` + +Example response: + +```json +{ + "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010", + "is_unique_domain_enabled": true, + "force_https": false, + "deployments": [ + { + "created_at": "2024-01-05T18:58:14.916Z", + "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010/", + "path_prefix": "", + "root_directory": null + }, + { + "created_at": "2024-01-05T18:58:46.042Z", + "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010/mr3", + "path_prefix": "mr3", + "root_directory": null + } + ] +} +``` diff --git a/lib/api/pages.rb b/lib/api/pages.rb index 30e126b34cbbb94007accf786f6d9fbefce42e01..8f038c175bd5fd64c9ec66fbb545bbb0e65a57f1 100644 --- a/lib/api/pages.rb +++ b/lib/api/pages.rb @@ -31,6 +31,34 @@ class Pages < ::API::Base no_content! end + desc 'Update pages settings' do + detail 'Update page settings for a project. User must have administrative access.' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pages] + end + params do + optional :pages_unique_domain_enabled, type: Boolean, desc: 'Whether to use unique domain' + optional :pages_https_only, type: Boolean, desc: 'Whether to force HTTPS' + end + patch ':id/pages' do + authenticated_with_can_read_all_resources! + authorize! :update_pages, user_project + + break not_found! unless user_project.pages_enabled? + + response = ::Pages::UpdateService.new(user_project, current_user, params).execute + + if response.success? + present ::Pages::ProjectSettings.new(response.payload[:project]), with: Entities::Pages::ProjectSettings + else + forbidden!(response.message) + end + end + desc 'Get pages settings' do detail 'Get pages URL and other settings. This feature was introduced in Gitlab 16.8' success code: 200 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a0f8363796cfb2fba8c970fbf6842dc99ebbb587..0ad864addb26db794f788ad882ddc524627f65db 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -51360,6 +51360,9 @@ msgstr "" msgid "The current user is not authorized to set pipeline schedule variables" msgstr "" +msgid "The current user is not authorized to update the page settings" +msgstr "" + msgid "The current user is not authorized to update the pipeline schedule" msgstr "" diff --git a/spec/requests/api/pages_spec.rb b/spec/requests/api/pages_spec.rb index 23ffeb143cbacf0916ea6d0a9edc4c77c2e00313..a4a066075a02beff7e0f70ea9297a5471706a241 100644 --- a/spec/requests/api/pages_spec.rb +++ b/spec/requests/api/pages_spec.rb @@ -88,4 +88,65 @@ end end end + + describe 'PATCH /projects/:id/pages' do + let(:path) { "/projects/#{project.id}/pages" } + let(:params) { { pages_unique_domain_enabled: true, pages_https_only: true } } + + before do + stub_pages_setting(external_https: true) + end + + context 'when the user is authorized' do + context 'and the update succeeds' do + it 'updates the pages settings and returns 200' do + patch api(path, admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['force_https']).to eq(true) + expect(json_response['is_unique_domain_enabled']).to eq(true) + end + end + end + + context 'when the user is unauthorized' do + it 'returns a 403 forbidden' do + patch api(path, user), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when pages feature is disabled' do + before do + stub_pages_setting(enabled: false) + end + + it 'returns a 404 not found' do + patch api(path, admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when there is no project' do + it 'returns 404 not found' do + non_existent_project_id = -1 + patch api("/projects/#{non_existent_project_id}/pages", admin, admin_mode: true), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the parameters are invalid' do + let(:invalid_params) { { pages_unique_domain_enabled: nil, pages_https_only: "not_a_boolean" } } + + it 'returns a 400 bad request' do + patch api(path, admin, admin_mode: true), params: invalid_params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('pages_https_only is invalid') + end + end + end end diff --git a/spec/services/pages/update_service_spec.rb b/spec/services/pages/update_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..35086956124c3040006cd42e1567567fac25e73e --- /dev/null +++ b/spec/services/pages/update_service_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Pages::UpdateService, feature_category: :pages do + let_it_be(:admin) { create(:admin) } + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:project) { create(:project) } + let(:params) { { pages_unique_domain_enabled: false, pages_https_only: false } } + + before do + stub_pages_setting(external_https: true) + end + + describe '#execute' do + context 'with sufficient permissions' do + let(:service) { described_class.new(project, admin, params) } + + before do + allow(admin).to receive(:can_read_all_resources?).and_return(true) + allow(service).to receive(:can?).with(admin, :update_pages, project).and_return(true) + end + + context 'when updating page setting succeeds' do + it 'updates page settings' do + create(:project_setting, project: project, pages_unique_domain_enabled: true, + pages_unique_domain: "random-unique-domain-here") + + expect { service.execute } + .to change { project.reload.pages_https_only }.from(true).to(false) + .and change { project.project_setting.pages_unique_domain_enabled }.from(true).to(false) + end + + it 'returns a success response' do + result = service.execute + + expect(result).to be_a(ServiceResponse) + expect(result).to be_success + expect(result.payload[:project]).to eq(project) + end + end + end + + context 'with insufficient permissions' do + let(:service) { described_class.new(project, user, params) } + + it 'returns a forbidden response' do + result = service.execute + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq(_('The current user is not authorized to update the page settings')) + end + end + end +end