Current section
Files
Jump to
Current section
Files
lib/mix/tasks/cloak.migrate.ex
defmodule Mix.Tasks.Cloak.Migrate do
@moduledoc """
Migrate all configured models 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 models to migrate.
Each model should be specified in this format:
{model_name, :encryption_field_name}
Where `:encryption_field_name` is the name of the field the model uses to
track it's encryption version.
config :cloak, :migration,
repo: MyApp.Repo,
models: [{MyApp.Model1, :encryption_version},
{MyApp.Model2, :encryption_version}]
## Usage
mix cloak.migrate
The task allows you to customize the repo and models which will be migrated at
runtime.
mix cloak.migrate -m MyApp.Model -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]
@config Application.get_env(:cloak, :migration)
@repo @config[:repo]
@models @config[:models] || []
@doc false
def run(args) do
info "=== Starting Migration ==="
{repo, models} = parse(args)
Mix.Task.run "app.start", args
Enum.each(models, &migrate(&1, repo))
info "=== Migration Complete ==="
end
defp parse(args) do
{opts, _, _} = OptionParser.parse(args, aliases: [m: :model,
f: :field,
r: :repo])
repo = case opts[:repo] do
nil -> @repo
repo -> to_module(repo)
end
models = case opts[:model] do
nil -> @models
model -> [{to_module(model), opts[:field]}]
end
validate!(repo, models)
{repo, models}
end
defp validate!(repo, [h|_t]) when repo == nil or not is_tuple(h) do
raise ArgumentError, """
You must specify which models 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,
models: [{MyApp.Model1, :encryption_version},
{MyApp.Model2, :encryption_version}]
Alternatively, you can pass in the model, field, and repo as command line
arguments to `mix cloak.migrate`:
mix cloak.migrate -r Repo -m ModelName -f encryption_version_field
"""
end
defp migrate({model, field}, repo) do
info "--- Migrating #{inspect model.__struct__} Model ---"
ids = ids_for({model, field}, repo)
info "#{length(ids)} records found needing migration"
for id <- ids do
model
|> repo.get(id)
|> migrate_row(repo, field)
end
end
defp ids_for({model, field}, repo) do
query = from m in model,
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
repo.update!(row)
end
end
defp to_module(name) do
to_existing_atom("Elixir." <> name)
end
end