Commit 3548dc4b authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '23000-pages-api' into 'master'

Resolve "Pages API"

Closes #23000

See merge request gitlab-org/gitlab-ce!13917
parents 7fbefa3d 8d1ab256
......@@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
after_create :update
after_save :update
after_destroy :update
after_create :update_daemon
after_save :update_daemon
after_destroy :update_daemon
def to_param
domain
......@@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base
private
def update
def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute
end
......
---
title: Add API endpoints for Pages Domains
merge_request: 13917
author: Travis Miller
type: added
......@@ -37,6 +37,7 @@ following locations:
- [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md)
- [Open source license templates](templates/licenses.md)
- [Pages Domains](pages_domains.md)
- [Pipelines](pipelines.md)
- [Pipeline Triggers](pipeline_triggers.md)
- [Pipeline Schedules](pipeline_schedules.md)
......
# Pages domains API
Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages](https://about.gitlab.com/features/pages/).
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
## List pages domains
Get a list of project pages domains. The user must have permissions to view pages domains.
```http
GET /projects/:id/pages/domains
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains
```
```json
[
{
"domain": "www.domain.example",
"url": "http://www.domain.example"
},
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
]
```
## Single pages domain
Get a single project pages domain. The user must have permissions to view pages domains.
```http
GET /projects/:id/pages/domains/:domain
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/www.domain.example
```
```json
{
"domain": "www.domain.example",
"url": "http://www.domain.example"
}
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
```json
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
```
## Create new pages domain
Creates a new pages domain. The user must have permissions to create new pages domains.
```http
POST /projects/:id/pages/domains
```
| Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
| `key` | file/string | no | The certificate key in PEM format. |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains
```
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains
```
```json
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
```
## Update pages domain
Updates an existing project pages domain. The user must have permissions to change an existing pages domains.
```http
PUT /projects/:id/pages/domains/:domain
```
| Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
| `key` | file/string | no | The certificate key in PEM format. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
```json
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
```
## Delete pages domain
Deletes an existing project pages domain.
```http
DELETE /projects/:id/pages/domains/:domain
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
......@@ -131,6 +131,7 @@ module API
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
mount ::API::PagesDomains
mount ::API::Pipelines
mount ::API::PipelineSchedules
mount ::API::ProjectHooks
......
......@@ -1043,5 +1043,22 @@ module API
expose :key
expose :value
end
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
expose :certificate
expose :certificate_text
end
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
pages_domain
end
end
end
end
......@@ -184,6 +184,10 @@ module API
end
end
def require_pages_enabled!
not_found! unless user_project.pages_available?
end
def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
......
module API
class PagesDomains < Grape::API
include PaginationParams
before do
authenticate!
require_pages_enabled!
end
after_validation do
normalize_params_file_to_string
end
helpers do
def find_pages_domain!
user_project.pages_domains.find_by(domain: params[:domain]) || not_found!('PagesDomain')
end
def pages_domain
@pages_domain ||= find_pages_domain!
end
def normalize_params_file_to_string
params.each do |k, v|
if v.is_a?(Hash) && v.key?(:tempfile)
params[k] = v[:tempfile].to_a.join('')
end
end
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all pages domains' do
success Entities::PagesDomain
end
params do
use :pagination
end
get ":id/pages/domains" do
authorize! :read_pages, user_project
present paginate(user_project.pages_domains.order(:domain)), with: Entities::PagesDomain
end
desc 'Get a single pages domain' do
success Entities::PagesDomain
end
params do
requires :domain, type: String, desc: 'The domain'
end
get ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
authorize! :read_pages, user_project
present pages_domain, with: Entities::PagesDomain
end
desc 'Create a new pages domain' do
success Entities::PagesDomain
end
params do
requires :domain, type: String, desc: 'The domain'
optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate'
optional :key, allow_blank: false, types: [File, String], desc: 'The key'
all_or_none_of :certificate, :key
end
post ":id/pages/domains" do
authorize! :update_pages, user_project
pages_domain_params = declared(params, include_parent_namespaces: false)
pages_domain = user_project.pages_domains.create(pages_domain_params)
if pages_domain.persisted?
present pages_domain, with: Entities::PagesDomain
else
render_validation_error!(pages_domain)
end
end
desc 'Updates a pages domain'
params do
requires :domain, type: String, desc: 'The domain'
optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate'
optional :key, allow_blank: false, types: [File, String], desc: 'The key'
end
put ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
authorize! :update_pages, user_project
pages_domain_params = declared(params, include_parent_namespaces: false)
# Remove empty private key if certificate is not empty.
if pages_domain_params[:certificate] && !pages_domain_params[:key]
pages_domain_params.delete(:key)
end
if pages_domain.update(pages_domain_params)
present pages_domain, with: Entities::PagesDomain
else
render_validation_error!(pages_domain)
end
end
desc 'Delete a pages domain'
params do
requires :domain, type: String, desc: 'The domain'
end
delete ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
authorize! :update_pages, user_project
status 204
pages_domain.destroy
end
end
end
end
{
"type": "array",
"items": {
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"expired": { "type": "boolean" },
"certificate": { "type": "string" },
"certificate_text": { "type": "string" }
},
"required": ["subject", "expired"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
}
require 'rails_helper'
describe API::PagesDomains do
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, :with_key, domain: 'expired.domain.test', project: project) }
let(:pages_domain_params) { build(:pages_domain, domain: 'www.other-domain.test').slice(:domain) }
let(:pages_domain_secure_params) { build(:pages_domain, :with_certificate, :with_key, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, :with_key, project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) }
let(:route) { "/projects/#{project.id}/pages/domains" }
let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" }
let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" }
let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" }
let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" }
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
end
describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do
it 'returns paginated pages domains' do
get api(route, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.map { |pages_domain| pages_domain['domain'] }).to include(pages_domain.domain)
expect(json_response.last).to have_key('domain')
end
end
context 'when pages is disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'get pages domains'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { get api(route, user) }
end
end
end
describe 'GET /projects/:project_id/pages/domains/:domain' do
shared_examples_for 'get pages domain' do
it 'returns pages domain' do
get api(route_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['domain']).to eq(pages_domain.domain)
expect(json_response['url']).to eq(pages_domain.url)
expect(json_response['certificate']).to be_nil
end
it 'returns pages domain with a certificate' do
get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['domain']).to eq(pages_domain_secure.domain)
expect(json_response['url']).to eq(pages_domain_secure.url)
expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
expect(json_response['certificate']['expired']).to be false
end
it 'returns pages domain with an expired certificate' do
get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['certificate']['expired']).to be true
end
end
context 'when domain is vacant' do
before do
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { get api(route_vacant_domain, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'get pages domain'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { get api(route, user) }
end
end
end
describe 'POST /projects/:project_id/pages/domains' do
let(:params) { pages_domain_params.slice(:domain) }
let(:params_secure) { pages_domain_secure_params.slice(:domain, :certificate, :key) }
shared_examples_for 'post pages domains' do
it 'creates a new pages domain' do
post api(route, user), params
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
expect(pages_domain.domain).to eq(params[:domain])
expect(pages_domain.certificate).to be_nil
expect(pages_domain.key).to be_nil
end
it 'creates a new secure pages domain' do
post api(route, user), params_secure
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
expect(pages_domain.domain).to eq(params_secure[:domain])
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
end
it 'fails to create pages domain without key' do
post api(route, user), pages_domain_secure_params.slice(:domain, :certificate)
expect(response).to have_gitlab_http_status(400)
end
it 'fails to create pages domain with key missmatch' do
post api(route, user), pages_domain_secure_key_missmatch_params.slice(:domain, :certificate, :key)
expect(response).to have_gitlab_http_status(400)
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'post pages domains'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { post api(route, user), params }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { post api(route, user), params }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { post api(route, user), params }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { post api(route, user), params }
end
end
end
describe 'PUT /projects/:project_id/pages/domains/:domain' do
let(:params_secure) { pages_domain_secure_params.slice(:certificate, :key) }
let(:params_secure_nokey) { pages_domain_secure_params.slice(:certificate) }
shared_examples_for 'put pages domain' do
it 'updates pages domain removing certificate' do
put api(route_secure_domain, user)
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain_secure.certificate).to be_nil
expect(pages_domain_secure.key).to be_nil
end
it 'updates pages domain adding certificate' do
put api(route_domain, user), params_secure
pages_domain.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
end
it 'updates pages domain with expired certificate' do
put api(route_expired_domain, user), params_secure
pages_domain_expired.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain_expired.certificate).to eq(params_secure[:certificate])
expect(pages_domain_expired.key).to eq(params_secure[:key])
end
it 'updates pages domain with expired certificate not updating key' do
put api(route_secure_domain, user), params_secure_nokey
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate])
end
it 'fails to update pages domain adding certificate without key' do
put api(route_domain, user), params_secure_nokey
expect(response).to have_gitlab_http_status(400)
end
it 'fails to update pages domain adding certificate with missing chain' do
put api(route_domain, user), pages_domain_secure_missing_chain_params.slice(:certificate)
expect(response).to have_gitlab_http_status(400)
end
it 'fails to update pages domain with key missmatch' do
put api(route_secure_domain, user), pages_domain_secure_key_missmatch_params.slice(:certificate, :key)
expect(response).to have_gitlab_http_status(400)
end
end
context 'when domain is vacant' do
before do
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { put api(route_vacant_domain, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'put pages domain'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { put api(route_domain, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { put api(route_domain, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { put api(route_domain, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { put api(route_domain, user) }
end
end
end
describe 'DELETE /projects/:project_id/pages/domains/:domain' do
shared_examples_for 'delete pages domain' do
it 'deletes a pages domain' do
delete api(route_domain, user)
expect(response).to have_gitlab_http_status(204)
end
end
context 'when domain is vacant' do
before do
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { delete api(route_vacant_domain, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'delete pages domain'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { delete api(route_domain, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { delete api(route_domain, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { delete api(route_domain, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { delete api(route_domain, user) }
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment