2018-07-25 05:30:33 -04:00
# frozen_string_literal: true
2019-03-28 09:17:42 -04:00
class PagesDomain < ApplicationRecord
2020-04-14 17:09:52 -04:00
include Presentable
2020-04-22 05:09:36 -04:00
include FromUnion
2020-08-12 17:09:54 -04:00
include AfterCommitQueue
2020-04-14 17:09:52 -04:00
2019-08-31 15:57:00 -04:00
VERIFICATION_KEY = 'gitlab-pages-verification-code'
2018-02-06 08:25:46 -05:00
VERIFICATION_THRESHOLD = 3 . days . freeze
2019-06-24 16:35:12 -04:00
SSL_RENEWAL_THRESHOLD = 30 . days . freeze
2018-02-06 08:25:46 -05:00
2019-06-21 08:06:12 -04:00
enum certificate_source : { user_provided : 0 , gitlab_provided : 1 } , _prefix : :certificate
2020-01-31 19:08:41 -05:00
enum scope : { instance : 0 , group : 1 , project : 2 } , _prefix : :scope
enum usage : { pages : 0 , serverless : 1 } , _prefix : :usage
2019-06-21 08:06:12 -04:00
2016-02-10 06:07:46 -05:00
belongs_to :project
2019-06-06 14:55:31 -04:00
has_many :acme_orders , class_name : " PagesDomainAcmeOrder "
2020-02-25 13:09:02 -05:00
has_many :serverless_domain_clusters , class_name : 'Serverless::DomainCluster' , inverse_of : :pages_domain
2016-02-10 06:07:46 -05:00
2020-04-14 17:09:52 -04:00
before_validation :clear_auto_ssl_failure , unless : :auto_ssl_enabled
2017-05-19 10:07:38 -04:00
validates :domain , hostname : { allow_numeric_hostname : true }
2017-02-21 19:40:04 -05:00
validates :domain , uniqueness : { case_sensitive : false }
2020-02-17 19:09:20 -05:00
validates :certificate , :key , presence : true , if : :usage_serverless?
2019-07-12 10:19:01 -04:00
validates :certificate , presence : { message : 'must be present if HTTPS-only is enabled' } ,
if : :certificate_should_be_present?
2018-01-03 03:07:03 -05:00
validates :certificate , certificate : true , if : - > ( domain ) { domain . certificate . present? }
2019-07-12 10:19:01 -04:00
validates :key , presence : { message : 'must be present if HTTPS-only is enabled' } ,
if : :certificate_should_be_present?
2019-09-06 20:29:03 -04:00
validates :key , certificate_key : true , named_ecdsa_key : true , if : - > ( domain ) { domain . key . present? }
2018-02-06 08:25:46 -05:00
validates :verification_code , presence : true , allow_blank : false
2016-02-10 06:07:46 -05:00
2016-02-12 10:05:17 -05:00
validate :validate_pages_domain
validate :validate_matching_key , if : - > ( domain ) { domain . certificate . present? || domain . key . present? }
2019-07-22 11:38:08 -04:00
validate :validate_intermediates , if : - > ( domain ) { domain . certificate . present? && domain . certificate_changed? }
2016-02-10 09:06:31 -05:00
2019-11-15 16:06:14 -05:00
default_value_for ( :auto_ssl_enabled , allow_nil : false ) { :: Gitlab :: LetsEncrypt . enabled? }
2020-01-31 19:08:41 -05:00
default_value_for :scope , allow_nil : false , value : :project
2019-12-14 10:07:56 -05:00
default_value_for :wildcard , allow_nil : false , value : false
2020-01-31 19:08:41 -05:00
default_value_for :usage , allow_nil : false , value : :pages
2019-11-15 16:06:14 -05:00
2016-06-03 15:01:54 -04:00
attr_encrypted :key ,
mode : :per_attribute_iv_and_salt ,
2016-06-28 04:14:24 -04:00
insecure_mode : true ,
2018-05-19 09:03:29 -04:00
key : Settings . attr_encrypted_db_key_base ,
2016-06-03 15:01:54 -04:00
algorithm : 'aes-256-cbc'
2016-02-10 06:07:46 -05:00
2018-02-06 08:25:46 -05:00
after_initialize :set_verification_code
2017-10-20 02:33:52 -04:00
after_create :update_daemon
2019-04-23 05:30:18 -04:00
after_update :update_daemon , if : :saved_change_to_pages_config?
2017-10-20 02:33:52 -04:00
after_destroy :update_daemon
2016-02-10 06:07:46 -05:00
2020-05-22 05:08:09 -04:00
scope :enabled , - > { where ( 'enabled_until >= ?' , Time . current ) }
2018-02-06 08:25:46 -05:00
scope :needs_verification , - > do
verified_at = arel_table [ :verified_at ]
enabled_until = arel_table [ :enabled_until ]
2020-05-22 05:08:09 -04:00
threshold = Time . current + VERIFICATION_THRESHOLD
2018-02-06 08:25:46 -05:00
where ( verified_at . eq ( nil ) . or ( enabled_until . eq ( nil ) . or ( enabled_until . lt ( threshold ) ) ) )
end
2019-06-24 16:35:12 -04:00
scope :need_auto_ssl_renewal , - > do
2020-04-22 05:09:36 -04:00
enabled_and_not_failed = where ( auto_ssl_enabled : true , auto_ssl_failed : false )
2019-06-24 16:35:12 -04:00
2020-04-22 05:09:36 -04:00
user_provided = enabled_and_not_failed . certificate_user_provided
certificate_not_valid = enabled_and_not_failed . where ( certificate_valid_not_after : nil )
certificate_expiring = enabled_and_not_failed
. where ( arel_table [ :certificate_valid_not_after ] . lt ( SSL_RENEWAL_THRESHOLD . from_now ) )
2019-06-24 16:35:12 -04:00
2020-04-22 05:09:36 -04:00
from_union ( [ user_provided , certificate_not_valid , certificate_expiring ] )
2019-06-24 16:35:12 -04:00
end
2020-05-22 05:08:09 -04:00
scope :for_removal , - > { where ( " remove_at < ? " , Time . current ) }
2019-04-30 08:05:54 -04:00
2020-02-11 13:08:58 -05:00
scope :with_logging_info , - > { includes ( project : [ :namespace , :route ] ) }
2020-02-17 19:09:20 -05:00
scope :instance_serverless , - > { where ( wildcard : true , scope : :instance , usage : :serverless ) }
2020-02-25 16:09:23 -05:00
def self . find_by_domain_case_insensitive ( domain )
find_by ( " LOWER(domain) = LOWER(?) " , domain )
end
2018-02-06 08:25:46 -05:00
def verified?
! ! verified_at
end
def unverified?
! verified?
end
def enabled?
! Gitlab :: CurrentSettings . pages_domain_verification_enabled? || enabled_until . present?
end
2018-01-03 03:07:03 -05:00
def https?
certificate . present?
end
2016-02-10 09:06:31 -05:00
def to_param
domain
end
2016-02-10 06:07:46 -05:00
def url
return unless domain
2018-01-07 20:27:19 -05:00
if certificate . present?
2016-02-15 09:01:42 -05:00
" https:// #{ domain } "
2016-02-10 06:07:46 -05:00
else
2016-02-15 09:01:42 -05:00
" http:// #{ domain } "
2016-02-10 06:07:46 -05:00
end
end
2016-02-10 09:06:31 -05:00
def has_matching_key?
2016-02-12 10:05:17 -05:00
return false unless x509
return false unless pkey
2016-02-10 09:06:31 -05:00
# We compare the public key stored in certificate with public key from certificate key
x509 . check_private_key ( pkey )
end
def has_intermediates?
return false unless x509
2016-02-12 10:05:17 -05:00
# self-signed certificates doesn't have the certificate chain
return true if x509 . verify ( x509 . public_key )
2016-02-10 09:06:31 -05:00
store = OpenSSL :: X509 :: Store . new
store . set_default_paths
# This forces to load all intermediate certificates stored in `certificate`
Tempfile . open ( 'certificate_chain' ) do | f |
f . write ( certificate )
f . flush
store . add_file ( f . path )
end
store . verify ( x509 )
rescue OpenSSL :: X509 :: StoreError
false
end
def expired?
return false unless x509
2017-11-14 04:02:39 -05:00
2020-05-22 05:08:09 -04:00
current = Time . current
2016-02-14 13:58:45 -05:00
current < x509 . not_before || x509 . not_after < current
2016-02-10 09:06:31 -05:00
end
2017-11-13 11:05:44 -05:00
def expiration
x509 & . not_after
end
2016-02-10 09:06:31 -05:00
def subject
return unless x509
2017-11-14 04:02:39 -05:00
2016-02-14 13:58:45 -05:00
x509 . subject . to_s
2016-02-10 09:06:31 -05:00
end
2016-02-12 10:05:17 -05:00
def certificate_text
@certificate_text || = x509 . try ( :to_text )
2016-02-10 09:06:31 -05:00
end
2018-02-06 08:25:46 -05:00
# Verification codes may be TXT records for domain or verification_domain, to
# support the use of CNAME records on domain.
def verification_domain
return unless domain . present?
" _ #{ VERIFICATION_KEY } . #{ domain } "
end
def keyed_verification_code
return unless verification_code . present?
" #{ VERIFICATION_KEY } = #{ verification_code } "
end
2019-06-06 15:14:09 -04:00
def certificate = ( certificate )
super ( certificate )
# set nil, if certificate is nil
self . certificate_valid_not_before = x509 & . not_before
self . certificate_valid_not_after = x509 & . not_after
end
2019-06-21 08:06:12 -04:00
def user_provided_key
key if certificate_user_provided?
end
def user_provided_key = ( key )
self . key = key
self . certificate_source = 'user_provided' if key_changed?
end
def user_provided_certificate
certificate if certificate_user_provided?
end
def user_provided_certificate = ( certificate )
self . certificate = certificate
self . certificate_source = 'user_provided' if certificate_changed?
end
def gitlab_provided_certificate = ( certificate )
self . certificate = certificate
self . certificate_source = 'gitlab_provided' if certificate_changed?
end
def gitlab_provided_key = ( key )
self . key = key
self . certificate_source = 'gitlab_provided' if key_changed?
end
2019-09-06 01:20:05 -04:00
def pages_virtual_domain
2019-09-25 05:06:04 -04:00
return unless pages_deployed?
2019-09-06 01:20:05 -04:00
Pages :: VirtualDomain . new ( [ project ] , domain : self )
end
2020-04-14 17:09:52 -04:00
def clear_auto_ssl_failure
self . auto_ssl_failed = false
end
2016-02-10 10:45:59 -05:00
private
2019-09-25 05:06:04 -04:00
def pages_deployed?
2020-08-12 17:09:54 -04:00
return false unless project
2019-10-01 08:05:59 -04:00
# TODO: remove once `pages_metadatum` is migrated
# https://gitlab.com/gitlab-org/gitlab/issues/33106
unless project . pages_metadatum
Gitlab :: BackgroundMigration :: MigratePagesMetadata
. new
. perform_on_relation ( Project . where ( id : project_id ) )
project . reset
end
2019-09-25 05:06:04 -04:00
project . pages_metadatum & . deployed?
end
2018-02-06 08:25:46 -05:00
def set_verification_code
return if self . verification_code . present?
self . verification_code = SecureRandom . hex ( 16 )
end
2018-08-27 11:31:01 -04:00
# rubocop: disable CodeReuse/ServiceClass
2017-10-20 02:33:52 -04:00
def update_daemon
2020-01-31 19:08:41 -05:00
return if usage_serverless?
2020-08-12 17:09:54 -04:00
return unless pages_deployed?
2019-12-14 10:07:56 -05:00
2020-08-24 11:10:11 -04:00
run_after_commit { PagesUpdateConfigurationWorker . perform_async ( project_id ) }
2016-02-10 09:06:31 -05:00
end
2018-08-27 11:31:01 -04:00
# rubocop: enable CodeReuse/ServiceClass
2016-02-10 09:06:31 -05:00
2019-04-23 05:30:18 -04:00
def saved_change_to_pages_config?
2019-01-15 16:05:36 -05:00
saved_change_to_project_id? ||
saved_change_to_domain? ||
saved_change_to_certificate? ||
saved_change_to_key? ||
2018-02-06 08:25:46 -05:00
became_enabled? ||
became_disabled?
end
def became_enabled?
2019-01-15 16:05:36 -05:00
enabled_until . present? && ! enabled_until_before_last_save . present?
2018-02-06 08:25:46 -05:00
end
def became_disabled?
2019-01-15 16:05:36 -05:00
! enabled_until . present? && enabled_until_before_last_save . present?
2018-02-06 08:25:46 -05:00
end
2016-02-10 09:06:31 -05:00
def validate_matching_key
unless has_matching_key?
self . errors . add ( :key , " doesn't match the certificate " )
end
end
def validate_intermediates
unless has_intermediates?
self . errors . add ( :certificate , 'misses intermediates' )
end
2016-02-10 06:07:46 -05:00
end
2016-02-12 10:05:17 -05:00
def validate_pages_domain
return unless domain
2017-11-14 04:02:39 -05:00
2017-05-19 10:07:38 -04:00
if domain . downcase . ends_with? ( Settings . pages . host . downcase )
2020-11-05 04:09:00 -05:00
self . errors . add ( :domain , " *. #{ Settings . pages . host } is restricted. Please compare our documentation at https://docs.gitlab.com/ee/administration/pages/ # advanced-configuration against your configuration. " )
2016-02-12 10:05:17 -05:00
end
end
def x509
2019-06-06 15:14:09 -04:00
return unless certificate . present?
2017-11-14 04:02:39 -05:00
2016-02-12 10:05:17 -05:00
@x509 || = OpenSSL :: X509 :: Certificate . new ( certificate )
rescue OpenSSL :: X509 :: CertificateError
nil
end
def pkey
return unless key
2017-11-14 04:02:39 -05:00
2019-09-06 20:29:03 -04:00
@pkey || = OpenSSL :: PKey . read ( key )
2016-02-12 10:05:17 -05:00
rescue OpenSSL :: PKey :: PKeyError , OpenSSL :: Cipher :: CipherError
nil
end
2019-07-12 10:19:01 -04:00
def certificate_should_be_present?
! auto_ssl_enabled? && project & . pages_https_only?
end
2016-02-10 06:07:46 -05:00
end
2020-02-11 13:08:58 -05:00
PagesDomain . prepend_if_ee ( '::EE::PagesDomain' )