Current section
Files
Jump to
Current section
Files
lib/mix/tasks/cloak.migrate.ex
defmodule Mix.Tasks.Cloak.Migrate do
@moduledoc """
Migrate all configured schemas to your new encryption configuration.
While Cloak will automatically decrypt rows which use an old decryption cipher
or key, this isn't usually enough. Usually, you want to retire the old key, so
it won't do to leave it configured indefinitely.
This task allows you to proactively upgrade all rows in your database to the
new encryption configuration, so that you can remove the old key.
## Before You Run This Task...
1. Ensure that you have configured your new encryption cipher.
2. Set the new cipher and/or key as the `:default`. Otherwise, running this
task will have no effect.
## Configuration
In order for the Mix task to update rows in the correct database, it must have
access to the correct repo, and a list of schemas to migrate.
Each schema should be specified in this format:
{schema_name, :encryption_field_name}
Where `:encryption_field_name` is the name of the field the schema uses to
track it's encryption version.
config :cloak, :migration,
repo: MyApp.Repo,
schemas: [{MyApp.Schema1, :encryption_version},
{MyApp.Schema2, :encryption_version}]
## Usage
mix cloak.migrate
The task allows you to customize the repo and schemas which will be migrated at
runtime.
mix cloak.migrate -m MyApp.Schema -f encryption_version -r MyApp.Repo
"""
use Mix.Task
import Ecto.Query, only: [from: 2]
import Logger, only: [info: 1]
import String, only: [to_existing_atom: 1]
@doc false
def run(args) do
_ = info("=== Starting Migration ===")
{repo, schemas} = parse(args)
Mix.Task.run("app.start", args)
Enum.each(schemas, &migrate(&1, repo))
_ = info("=== Migration Complete ===")
:ok
end
defp parse(args) do
{opts, _, _} = OptionParser.parse(args, aliases: [m: :schema, f: :field, r: :repo])
repo =
case opts[:repo] do
nil -> Application.get_env(:cloak, :migration)[:repo]
repo -> to_module(repo)
end
schemas =
case opts[:schema] do
nil -> Application.get_env(:cloak, :migration)[:schemas]
schema -> [{to_module(schema), String.to_atom(opts[:field])}]
end
validate!(repo, schemas)
{repo, schemas}
end
defp validate!(repo, [h | _t]) when repo == nil or not is_tuple(h) do
raise ArgumentError, """
You must specify which schemas you wish to migrate and which repo to use.
You can do this in your Mix config, like so:
config :cloak, :migration,
repo: MyApp.Repo,
schemas: [{MyApp.Schema1, :encryption_version},
{MyApp.Schema2, :encryption_version}]
Alternatively, you can pass in the schema, field, and repo as command line
arguments to `mix cloak.migrate`:
mix cloak.migrate -r Repo -m SchemaName -f encryption_version_field
"""
end
defp validate!(_repo, _schemas), do: :ok
defp migrate({schema, field}, repo) do
_ = info("--- Migrating #{inspect(schema)} Schema ---")
ids = ids_for({schema, field}, repo)
_ = info("#{length(ids)} records found needing migration")
for id <- ids do
schema
|> repo.get(id)
|> migrate_row(repo, field)
end
end
defp ids_for({schema, field}, repo) do
query =
from(
m in schema,
where: field(m, ^field) != ^Cloak.version(),
select: m.id
)
repo.all(query)
end
defp migrate_row(row, repo, field) do
version = Map.get(row, field)
if version != Cloak.version() do
row
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_change(field, Cloak.version())
|> repo.update!
end
end
defp to_module(name) do
to_existing_atom("Elixir." <> name)
end
end