Add redis storage to have possibility to invalidate sessions

account
Никита Поздняков 2 years ago
parent 25369e7e27
commit 00ee70f5a9
No known key found for this signature in database
GPG Key ID: F344106F9804FE5F
  1. 1
      .github/workflows/config.yml
  2. 3
      apps/block_scout_web/lib/block_scout_web/controllers/account/auth_controller.ex
  3. 3
      apps/block_scout_web/lib/block_scout_web/endpoint.ex
  4. 223
      apps/block_scout_web/lib/block_scout_web/plug/redis_cookie.ex
  5. 9
      apps/explorer/lib/explorer/application.ex
  6. 3
      apps/explorer/mix.exs
  7. 11
      config/runtime/dev.exs
  8. 11
      config/runtime/prod.exs
  9. 11
      config/runtime/test.exs
  10. 6
      docker-compose/docker-compose.yml
  11. 2
      docker-compose/envs/common-blockscout.env
  12. 3
      mix.lock

@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- account - account
- np-add-redis-storage-for-sessions
env: env:
MIX_ENV: test MIX_ENV: test

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Account.AuthController do
alias BlockScoutWeb.Models.UserFromAuth alias BlockScoutWeb.Models.UserFromAuth
alias Explorer.Account alias Explorer.Account
alias Plug.CSRFProtection
plug(Ueberauth) plug(Ueberauth)
@ -34,6 +35,8 @@ defmodule BlockScoutWeb.Account.AuthController do
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
case UserFromAuth.find_or_create(auth) do case UserFromAuth.find_or_create(auth) do
{:ok, user} -> {:ok, user} ->
CSRFProtection.get_csrf_token()
conn conn
|> put_session(:current_user, user) |> put_session(:current_user, user)
|> redirect(to: root()) |> redirect(to: root())

@ -59,9 +59,10 @@ defmodule BlockScoutWeb.Endpoint do
# The session will be stored in the cookie and signed, # The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with. # this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it. # Set :encryption_salt if you would also like to encrypt it.
plug( plug(
Plug.Session, Plug.Session,
store: :cookie, store: BlockScoutWeb.Plug.RedisCookie,
key: "_explorer_key", key: "_explorer_key",
signing_salt: "iC2ksJHS", signing_salt: "iC2ksJHS",
same_site: "Lax" same_site: "Lax"

@ -0,0 +1,223 @@
defmodule BlockScoutWeb.Plug.RedisCookie do
@moduledoc """
Extended version of Plug.Session.COOKIE from https://github.com/elixir-plug/plug/blob/main/lib/plug/session/cookie.ex
Added Redis to have a possibility to invalidate session
"""
require Logger
@behaviour Plug.Session.Store
alias Plug.Crypto
alias Plug.Crypto.{KeyGenerator, MessageEncryptor, MessageVerifier}
@impl true
def init(opts) do
opts
|> build_opts()
|> build_rotating_opts(opts[:rotating_options])
|> Map.delete(:secret_key_base)
end
@impl true
def get(conn, raw_cookie, opts) do
opts = Map.put(opts, :secret_key_base, conn.secret_key_base)
[opts | opts.rotating_options]
|> Enum.find_value(:error, &read_raw_cookie(raw_cookie, &1))
|> decode(opts.serializer, opts.log)
|> check_in_redis(raw_cookie)
end
@impl true
def put(conn, _sid, term, opts) do
%{serializer: serializer, key_opts: key_opts, signing_salt: signing_salt} = opts
binary = encode(term, serializer)
opts
|> case do
%{encryption_salt: nil} ->
MessageVerifier.sign(binary, derive(conn.secret_key_base, signing_salt, key_opts))
%{encryption_salt: encryption_salt} ->
MessageEncryptor.encrypt(
binary,
derive(conn.secret_key_base, encryption_salt, key_opts),
derive(conn.secret_key_base, signing_salt, key_opts)
)
end
|> store_to_redis()
end
@impl true
def delete(_conn, sid, _opts) do
remove_from_redis(sid)
:ok
end
defp encode(term, :external_term_format) do
:erlang.term_to_binary(term)
end
defp encode(term, serializer) do
{:ok, binary} = serializer.encode(term)
binary
end
defp decode({:ok, binary}, :external_term_format, log) do
{:term,
try do
Crypto.non_executable_binary_to_term(binary)
rescue
e ->
Logger.log(
log,
"Plug.Session could not decode incoming session cookie. Reason: " <>
Exception.message(e)
)
%{}
end}
end
defp decode({:ok, binary}, serializer, _log) do
case serializer.decode(binary) do
{:ok, term} -> {:custom, term}
_ -> {:custom, %{}}
end
end
defp decode(:error, _serializer, false) do
{nil, %{}}
end
defp decode(:error, _serializer, log) do
Logger.log(
log,
"Plug.Session could not verify incoming session cookie. " <>
"This may happen when the session settings change or a stale cookie is sent."
)
{nil, %{}}
end
defp prederive(secret_key_base, value, key_opts)
when is_binary(secret_key_base) and is_binary(value) do
{:prederived, derive(secret_key_base, value, Keyword.delete(key_opts, :cache))}
end
defp prederive(_secret_key_base, value, _key_opts) do
value
end
defp derive(_secret_key_base, {:prederived, value}, _key_opts) do
value
end
defp derive(secret_key_base, {module, function, args}, key_opts) do
derive(secret_key_base, apply(module, function, args), key_opts)
end
defp derive(secret_key_base, key, key_opts) do
secret_key_base
|> validate_secret_key_base()
|> KeyGenerator.generate(key, key_opts)
end
defp validate_secret_key_base(nil),
do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be set")
defp validate_secret_key_base(secret_key_base) when byte_size(secret_key_base) < 64,
do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be at least 64 bytes")
defp validate_secret_key_base(secret_key_base), do: secret_key_base
defp check_signing_salt(opts) do
case opts[:signing_salt] do
nil -> raise ArgumentError, "cookie store expects :signing_salt as option"
salt -> salt
end
end
defp check_serializer(serializer) when is_atom(serializer), do: serializer
defp check_serializer(_),
do: raise(ArgumentError, "cookie store expects :serializer option to be a module")
defp read_raw_cookie(raw_cookie, opts) do
signing_salt = derive(opts.secret_key_base, opts.signing_salt, opts.key_opts)
opts
|> case do
%{encryption_salt: nil} ->
MessageVerifier.verify(raw_cookie, signing_salt)
%{encryption_salt: _} ->
encryption_salt = derive(opts.secret_key_base, opts.encryption_salt, opts.key_opts)
MessageEncryptor.decrypt(raw_cookie, encryption_salt, signing_salt)
end
|> case do
:error -> nil
result -> result
end
end
defp build_opts(opts) do
encryption_salt = opts[:encryption_salt]
signing_salt = check_signing_salt(opts)
iterations = Keyword.get(opts, :key_iterations, 1000)
length = Keyword.get(opts, :key_length, 32)
digest = Keyword.get(opts, :key_digest, :sha256)
log = Keyword.get(opts, :log, :debug)
secret_key_base = Keyword.get(opts, :secret_key_base)
key_opts = [iterations: iterations, length: length, digest: digest, cache: Plug.Keys]
serializer = check_serializer(opts[:serializer] || :external_term_format)
%{
secret_key_base: secret_key_base,
encryption_salt: prederive(secret_key_base, encryption_salt, key_opts),
signing_salt: prederive(secret_key_base, signing_salt, key_opts),
key_opts: key_opts,
serializer: serializer,
log: log
}
end
defp build_rotating_opts(opts, rotating_opts) when is_list(rotating_opts) do
Map.put(opts, :rotating_options, Enum.map(rotating_opts, &build_opts/1))
end
defp build_rotating_opts(opts, _), do: Map.put(opts, :rotating_options, [])
defp store_to_redis(cookie) do
Redix.command(:redix, ["SET", hash(cookie), 1])
cookie
end
defp remove_from_redis(sid) do
Redix.command(:redix, ["DEL", sid])
end
defp check_in_redis({sid, map}, _cookie) when is_nil(sid) or map == %{}, do: {nil, %{}}
defp check_in_redis({_sid, session}, cookie) do
hash = hash(cookie)
case Redix.command(:redix, ["GET", hash]) do
{:ok, one} when one in [1, "1"] ->
{hash, session}
_ ->
{nil, %{}}
end
end
defp hash(cookie) do
:sha256
|> :crypto.hash(cookie)
|> Base.encode16()
end
end

@ -67,7 +67,8 @@ defmodule Explorer.Application do
con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)), con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
Transactions, Transactions,
Accounts, Accounts,
Uncles Uncles,
{Redix, redix_opts()}
] ]
children = base_children ++ configurable_children() children = base_children ++ configurable_children()
@ -176,4 +177,10 @@ defmodule Explorer.Application do
id: {ConCache, name} id: {ConCache, name}
) )
end end
defp redix_opts do
config = Application.get_env(:explorer, Redix)
[name: :redix, host: config[:host], port: config[:port]]
end
end end

@ -114,7 +114,8 @@ defmodule Explorer.Mixfile do
{:con_cache, "~> 1.0"}, {:con_cache, "~> 1.0"},
{:tesla, "~> 1.4.4"}, {:tesla, "~> 1.4.4"},
{:cbor, "~> 1.0"}, {:cbor, "~> 1.0"},
{:cloak_ecto, "~> 1.2.0"} {:cloak_ecto, "~> 1.2.0"},
{:redix, "~> 1.1"}
] ]
end end

@ -103,6 +103,17 @@ variant =
Code.require_file("#{variant}.exs", "apps/explorer/config/dev") Code.require_file("#{variant}.exs", "apps/explorer/config/dev")
redis_port =
case System.get_env("ACCOUNT_REDIS_PORT") && Integer.parse(System.get_env("ACCOUNT_REDIS_PORT")) do
{port, _} -> port
:error -> nil
nil -> nil
end
config :explorer, Redix,
host: System.get_env("ACCOUNT_REDIS_HOST_URL") || "127.0.0.1",
port: redis_port || 6379
############### ###############
### Indexer ### ### Indexer ###
############### ###############

@ -70,6 +70,17 @@ variant =
Code.require_file("#{variant}.exs", "apps/explorer/config/prod") Code.require_file("#{variant}.exs", "apps/explorer/config/prod")
redis_port =
case System.get_env("ACCOUNT_REDIS_PORT") && Integer.parse(System.get_env("ACCOUNT_REDIS_PORT")) do
{port, _} -> port
:error -> nil
nil -> nil
end
config :explorer, Redix,
host: System.get_env("ACCOUNT_REDIS_HOST_URL"),
port: redis_port
############### ###############
### Indexer ### ### Indexer ###
############### ###############

@ -22,6 +22,17 @@ variant =
Code.require_file("#{variant}.exs", "apps/explorer/config/test") Code.require_file("#{variant}.exs", "apps/explorer/config/test")
redis_port =
case System.get_env("ACCOUNT_REDIS_PORT") && Integer.parse(System.get_env("ACCOUNT_REDIS_PORT")) do
{port, _} -> port
:error -> nil
nil -> nil
end
config :explorer, Redix,
host: System.get_env("ACCOUNT_REDIS_HOST_URL") || "127.0.0.1",
port: redis_port || 6379
############### ###############
### Indexer ### ### Indexer ###
############### ###############

@ -1,6 +1,12 @@
version: '3.8' version: '3.8'
services: services:
redis:
image: "redis:alpine"
command: redis-server
ports:
- 6379:6379
db: db:
image: postgres:14 image: postgres:14
restart: always restart: always

