Security

Security

dbzero provides security controls for applications that need to protect persisted object data without giving up the natural Python object model. These controls let application code continue to work with memo objects through attributes, queries, references, and collections, while dbzero enforces authorization rules at the points where protected data is read, filtered, or modified.

Protected Fields

dbzero-procommercial edition

Protected fields are a dbzero-pro feature for field-level access control on memo objects. Declare a protected memo class with protect_fields=True:

import dbzero as db0
 
@db0.memo(protect_fields=True)
class Customer:
    def __init__(self, name: str, ssn: str):
        self.name = name
        self.ssn = ssn

Once a memo class is protected, dbzero checks field access when Python code reads, creates, updates, or deletes protected fields. This means protection happens at normal object access points such as customer.ssn, customer.ssn = value, and field initialization inside __init__.

Denied field reads raise PermissionError by default. If data masking is configured with a missing_value_placeholder, denied reads return that placeholder instead. This is useful for serialization or API responses where inaccessible fields should be omitted or masked instead of failing the whole operation.

Protected-field metadata is persisted with the memo class. Derived memo classes inherit field protection and cannot disable it while a protected base class is in use.

Activating Data Masking

Protected fields require data masking to be initialized for the process. Configure it when calling db0.init() with the data_masking option:

from contextvars import ContextVar
import dbzero as db0
 
account_id = ContextVar("account_id")
 
db0.init(
    "/var/lib/my-app/dbzero",
    prefix="main",
    data_masking={
        "context_var": account_id,
        "prefix": "main",
        "missing_value_placeholder": None,
    },
)

The data_masking mapping accepts:

  • context_var: a required ContextVar containing the current account id.
  • prefix: an optional prefix, prefix object, or sequence of prefixes for prefix-scoped masking. Omit it for workspace-wide masking.
  • missing_value_placeholder: an optional value returned when a field read is denied.
  • mode: an optional mode string. It defaults to "RELEASE". Use "DEBUG" only for development and tests.

Set the account id for each request, task, or execution context before accessing protected data:

account_id.set(123)

Field permissions are managed per protected memo class and account. A typical setup defines a field-access enum and grants only the fields an account may use:

@db0.enum(values=["CREATE", "READ", "UPDATE", "DELETE"])
class FieldAccess:
    pass
 
db0.set_field_access(Customer, 123, (FieldAccess.READ,), "name")
db0.set_field_access(Customer, 123, (FieldAccess.CREATE,), "name", "ssn")
 
customer = Customer("Alice", "123-45-6789")
 
assert customer.name == "Alice"
 
# Raises PermissionError, or returns the configured placeholder.
customer.ssn

Use protected fields for application data where field visibility or mutation rights vary by account, tenant, role, or execution context.

Data Filtering Predicates

dbzero-procommercial edition

Data filtering predicates provide a row-level security layer for dbzero objects. They are useful when whole memo objects should be visible only when they match the current account, tenant, role, or other authorization context.

Declare access-controlled memo classes with access_control=True:

import dbzero as db0
 
@db0.memo(access_control=True)
class Document:
    def __init__(self, title: str, body: str):
        self.title = title
        self.body = body

When data filtering is active, dbzero applies the current predicate at application-visible access boundaries. This includes find(), fetch(), deserialized queries, and memo references exposed through object fields or dbzero collections. Objects outside the current predicate are filtered out or denied before application code receives them.

Use this layer for multi-tenancy, per-user grants, role-based visibility, account scoping, and high-granularity security filters.

Activating Data Filtering

Configure data filtering when calling db0.init() with the data_filter option:

from contextvars import ContextVar
import dbzero as db0
 
filter_predicate = ContextVar("filter_predicate")
 
db0.init(
    "/var/lib/my-app/dbzero",
    prefix="main",
    data_filter={
        "context_var": filter_predicate,
        "prefix": "main",
    },
)

The data_filter mapping accepts:

  • context_var: a required ContextVar containing the current db0.predicate(...).
  • prefix: an optional prefix, prefix object, or sequence of prefixes where filtering is enabled. Omit it for workspace-wide filtering.
  • mode: an optional mode string. It defaults to "RELEASE". Use "DEBUG" only for development and tests.

Set the predicate for each request, task, or execution context before querying protected data:

tenant_id = "tenant-a"
 
filter_predicate.set(
    db0.predicate(db0.as_tag("TENANT", tenant_id))
)

Then tag protected objects with the same access relation:

document = Document("Invoice", "...")
db0.tags(document).add(db0.as_tag("TENANT", tenant_id))
 
visible_documents = list(db0.find(Document))
assert visible_documents == [document]

Predicates must be built with db0.predicate(...), not db0.find(...). Predicate objects use the same criteria grammar as find(), but they are not directly iterable, countable, truth-testable, indexable, or sliceable. This prevents predicate construction from becoming a separate data leak.

For higher-granularity policies, combine grant and deny predicates:

filter_predicate.set(
    db0.predicate(
        [
            db0.as_tag("GRANT", account),
            db0.as_tag("GRANT", role),
        ],
        db0.no(db0.as_tag("DENY", account)),
    )
)