Module: OpenLoam::Export
- Defined in:
- lib/open_loam/export.rb
Overview
CSV export of a tenant-scoped relation, POLICY- and ENCRYPTION-aware:
OpenLoam::Export.csv(Equipment.all, actor: current_actor)
- only fields the actor's role may READ are columns (OpenLoam::Policy#readable?);
- an ENCRYPTED column is NEVER exported in the clear — its cell is "[encrypted]" (the same redaction as the audit trail), so a bulk export can never become a plaintext dump of PII a role shouldn't see;
- declared custom fields go through the same read check (OpenLoam::Policy#custom_field_readable?) — a dictionary field exports its stored code, so the file round-trips back through OpenLoam::Import;
- tenant isolation is free — the relation is already scoped.
Prototype scale: builds the CSV in memory with the stdlib CSV. A very large export would stream row-by-row through an enumerator body — the same column logic, a different sink.
Constant Summary collapse
- REDACTED =
"[encrypted]".freeze
- SKIP_COLUMNS =
Never exported: tenant plumbing and optimistic-locking bookkeeping.
%w[tenant_id lock_version deleted_at].freeze
Class Method Summary collapse
- .cell(record, column) ⇒ Object
- .csv(scope, actor:) ⇒ Object
-
.exportable_columns(model, actor) ⇒ Object
The ordered column spec: readable real columns (encrypted ones kept but redacted), then declared custom fields.
- .policy_for(model, actor) ⇒ Object
Class Method Details
.cell(record, column) ⇒ Object
61 62 63 64 65 66 67 68 |
# File 'lib/open_loam/export.rb', line 61 def cell(record, column) value = case column[:kind] when :encrypted then REDACTED when :custom then (record.custom_field(column[:name]) rescue nil) else record.public_send(column[:name]) end OpenLoam::Csv.safe(value) # neutralize CSV formula injection (=, +, -, @, tab/CR) end |
.csv(scope, actor:) ⇒ Object
27 28 29 30 31 32 33 34 35 |
# File 'lib/open_loam/export.rb', line 27 def csv(scope, actor:) model = scope.klass columns = exportable_columns(model, actor) CSV.generate do |out| out << columns.map { |c| c[:header] } scope.find_each { |record| out << columns.map { |c| cell(record, c) } } end end |
.exportable_columns(model, actor) ⇒ Object
The ordered column spec: readable real columns (encrypted ones kept but redacted), then declared custom fields.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/open_loam/export.rb', line 39 def exportable_columns(model, actor) policy = policy_for(model, actor) encrypted = model.respond_to?(:open_loam_encrypted_attributes) ? model.open_loam_encrypted_attributes : [] # The blind-index columns behind searchable encrypted fields (e.g. # email_hash) are internal HMACs — never export them either. blind = model.respond_to?(:open_loam_searchable_encrypted_attributes) ? model.open_loam_searchable_encrypted_attributes.map { |a| "#{a}_hash" } : [] columns = model.column_names.reject { |c| SKIP_COLUMNS.include?(c) || blind.include?(c) || c == "custom_fields" } .select { |c| policy.readable?(c) } .map { |c| { header: c, name: c, kind: encrypted.include?(c) ? :encrypted : :column } } if model.respond_to?(:custom_field_definitions) model.custom_field_definitions.order(:name).each do |definition| next unless policy.custom_field_readable?(definition.name) columns << { header: definition.name, name: definition.name, kind: :custom } end end columns end |