Module: Apartment::TenantNameValidator

Defined in:
lib/apartment/tenant_name_validator.rb

Class Method Summary collapse

Class Method Details

.validate!(name, strategy:, adapter_name: nil) ⇒ Object

Validate a tenant name against common and engine-specific rules. Raises ConfigurationError on invalid names. Pure in-memory check — no IO.



9
10
11
12
13
14
15
16
17
18
# File 'lib/apartment/tenant_name_validator.rb', line 9

def validate!(name, strategy:, adapter_name: nil)
  validate_common!(name)
  case strategy
  when :schema
    validate_postgresql_identifier!(name)
  when :database_name
    validate_for_adapter!(name, adapter_name)
  end
  # :shard and :database_config use common validation only (not yet implemented).
end

.validate_common!(name) ⇒ Object

— Common rules (all engines) —

Raises:



22
23
24
25
26
27
28
29
30
31
# File 'lib/apartment/tenant_name_validator.rb', line 22

def validate_common!(name)
  raise(ConfigurationError, 'Tenant name must be a String') unless name.is_a?(String)
  raise(ConfigurationError, 'Tenant name cannot be empty') if name.empty?
  raise(ConfigurationError, "Tenant name contains NUL byte: #{name.inspect}") if name.include?("\x00")
  raise(ConfigurationError, "Tenant name contains whitespace: #{name.inspect}") if name.match?(/\s/)
  raise(ConfigurationError, "Tenant name contains colon: #{name.inspect}") if name.include?(':')
  return unless name.length > 255

  raise(ConfigurationError, "Tenant name too long (#{name.length} chars, max 255): #{name.inspect}")
end

.validate_for_adapter!(name, adapter_name) ⇒ Object

— Dispatcher for :database_name strategy —



79
80
81
82
83
84
85
# File 'lib/apartment/tenant_name_validator.rb', line 79

def validate_for_adapter!(name, adapter_name)
  case adapter_name
  when /mysql/i, /trilogy/i then validate_mysql_database_name!(name)
  when /postgresql/i, /postgis/i then validate_postgresql_identifier!(name)
  when /sqlite/i then validate_sqlite_path!(name)
  end
end

.validate_mysql_database_name!(name) ⇒ Object

— MySQL database names — Max 64 chars, allowed: [a-zA-Z0-9_$-], no leading digit, no trailing dot.

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/apartment/tenant_name_validator.rb', line 54

def validate_mysql_database_name!(name)
  if name.length > 64
    raise(ConfigurationError, "MySQL database name too long (#{name.length} chars, max 64): #{name.inspect}")
  end
  raise(ConfigurationError, "MySQL database name cannot start with a digit: #{name.inspect}") if name.match?(/\A\d/)
  raise(ConfigurationError, "MySQL database name cannot end with a period: #{name.inspect}") if name.end_with?('.')
  return unless name.match?(/[^a-zA-Z0-9_$-]/)

  raise(ConfigurationError,
        "Invalid MySQL database name: #{name.inspect}. " \
        'Allowed characters: letters, digits, underscore, dollar sign, hyphen')
end

.validate_postgresql_identifier!(name) ⇒ Object

— PostgreSQL identifiers (schema names, database names) — Hyphens are allowed — our adapters quote via quote_table_name. Cannot start with pg_ (reserved prefix).

Raises:



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/apartment/tenant_name_validator.rb', line 37

def validate_postgresql_identifier!(name)
  if name.length > 63
    raise(ConfigurationError, "PostgreSQL identifier too long (#{name.length} chars, max 63): #{name.inspect}")
  end
  unless name.match?(/\A[a-zA-Z_][a-zA-Z0-9_-]*\z/)
    raise(ConfigurationError,
          "Invalid PostgreSQL identifier: #{name.inspect}. " \
          'Must start with letter/underscore, contain only letters, digits, underscores, hyphens')
  end
  return unless name.start_with?('pg_')

  raise(ConfigurationError, "Tenant name cannot start with 'pg_' (reserved prefix): #{name.inspect}")
end

.validate_sqlite_path!(name) ⇒ Object

— SQLite file paths — No path traversal, filesystem-safe characters.

Raises:



70
71
72
73
74
75
# File 'lib/apartment/tenant_name_validator.rb', line 70

def validate_sqlite_path!(name)
  raise(ConfigurationError, "SQLite tenant name contains path traversal: #{name.inspect}") if name.include?('..')
  return unless name.match?(%r{[/\\]})

  raise(ConfigurationError, "SQLite tenant name contains path separators: #{name.inspect}")
end