2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2019-07-07 11:18:12 +05:30
class PagesDomain < ApplicationRecord
2020-04-22 19:07:51 +05:30
include Presentable
2020-05-24 23:13:21 +05:30
include FromUnion
2020-10-24 23:57:45 +05:30
include AfterCommitQueue
2020-04-22 19:07:51 +05:30
2019-12-04 20:38:33 +05:30
VERIFICATION_KEY = 'gitlab-pages-verification-code'
2018-03-17 18:26:18 +05:30
VERIFICATION_THRESHOLD = 3 . days . freeze
2019-09-30 21:07:59 +05:30
SSL_RENEWAL_THRESHOLD = 30 . days . freeze
enum certificate_source : { user_provided : 0 , gitlab_provided : 1 } , _prefix : :certificate
2020-03-13 15:44:24 +05:30
enum scope : { instance : 0 , group : 1 , project : 2 } , _prefix : :scope
enum usage : { pages : 0 , serverless : 1 } , _prefix : :usage
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
belongs_to :project
2019-09-04 21:01:54 +05:30
has_many :acme_orders , class_name : " PagesDomainAcmeOrder "
2020-04-08 14:13:33 +05:30
has_many :serverless_domain_clusters , class_name : 'Serverless::DomainCluster' , inverse_of : :pages_domain
2017-08-17 22:00:37 +05:30
2020-04-22 19:07:51 +05:30
before_validation :clear_auto_ssl_failure , unless : :auto_ssl_enabled
2017-09-10 17:25:29 +05:30
validates :domain , hostname : { allow_numeric_hostname : true }
2017-08-17 22:00:37 +05:30
validates :domain , uniqueness : { case_sensitive : false }
2020-03-13 15:44:24 +05:30
validates :certificate , :key , presence : true , if : :usage_serverless?
2019-09-30 21:07:59 +05:30
validates :certificate , presence : { message : 'must be present if HTTPS-only is enabled' } ,
if : :certificate_should_be_present?
2018-05-09 12:01:36 +05:30
validates :certificate , certificate : true , if : - > ( domain ) { domain . certificate . present? }
2019-09-30 21:07:59 +05:30
validates :key , presence : { message : 'must be present if HTTPS-only is enabled' } ,
if : :certificate_should_be_present?
2019-12-04 20:38:33 +05:30
validates :key , certificate_key : true , named_ecdsa_key : true , if : - > ( domain ) { domain . key . present? }
2018-03-17 18:26:18 +05:30
validates :verification_code , presence : true , allow_blank : false
2017-08-17 22:00:37 +05:30
validate :validate_pages_domain
validate :validate_matching_key , if : - > ( domain ) { domain . certificate . present? || domain . key . present? }
2019-09-30 21:07:59 +05:30
validate :validate_intermediates , if : - > ( domain ) { domain . certificate . present? && domain . certificate_changed? }
2017-08-17 22:00:37 +05:30
2021-02-22 17:27:13 +05:30
default_value_for ( :auto_ssl_enabled , allows_nil : false ) { :: Gitlab :: LetsEncrypt . enabled? }
default_value_for :scope , allows_nil : false , value : :project
default_value_for :wildcard , allows_nil : false , value : false
default_value_for :usage , allows_nil : false , value : :pages
2019-12-26 22:10:19 +05:30
2017-08-17 22:00:37 +05:30
attr_encrypted :key ,
mode : :per_attribute_iv_and_salt ,
insecure_mode : true ,
2018-11-08 19:23:39 +05:30
key : Settings . attr_encrypted_db_key_base ,
2017-08-17 22:00:37 +05:30
algorithm : 'aes-256-cbc'
2018-03-17 18:26:18 +05:30
after_initialize :set_verification_code
after_create :update_daemon
2019-07-31 22:56:46 +05:30
after_update :update_daemon , if : :saved_change_to_pages_config?
2018-03-17 18:26:18 +05:30
after_destroy :update_daemon
2020-06-23 00:09:42 +05:30
scope :enabled , - > { where ( 'enabled_until >= ?' , Time . current ) }
2018-03-17 18:26:18 +05:30
scope :needs_verification , - > do
verified_at = arel_table [ :verified_at ]
enabled_until = arel_table [ :enabled_until ]
2020-06-23 00:09:42 +05:30
threshold = Time . current + VERIFICATION_THRESHOLD
2018-03-17 18:26:18 +05:30
where ( verified_at . eq ( nil ) . or ( enabled_until . eq ( nil ) . or ( enabled_until . lt ( threshold ) ) ) )
end
2019-09-30 21:07:59 +05:30
scope :need_auto_ssl_renewal , - > do
2020-05-24 23:13:21 +05:30
enabled_and_not_failed = where ( auto_ssl_enabled : true , auto_ssl_failed : false )
2019-09-30 21:07:59 +05:30
2020-05-24 23:13:21 +05:30
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-09-30 21:07:59 +05:30
2020-05-24 23:13:21 +05:30
from_union ( [ user_provided , certificate_not_valid , certificate_expiring ] )
2019-09-30 21:07:59 +05:30
end
2020-06-23 00:09:42 +05:30
scope :for_removal , - > { where ( " remove_at < ? " , Time . current ) }
2019-07-31 22:56:46 +05:30
2020-03-13 15:44:24 +05:30
scope :with_logging_info , - > { includes ( project : [ :namespace , :route ] ) }
scope :instance_serverless , - > { where ( wildcard : true , scope : :instance , usage : :serverless ) }
2020-04-08 14:13:33 +05:30
def self . find_by_domain_case_insensitive ( domain )
find_by ( " LOWER(domain) = LOWER(?) " , domain )
end
2018-03-17 18:26:18 +05:30
def verified?
! ! verified_at
end
def unverified?
! verified?
end
def enabled?
! Gitlab :: CurrentSettings . pages_domain_verification_enabled? || enabled_until . present?
end
2017-08-17 22:00:37 +05:30
2018-05-09 12:01:36 +05:30
def https?
certificate . present?
end
2017-08-17 22:00:37 +05:30
def to_param
domain
end
def url
return unless domain
2018-03-17 18:26:18 +05:30
if certificate . present?
2017-08-17 22:00:37 +05:30
" https:// #{ domain } "
else
" http:// #{ domain } "
end
end
def has_matching_key?
return false unless x509
return false unless pkey
# 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
# self-signed certificates doesn't have the certificate chain
return true if x509 . verify ( x509 . public_key )
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
2018-03-17 18:26:18 +05:30
2020-06-23 00:09:42 +05:30
current = Time . current
2017-08-17 22:00:37 +05:30
current < x509 . not_before || x509 . not_after < current
end
2018-03-17 18:26:18 +05:30
def expiration
x509 & . not_after
end
2017-08-17 22:00:37 +05:30
def subject
return unless x509
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
x509 . subject . to_s
end
def certificate_text
@certificate_text || = x509 . try ( :to_text )
end
2018-03-17 18:26:18 +05:30
# 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-09-04 21:01:54 +05:30
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-09-30 21:07:59 +05:30
def user_provided_key
key if certificate_user_provided?
end
def user_provided_key = ( key )
self . key = key
2021-03-08 18:12:59 +05:30
self . certificate_source = 'user_provided' if attribute_changed? ( :key )
2019-09-30 21:07:59 +05:30
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
2021-03-08 18:12:59 +05:30
self . certificate_source = 'gitlab_provided' if attribute_changed? ( :key )
2019-09-30 21:07:59 +05:30
end
2019-12-04 20:38:33 +05:30
def pages_virtual_domain
2019-12-21 20:55:43 +05:30
return unless pages_deployed?
2019-12-04 20:38:33 +05:30
Pages :: VirtualDomain . new ( [ project ] , domain : self )
end
2020-04-22 19:07:51 +05:30
def clear_auto_ssl_failure
self . auto_ssl_failed = false
end
2017-08-17 22:00:37 +05:30
private
2019-12-21 20:55:43 +05:30
def pages_deployed?
2020-10-24 23:57:45 +05:30
return false unless project
2019-12-21 20:55:43 +05:30
# 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
project . pages_metadatum & . deployed?
end
2018-03-17 18:26:18 +05:30
def set_verification_code
return if self . verification_code . present?
self . verification_code = SecureRandom . hex ( 16 )
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def update_daemon
2020-03-13 15:44:24 +05:30
return if usage_serverless?
2020-10-24 23:57:45 +05:30
return unless pages_deployed?
2020-01-01 13:55:28 +05:30
2020-11-24 15:15:51 +05:30
run_after_commit { PagesUpdateConfigurationWorker . perform_async ( project_id ) }
2017-08-17 22:00:37 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
2019-07-31 22:56:46 +05:30
def saved_change_to_pages_config?
saved_change_to_project_id? ||
saved_change_to_domain? ||
saved_change_to_certificate? ||
saved_change_to_key? ||
2018-03-17 18:26:18 +05:30
became_enabled? ||
became_disabled?
end
def became_enabled?
2019-07-31 22:56:46 +05:30
enabled_until . present? && ! enabled_until_before_last_save . present?
2018-03-17 18:26:18 +05:30
end
def became_disabled?
2019-07-31 22:56:46 +05:30
! enabled_until . present? && enabled_until_before_last_save . present?
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
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
end
def validate_pages_domain
return unless domain
2018-03-17 18:26:18 +05:30
2017-09-10 17:25:29 +05:30
if domain . downcase . ends_with? ( Settings . pages . host . downcase )
2021-01-29 00:20:46 +05:30
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. " )
2017-08-17 22:00:37 +05:30
end
end
def x509
2019-09-04 21:01:54 +05:30
return unless certificate . present?
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
@x509 || = OpenSSL :: X509 :: Certificate . new ( certificate )
rescue OpenSSL :: X509 :: CertificateError
nil
end
def pkey
return unless key
2018-03-17 18:26:18 +05:30
2019-12-04 20:38:33 +05:30
@pkey || = OpenSSL :: PKey . read ( key )
2017-08-17 22:00:37 +05:30
rescue OpenSSL :: PKey :: PKeyError , OpenSSL :: Cipher :: CipherError
nil
end
2019-09-30 21:07:59 +05:30
def certificate_should_be_present?
! auto_ssl_enabled? && project & . pages_https_only?
end
2017-08-17 22:00:37 +05:30
end
2020-03-13 15:44:24 +05:30
2021-06-08 01:23:25 +05:30
PagesDomain . prepend_mod_with ( 'PagesDomain' )