defmodule Mix.Tasks.NervesHub.User do use Mix.Task import Mix.NervesHubCLI.Utils alias NervesHubCLI.{User, Config} alias Mix.NervesHubCLI.Shell alias X509.{Certificate, PrivateKey, CSR} @shortdoc "Manages your NervesHub user account" @moduledoc """ Manage your NervesHub user account. Users are authenticated to the NervesHub API by supplying a valid client certificate with every request. User certificates can be generated and managed on https://www.nerves-hub.org/account/certificates NervesHub will look for the following files in the location of $NERVES_HUB_HOME ca.pem: A file that contains all known NervesHub Certificate Authority certificates needed to authenticate. user.pem: A signed user account certificate. user-key.pem: The user account certificate private key. ## whoami mix nerves_hub.user whoami ## register mix nerves_hub.user register ## auth mix nerves_hub.user auth ## deauth mix nerves_hub.user deauth ## cert export mix nerves_hub.user cert export ### Command-line options * `--path` - (Optional) A local location for exporting certificate. """ @switches [path: :string] def run(args) do _ = Application.ensure_all_started(:nerves_hub_cli) {opts, args} = OptionParser.parse!(args, strict: @switches) show_api_endpoint() case args do ["whoami"] -> whoami() ["register"] -> register() ["auth"] -> auth() ["deauth"] -> deauth() ["cert", "export"] -> cert_export(opts) _ -> render_help() end end @spec render_help() :: no_return() def render_help() do Shell.raise(""" Invalid arguments to `mix nerves_hub.user`. Usage: mix nerves_hub.user whoami mix nerves_hub.user register mix nerves_hub.user auth mix nerves_hub.user deauth mix nerves_hub.user cert export Run `mix help nerves_hub.user` for more information. """) end def whoami do auth = Shell.request_auth() case NervesHubUserAPI.User.me(auth) do {:ok, %{"data" => data}} -> %{"username" => username, "email" => email} = data Shell.info(""" username: #{username} email: #{email} """) error -> Shell.render_error(error) end end def register() do email = Shell.prompt("Email address:") |> String.trim() username = Shell.prompt("Username:") |> String.trim() password = Shell.password_get("NervesHub password:") |> String.trim() confirm = Shell.password_get("NervesHub password (confirm):") |> String.trim() unless String.equivalent?(password, confirm) do Mix.raise("Entered passwords do not match") end Shell.info("Registering account...") register(username, email, password) end def auth() do username_or_email = Shell.prompt("Username or email address:") |> String.trim() password = Shell.password_get("NervesHub password:") |> String.trim() Shell.info("Authenticating...") case NervesHubUserAPI.User.auth(username_or_email, password) do {:ok, %{"data" => %{"email" => email, "username" => username}}} -> Shell.info("Success") generate_certificate(username, email, password) {:error, %{"errors" => errors}} -> Shell.error("Account authentication failed \n") Shell.render_error(errors) error -> Shell.render_error(error) end end def deauth() do if Shell.yes?("Deauthorize the current user?") do User.deauth() end end def cert_export(opts) do path = opts[:path] || NervesHubCLI.home_dir() password = Shell.password_get("Local user password:") with :ok <- File.mkdir_p(path), {:ok, %{key: key, cert: cert}} <- User.auth(password), key_pem <- PrivateKey.to_pem(key), cert_pem <- Certificate.to_pem(cert), filename <- certs_tar_file_name(path), {:ok, tar} <- :erl_tar.open(to_charlist(filename), [:write, :compressed]), :ok <- :erl_tar.add(tar, {'cert.pem', cert_pem}, []), :ok <- :erl_tar.add(tar, {'key.pem', key_pem}, []), :ok <- :erl_tar.close(tar) do Shell.info("User certs exported to: #{filename}") else error -> Shell.render_error(error) end end defp certs_tar_file_name(path), do: Path.join(path, "nerves_hub-certs.tar.gz") defp register(username, email, account_password) do case NervesHubUserAPI.User.register(username, email, account_password) do {:ok, %{"data" => %{"email" => ^email, "username" => ^username}}} -> Shell.info("Account created") generate_certificate(username, email, account_password) {:error, %{"errors" => errors}} -> Shell.error("Account creation failed \n") Shell.render_error(errors) error -> Shell.render_error(error) end end defp generate_certificate(username, email, account_password) do Shell.info("") Shell.info("NervesHub uses client-side SSL certificates to authenticate CLI requests.") Shell.info("") Shell.info("The next step will create an SSL certificate and store it in your ") Shell.info( "'#{NervesHubCLI.home_dir()}' directory. A password is required to protect it. This password" ) Shell.info("does not need to be your NervesHub password. It will never be sent to NervesHub") Shell.info("or any other computer. If you lose it, you will need to run") Shell.info("'mix nerves_hub.user auth' and create a new certificate.") Shell.info("") local_password = Shell.password_get("Please enter a local password:") key = PrivateKey.new_ec(:secp256r1) pem_key = PrivateKey.to_pem(key) csr = CSR.new(key, "/O=#{username}") pem_csr = CSR.to_pem(csr) with safe_csr <- Base.encode64(pem_csr), description <- NervesHubCLI.default_description(), {:ok, %{"data" => %{"cert" => pem_cert}}} <- NervesHubUserAPI.User.sign(email, account_password, safe_csr, description), :ok <- User.save_certs(pem_cert, pem_key, local_password), :ok <- Config.put(:email, email), :ok <- Config.put(:org, username) do Shell.info("Certificate created successfully.") else error -> User.deauth() Shell.render_error(error) end end end