Skip to content

Move provider API tokens to encrypted model fields #213

Description

@scanner

Summary

Provider API tokens (server tokens and account API keys used to talk to email providers like Postmark and ForwardEmail) are currently stored as a JSON blob in the EMAIL_SERVER_TOKENS environment variable and looked up via app/as_email/provider_tokens.py. Moving them into encrypted model fields makes token management easier operationally (editable via the Django admin UI rather than requiring env var changes and restarts) and consistent with how IMAP credentials are already handled via django-fernet-encrypted-fields.

Current Structure

  • settings.EMAIL_SERVER_TOKENS — a parsed JSON dict loaded from an env var:
    {
      "postmark": {
        "example.com": "pm-server-token",
        "account_api_key": "pm-account-key"
      },
      "forwardemail": {
        "account_api_key": "fe-account-key"
      }
    }
  • app/as_email/provider_tokens.pyget_provider_token() and has_provider_token() look up tokens from the settings dict.
  • Provider backends (postmark.py, forwardemail.py) call get_provider_token() to obtain credentials at runtime.

Proposed Design

New model: ServerProviderToken

A Server can have multiple providers (send_provider FK + receive_providers M2M), so the per-server token is stored on the (Server, Provider) pair. This is the only design that allows tokens to be individually encrypted at rest via django-fernet-encrypted-fields — a JSONField approach cannot encrypt individual values nested inside JSON.

class ServerProviderToken(models.Model):
    server   = models.ForeignKey(Server, on_delete=models.CASCADE, related_name="provider_tokens")
    provider = models.ForeignKey(Provider, on_delete=models.CASCADE, related_name="server_tokens")
    token    = EncryptedCharField(max_length=1024)

    class Meta:
        unique_together = [("server", "provider")]

Account-level keys (not tied to a specific server domain) go on the Provider model itself:

class Provider(models.Model):
    # ... existing fields ...
    account_api_key = EncryptedCharField(max_length=1024, blank=True, default="")

Removal

  • Remove EMAIL_SERVER_TOKENS from settings.py, .env, docker-compose, and Drone CI
  • Remove app/as_email/provider_tokens.py
  • Update provider backends (postmark.py, forwardemail.py) to read tokens from ServerProviderToken / Provider.account_api_key
  • Update all tests that set settings.EMAIL_SERVER_TOKENS[...] directly

Benefits

  • Tokens encrypted at rest using the same SALT_KEY/django-fernet-encrypted-fields mechanism already in use for IMAP passwords — consistent storage pattern for all secrets
  • Eliminates a large JSON blob from environment / container configuration
  • Tokens editable via Django admin UI without requiring a service restart
  • Referential integrity and cascade deletes enforced at the DB level
  • Clearer data model: each (Server, Provider) pair carries its own encrypted token

Acceptance Criteria

  • EMAIL_SERVER_TOKENS env var removed everywhere
  • app/as_email/provider_tokens.py deleted
  • ServerProviderToken model added with encrypted token field
  • Provider.account_api_key encrypted field added
  • Provider backends read tokens from model fields
  • Django admin allows editing tokens
  • All existing tests updated and passing
  • Migration provided

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions