From dfdd77b773f146a4f55856fdd1cec7789ca94e51 Mon Sep 17 00:00:00 2001
From: TzeYiing
Date: Fri, 18 Oct 2024 23:02:42 +0800
Subject: [PATCH 1/5] feat: initial ingest query scopes implementation
---
lib/logflare/auth.ex | 52 ++++-
lib/logflare/sources.ex | 2 +-
.../plugs/verify_resource_access.ex | 66 +++++++
.../plugs/verify_resource_ownership.ex | 37 ----
lib/logflare_web/live/access_tokens_live.ex | 122 ++++++++++--
lib/logflare_web/router.ex | 8 +-
test/logflare/auth_test.exs | 78 +++++++-
.../live/access_tokens_live_test.exs | 70 ++++++-
.../plugs/verify_api_access_test.exs | 28 ---
.../plugs/verify_resource_access_test.exs | 178 ++++++++++++++++++
.../plugs/verify_resource_ownership_test.exs | 91 ---------
11 files changed, 541 insertions(+), 191 deletions(-)
create mode 100644 lib/logflare_web/controllers/plugs/verify_resource_access.ex
delete mode 100644 lib/logflare_web/controllers/plugs/verify_resource_ownership.ex
create mode 100644 test/logflare_web/plugs/verify_resource_access_test.exs
delete mode 100644 test/logflare_web/plugs/verify_resource_ownership_test.exs
diff --git a/lib/logflare/auth.ex b/lib/logflare/auth.ex
index 211c23c45..b6d0e7ba6 100644
--- a/lib/logflare/auth.ex
+++ b/lib/logflare/auth.ex
@@ -164,25 +164,57 @@ defmodule Logflare.Auth do
resource_owner = Keyword.get(config, :resource_owner)
with {:ok, access_token} <- ExOauth2Provider.authenticate_token(str_token, config),
- token_scopes <- String.split(access_token.scopes || ""),
- :ok <- check_scopes(token_scopes, required_scopes),
+ :ok <- check_scopes(access_token, required_scopes),
owner <- get_resource_owner_by_id(resource_owner, access_token.resource_owner_id) do
{:ok, owner}
- else
- {:scope, false} -> {:error, :unauthorized}
- err -> err
end
end
defp get_resource_owner_by_id(User, id), do: Users.Cache.get(id)
defp get_resource_owner_by_id(Partner, id), do: Partners.Cache.get_partner(id)
- defp check_scopes(token_scopes, required) do
+ @doc """
+ Checks that an access token contains all scopes that are provided in a given required scopes list.
+
+ Private scope will allways return `:ok`
+ iex> check_scopes(access_token, ["private"])
+
+ iex> check_scopes(%OauthAccessToken{scopes: "ingest:source:1"}, ["ingest:source:1"])
+ If multiple scopes are provided, each scope must be in the access token's scope string
+ :ok
+ only can ingest to token-specified scopes
+ iex> check_scopes(%OauthAccessToken{scopes: "ingest:source:1"}, ["ingest"])
+ {:error, :unauthorized}
+
+ if scopes to check for are missing, will be unauthorized
+ iex> check_scopes(%OauthAccessToken{scopes: "ingest"}, ["ingest", "source:1"])
+ {:error, :unauthorized}
+
+ """
+ def check_scopes(%_{} = access_token, required_scopes) when is_list(required_scopes) do
+ token_scopes = String.split(access_token.scopes || "")
+
cond do
- "private" in token_scopes -> :ok
- Enum.empty?(required) -> :ok
- Enum.any?(token_scopes, fn scope -> scope in required end) -> :ok
- true -> {:error, :unauthorized}
+ "private" in token_scopes ->
+ :ok
+
+ Enum.empty?(required_scopes) ->
+ :ok
+
+ # legacy behaviours
+ # empty scope
+ Enum.empty?(token_scopes) and Enum.any?(required_scopes, &String.starts_with?(&1, "ingest")) ->
+ :ok
+
+ # deprecated scope
+ "public" in token_scopes and Enum.any?(required_scopes, &String.starts_with?(&1, "ingest")) ->
+ :ok
+
+ Enum.any?(token_scopes, fn scope -> scope in required_scopes end) ->
+ :ok
+
+ true ->
+ {:error, :unauthorized}
end
end
diff --git a/lib/logflare/sources.ex b/lib/logflare/sources.ex
index 20c30e3d5..e2f65442e 100644
--- a/lib/logflare/sources.ex
+++ b/lib/logflare/sources.ex
@@ -117,7 +117,7 @@ defmodule Logflare.Sources do
get_source_by_token(source_token)
end
- def get(source_id) when is_integer(source_id) do
+ def get(source_id) when is_integer(source_id) or is_binary(source_id) do
Repo.get(Source, source_id)
|> put_retention_days()
end
diff --git a/lib/logflare_web/controllers/plugs/verify_resource_access.ex b/lib/logflare_web/controllers/plugs/verify_resource_access.ex
new file mode 100644
index 000000000..cce3ed196
--- /dev/null
+++ b/lib/logflare_web/controllers/plugs/verify_resource_access.ex
@@ -0,0 +1,66 @@
+defmodule LogflareWeb.Plugs.VerifyResourceAccess do
+ @moduledoc """
+ Plug that checks for ownership of the a provided resource.
+
+ If the `:user` assign is not set, verification is assumed to have passed and as passthroguh is performed.
+ Also checks any API scopes that are set.
+ If no resource is set, performs a passthrough.
+ """
+ alias Logflare.Source
+ alias Logflare.Endpoints.Query
+ alias Logflare.User
+ alias Logflare.Auth
+ alias LogflareWeb.Api.FallbackController
+ def init(_opts), do: nil
+
+ def call(%{assigns: %{endpoint: %Query{enable_auth: false}}} = conn, _opts) do
+ conn
+ end
+
+ # check source
+ def call(
+ %{
+ assigns: %{
+ access_token: access_token,
+ user: %User{id: id},
+ source: %Source{id: source_id, user_id: user_id}
+ }
+ } = conn,
+ _opts
+ )
+ when id == user_id do
+ if :ok == Auth.check_scopes(access_token, ["ingest", "ingest:source:#{source_id}"]) or
+ :ok == Auth.check_scopes(access_token, ["ingest", "ingest:collection:#{source_id}"]) do
+ conn
+ else
+ FallbackController.call(conn, {:error, :unauthorized})
+ end
+ end
+
+ # check endpoint
+ def call(
+ %{
+ assigns: %{
+ access_token: access_token,
+ user: %User{id: id},
+ endpoint: %Query{id: endpoint_id, user_id: user_id}
+ }
+ } = conn,
+ _opts
+ )
+ when id == user_id do
+ if :ok == Auth.check_scopes(access_token, ["query", "query:endpoint:#{endpoint_id}"]) do
+ conn
+ else
+ FallbackController.call(conn, {:error, :unauthorized})
+ end
+ end
+
+ # halts all others
+ def call(%{assigns: assigns} = conn, _) when is_map_key(assigns, :resource_type) do
+ FallbackController.call(conn, {:error, :unauthorized})
+ end
+
+ # no resource is set, passthrough
+ def call(conn, _), do: conn
+end
diff --git a/lib/logflare_web/controllers/plugs/verify_resource_ownership.ex b/lib/logflare_web/controllers/plugs/verify_resource_ownership.ex
deleted file mode 100644
index 96ddb4670..000000000
--- a/lib/logflare_web/controllers/plugs/verify_resource_ownership.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-defmodule LogflareWeb.Plugs.VerifyResourceOwnership do
- @moduledoc """
- Plug that checks for ownership of the a provided resource.
-
- If the `:user` assign is not set, verification is assumed to have passed and as passthroguh is performed.
- If no resource is set, performs a passthrough.
- """
- alias Logflare.Source
- alias Logflare.Endpoints.Query
- alias Logflare.User
- alias LogflareWeb.Api.FallbackController
- def init(_opts), do: nil
-
- def call(%{assigns: %{endpoint: %Query{enable_auth: false}}} = conn, _opts) do
- conn
- end
-
- # check source
- def call(%{assigns: %{user: %User{id: id}, source: %Source{user_id: user_id}}} = conn, _opts)
- when id == user_id do
- conn
- end
-
- # check endpoint
- def call(%{assigns: %{user: %User{id: id}, endpoint: %Query{user_id: user_id}}} = conn, _opts)
- when id == user_id do
- conn
- end
-
- # halts all others
- def call(%{assigns: assigns} = conn, _) when is_map_key(assigns, :resource_type) do
- FallbackController.call(conn, {:error, :unauthorized})
- end
-
- # no resource is set, passthrough
- def call(conn, _), do: conn
-end
diff --git a/lib/logflare_web/live/access_tokens_live.ex b/lib/logflare_web/live/access_tokens_live.ex
index ecc98fd3c..0daed6a52 100644
--- a/lib/logflare_web/live/access_tokens_live.ex
+++ b/lib/logflare_web/live/access_tokens_live.ex
@@ -3,6 +3,8 @@ defmodule LogflareWeb.AccessTokensLive do
use LogflareWeb, :live_view
require Logger
alias Logflare.Auth
+ alias Logflare.Sources
+ alias Logflare.Endpoints
def render(assigns) do
~H"""
@@ -26,27 +28,36 @@ defmodule LogflareWeb.AccessTokensLive do
The X-API-KEY
header method expects the header format X-API-KEY: your-access-token
.
The api_key
query parameter method expects the search format ?api_key=your-access-token
.
- <.form for={%{}} action="#" phx-submit="create-token" class={["mt-4", "jumbotron jumbotron-fluid tw-p-4", if(@show_create_form == false, do: "hidden")]}>
+ <.form for={@create_token_form} action="#" phx-change="update-token-form" phx-submit="create-token" class={["mt-4", "jumbotron jumbotron-fluid tw-p-4", if(@show_create_form == false, do: "hidden")]}>
<%= for %{value: value, description: description} <- [%{
- value: "public",
- description: "For ingestion and endpoints"
+ value: "ingest",
+ description: "For ingestion into a source"
}, %{
+ value: "query",
+ description: "For querying an endpoint"
+ },%{
value: "private",
- description: "For account management"
+ description: "For account management, has all privileges"
}] do %>
-
@@ -101,7 +112,13 @@ defmodule LogflareWeb.AccessTokensLive do
- <%= scope %>
+
+ <%= case scope do
+ "ingest" <> _ -> get_ingest_label(assigns, scope)
+ "query" <> _ -> get_query_label(assigns, scope)
+ scope -> scope
+ end %>
+
|
<%= Calendar.strftime(token.inserted_at, "%d %b %Y, %I:%M:%S %p") %>
@@ -123,14 +140,22 @@ defmodule LogflareWeb.AccessTokensLive do
"""
end
+ @default_create_form %{"description" => "", "scopes" => ["ingest"]}
def mount(_params, %{"user_id" => user_id}, socket) do
user = Logflare.Users.get(user_id)
+ sources = Sources.list_sources_by_user(user)
+ endpoints = Endpoints.list_endpoints_by(user_id: user.id)
socket =
socket
|> assign(:user, user)
|> assign(:show_create_form, false)
|> assign(:created_token, nil)
+ |> assign(:sources, sources)
+ |> assign(:endpoints, endpoints)
+ |> assign(scopes_ingest_sources: %{})
+ |> assign(scopes_query_endpoints: %{})
+ |> assign(create_token_form: @default_create_form)
|> do_refresh()
{:ok, socket}
@@ -147,13 +172,13 @@ defmodule LogflareWeb.AccessTokensLive do
def handle_event(
"create-token",
params,
- %{assigns: %{user: user}} = socket
+ %{assigns: %{user: user, create_token_form: create_token_form}} = socket
) do
Logger.debug(
"Creating access token for user, user_id=#{inspect(user.id)}, params: #{inspect(params)}"
)
- attrs = Map.take(params, ["description", "scopes"])
+ attrs = Map.take(create_token_form, ["description", "scopes"])
{:ok, token} = Auth.create_access_token(user, attrs)
@@ -161,11 +186,34 @@ defmodule LogflareWeb.AccessTokensLive do
socket
|> do_refresh()
|> assign(:show_create_form, false)
+ |> assign(:create_token_form, @default_create_form)
|> assign(:created_token, token)
{:noreply, socket}
end
+ def handle_event(
+ "update-token-form",
+ payload,
+ socket
+ ) do
+ data =
+ Map.drop(payload, ["_csrf_token", "_target"])
+ |> Map.update!("scopes", fn scopes ->
+ if Enum.any?(scopes, &(&1 =~ "ingest:")) do
+ Enum.filter(scopes, &(&1 == "ingest"))
+ else
+ scopes
+ end
+ end)
+
+ merged = Map.merge(socket.assigns.create_token_form, data)
+
+ dbg(merged)
+
+ {:noreply, assign(socket, :create_token_form, merged)}
+ end
+
def handle_event(
"revoke-token",
%{"token-id" => token_id},
@@ -185,8 +233,58 @@ defmodule LogflareWeb.AccessTokensLive do
defp do_refresh(%{assigns: %{user: user}} = socket) do
tokens = user |> Auth.list_valid_access_tokens() |> Enum.sort_by(& &1.inserted_at, :desc)
+ scopes_ingest_sources =
+ for token <- tokens,
+ str_id <- parse_ingest_scope_source_id(token.scopes),
+ source = Sources.get(str_id),
+ into: socket.assigns.scopes_ingest_sources do
+ {str_id, source}
+ end
+
+ scopes_query_endpoints =
+ for token <- tokens,
+ str_id <- parse_query_scope_endpoint_id(token.scopes),
+ endpoint = Endpoints.get_endpoint_query(str_id),
+ into: socket.assigns.scopes_query_endpoints do
+ {str_id, endpoint}
+ end
+
socket
|> assign(access_tokens: tokens)
+ |> assign(scopes_ingest_sources: scopes_ingest_sources)
+ |> assign(scopes_query_endpoints: scopes_query_endpoints)
|> assign(created_token: nil)
end
+
+ # get list of string ids from scopes string
+ defp parse_ingest_scope_source_id(scopes) do
+ Regex.scan(~r/ingest:source:([0-9]+)/, scopes, capture: :all_but_first)
+ |> List.flatten()
+ end
+
+ # get list of string ids from scopes string
+ defp parse_query_scope_endpoint_id(scopes) do
+ Regex.scan(~r/query:endpoint:([0-9]+)/, scopes, capture: :all_but_first)
+ |> List.flatten()
+ end
+
+ defp get_query_label(_assigns, "query"), do: "query"
+
+ defp get_query_label(%{scopes_query_endpoints: endpoint_map}, "query:endpoint:" <> str_id) do
+ if endpoint = Map.get(endpoint_map, str_id) do
+ "query for #{endpoint.name}"
+ else
+ "query for (deleted)"
+ end
+ end
+
+ defp get_ingest_label(_assigns, "ingest"), do: "ingest"
+
+ defp get_ingest_label(%{scopes_ingest_sources: source_map}, "ingest:source:" <> str_id) do
+ if source = Map.get(source_map, str_id) do
+ "ingest for #{source.name}"
+ else
+ "ingest for (deleted)"
+ end
+ end
end
diff --git a/lib/logflare_web/router.ex b/lib/logflare_web/router.ex
index d72ad3d8e..a5ee417d0 100644
--- a/lib/logflare_web/router.ex
+++ b/lib/logflare_web/router.ex
@@ -70,15 +70,15 @@ defmodule LogflareWeb.Router do
end
pipeline :require_endpoint_auth do
- plug(LogflareWeb.Plugs.VerifyApiAccess, scopes: ~w(public))
+ plug(LogflareWeb.Plugs.VerifyApiAccess)
plug(LogflareWeb.Plugs.FetchResource)
- plug(LogflareWeb.Plugs.VerifyResourceOwnership)
+ plug(LogflareWeb.Plugs.VerifyResourceAccess)
end
pipeline :require_ingest_api_auth do
- plug(LogflareWeb.Plugs.VerifyApiAccess, scopes: ~w(public))
+ plug(LogflareWeb.Plugs.VerifyApiAccess)
plug(LogflareWeb.Plugs.FetchResource)
- plug(LogflareWeb.Plugs.VerifyResourceOwnership)
+ plug(LogflareWeb.Plugs.VerifyResourceAccess)
# We are ensuring source start in Logs.ingest
# plug LogflareWeb.Plugs.EnsureSourceStarted
plug(LogflareWeb.Plugs.SetPlanFromCache)
diff --git a/test/logflare/auth_test.exs b/test/logflare/auth_test.exs
index be26155f4..5432c0519 100644
--- a/test/logflare/auth_test.exs
+++ b/test/logflare/auth_test.exs
@@ -41,18 +41,82 @@ defmodule Logflare.AuthTest do
end
end
- test "verify_access_token/2 public scope", %{user: user} do
- # no scope set
+ test "verify_access_token/2 ingest scope", %{user: user} do
+ # no scope set on token, defaults to ingest to any sources
{:ok, key} = Auth.create_access_token(user)
- assert {:ok, _} = Auth.verify_access_token(key.token, ~w(public))
- assert {:ok, _} = Auth.verify_access_token(key.token, "public")
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(ingest))
+ assert {:ok, _} = Auth.verify_access_token(key.token, "ingest")
assert {:ok, _} = Auth.verify_access_token(key.token)
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(ingest source:2))
# scope is set
- {:ok, key} = Auth.create_access_token(user, %{scopes: "public"})
- assert {:ok, _} = Auth.verify_access_token(key.token, ~w(public))
- assert {:ok, _} = Auth.verify_access_token(key.token, "public")
+ {:ok, key} = Auth.create_access_token(user, %{scopes: "ingest"})
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(ingest))
+ assert {:ok, _} = Auth.verify_access_token(key.token, "ingest")
+ assert {:ok, _} = Auth.verify_access_token(key.token)
+
+ # scope to a specific resource
+ # source and collection are resource aliases. i.e. they refer to the same resource.
+ for name <- ["source", "collection"] do
+ {:ok, key} = Auth.create_access_token(user, %{scopes: "ingest:#{name}:3 ingest:#{name}:1"})
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(ingest:#{name}:1))
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(ingest:#{name}:3))
+ end
+ end
+
+ test "verify_access_token/2 query scope", %{user: user} do
+ # no scope set on token
+ {:ok, key} = Auth.create_access_token(user)
+ assert {:error, _} = Auth.verify_access_token(key.token, ~w(query))
+
+ # scope is set on token
+ {:ok, key} = Auth.create_access_token(user, %{scopes: "query"})
+ assert {:error, _} = Auth.verify_access_token(key.token, ~w(ingest))
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(query))
assert {:ok, _} = Auth.verify_access_token(key.token)
+
+ # scope to a specific resource
+ {:ok, key} = Auth.create_access_token(user, %{scopes: "query:endpoint:3 query:endpoint:1"})
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(query:endpoint:1))
+ assert {:ok, _} = Auth.verify_access_token(key.token, ~w(query:endpoint:3))
+ end
+
+ test "check_scopes/2 private scope ", %{user: user} do
+ {:ok, key} = Auth.create_access_token(user, %{scopes: "private"})
+ assert :ok = Auth.check_scopes(key, ~w(query))
+ assert :ok = Auth.check_scopes(key, ~w(ingest))
+ assert :ok = Auth.check_scopes(key, ~w(ingest:endpoint:3))
+ assert :ok = Auth.check_scopes(key, ~w(ingest:source:3))
+ end
+
+ test "check_scopes/2 default empty scopes", %{user: user} do
+ # empty scopes means ingest into any source, the legacy behaviour
+ {:ok, key} = Auth.create_access_token(user, %{scopes: ""})
+ assert :ok = Auth.check_scopes(key, ~w(ingest))
+ assert :ok = Auth.check_scopes(key, ~w(ingest:source:1))
+ end
+
+ test "check_scopes/2 deprecated public scope", %{user: user} do
+ # empty scopes means ingest into any source, the legacy behaviour
+ {:ok, key} = Auth.create_access_token(user, %{scopes: "public"})
+ assert :ok = Auth.check_scopes(key, ~w(ingest))
+ assert :ok = Auth.check_scopes(key, ~w(ingest:source:1))
+ assert {:error, :unauthorized} = Auth.check_scopes(key, ~w(query))
+ assert {:error, :unauthorized} = Auth.check_scopes(key, ~w(private))
+ end
+
+ test "check_scopes/2 matches all required scopes ", %{user: user} do
+ # should only allow ingest into source 1
+ {:ok, key} = Auth.create_access_token(user, %{scopes: "ingest:source:1 ingest:source:4"})
+ assert {:error, _} = Auth.check_scopes(key, ~w(ingest))
+ assert {:error, _} = Auth.check_scopes(key, ~w(ingest:source:3))
+ assert {:error, _} = Auth.check_scopes(key, ~w(query))
+ assert {:error, _} = Auth.check_scopes(key, ~w(query:source:4))
+ assert {:error, _} = Auth.check_scopes(key, ~w(query:source:1))
+
+ assert :ok = Auth.check_scopes(key, ~w(ingest:source:1))
+ assert :ok = Auth.check_scopes(key, ~w(ingest:source:4))
+ assert :ok = Auth.check_scopes(key, ~w(ingest:source:4 ingest:source:1))
end
test "verify_access_token/2 private scope", %{user: user} do
diff --git a/test/logflare_web/live/access_tokens_live_test.exs b/test/logflare_web/live/access_tokens_live_test.exs
index 4ef0a4f44..4e2a5d1d9 100644
--- a/test/logflare_web/live/access_tokens_live_test.exs
+++ b/test/logflare_web/live/access_tokens_live_test.exs
@@ -31,7 +31,7 @@ defmodule LogflareWeb.AccessTokensLiveTest do
assert html =~ "Copy"
end
- test "public token", %{conn: conn, user: user} do
+ test "deprecated: public token", %{conn: conn, user: user} do
token = insert(:access_token, scopes: "public", resource_owner: user)
{:ok, view, _html} = live(conn, ~p"/access-tokens")
html = render(view)
@@ -46,6 +46,55 @@ defmodule LogflareWeb.AccessTokensLiveTest do
assert html =~ "No description"
end
+ test "create token - ingest into any", %{conn: conn} do
+ {:ok, view, _html} = live(conn, ~p"/access-tokens")
+ html = do_ui_create_token(view, "ingest")
+ html = render(view)
+ # able to copy, visible
+ assert view
+ |> element("button", "Copy")
+ |> has_element?()
+
+ assert html =~ "ingest"
+ end
+
+ test "create token - ingest into one source", %{conn: conn, user: user} do
+ source = insert(:source, user: user)
+ {:ok, view, _html} = live(conn, ~p"/access-tokens")
+ html = do_ui_create_token(view, "ingest:source:#{source.id}")
+ # able to copy, visible
+ assert view
+ |> element("button", "Copy")
+ |> has_element?()
+
+ assert html =~ "ingest for #{source.name}"
+ end
+
+ test "create token - query for one endpoint", %{conn: conn, user: user} do
+ endpoint = insert(:endpoint, user: user)
+ {:ok, view, _html} = live(conn, ~p"/access-tokens")
+ html = do_ui_create_token(view, "query:endpoint:#{endpoint.id}")
+
+ assert view
+ |> element("button", "Copy")
+ |> has_element?()
+
+ assert html =~ "query for #{endpoint.name}"
+ end
+
+ test "create ingest token", %{conn: conn} do
+ {:ok, view, _html} = live(conn, ~p"/access-tokens")
+
+ html = do_ui_create_token(view, "ingest")
+
+ assert view
+ |> element("button", "Copy")
+ |> has_element?()
+
+ assert html =~ "some description"
+ assert html =~ "ingest"
+ end
+
test "create private token", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/access-tokens")
@@ -81,4 +130,23 @@ defmodule LogflareWeb.AccessTokensLiveTest do
refute html =~ token.token
assert html =~ "private"
end
+
+ # returns the rendered table html
+ defp do_ui_create_token(view, scopes) do
+ assert view
+ |> element("button", "Create access token")
+ |> render_click()
+
+ assert view |> element("button", "Create") |> has_element?()
+ assert view |> element("label", "Scope") |> has_element?()
+
+ assert view
+ |> element("form")
+ |> render_submit(%{
+ description: "some description",
+ scopes: scopes
+ }) =~ "created successfully"
+
+ view |> element("table") |> render()
+ end
end
diff --git a/test/logflare_web/plugs/verify_api_access_test.exs b/test/logflare_web/plugs/verify_api_access_test.exs
index 3947bf8c7..ead926161 100644
--- a/test/logflare_web/plugs/verify_api_access_test.exs
+++ b/test/logflare_web/plugs/verify_api_access_test.exs
@@ -176,34 +176,6 @@ defmodule LogflareWeb.Plugs.VerifyApiAccessTest do
end
end
- test "public scope", %{user: user} do
- {:ok, access_token} = Logflare.Auth.create_access_token(user, %{scopes: "public"})
-
- build_conn(:get, "/any", %{})
- |> put_req_header("x-api-key", access_token.token)
- |> VerifyApiAccess.call(%{scopes: ~w(public)})
- |> assert_authorized(user)
-
- build_conn(:get, "/any", %{})
- |> put_req_header("x-api-key", access_token.token)
- |> VerifyApiAccess.call(%{scopes: ~w(public)})
- |> assert_authorized(user)
-
- # no scope set
- {:ok, access_token} = Logflare.Auth.create_access_token(user)
-
- build_conn(:get, "/any", %{})
- |> put_req_header("x-api-key", access_token.token)
- |> VerifyApiAccess.call(%{scopes: ~w(public)})
- |> assert_authorized(user)
-
- # user.api_key
- build_conn(:get, "/any", %{})
- |> put_req_header("x-api-key", user.api_key)
- |> VerifyApiAccess.call(%{scopes: ~w(public)})
- |> assert_authorized(user)
- end
-
test "private scope", %{user: user} do
{:ok, access_token} = Logflare.Auth.create_access_token(user, %{scopes: "private"})
diff --git a/test/logflare_web/plugs/verify_resource_access_test.exs b/test/logflare_web/plugs/verify_resource_access_test.exs
new file mode 100644
index 000000000..5828b4af6
--- /dev/null
+++ b/test/logflare_web/plugs/verify_resource_access_test.exs
@@ -0,0 +1,178 @@
+defmodule LogflareWeb.Plugs.VerifyResourceAccessTest do
+ @moduledoc false
+ use LogflareWeb.ConnCase
+ alias LogflareWeb.Plugs.VerifyResourceAccess
+
+ setup do
+ user = insert(:user)
+ endpoint = insert(:endpoint, user: user, enable_auth: true)
+ source = insert(:source, user: user)
+ {:ok, user: user, source: source, endpoint: endpoint}
+ end
+
+ describe "source" do
+ setup %{source: source, user: user} do
+ conn =
+ build_conn(:post, "/logs", %{"source" => Atom.to_string(source.token)})
+ |> assign(:user, user)
+ |> assign(:source, source)
+ |> assign(:resource_type, :source)
+
+ [conn: conn]
+ end
+
+ test "valid - ingest into any", %{conn: initial, user: user} do
+ for scopes <- [
+ "",
+ "public",
+ "ingest"
+ ] do
+ {:ok, access_token} = Logflare.Auth.create_access_token(user, %{scopes: scopes})
+
+ conn =
+ initial
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ refute conn.halted
+ end
+ end
+
+ test "valid - ingest into one source", %{conn: conn, source: source, user: user} do
+ {:ok, access_token} =
+ Logflare.Auth.create_access_token(user, %{scopes: "ingest:source:#{source.id}"})
+
+ conn =
+ conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ refute conn.halted
+ end
+
+ test "invalid - no access token", %{conn: initial_conn, source: source, user: user} do
+ # no access token
+ conn =
+ initial_conn
+ |> assign(:user, insert(:user))
+ |> VerifyResourceAccess.call(%{})
+
+ assert conn.halted
+ assert conn.status == 401
+
+ # invalid scope check
+ {:ok, access_token} =
+ Logflare.Auth.create_access_token(user, %{scopes: "ingest:source:#{source.id + 4}"})
+
+ conn =
+ initial_conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ assert conn.halted
+ assert conn.status == 401
+ end
+
+ test "invalid - specific source", %{conn: initial_conn, source: source, user: user} do
+ {:ok, access_token} =
+ Logflare.Auth.create_access_token(user, %{scopes: "ingest:source:#{source.id + 4}"})
+
+ conn =
+ initial_conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ assert conn.halted and conn.status == 401
+ end
+ end
+
+ describe "endpoints" do
+ setup %{endpoint: endpoint, user: user} do
+ conn =
+ build_conn(:get, "/endpoints/query/#{endpoint.token}", %{"token" => endpoint.token})
+ |> assign(:user, user)
+ |> assign(:resource_type, :endpoint)
+ |> assign(:endpoint, endpoint)
+
+ [conn: conn]
+ end
+
+ test "valid - query one", %{conn: conn, user: user, endpoint: endpoint} do
+ {:ok, access_token} =
+ Logflare.Auth.create_access_token(user, %{scopes: "query:endpoint:#{endpoint.id}"})
+
+ conn =
+ conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ refute conn.halted
+ end
+
+ test "valid - query any", %{conn: conn, user: user} do
+ {:ok, access_token} = Logflare.Auth.create_access_token(user, %{scopes: "query"})
+
+ conn =
+ conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ refute conn.halted
+ end
+
+ test "invalid - wrong scope action", %{conn: conn, user: user} do
+ {:ok, access_token} = Logflare.Auth.create_access_token(user, %{scopes: "ingest"})
+
+ conn =
+ conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ assert conn.halted and conn.status == 401
+ end
+
+ test "invalid - wrong resource scope", %{conn: conn, user: user, endpoint: endpoint} do
+ {:ok, access_token} =
+ Logflare.Auth.create_access_token(user, %{scopes: "query:endpoint:#{endpoint.id + 4}"})
+
+ conn =
+ conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ assert conn.halted and conn.status == 401
+ end
+
+ test "invalid - no scope", %{conn: conn, user: user} do
+ {:ok, access_token} = Logflare.Auth.create_access_token(user, %{scopes: ""})
+
+ conn =
+ conn
+ |> assign(:access_token, access_token)
+ |> VerifyResourceAccess.call(%{})
+
+ assert conn.halted and conn.status == 401
+ end
+ end
+
+ test "no resource provided", %{user: user} do
+ conn =
+ build_conn(:get, "/any", %{})
+ |> assign(:user, user)
+
+ refute conn.halted
+ end
+
+ test "no user/resource provided" do
+ conn = build_conn(:get, "/any", %{})
+ refute conn.halted
+ end
+
+ test "no user provided", %{endpoint: endpoint} do
+ conn =
+ build_conn(:get, "/any", %{})
+ |> assign(:endpoint, endpoint)
+
+ refute conn.halted
+ end
+end
diff --git a/test/logflare_web/plugs/verify_resource_ownership_test.exs b/test/logflare_web/plugs/verify_resource_ownership_test.exs
deleted file mode 100644
index 417cf077d..000000000
--- a/test/logflare_web/plugs/verify_resource_ownership_test.exs
+++ /dev/null
@@ -1,91 +0,0 @@
-defmodule LogflareWeb.Plugs.VerifyResourceOwnershipTest do
- @moduledoc false
- use LogflareWeb.ConnCase
- alias LogflareWeb.Plugs.VerifyResourceOwnership
-
- setup do
- user = insert(:user)
- endpoint = insert(:endpoint, user: user, enable_auth: true)
- source = insert(:source, user: user)
- {:ok, user: user, source: source, endpoint: endpoint}
- end
-
- describe "source" do
- setup %{source: source, user: user} do
- conn =
- build_conn(:post, "/logs", %{"source" => Atom.to_string(source.token)})
- |> assign(:user, user)
- |> assign(:source, source)
- |> assign(:resource_type, :source)
-
- [conn: conn]
- end
-
- test "valid", %{conn: conn} do
- conn = VerifyResourceOwnership.call(conn, %{})
-
- refute conn.halted
- end
-
- test "invalid", %{conn: conn} do
- conn =
- conn
- |> assign(:user, insert(:user))
- |> VerifyResourceOwnership.call(%{})
-
- assert conn.halted
- assert conn.status == 401
- end
- end
-
- describe "endpoints" do
- setup %{endpoint: endpoint, user: user} do
- conn =
- build_conn(:get, "/endpoints/query/#{endpoint.token}", %{"token" => endpoint.token})
- |> assign(:user, user)
- |> assign(:resource_type, :endpoint)
- |> assign(:endpoint, endpoint)
-
- [conn: conn]
- end
-
- test "valid", %{conn: conn} do
- conn =
- conn
- |> VerifyResourceOwnership.call(%{})
-
- refute conn.halted
- end
-
- test "invalid", %{conn: conn} do
- conn =
- conn
- |> assign(:user, insert(:user))
- |> VerifyResourceOwnership.call(%{})
-
- assert conn.halted
- assert conn.status == 401
- end
- end
-
- test "no resource provided", %{user: user} do
- conn =
- build_conn(:get, "/any", %{})
- |> assign(:user, user)
-
- refute conn.halted
- end
-
- test "no user/resource provided" do
- conn = build_conn(:get, "/any", %{})
- refute conn.halted
- end
-
- test "no user provided", %{endpoint: endpoint} do
- conn =
- build_conn(:get, "/any", %{})
- |> assign(:endpoint, endpoint)
-
- refute conn.halted
- end
-end
From 90bbf1c5069a5ff41c8d7cd31460a88b178ca3a2 Mon Sep 17 00:00:00 2001
From: TzeYiing
Date: Fri, 8 Nov 2024 03:18:32 +0800
Subject: [PATCH 2/5] feat: ui adjustments
---
lib/logflare_web/live/access_tokens_live.ex | 82 +++++++++++++------
.../live/access_tokens_live_test.exs | 38 ++++-----
2 files changed, 71 insertions(+), 49 deletions(-)
diff --git a/lib/logflare_web/live/access_tokens_live.ex b/lib/logflare_web/live/access_tokens_live.ex
index 0daed6a52..fb2555060 100644
--- a/lib/logflare_web/live/access_tokens_live.ex
+++ b/lib/logflare_web/live/access_tokens_live.ex
@@ -40,22 +40,26 @@ defmodule LogflareWeb.AccessTokensLive do
<%= for %{value: value, description: description} <- [%{
value: "ingest",
- description: "For ingestion into a source"
+ description: "For ingestion into a source. Allows ingest into all sources if no specific source is selected."
}, %{
value: "query",
- description: "For querying an endpoint"
+ description: "For querying an endpoint. Allows querying of all endpoints if no specific endpoint is selected"
},%{
value: "private",
description: "For account management, has all privileges"
}] do %>
-
-
@@ -112,7 +116,7 @@ defmodule LogflareWeb.AccessTokensLive do
|
-
+
<%= case scope do
"ingest" <> _ -> get_ingest_label(assigns, scope)
"query" <> _ -> get_query_label(assigns, scope)
@@ -125,7 +129,7 @@ defmodule LogflareWeb.AccessTokensLive do
|
- |