From 3f657562e6a47bf131efc01af7a0aeaf7e5e58ab Mon Sep 17 00:00:00 2001 From: MarioRodrigues10 Date: Thu, 20 Oct 2022 10:31:52 +0100 Subject: [PATCH 1/4] integrate boruta --- config/config.exs | 26 ++++++++ lib/store/api.ex | 85 +++++++++++++++++++++++++ lib/store/oauth.ex | 70 ++++++++++++++++++++ lib/store/oauth/oauth_errors.ex | 17 +++++ lib/store/oauth/resource_owners.ex | 51 +++++++++++++++ lib/store/oauth/scopes.ex | 13 ++++ lib/store_web/channels/api_socket.ex | 65 +++++++++++++++++++ lib/store_web/plugs/api_context_plug.ex | 75 ++++++++++++++++++++++ lib/store_web/views/oauth_view.ex | 82 ++++++++++++++++++++++++ mix.exs | 7 +- mix.lock | 14 ++++ test/support/conn_case.ex | 29 +++++++++ 12 files changed, 533 insertions(+), 1 deletion(-) create mode 100644 lib/store/api.ex create mode 100644 lib/store/oauth.ex create mode 100644 lib/store/oauth/oauth_errors.ex create mode 100644 lib/store/oauth/resource_owners.ex create mode 100644 lib/store/oauth/scopes.ex create mode 100644 lib/store_web/channels/api_socket.ex create mode 100644 lib/store_web/plugs/api_context_plug.ex create mode 100644 lib/store_web/views/oauth_view.ex diff --git a/config/config.exs b/config/config.exs index fccc3d2..df8f281 100644 --- a/config/config.exs +++ b/config/config.exs @@ -66,6 +66,32 @@ config :tailwind, cd: Path.expand("../assets", __DIR__) ] + config :boruta, Boruta.Oauth, + repo: Store.Repo, + cache_backend: Boruta.Cache, + contexts: [ + access_tokens: Boruta.Ecto.AccessTokens, + clients: Boruta.Ecto.Clients, + codes: Boruta.Ecto.Codes, + # mandatory for user flows + resource_owners: Store.Oauth.ResourceOwners, + scopes: Boruta.Ecto.Scopes + ], + max_ttl: [ + authorization_code: 60, + access_token: 60 * 60 * 24, + refresh_token: 60 * 60 * 24 * 30 + ], + token_generator: Boruta.TokenGenerator + +config :boruta, Boruta.Cache, + primary: [ + # => 1 day + gc_interval: :timer.hours(6), + backend: :shards, + partitions: 2 + ] + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/lib/store/api.ex b/lib/store/api.ex new file mode 100644 index 0000000..fc56db9 --- /dev/null +++ b/lib/store/api.ex @@ -0,0 +1,85 @@ +defmodule Store.Api do + @moduledoc """ + """ + alias Absinthe.Relay.Connection + alias Store.Repo + + defmodule Access do + @moduledoc """ + Common struct for ensuring API access + """ + defstruct role: :user, + user: nil, + access_type: nil, + access_identifier: nil, + scopes: %{ + public: false, + email: false, + password: false, + } + end + + @error_not_found "Could not find resource" + @error_access_denied "Access denied" + + def error_not_found, do: {:error, @error_not_found} + + def error_access_denied, do: {:error, @error_access_denied} + + @doc """ + Return a Connection.from_query with a count (if last is specified). + """ + def connection_from_query_with_count(query, args, options \\ []) do + options = + if Map.has_key?(args, :last) do + # Only count records if we're starting at the end + Keyword.put(options, :count, Repo.aggregate(query, :count, :id)) + else + options + end + + # Set a complete max of 1000 records + options = Keyword.put(options, :max, 1000) + + # If the user didn't select an order, just give them the first 100 + args = + if Map.has_key?(args, :first) == false and Map.has_key?(args, :last) == false do + Map.put(args, :first, 100) + else + args + end + + Connection.from_query(query, &Repo.replica().all/1, args, options) + end + + @doc """ + Parse a list of changeset errors into actionable API errors + """ + def parse_ecto_changeset_errors(%Ecto.Changeset{} = changeset) do + changeset + |> Ecto.Changeset.traverse_errors(fn {msg, opts} -> + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", to_string(value)) + end) + end) + |> Enum.map(fn {key, value} -> "#{key}: #{value}" end) + end + + @doc """ + If a potentially_local_path is a locally uploaded Waffle file (for example a default chat background), + this function will create a full static URL for us to return to the API. If the potentially_local_path is + instead a full URL, we'll just return it. + """ + def resolve_full_url(potentially_local_path) when is_binary(potentially_local_path) do + if String.starts_with?(potentially_local_path, ["http://", "https://"]) do + potentially_local_path + else + StoreWeb.Router.Helpers.static_url( + StoreWeb.Endpoint, + potentially_local_path + ) + end + end + + def resolve_full_url(_), do: nil +end diff --git a/lib/store/oauth.ex b/lib/store/oauth.ex new file mode 100644 index 0000000..4cab46f --- /dev/null +++ b/lib/store/oauth.ex @@ -0,0 +1,70 @@ +defmodule Store.Oauth do + @moduledoc """ + Helper functions for resolving oauth actions + """ + + require Logger + + # + # API Access Resolution + # + def get_api_access_from_token(%Boruta.Oauth.Token{} = token) do + resolve_resource_owner(token.resource_owner, token) + end + + def get_unprivileged_api_access_from_client(%Boruta.Oauth.Client{} = client) do + {:ok, + %Store.Api.Access{ + role: :user, + user: nil, + access_type: "app", + access_identifier: client.id + }} + end + + defp resolve_resource_owner( + %Boruta.Oauth.ResourceOwner{} = resource_owner, + %Boruta.Oauth.Token{} = token + ) do + case Store.Oauth.ResourceOwners.get_from(resource_owner) do + %Store.Accounts.User{} = user -> + access_for_user(user, token.scope) + + _ -> + Logger.error("Unexpected resource owner for token: #{token.value}") + {:error, "Unexpected resource owner for token: #{token.value}"} + end + end + + defp resolve_resource_owner(_, %Boruta.Oauth.Token{} = token) do + owner = Store.Apps.get_app_owner_by_client_id!(token.client.id) + + access_for_user(owner, token.scope) + end + + def access_for_user(%Store.Accounts.User{} = user, scope) do + {:ok, + Map.merge( + %Store.Api.Access{ + role: :admin, + user: user, + access_type: "user", + access_identifier: user.username + }, + parse_scope(scope) + )} + end + + defp parse_scope(scope) when is_binary(scope) do + %{ + scopes: + Enum.reduce(String.split(scope), %{}, fn x, acc -> + Map.put(acc, String.to_atom(x), true) + end) + } + end + + defp parse_scope(_) do + %{} + end +end diff --git a/lib/store/oauth/oauth_errors.ex b/lib/store/oauth/oauth_errors.ex new file mode 100644 index 0000000..da6476a --- /dev/null +++ b/lib/store/oauth/oauth_errors.ex @@ -0,0 +1,17 @@ +defmodule Store.OauthHandler.OauthError do + @moduledoc false + + def add_error({:error, params}, _), do: {:error, params} + + def add_error({:ok, params}, {:error, error, status}) do + {:error, Map.merge(params, %{error: error, error_status: status})} + end + + def not_accessable do + {:error, %{error: :not_accessable}, :not_accessable} + end + + def invalid_ownership do + {:error, %{error: :invalid_ownership}, :invalid_ownership} + end +end diff --git a/lib/store/oauth/resource_owners.ex b/lib/store/oauth/resource_owners.ex new file mode 100644 index 0000000..9d0970c --- /dev/null +++ b/lib/store/oauth/resource_owners.ex @@ -0,0 +1,51 @@ +defmodule Store.Oauth.ResourceOwners do + @moduledoc false + + @behaviour Boruta.Oauth.ResourceOwners + alias Boruta.Oauth.ResourceOwner + alias Store.Accounts.User + alias Store.Repo + + @impl Boruta.Oauth.ResourceOwners + def get_by(email: email) do + case Repo.replica().get_by(User, email: email) do + %User{id: id, email: email} -> + {:ok, %ResourceOwner{sub: Integer.to_string(id)}} + + _ -> + {:error, "User not found."} + end + end + + def get_by(sub: sub) do + case Repo.replica().get_by(User, id: String.to_integer(sub)) do + %User{id: id} -> + {:ok, %ResourceOwner{sub: Integer.to_string(id)}} + + _ -> + {:error, "User not found."} + end + end + + def get_from(%Boruta.Oauth.ResourceOwner{} = resource_owner) do + case resource_owner do + %{email: email} -> Repo.replica().get_by(User, email: email) + %{sub: sub} -> Repo.replica().get_by(User, id: String.to_integer(sub)) + _ -> nil + end + end + + @impl Boruta.Oauth.ResourceOwners + def check_password(resource_owner, password) do + user = Store.Accounts.get_by_email(resource_owner.email) + + if User.valid_password?(user, password) do + :ok + else + {:error, "Invalid password or email."} + end + end + + @impl Boruta.Oauth.ResourceOwners + def authorized_scopes(%ResourceOwner{}), do: [] +end diff --git a/lib/store/oauth/scopes.ex b/lib/store/oauth/scopes.ex new file mode 100644 index 0000000..f54ef11 --- /dev/null +++ b/lib/store/oauth/scopes.ex @@ -0,0 +1,13 @@ +defmodule Store.Oauth.Scopes do + @moduledoc false + + import StoreWeb.Gettext + + def scope_gettext(scope) do + case scope do + "public" -> gettext("scopepublic") + "email" -> gettext("scopeemail") + "password" -> gettext("password") + end + end +end diff --git a/lib/store_web/channels/api_socket.ex b/lib/store_web/channels/api_socket.ex new file mode 100644 index 0000000..25b7345 --- /dev/null +++ b/lib/store_web/channels/api_socket.ex @@ -0,0 +1,65 @@ +defmodule StoreWeb.GraphApiSocket do + @moduledoc """ + Allow for connections to the API socket with either an API token or a client id. + + Client ID is for read API access only. + """ + use Phoenix.Socket + + use Absinthe.Phoenix.Socket, + schema: Store.Api.Schema + + @impl true + def connect(%{"client_id" => client_id}, socket, _connect_info) do + with %Boruta.Oauth.Client{} = client <- Boruta.Ecto.Clients.get_client(client_id), + {:ok, %Store.Api.Access{} = access} <- + Store.Oauth.get_unprivileged_api_access_from_client(client) do + {:ok, + socket + |> assign(:user_id, nil) + |> assign(:socket_id, client.id) + |> Absinthe.Phoenix.Socket.put_options( + context: %{ + access: access + } + )} + else + _ -> :error + end + end + + @impl true + def connect(%{"token" => access_token}, socket, _connect_info) do + with {:ok, %Boruta.Oauth.Token{} = token} <- + Boruta.Oauth.Authorization.AccessToken.authorize(value: access_token), + {:ok, %Store.Api.Access{} = access} <- + Store.Oauth.get_api_access_from_token(token) do + {:ok, + socket + |> assign(:user_id, access.user.id) + |> assign(:socket_id, token.value) + |> Absinthe.Phoenix.Socket.put_options( + context: %{ + access: access + } + )} + else + _ -> :error + end + end + + def connect(_, _, _) do + :error + end + + # Socket id's are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "graph_api_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # Returning `nil` makes this socket anonymous. + @impl true + def id(socket), do: "graph_api_socket:#{socket.assigns.socket_id}" +end diff --git a/lib/store_web/plugs/api_context_plug.ex b/lib/store_web/plugs/api_context_plug.ex new file mode 100644 index 0000000..0222c17 --- /dev/null +++ b/lib/store_web/plugs/api_context_plug.ex @@ -0,0 +1,75 @@ +defmodule StoreWeb.Plugs.ApiContextPlug do + @behaviour Plug + + import Plug.Conn + import Phoenix.Controller, only: [json: 2] + + def init(opts), do: opts + + def call(conn, opts) do + case authorize(parse_token_from_header(conn, opts)) do + {:ok, %Store.Api.Access{} = access} -> + conn + |> Absinthe.Plug.put_options( + context: %{ + access: access + } + ) + + {:error, %Boruta.Oauth.Error{} = reason} -> + conn + |> put_status(:unauthorized) + |> json(%{ + errors: [ + %{ + message: reason.error_description, + header_error: reason.error + } + ] + }) + |> halt() + + _ -> + conn + |> put_status(:unauthorized) + |> json(%{errors: [%{message: "You must be logged in to access the api"}]}) + |> halt() + end + end + + defp authorize({:bearer, token}) do + case Boruta.Oauth.Authorization.AccessToken.authorize(value: token) do + {:ok, %Boruta.Oauth.Token{} = token} -> + Store.Oauth.get_api_access_from_token(token) + + {:error, msg} -> + {:error, msg} + end + end + + defp authorize({:client, client_id}) do + client_id = Store.OauthMigration.convert_client_id(client_id) + + case Boruta.Ecto.Clients.get_client(client_id) do + %Boruta.Oauth.Client{} = client -> + Store.Oauth.get_unprivileged_api_access_from_client(client) + Glimesh + {:error, msg} -> + {:error, msg} + end + end + + defp authorize(_) do + {:error, :unauthorized} + end + + defp parse_token_from_header(conn, _opts) do + case get_req_header(conn, "authorization") do + ["Bearer " <> token] -> {:bearer, token} + ["bearer " <> token] -> {:bearer, token} + ["Client-ID " <> token] -> {:client, token} + ["client-id " <> token] -> {:client, token} + _ -> false + end + end +end diff --git a/lib/store_web/views/oauth_view.ex b/lib/store_web/views/oauth_view.ex new file mode 100644 index 0000000..60e8434 --- /dev/null +++ b/lib/store_web/views/oauth_view.ex @@ -0,0 +1,82 @@ +defmodule StoreWeb.OauthView do + use StoreWeb, :view + + require Ecto.Query + + alias Boruta.Oauth.IntrospectResponse + alias Boruta.Oauth.TokenResponse + + def render("introspect.json", %{response: %IntrospectResponse{active: false}}) do + %{"active" => false} + end + + def render("introspect.json", %{ + response: %IntrospectResponse{ + active: active, + client_id: client_id, + username: username, + scope: scope, + sub: sub, + iss: iss, + exp: exp, + iat: iat + } + }) do + %{ + active: active, + client_id: client_id, + username: username, + scope: scope, + sub: sub_to_integer(sub), + iss: iss, + exp: exp, + iat: iat + } + end + + def render("token.json", %{ + response: %TokenResponse{ + token_type: token_type, + access_token: access_token, + expires_in: expires_in, + refresh_token: refresh_token + } + }) do + # Hack to get expiration time + token = + Ecto.Query.from("oauth_tokens", + where: [value: ^access_token], + select: [:inserted_at, :scope], + limit: 1 + ) + |> Boruta.Repo.one() + + %{ + token_type: token_type, + access_token: access_token, + expires_in: expires_in, + refresh_token: refresh_token, + created_at: NaiveDateTime.truncate(token.inserted_at, :second), + scope: token.scope + } + end + + def render("error.json", %{error: error, error_description: error_description}) do + %{ + error: error, + error_description: error_description + } + end + + defp sub_to_integer(sub) when is_binary(sub) do + String.to_integer(sub) + end + + defp sub_to_integer(sub) when is_integer(sub) do + sub + end + + defp sub_to_integer(_) do + nil + end +end diff --git a/mix.exs b/mix.exs index 44021c2..145861c 100644 --- a/mix.exs +++ b/mix.exs @@ -54,8 +54,13 @@ defmodule Store.MixProject do {:qrcode_ex, "~> 0.1.1"}, {:jason, "~> 1.2"}, {:plug_cowboy, "~> 2.5"}, + {:boruta, "~> 2.1.5"}, {:tailwind, "~> 0.1", runtime: Mix.env() == :dev}, - {:icons, "~> 0.9"} + {:icons, "~> 0.9"}, + {:absinthe, "~> 1.5"}, + {:absinthe_plug, "~> 1.5"}, + {:absinthe_phoenix, "~> 2.0"}, + {:absinthe_relay, "~> 1.5"}, ] end diff --git a/mix.lock b/mix.lock index c6268fa..33c22bf 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,10 @@ %{ + "absinthe": {:hex, :absinthe, "1.7.0", "36819e7b1fd5046c9c734f27fe7e564aed3bda59f0354c37cd2df88fd32dd014", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "566a5b5519afc9b29c4d367f0c6768162de3ec03e9bf9916f9dc2bcbe7c09643"}, + "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.2", "e607b438db900049b9b3760f8ecd0591017a46122fffed7057bf6989020992b5", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "d36918925c380dc7d2ed7d039c9a3b4182ec36723f7417a68745ade5aab22f8d"}, + "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, + "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, + "boruta": {:hex, :boruta, "2.1.5", "fd697a9fa0ed028c3599f2af8c8e4b12396b8852b0c6086fd27dec34d9935d77", [:mix], [{:ecto_sql, ">= 3.5.2", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ex_json_schema, "~> 0.6", [hex: :ex_json_schema, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:nebulex, "~> 2.0", [hex: :nebulex, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:puid, "~> 1.0", [hex: :puid, repo: "hexpm", optional: false]}, {:secure_random, "~> 0.5", [hex: :secure_random, repo: "hexpm", optional: false]}, {:shards, "~> 1.0", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm", "dc01186a4d9ee6bb84ffefec8b2cb439ed45dc309c343211479e1e99040bb3a1"}, "castore": {:hex, :castore, "0.1.17", "ba672681de4e51ed8ec1f74ed624d104c0db72742ea1a5e74edbc770c815182f", [:mix], [], "hexpm", "d9844227ed52d26e7519224525cb6868650c272d4a3d327ce3ca5570c12163f9"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, @@ -7,6 +12,7 @@ "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "crypto_rand": {:hex, :crypto_rand, "1.0.3", "8773799e3ed124ce8a81feb935fffbeb3b6621ea2b841c2bbaff4fd09752fd66", [:mix], [], "hexpm", "701a76fea71119fe60aea02f50191bf82a60cb687046931e4d6de6acb8da1685"}, "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "ecto": {:hex, :ecto, "3.8.2", "7b9aca632f9da80ffed525354e4de466a66e042abcbc8509b6b600072c8d8ee0", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "afe2912cc23f61a6a8466c158331d54e0f427029dd97ca936644bc116d6599b3"}, @@ -15,6 +21,7 @@ "esbuild": {:hex, :esbuild, "0.4.0", "9f17db148aead4cf1e6e6a584214357287a93407b5fb51a031f122b61385d4c2", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "b61e4e6b92ffe45e4ee4755a22de6211a67c67987dc02afb35a425a0add1d447"}, "ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.3.3", "61412e524616ea31d3f31675d8bc4c73f277e367dee0ae8245610446f9b778aa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0044f0b6f9ce925666021eafd630de64c2b3404d79c85245cc7c8a9a32d7f104"}, + "ex_json_schema": {:hex, :ex_json_schema, "0.9.2", "c9a42e04e70cd70eb11a8903a22e8ec344df16edef4cb8e6ec84ed0caffc9f0f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4854329cb352b6c01c4c4b8dbfb3be14dc5bea19ea13e0eafade4ff22ba55224"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "floki": {:hex, :floki, "0.32.1", "dfe3b8db3b793939c264e6f785bca01753d17318d144bd44b407fb3493acaa87", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "d4b91c713e4a784a3f7b1e3cc016eefc619f6b1c3898464222867cafd3c681a3"}, "flop": {:hex, :flop, "0.17.0", "2dfd10a25df863cd98a06ba4e98ebcc554df9ca00a8c50e6a82152389c5e37f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "7cf662303a5041c8d8d9ea0330a0d1b5379cf819b6602ce45723733fd8b1d2f3"}, @@ -24,9 +31,13 @@ "icons": {:hex, :icons, "0.9.1", "a205eed3ac7074481043d74c2ddfcb24b107c0507bac7781264f0175e85e7f97", [:mix], [{:phoenix_html, "~> 3.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "ee29ad1cba0747614fc0855d58760d2039455bbaa7a4463ebf84d435e8b4cf97"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "joken": {:hex, :joken, "2.5.0", "09be497d804b8115eb6f07615cef2e60c2a1008fb89dc0aef0d4c4b4609b99aa", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "22b25c89617c5ed8ca7b31026340a25ea0f9ca7160f9706b79be9ed81fdf74e7"}, + "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "nebulex": {:hex, :nebulex, "2.4.1", "d06a1c3380010d6663511e3a630755ff07f7fd7a04fd0a3acd25834b3b296b17", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.0", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "e19e925b70f041e2b3b6068d82ab7613fa5a28e4c94a96dfe16dc37eabe6b3b5"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix": {:hex, :phoenix, "1.6.7", "f1de32418bbbcd471f4fe74d3860ee9c8e8c6c36a0ec173be8ff468a5d72ac90", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b354a4f11d9a2f3a380fb731042dae064f22d7aed8c7e7c024a2459f12994aad"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, @@ -40,8 +51,11 @@ "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, "postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"}, + "puid": {:hex, :puid, "1.1.2", "4acf2a576afc5896c393459d259e733ad20dd96c969152fcddb68f35c1f5ba4a", [:mix], [{:crypto_rand, "~> 1.0", [hex: :crypto_rand, repo: "hexpm", optional: false]}], "hexpm", "fbd1691e29e576c4fbf23852f4d256774702ad1f2a91b37e4344f7c278f1ffaa"}, "qrcode_ex": {:hex, :qrcode_ex, "0.1.1", "8907a7558325babd30f7f43ff85a0169ef65c30820d68e90d792802318f9a062", [:mix], [], "hexpm", "9eb0b397fb3a1c3b16e55b6de6f845a0b4e7b7100ade39eb59fad98fb62455a7"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, + "shards": {:hex, :shards, "1.0.1", "1bdbbf047db27f3c3eb800a829d4a47062c84d5543cbfebcfc4c14d038bf9220", [:make, :rebar3], [], "hexpm", "2c57788afbf053c4024366772892beee89b8b72e884e764fb0a075dfa7442041"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"}, "swoosh": {:hex, :swoosh, "1.6.6", "6018c6f4659ac0b4f30684982993b7812b2bb97436d39f76fcfa8c9e3ae74f85", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e92c7206efd442f08484993676ab072afab2f2bb1e87e604230bb1183c5980de"}, diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 61348d8..951d6db 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -62,4 +62,33 @@ defmodule StoreWeb.ConnCase do |> Phoenix.ConnTest.init_test_session(%{}) |> Plug.Conn.put_session(:user_token, token) end + + def register_admin_and_set_user_token(%{conn: conn}) do + user = Store.AccountsFixtures.admin_fixture() + + create_token_and_return_context(conn, user) + end + + def register_and_set_user_token(%{conn: conn}) do + user = Store.AccountsFixtures.user_fixture() + + create_token_and_return_context(conn, user) + end + + def create_token_and_return_context(conn, user, scopes \\ "public email password") do + {:ok, app} = Store.ApiFixtures.app_fixture(user) + + {:ok, %{token: %Boruta.Oauth.Token{value: token}}} = + Boruta.Oauth.Authorization.token(%Boruta.Oauth.ClientCredentialsRequest{ + client_id: app.client.id, + client_secret: app.client.secret, + scope: scopes + }) + + %{ + conn: conn |> Plug.Conn.put_req_header("authorization", "Bearer #{token}"), + user: user, + token: token + } + end end From d429b50265f214f8743b75d91fc55b7d93dcae65 Mon Sep 17 00:00:00 2001 From: MarioRodrigues10 Date: Thu, 20 Oct 2022 10:34:16 +0100 Subject: [PATCH 2/4] add api logger --- lib/store_web/api_logger.ex | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 lib/store_web/api_logger.ex diff --git a/lib/store_web/api_logger.ex b/lib/store_web/api_logger.ex new file mode 100644 index 0000000..eda8f20 --- /dev/null +++ b/lib/store_web/api_logger.ex @@ -0,0 +1,34 @@ +defmodule StoreWeb.ApiLogger do + @moduledoc """ + Store API Logger + """ + require Logger + + def start_logger do + :telemetry.attach( + "absinthe-query-logger", + [:absinthe, :execute, :operation, :start], + &StoreWeb.ApiLogger.log_query/4, + nil + ) + end + + def log_query(_event, _time, %{blueprint: %{input: raw_query}, options: options}, _) + when is_binary(raw_query) do + case options do + %{context: context} -> + access = + Keyword.get(context, :access, %{ + access_type: "Unknown", + access_identifier: "Unknown" + }) + + Logger.info( + "[Absinthe Operation] Access Type: #{access.access_type} Access Identifier: #{access.access_identifier} Query: #{inspect(raw_query)} " + ) + + _ -> + nil + end + end +end From f65a92fc557e450205f7e057c9d02c3276981e1c Mon Sep 17 00:00:00 2001 From: MarioRodrigues10 Date: Thu, 20 Oct 2022 10:35:07 +0100 Subject: [PATCH 3/4] integrate logger --- lib/store/application.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/store/application.ex b/lib/store/application.ex index d0e9f3e..164d858 100644 --- a/lib/store/application.ex +++ b/lib/store/application.ex @@ -26,6 +26,8 @@ defmodule Store.Application do Supervisor.start_link(children, opts) end + StoreWeb.ApiLogger.start_logger() + # Tell Phoenix to update the endpoint configuration # whenever the application is updated. @impl true From b8a82682f717fc95d72ff915775818c5fb77df0a Mon Sep 17 00:00:00 2001 From: MarioRodrigues10 Date: Thu, 20 Oct 2022 10:40:54 +0100 Subject: [PATCH 4/4] update endpoint --- lib/store_web/endpoint.ex | 9 +++++++++ lib/store_web/plugs/api_context_plug.ex | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/store_web/endpoint.ex b/lib/store_web/endpoint.ex index 7fc19d7..75a320c 100644 --- a/lib/store_web/endpoint.ex +++ b/lib/store_web/endpoint.ex @@ -12,6 +12,15 @@ defmodule StoreWeb.Endpoint do socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]) + + + socket "/api/graph", StoreWeb.GraphApiSocket, + # We can check_origin: false here because the only method of using this connection + # is by having an existing API key you are authorized to use. This allows for devs + # to run third party apps on their own client websites. + websocket: [check_origin: false], + longpoll: false + # Serve at "/" the static files from "priv/static" directory. # # You should set gzip to true if you are running phx.digest diff --git a/lib/store_web/plugs/api_context_plug.ex b/lib/store_web/plugs/api_context_plug.ex index 0222c17..5e1cf67 100644 --- a/lib/store_web/plugs/api_context_plug.ex +++ b/lib/store_web/plugs/api_context_plug.ex @@ -53,7 +53,6 @@ defmodule StoreWeb.Plugs.ApiContextPlug do case Boruta.Ecto.Clients.get_client(client_id) do %Boruta.Oauth.Client{} = client -> Store.Oauth.get_unprivileged_api_access_from_client(client) - Glimesh {:error, msg} -> {:error, msg} end