From 477ba2b3465736cdccfb6cb6a36f78447942e310 Mon Sep 17 00:00:00 2001 From: Krasimir Angelov Date: Fri, 6 Sep 2019 16:06:25 +1200 Subject: Add skeleton Pages internal API Basic `/internal/pages` endpoint that will be used for Pages virtual domains internal API. The endpoint is currently behind feature flag and provides authetication similar to how Workhorse is authenticating with the GitLab. --- .gitignore | 1 + config/gitlab.yml.example | 3 ++ config/initializers/1_settings.rb | 1 + lib/api/api.rb | 1 + lib/api/internal/pages.rb | 27 ++++++++++++++++ lib/gitlab/pages.rb | 17 +++++++++- spec/lib/gitlab/pages_spec.rb | 29 +++++++++++++++++ spec/requests/api/internal/pages_spec.rb | 54 ++++++++++++++++++++++++++++++++ 8 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 lib/api/internal/pages.rb create mode 100644 spec/lib/gitlab/pages_spec.rb create mode 100644 spec/requests/api/internal/pages_spec.rb diff --git a/.gitignore b/.gitignore index fcbb4c352a9..7310c04d117 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ eslint-report.html /vendor/gitaly-ruby /builds* /.gitlab_workhorse_secret +/.gitlab_pages_shared_secret /webpack-report/ /knapsack/ /rspec_flaky/ diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e3693f612e3..aa7c2d343a8 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -321,6 +321,9 @@ production: &base # external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages admin: address: unix:/home/git/gitlab/tmp/sockets/private/pages-admin.socket # TCP connections are supported too (e.g. tcp://host:port) + # File that contains the shared secret key for verifying access for gitlab-pages. + # Default is '.gitlab_pages_shared_secret' relative to Rails.root (i.e. root of the GitLab app). + # secret_file: /home/git/gitlab/.gitlab_pages_shared_secret ## Mattermost ## For enabling Add to Mattermost button diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4160f488a7a..dbbb7ba1b60 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -292,6 +292,7 @@ Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pa Settings.pages['admin'] ||= Settingslogic.new({}) Settings.pages.admin['certificate'] ||= '' +Settings.pages['secret_file'] ||= Rails.root.join('.gitlab_pages_shared_secret') # # Geo diff --git a/lib/api/api.rb b/lib/api/api.rb index aa6a67d817a..de6e528ed09 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -119,6 +119,7 @@ module API mount ::API::GroupVariables mount ::API::ImportGithub mount ::API::Internal::Base + mount ::API::Internal::Pages mount ::API::Issues mount ::API::JobArtifacts mount ::API::Jobs diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb new file mode 100644 index 00000000000..6ea048bde03 --- /dev/null +++ b/lib/api/internal/pages.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + # Pages Internal API + module Internal + class Pages < Grape::API + before do + not_found! unless Feature.enabled?(:pages_internal_api) + authenticate_gitlab_pages_request! + end + + helpers do + def authenticate_gitlab_pages_request! + unauthorized! unless Gitlab::Pages.verify_api_request(headers) + end + end + + namespace 'internal' do + namespace 'pages' do + get "/" do + status :ok + end + end + end + end + end +end diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb index 16df0700b08..4899b1d3234 100644 --- a/lib/gitlab/pages.rb +++ b/lib/gitlab/pages.rb @@ -1,7 +1,22 @@ # frozen_string_literal: true module Gitlab - module Pages + class Pages VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze + INTERNAL_API_REQUEST_HEADER = 'Gitlab-Pages-Api-Request'.freeze + + include JwtAuthenticatable + + class << self + def verify_api_request(request_headers) + decode_jwt_for_issuer('gitlab-pages', request_headers[INTERNAL_API_REQUEST_HEADER]) + rescue JWT::DecodeError + false + end + + def secret_path + Gitlab.config.pages.secret_file + end + end end end diff --git a/spec/lib/gitlab/pages_spec.rb b/spec/lib/gitlab/pages_spec.rb new file mode 100644 index 00000000000..affa2ebab2a --- /dev/null +++ b/spec/lib/gitlab/pages_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Pages do + let(:pages_shared_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) } + + before do + allow(described_class).to receive(:secret).and_return(pages_shared_secret) + end + + describe '.verify_api_request' do + let(:payload) { { 'iss' => 'gitlab-pages' } } + + it 'returns false if fails to validate the JWT' do + encoded_token = JWT.encode(payload, 'wrongsecret', 'HS256') + headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + + expect(described_class.verify_api_request(headers)).to eq(false) + end + + it 'returns the decoded JWT' do + encoded_token = JWT.encode(payload, described_class.secret, 'HS256') + headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + + expect(described_class.verify_api_request(headers)).to eq([{ "iss" => "gitlab-pages" }, { "alg" => "HS256" }]) + end + end +end diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb new file mode 100644 index 00000000000..0b3c5be9c45 --- /dev/null +++ b/spec/requests/api/internal/pages_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Internal::Pages do + describe "GET /internal/pages" do + let(:pages_shared_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) } + + before do + allow(Gitlab::Pages).to receive(:secret).and_return(pages_shared_secret) + end + + def query_host(host, headers = {}) + get api("/internal/pages"), headers: headers, params: { host: host } + end + + context 'feature flag disabled' do + before do + stub_feature_flags(pages_internal_api: false) + end + + it 'responds with 404 Not Found' do + query_host('pages.gitlab.io') + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'feature flag enabled' do + context 'not authenticated' do + it 'responds with 401 Unauthorized' do + query_host('pages.gitlab.io') + + expect(response).to have_gitlab_http_status(401) + end + end + + context 'authenticated' do + def query_host(host) + jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256') + headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token } + + super(host, headers) + end + + it 'responds with 200 OK' do + query_host('pages.gitlab.io') + + expect(response).to have_gitlab_http_status(200) + end + end + end + end +end -- cgit v1.2.1