Class: ActiveStorageEncryption::EncryptedGCSService

Inherits:
ActiveStorage::Service::GCSService
  • Object
show all
Includes:
PrivateUrlPolicy
Defined in:
lib/active_storage_encryption/encrypted_gcs_service.rb

Constant Summary collapse

GCS_ENCRYPTION_KEY_LENGTH_BYTES =

google wants to get a 32 byte key

32

Constants included from PrivateUrlPolicy

PrivateUrlPolicy::DEFAULT_POLICY

Instance Method Summary collapse

Methods included from PrivateUrlPolicy

#initialize, #private_url_for_streaming_via_controller, #private_url_policy, #private_url_policy=

Instance Method Details

#compose(source_keys, destination_key, encryption_key:, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}) ⇒ Object

Raises:

  • (NotImplementedError)


112
113
114
115
116
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 112

def compose(source_keys, destination_key, encryption_key:, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
  # Because we will always have a different encryption_key on a blob when created and google requires us to have the same encryption_keys on all source blobs
  # we need to work this out a bit more. For now we don't need this and thus won't support it in this service.
  raise NotImplementedError, "Currently composing files is not supported"
end

#download(key, encryption_key: nil, &block) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 75

def download(key, encryption_key: nil, &block)
  if block_given?
    instrument :streaming_download, key: key do
      stream(key, encryption_key: encryption_key, &block)
    end
  else
    instrument :download, key: key do
      file_for(key).download(encryption_key: derive_service_encryption_key(encryption_key)).string
    rescue Google::Cloud::NotFoundError => e
      raise ActiveStorage::FileNotFoundError, e
    end
  end
end

#download_chunk(key, range, encryption_key: nil) ⇒ Object



89
90
91
92
93
94
95
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 89

def download_chunk(key, range, encryption_key: nil)
  instrument :download_chunk, key: key, range: range do
    file_for(key).download(range: range, encryption_key: derive_service_encryption_key(encryption_key)).string
  rescue Google::Cloud::NotFoundError => e
    raise ActiveStorage::FileNotFoundError, e
  end
end

#encrypted?Boolean

Returns:

  • (Boolean)


10
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 10

def encrypted? = true

#headers_for_direct_upload(key, checksum:, encryption_key:, filename: nil, disposition: nil, content_type: nil, custom_metadata: {}) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 60

def headers_for_direct_upload(key, checksum:, encryption_key:, filename: nil, disposition: nil, content_type: nil, custom_metadata: {}, **)
  headers = {
    "Content-Type" => content_type,
    "Content-MD5" => checksum, # Not strictly required, but it ensures the file bytes we upload match what we want. This way google will error when we upload garbage.
    **gcs_encryption_key_headers(derive_service_encryption_key(encryption_key)),
    **()
  }
  headers["Content-Disposition"] = content_disposition_with(type: disposition, filename: filename) if filename

  if @config[:cache_control].present?
    headers["Cache-Control"] = @config[:cache_control]
  end
  headers
end

#public?Boolean

Returns:

  • (Boolean)


12
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 12

def public? = false

#service_nameObject



14
15
16
17
18
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 14

def service_name
  # ActiveStorage::Service::DiskService => Disk
  # Overridden because in Rails 8 this is "self.class.name.split("::").third.remove("Service")"
  self.class.name.split("::").last.remove("Service")
end

#stream(key, encryption_key: nil) ⇒ Object

Reads the file for the given key in chunks, yielding each to the block.

Raises:

  • (ActiveStorage::FileNotFoundError)


98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 98

def stream(key, encryption_key: nil)
  file = file_for(key, skip_lookup: false)

  chunk_size = 5.megabytes
  offset = 0

  raise ActiveStorage::FileNotFoundError unless file.present?

  while offset < file.size
    yield file.download(range: offset..(offset + chunk_size - 1), encryption_key: derive_service_encryption_key(encryption_key)).string
    offset += chunk_size
  end
end

#upload(key, io, encryption_key: nil, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {}) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 20

def upload(key, io, encryption_key: nil, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {})
  instrument :upload, key: key, checksum: checksum do
    # GCS's signed URLs don't include params such as response-content-type response-content_disposition
    # in the signature, which means an attacker can modify them and bypass our effort to force these to
    # binary and attachment when the file's content type requires it. The only way to force them is to
    # store them as object's metadata.
    content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
    bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition, metadata: , encryption_key: derive_service_encryption_key(encryption_key))
  rescue Google::Cloud::InvalidArgumentError => e
    raise ActiveStorage::IntegrityError, e
  end
end

#url_for_direct_upload(key, expires_in:, checksum:, encryption_key:, content_type: nil, custom_metadata: {}, filename: nil) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/active_storage_encryption/encrypted_gcs_service.rb', line 33

def url_for_direct_upload(key, expires_in:, checksum:, encryption_key:, content_type: nil, custom_metadata: {}, filename: nil, **)
  instrument :url, key: key do |payload|
    headers = headers_for_direct_upload(key, checksum:, encryption_key:, content_type:, filename:, custom_metadata:)

    version = :v4

    args = {
      content_md5: checksum,
      expires: expires_in,
      headers: headers,
      method: "PUT",
      version: version
    }

    if @config[:iam]
      args[:issuer] = issuer
      args[:signer] = signer
    end

    generated_url = bucket.signed_url(key, **args)

    payload[:url] = generated_url

    generated_url
  end
end