Module: CamaleonCms::SetupToken

Defined in:
lib/camaleon_cms/setup_token.rb

Overview

Gates the first-run installer so setup can be completed only by someone with access to the server's environment or filesystem — not by any anonymous visitor who reaches the app on a fresh deploy (audit finding C1, vector 2). The expected token comes from ENV when set, otherwise from a generated 0600 file under tmp/; generating it logs its location once so the operator can read it. It is required only while no site exists, and is cleared once one does. See openspec/specs/installer-access-control/spec.md.

Constant Summary collapse

ENV_KEY =
'CAMALEON_SETUP_TOKEN'.freeze

Class Method Summary collapse

Class Method Details

.clear!Object

Invalidate the generated token once setup has completed (the ENV override, if any, is untouched).



42
43
44
45
46
# File 'lib/camaleon_cms/setup_token.rb', line 42

def clear!
  File.delete(file_path) if File.exist?(file_path)
rescue StandardError
  nil
end

.file_pathObject



48
49
50
# File 'lib/camaleon_cms/setup_token.rb', line 48

def file_path
  Rails.root.join('tmp/camaleon_setup_token').to_s
end

.read_or_generate_fileObject



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/camaleon_cms/setup_token.rb', line 52

def read_or_generate_file
  return File.read(file_path).strip if File.exist?(file_path)

  token = SecureRandom.hex(32)
  FileUtils.mkdir_p(File.dirname(file_path))
  # 0600 from the first byte (write-then-chmod would leave a world-readable window at default
  # umask), EXCL so a concurrent generator cannot clobber a token already handed to an operator.
  File.open(file_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |f| f.write(token) }
  Rails.logger.info("Camaleon CMS: setup token written to #{file_path} — required to run the installer.")
  token
rescue Errno::EEXIST
  # Lost the creation race between the exist? check and the open: adopt the winner's token.
  read_or_generate_file
rescue StandardError
  nil
end

.required?Boolean

The installer is gated only until the first site exists; afterwards it is closed anyway.

Returns:

  • (Boolean)


17
18
19
20
21
# File 'lib/camaleon_cms/setup_token.rb', line 17

def required?
  CamaleonCms::Site.count == 0
rescue StandardError
  false
end

.valid?(candidate) ⇒ Boolean

Constant-time comparison; false for a blank candidate or when no expected token is available.

Returns:

  • (Boolean)


33
34
35
36
37
38
39
# File 'lib/camaleon_cms/setup_token.rb', line 33

def valid?(candidate)
  candidate = candidate.to_s
  expected = value.to_s
  return false if candidate.empty? || expected.empty?

  ActiveSupport::SecurityUtils.secure_compare(candidate, expected)
end

.valueObject

The expected token: an explicit ENV override wins; otherwise a generated file (created on first read). Returns nil only if no ENV value is set and the file cannot be created.



25
26
27
28
29
30
# File 'lib/camaleon_cms/setup_token.rb', line 25

def value
  env = ENV[ENV_KEY].to_s
  return env if env.present?

  read_or_generate_file
end