@ -138,3 +138,5 @@ RUST_VERIFICATION_SERVICE_URL=http://host.docker.internal:8043/
# ACCOUNT_SENDGRID_SENDER= # ACCOUNT_SENDGRID_SENDER=
# ACCOUNT_SENDGRID_TEMPLATE= # ACCOUNT_SENDGRID_TEMPLATE=
ACCOUNT_CLOAK_KEY= ACCOUNT_CLOAK_KEY=
ACCOUNT_REDIS_HOST_URL=http://host.docker.internal
ACCOUNT_REDIS_PORT=6379

@ -16,9 +16,9 @@
"cbor": {:hex, :cbor, "1.0.0", "35d33a26f6420ce3d2d01c0b1463a748b34c537d5609fc40116daf3666700d36", [:mix], [], "hexpm", "cc5e21e0fa5a0330715a3806c67bc294f8b65d07160f751b5bd6058bed1962ac"}, "cbor": {:hex, :cbor, "1.0.0", "35d33a26f6420ce3d2d01c0b1463a748b34c537d5609fc40116daf3666700d36", [:mix], [], "hexpm", "cc5e21e0fa5a0330715a3806c67bc294f8b65d07160f751b5bd6058bed1962ac"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"cldr_utils": {:hex, :cldr_utils, "2.19.1", "5a7bcd2f2fd432c548e494e850bba8a9e838f1b10202f682ea1d9809d74eff31", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "fbd10f79363e70f3d893ab21e195f444ca87c2c80120b5911761491da4489620"}, "cldr_utils": {:hex, :cldr_utils, "2.19.1", "5a7bcd2f2fd432c548e494e850bba8a9e838f1b10202f682ea1d9809d74eff31", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "fbd10f79363e70f3d893ab21e195f444ca87c2c80120b5911761491da4489620"},
"coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
"cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"}, "cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
"cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"}, "cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"},
"coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
"con_cache": {:hex, :con_cache, "1.0.0", "6405e2bd5d5005334af72939432783562a8c35a196c2e63108fe10bb97b366e6", [:mix], [], "hexpm", "4d1f5cb1a67f3c1a468243dc98d10ac83af7f3e33b7e7c15999dc2c9bc0a551e"}, "con_cache": {:hex, :con_cache, "1.0.0", "6405e2bd5d5005334af72939432783562a8c35a196c2e63108fe10bb97b366e6", [:mix], [], "hexpm", "4d1f5cb1a67f3c1a468243dc98d10ac83af7f3e33b7e7c15999dc2c9bc0a551e"},
@ -120,6 +120,7 @@
"que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"}, "que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"ratio": {:hex, :ratio, "2.4.2", "c8518f3536d49b1b00d88dd20d49f8b11abb7819638093314a6348139f14f9f9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "441ef6f73172a3503de65ccf1769030997b0d533b1039422f1e5e0e0b4cbf89e"}, "ratio": {:hex, :ratio, "2.4.2", "c8518f3536d49b1b00d88dd20d49f8b11abb7819638093314a6348139f14f9f9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "441ef6f73172a3503de65ccf1769030997b0d533b1039422f1e5e0e0b4cbf89e"},
"redix": {:hex, :redix, "1.1.5", "6fc460d66a5c2287e83e6d73dddc8d527ff59cb4d4f298b41e03a4db8c3b2bd5", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "679afdd4c14502fe9c11387ff1cdcb33065a1cf511097da1eee407f17c7a418b"},
"remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"}, "remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"},
"rustler": {:hex, :rustler, "0.24.0", "b8362a2fee1c9d2c7373b0bfdc98f75bbc02864efcec50df173fe6c4f72d4cc4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "2773167fca68a6525822ad977b41368ea3c2af876c42ebaa7c9d6bb69b67f1ce"}, "rustler": {:hex, :rustler, "0.24.0", "b8362a2fee1c9d2c7373b0bfdc98f75bbc02864efcec50df173fe6c4f72d4cc4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "2773167fca68a6525822ad977b41368ea3c2af876c42ebaa7c9d6bb69b67f1ce"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},

Loading…
Cancel
Save