409 lines
14 KiB
Python
409 lines
14 KiB
Python
from collections import defaultdict
|
|
|
|
from odoo import api, models
|
|
|
|
|
|
class CxTowerVaultMixin(models.AbstractModel):
|
|
"""Mixin for vault functionality.
|
|
|
|
This mixin provides methods to securely store and retrieve sensitive data
|
|
in the vault. Inheriting models must define SECRET_FIELDS list with field
|
|
names that should be stored in the vault.
|
|
"""
|
|
|
|
_name = "cx.tower.vault.mixin"
|
|
_description = "Cetmix Tower Vault Mixin"
|
|
|
|
SECRET_VALUE_PLACEHOLDER = "*****"
|
|
SECRET_FIELDS = []
|
|
|
|
def _read(self, fields): # pylint: disable=missing-return # doesn't return anything
|
|
"""Substitute fields based on api.
|
|
|
|
This method replaces values of secret fields with a placeholder value
|
|
when they are read from the database.
|
|
|
|
Args:
|
|
fields (list): List of fields to read
|
|
"""
|
|
super()._read(fields)
|
|
|
|
show_all = not fields
|
|
secret_fields = (
|
|
self.SECRET_FIELDS
|
|
if show_all
|
|
else [f for f in self.SECRET_FIELDS if f in fields]
|
|
)
|
|
|
|
for record in self:
|
|
for secret_field in secret_fields:
|
|
try:
|
|
record._cache[secret_field] = self.SECRET_VALUE_PLACEHOLDER
|
|
except Exception: # pylint: disable=except-pass
|
|
# skip SpecialValue
|
|
# (e.g. for missing record or access right)
|
|
pass
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
"""Override create to handle secret values securely.
|
|
|
|
Extracts secret fields, stores them in vault, and prevents
|
|
actual secret values from being saved in the main table.
|
|
|
|
Args:
|
|
vals_list (list): List of dictionaries containing field values
|
|
for record creation
|
|
|
|
Returns:
|
|
recordset: Created records with secret values stored in vault
|
|
|
|
Note:
|
|
Secret fields are automatically processed and stored securely.
|
|
The main database table never contains actual secret values.
|
|
"""
|
|
|
|
# Step 1: Extract secret fields and generate temporary IDs
|
|
secret_vals = self._extract_and_replace_secret_fields(vals_list)
|
|
|
|
# Step 2: Create records with batch operation
|
|
records = super().create(vals_list)
|
|
|
|
# Step 3: Update vault records with real IDs
|
|
if secret_vals:
|
|
self._process_secret_values_after_creation(records, secret_vals)
|
|
|
|
return records
|
|
|
|
def write(self, vals):
|
|
"""Override write to handle secret fields.
|
|
|
|
Extracts secret field values from vals dictionary and stores them securely
|
|
in the vault instead of the main database table. The remaining non-secret
|
|
fields are processed by the standard write method.
|
|
|
|
Args:
|
|
vals (dict): Dictionary of field values to write to records
|
|
|
|
Returns:
|
|
bool: Result of the parent write operation
|
|
|
|
Note:
|
|
Secret fields defined in SECRET_FIELDS are automatically intercepted
|
|
and stored in vault. Cache is invalidated for all secret fields when
|
|
any secret field is modified.
|
|
"""
|
|
# Extract secret fields
|
|
secret_values = {}
|
|
for secret_field in self.SECRET_FIELDS:
|
|
if secret_field in vals:
|
|
secret_values[secret_field] = vals.pop(secret_field)
|
|
|
|
res = super().write(vals)
|
|
|
|
if secret_values:
|
|
self._set_secret_values(secret_values)
|
|
# Invalidate cache for all secret fields
|
|
self.invalidate_recordset(self.SECRET_FIELDS)
|
|
|
|
return res
|
|
|
|
def unlink(self):
|
|
"""Override unlink to delete vault records.
|
|
|
|
Automatically removes all associated vault records after deleting
|
|
the main records to prevent orphaned secret data in the vault.
|
|
|
|
Returns:
|
|
bool: Result of the parent unlink operation
|
|
|
|
Note:
|
|
Vault cleanup is performed automatically and cannot be bypassed.
|
|
"""
|
|
ids = self.ids
|
|
|
|
res = super().unlink()
|
|
|
|
# Find all vault records for these records
|
|
vault_records = (
|
|
self.env["cx.tower.vault"]
|
|
.sudo()
|
|
.search([("res_model", "=", self._name), ("res_id", "in", ids)])
|
|
)
|
|
|
|
# Delete vault records
|
|
if vault_records:
|
|
vault_records.sudo().unlink()
|
|
|
|
return res
|
|
|
|
def _get_secret_value(self, field_name):
|
|
"""Retrieves the actual secret value for a specific field for a single record.
|
|
|
|
This method is the only way to get the real secret field value because:
|
|
- Direct field access (e.g., self.secret_field)
|
|
returns placeholder due to _read() override
|
|
- The actual field in the main table is empty/NULL
|
|
as values are stored in vault
|
|
|
|
Args:
|
|
field_name (str): Name of the secret field to retrieve
|
|
|
|
Returns:
|
|
str or None: The actual secret value, or None if not found or field
|
|
is not in SECRET_FIELDS
|
|
|
|
Note:
|
|
This method bypasses Odoo's ORM field access to avoid getting
|
|
placeholder values returned by the overridden _read() method.
|
|
"""
|
|
|
|
self.ensure_one()
|
|
|
|
return self._get_secret_values([field_name]).get(self.id, {}).get(field_name)
|
|
|
|
def _get_secret_values(self, fields_list=None):
|
|
"""Retrieve secret values from the vault for specified fields.
|
|
|
|
This method fetches secret values stored in the vault for all records
|
|
in the current recordset and specified fields (or all SECRET_FIELDS).
|
|
|
|
Args:
|
|
fields_list (list, optional): List of field names to retrieve.
|
|
Defaults to all SECRET_FIELDS.
|
|
|
|
Returns:
|
|
dict: Dictionary mapping record IDs to their secret field values.
|
|
Structure: {res_id: {field_name: secret_value}}
|
|
|
|
Example:
|
|
{1: {'ssh_password': 'secret123', 'host_key': 'key456'},
|
|
2: {'ssh_password': 'secret789'}}
|
|
|
|
Note:
|
|
This method searches vault records using standard domain filtering
|
|
by res_id, and field_name for reliable record matching.
|
|
If a record has no secret values this record is not included in the result.
|
|
"""
|
|
# If no records, return empty dict
|
|
if not self:
|
|
return {}
|
|
|
|
# Prepare fields to fetch
|
|
fields_to_fetch = (
|
|
[f for f in fields_list if f in self.SECRET_FIELDS]
|
|
if fields_list
|
|
else self.SECRET_FIELDS
|
|
)
|
|
# If no fields to fetch, return empty dict
|
|
if not fields_to_fetch:
|
|
return {}
|
|
|
|
# Search vault records for all records and all secret fields
|
|
domain = [
|
|
("res_model", "=", self._name),
|
|
("res_id", "in", self.ids),
|
|
("field_name", "in", fields_to_fetch),
|
|
]
|
|
vault_records = (
|
|
self.env["cx.tower.vault"]
|
|
.sudo()
|
|
.search_read(
|
|
domain,
|
|
["res_id", "field_name", "data"],
|
|
)
|
|
)
|
|
res = defaultdict(dict)
|
|
for record in vault_records:
|
|
res[record["res_id"]][record["field_name"]] = record["data"]
|
|
|
|
return dict(res)
|
|
|
|
def _set_secret_values(self, vals):
|
|
"""Store secret values in the vault.
|
|
|
|
This method stores sensitive data in the vault for all records in the recordset.
|
|
It either updates existing vault records or creates new ones for each
|
|
record-field pair in the vals dictionary.
|
|
|
|
This method can be overridden to implement custom storage mechanisms
|
|
for secret values, such as external key management systems or
|
|
encryption services.
|
|
|
|
Args:
|
|
vals (dict): Dictionary mapping field names to their secret values
|
|
to be stored in the vault for all records
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not vals or not self:
|
|
return
|
|
|
|
# Get all existing vault records in ONE SQL query
|
|
domain = [
|
|
("res_model", "=", self._name),
|
|
("res_id", "in", self.ids),
|
|
("field_name", "in", list(vals.keys())),
|
|
]
|
|
existing_vault_records = self.env["cx.tower.vault"].sudo().search(domain)
|
|
|
|
# Prepare data for batch operations
|
|
vals_to_update_records = defaultdict(lambda: self.env["cx.tower.vault"])
|
|
records_to_unlink = self.env["cx.tower.vault"]
|
|
records_to_create = []
|
|
|
|
# Index existing records by (res_id, field_name) for O(1) lookups
|
|
existing_map = {(v.res_id, v.field_name): v for v in existing_vault_records}
|
|
|
|
# Only allow known secret fields to be set
|
|
allowed_fields = set(self.SECRET_FIELDS)
|
|
|
|
# Process each record and field combination
|
|
for record in self:
|
|
for field, value in vals.items():
|
|
if field not in allowed_fields:
|
|
continue
|
|
# Fast lookup for existing record
|
|
existing_record = existing_map.get((record.id, field))
|
|
if existing_record:
|
|
if value is False or value is None:
|
|
records_to_unlink |= existing_record
|
|
else:
|
|
vals_to_update_records[value] |= existing_record
|
|
|
|
else:
|
|
if value is False or value is None:
|
|
continue
|
|
|
|
records_to_create.append(
|
|
{
|
|
"res_model": self._name,
|
|
"res_id": record.id,
|
|
"field_name": field,
|
|
"data": value,
|
|
}
|
|
)
|
|
|
|
# Batch operations
|
|
for value, records in vals_to_update_records.items():
|
|
records.sudo().write({"data": value})
|
|
|
|
if records_to_create:
|
|
self.env["cx.tower.vault"].sudo().create(records_to_create)
|
|
if records_to_unlink:
|
|
records_to_unlink.sudo().unlink()
|
|
|
|
def _extract_and_replace_secret_fields(self, vals_list):
|
|
"""Extract secret fields and replace with temporary identifiers.
|
|
|
|
Processes value dictionaries for record creation, replacing secret field values
|
|
with unique temporary identifiers. The actual secret values are mapped to these
|
|
temporary identifiers for later secure storage in the vault system.
|
|
|
|
Args:
|
|
vals_list (list): List of value dictionaries for record creation.
|
|
|
|
Returns:
|
|
dict: Mapping of temporary identifiers to secret values.
|
|
Note: vals_list is modified in-place to contain temp identifiers.
|
|
|
|
Note:
|
|
Used during record creation as part of the secure secret storage workflow.
|
|
"""
|
|
temp_id_counter = 0
|
|
secret_vals = {}
|
|
|
|
for vals in vals_list:
|
|
for secret_field in self.SECRET_FIELDS:
|
|
if (
|
|
secret_field in vals
|
|
and vals[secret_field] is not False
|
|
and vals[secret_field] is not None
|
|
):
|
|
temp_id_counter += 1
|
|
temp_identifier = str(temp_id_counter)
|
|
secret_vals[temp_identifier] = vals[secret_field]
|
|
vals[secret_field] = temp_identifier
|
|
|
|
return secret_vals
|
|
|
|
def _process_secret_values_after_creation(self, records, secret_vals):
|
|
"""Process secret values after records creation.
|
|
|
|
Replaces temporary identifiers with actual secret values in the vault
|
|
and invalidates cache for affected fields.
|
|
|
|
Args:
|
|
records (recordset): Newly created records with temporary identifiers
|
|
secret_vals (dict): Mapping of temporary identifiers to secret values
|
|
|
|
Returns:
|
|
None
|
|
|
|
Note:
|
|
Called automatically during create() process. Should not be used directly.
|
|
"""
|
|
fields_str = ", ".join(self.SECRET_FIELDS)
|
|
query = f"SELECT id, {fields_str} FROM {self._table} WHERE id in %s"
|
|
self.env.cr.execute(query, (tuple(records.ids),))
|
|
records_dict = self.env.cr.dictfetchall()
|
|
|
|
for record_dict in records_dict:
|
|
self._process_single_record_secrets(record_dict, secret_vals)
|
|
|
|
records._clear_temp_values()
|
|
records.invalidate_recordset(self.SECRET_FIELDS)
|
|
|
|
def _process_single_record_secrets(self, record_dict, secret_vals):
|
|
"""Process secrets for a single record.
|
|
|
|
Replaces temporary identifiers with actual secret values for one record,
|
|
clears temporary values from main table and stores secrets in vault.
|
|
|
|
Args:
|
|
record_dict (dict): Dictionary with record data
|
|
including temporary identifiers
|
|
secret_vals (dict): Mapping of temporary identifiers to actual secret values
|
|
|
|
Returns:
|
|
None
|
|
|
|
Note:
|
|
Internal method used by _process_secret_values_after_creation.
|
|
"""
|
|
record_id = record_dict.get("id")
|
|
vault_vals = {}
|
|
field_temp_id_pairs = (
|
|
(field_name, record_dict[field_name]) for field_name in self.SECRET_FIELDS
|
|
)
|
|
|
|
# Collect secret values and fields to clear
|
|
for field_name, temp_identifier in field_temp_id_pairs:
|
|
secret_value = secret_vals.get(temp_identifier)
|
|
if secret_value:
|
|
vault_vals[field_name] = secret_value
|
|
|
|
# Update database and vault if needed
|
|
if vault_vals:
|
|
record = self.browse(record_id)
|
|
record._set_secret_values(vault_vals)
|
|
|
|
def _clear_temp_values(self):
|
|
"""Clear temporary values from main table.
|
|
|
|
Sets all SECRET_FIELDS to NULL in the database to remove temporary
|
|
identifiers after secret values have been stored in vault.
|
|
Works with multiple records in the recordset.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Note:
|
|
Internal method used during secret processing workflow.
|
|
Clears all SECRET_FIELDS for all records in the current recordset.
|
|
"""
|
|
set_clause = ", ".join(f"{field} = NULL" for field in self.SECRET_FIELDS)
|
|
query = f"UPDATE {self._table} SET {set_clause} WHERE id in %s"
|
|
self.env.cr.execute(query, (tuple(self.ids),))
|