# Pleroma: A lightweight social networking server # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.Config do use Mix.Task import Ecto.Query import Mix.Pleroma alias Pleroma.ConfigDB alias Pleroma.Repo alias Pleroma.Config.ConfigurableFromDatabase @shortdoc "Manages the location of the config" @moduledoc File.read!("docs/docs/administration/CLI_tasks/config.md") def run(["migrate_to_db"]) do check_configdb(fn -> start_pleroma() migrate_to_db() end) end def run(["migrate_from_db" | options]) do check_configdb(fn -> start_pleroma() {opts, _} = OptionParser.parse!(options, strict: [env: :string, delete: :boolean, path: :string], aliases: [d: :delete] ) migrate_from_db(opts) end) end def run(["dump"]) do check_configdb(fn -> start_pleroma() header = config_header() settings = ConfigDB |> Repo.all() |> Enum.sort() unless settings == [] do shell_info("#{header}") Enum.each(settings, &dump(&1)) else shell_error("No settings in ConfigDB.") end end) end def run(["dump", group, key]) do check_configdb(fn -> start_pleroma() group = maybe_atomize(group) key = maybe_atomize(key) group |> ConfigDB.get_by_group_and_key(key) |> dump() end) end def run(["dump", group]) do check_configdb(fn -> start_pleroma() group = maybe_atomize(group) dump_group(group) end) end def run(["dump_to_file", group, key, fname]) do check_configdb(fn -> start_pleroma() group = maybe_atomize(group) key = maybe_atomize(key) config = ConfigDB.get_by_group_and_key(group, key) json = %{ group: ConfigDB.to_json_types(config.group), key: ConfigDB.to_json_types(config.key), value: ConfigDB.to_json_types(config.value) } |> Jason.encode!() |> Jason.Formatter.pretty_print() File.write(fname, json) shell_info("Wrote #{group}_#{key}.json") end) end def run(["load_from_file", fname]) do check_configdb(fn -> start_pleroma() json = File.read!(fname) config = Jason.decode!(json) group = ConfigDB.to_elixir_types(config["group"]) key = ConfigDB.to_elixir_types(config["key"]) value = ConfigDB.to_elixir_types(config["value"]) params = %{group: group, key: key, value: value} ConfigDB.update_or_create(params) shell_info("Loaded #{config["group"]}, #{config["key"]}") end) end def run(["groups"]) do check_configdb(fn -> start_pleroma() groups = ConfigDB |> distinct([c], true) |> select([c], c.group) |> Repo.all() if length(groups) > 0 do shell_info("The following configuration groups are set in ConfigDB:\r\n") groups |> Enum.each(fn x -> shell_info("- #{x}") end) shell_info("\r\n") end end) end def run(["reset", "--force"]) do check_configdb(fn -> start_pleroma() truncatedb() shell_info("The ConfigDB settings have been removed from the database.") end) end def run(["reset"]) do check_configdb(fn -> start_pleroma() shell_info("The following settings will be permanently removed:") ConfigDB |> Repo.all() |> Enum.sort() |> Enum.each(&dump(&1)) shell_error("\nTHIS CANNOT BE UNDONE!") if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do truncatedb() shell_info("The ConfigDB settings have been removed from the database.") else shell_error("No changes made.") end end) end def run(["delete", "--force", group, key]) do start_pleroma() group = maybe_atomize(group) key = maybe_atomize(key) with true <- key_exists?(group, key) do shell_info("The following settings will be removed from ConfigDB:\n") group |> ConfigDB.get_by_group_and_key(key) |> dump() delete_key(group, key) else _ -> shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.") end end def run(["delete", "--force", group]) do start_pleroma() group = maybe_atomize(group) with true <- group_exists?(group) do shell_info("The following settings will be removed from ConfigDB:\n") dump_group(group) delete_group(group) else _ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.") end end def run(["delete", group, key]) do start_pleroma() group = maybe_atomize(group) key = maybe_atomize(key) with true <- key_exists?(group, key) do shell_info("The following settings will be removed from ConfigDB:\n") group |> ConfigDB.get_by_group_and_key(key) |> dump() if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do delete_key(group, key) else shell_error("No changes made.") end else _ -> shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.") end end def run(["delete", group]) do start_pleroma() group = maybe_atomize(group) with true <- group_exists?(group) do shell_info("The following settings will be removed from ConfigDB:\n") dump_group(group) if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do delete_group(group) else shell_error("No changes made.") end else _ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.") end end # Primarily a developer tool to check nothing was missed from # db configwhitelist def run(["check-allowed"]) do start_pleroma() Pleroma.Docs.JSON.compile() raw = Pleroma.Docs.JSON.compiled_descriptions() whitelisted = Enum.filter(raw, &ConfigurableFromDatabase.whitelisted_config?/1) raw_map = MapSet.new(raw) whitelisted_map = MapSet.new(whitelisted) IO.puts( "Config keys defined in description.exs but not listed as explicitly allowed in the db" ) IO.puts( " Please check that standard admins should not need to touch the listed settings whilst the server is live." ) IO.puts( " !! Please remember that admins are not neccesarily sysadmins nor are they immune to oauth/password leakage." ) IO.puts("-------------") MapSet.difference(raw_map, whitelisted_map) |> Enum.each(fn map -> IO.puts("#{map[:group]}, #{map[:key]} (#{map[:label]})") IO.puts(map[:db_exclusion_reason] || "No exclusion reason set") IO.puts("++") end) end @spec migrate_to_db(Path.t() | nil) :: any() def migrate_to_db(file_path \\ nil) do with :ok <- Pleroma.Config.DeprecationWarnings.warn() do config_file = if file_path do file_path else if Pleroma.Config.get(:release) do Pleroma.Config.get(:config_path) else "config/#{Pleroma.Config.get(:env)}.secret.exs" end end do_migrate_to_db(config_file) else _ -> shell_error("Migration is not allowed until all deprecation warnings have been resolved.") end end defp do_migrate_to_db(config_file) do if File.exists?(config_file) do shell_info("Migrating settings from file: #{Path.expand(config_file)}") truncatedb() custom_config = config_file |> read_file() |> elem(0) custom_config |> Keyword.keys() |> Enum.each(&create(&1, custom_config)) else shell_info("To migrate settings, you must define custom settings in #{config_file}.") end end defp create(group, settings) do group |> Pleroma.Config.Loader.filter_group(settings) |> Enum.filter(fn {key, _value} -> Pleroma.Config.ConfigurableFromDatabase.whitelisted_config?(group, key) end) |> Enum.each(fn {key, value} -> {:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value}) shell_info("Settings for key #{key} migrated.") end) shell_info("Settings for group #{inspect(group)} migrated.") end defp migrate_from_db(opts) do env = opts[:env] || Pleroma.Config.get(:env) filename = "#{env}.exported_from_db.secret.exs" config_path = cond do opts[:path] -> opts[:path] Pleroma.Config.get(:release) -> :config_path |> Pleroma.Config.get() |> Path.dirname() true -> "config" end |> Path.join(filename) with {:ok, file} <- File.open(config_path, [:write, :utf8]) do write_config(file, config_path, opts) shell_info("Database configuration settings have been exported to #{config_path}") else _ -> shell_error("Impossible to save settings to this directory #{Path.dirname(config_path)}") tmp_config_path = Path.join(System.tmp_dir!(), filename) file = File.open!(tmp_config_path) shell_info( "Saving database configuration settings to #{tmp_config_path}. Copy it to the #{Path.dirname(config_path)} manually." ) write_config(file, tmp_config_path, opts) end end defp write_config(file, path, opts) do IO.write(file, config_header()) ConfigDB |> Repo.all() |> Enum.each(&write_and_delete(&1, file, opts[:delete])) :ok = File.close(file) System.cmd("mix", ["format", path]) end if Code.ensure_loaded?(Config.Reader) do defp config_header, do: "import Config\r\n\r\n" defp read_file(config_file), do: Config.Reader.read_imports!(config_file) else defp config_header, do: "use Mix.Config\r\n\r\n" defp read_file(config_file), do: Mix.Config.eval!(config_file) end defp write_and_delete(config, file, delete?) do config |> write(file) |> delete(delete?) end defp write(config, file) do value = inspect(config.value, limit: :infinity) IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n") config end defp delete(config, true) do {:ok, _} = Repo.delete(config) shell_info( "config #{inspect(config.group)}, #{inspect(config.key)} was deleted from the ConfigDB." ) end defp delete(_config, _), do: :ok defp dump(%ConfigDB{} = config) do value = inspect(config.value, limit: :infinity) shell_info("config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n") end defp dump(_), do: :noop defp dump_group(group) when is_atom(group) do group |> ConfigDB.get_all_by_group() |> Enum.each(&dump/1) end defp group_exists?(group) do group |> ConfigDB.get_all_by_group() |> Enum.any?() end defp key_exists?(group, key) do group |> ConfigDB.get_by_group_and_key(key) |> is_nil |> Kernel.!() end defp maybe_atomize(arg) when is_atom(arg), do: arg defp maybe_atomize(":" <> arg), do: maybe_atomize(arg) defp maybe_atomize(arg) when is_binary(arg) do if ConfigDB.module_name?(arg) do String.to_existing_atom("Elixir." <> arg) else String.to_atom(arg) end end defp check_configdb(callback) do with true <- Pleroma.Config.get([:configurable_from_database]) do callback.() else _ -> shell_error( "ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration." ) end end defp delete_key(group, key) do check_configdb(fn -> ConfigDB.delete(%{group: group, key: key}) end) end defp delete_group(group) do check_configdb(fn -> group |> ConfigDB.get_all_by_group() |> Enum.each(&ConfigDB.delete/1) end) end defp truncatedb do Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;") Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") end end