diff --git a/docs/docs.logflare.com/docs/concepts/access-tokens/index.md b/docs/docs.logflare.com/docs/concepts/access-tokens/index.md
index f33044cd7..7f2257c2d 100644
--- a/docs/docs.logflare.com/docs/concepts/access-tokens/index.md
+++ b/docs/docs.logflare.com/docs/concepts/access-tokens/index.md
@@ -36,9 +36,9 @@ There are 3 supported methods to attach an accees token with an API request:
## Client-side is Public
-Access tokens are considered public for client-side usage. Public-only tokens will not be able to access the Logflare Management API, which provides capabilities to manage user resources on Logflare.
+Access tokens can be exposed for client-side usage. Consider restricting tokens to ingest into specific sources or to considered public for client-side usage. Public-only tokens will not be able to access the Logflare Management API beyond the ingest/query APIs.
-Please use a separate private access token for the Management API.
+Private access tokens should not be exposed to the client side, as private access tokens have complete access to all management APIs used for account control.
## Rotation
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..3dc431bc6
--- /dev/null
+++ b/lib/logflare_web/controllers/plugs/verify_resource_access.ex
@@ -0,0 +1,82 @@
+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:
+ %{
+ user: %User{id: id},
+ source: %Source{id: source_id, user_id: user_id}
+ } = assigns
+ } = conn,
+ _opts
+ )
+ when id == user_id do
+ access_token = Map.get(assigns, :access_token)
+
+ # legacy api key
+ cond do
+ access_token == nil ->
+ conn
+
+ :ok == Auth.check_scopes(access_token, ["ingest", "ingest:source:#{source_id}"]) or
+ :ok == Auth.check_scopes(access_token, ["ingest", "ingest:collection:#{source_id}"]) ->
+ conn
+
+ true ->
+ FallbackController.call(conn, {:error, :unauthorized})
+ end
+ end
+
+ # check endpoint
+ def call(
+ %{
+ assigns:
+ %{
+ user: %User{id: id},
+ endpoint: %Query{id: endpoint_id, user_id: user_id}
+ } = assigns
+ } = conn,
+ _opts
+ )
+ when id == user_id do
+ access_token = Map.get(assigns, :access_token)
+
+ cond do
+ # legacy api key
+ access_token == nil ->
+ conn
+
+ :ok == Auth.check_scopes(access_token, ["query", "query:endpoint:#{endpoint_id}"]) ->
+ conn
+
+ true ->
+ 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..305022491 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,40 @@ 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.
-
+
A short description for identifying what this access token is to be used for.
<%= for %{value: value, description: description} <- [%{
- value: "public",
- description: "For ingestion and endpoints"
+ value: "ingest",
+ description: "For ingestion into a source. Allows ingest into all sources if no specific source is selected."
}, %{
+ value: "query",
+ description: "For querying an endpoint. Allows querying of all endpoints if no specific endpoint is selected"
+ },%{
value: "private",
- description: "For account management"
+ description: "For account management, has all privileges"
}] do %>
-
-
-
- <%= description %>
+
+
+
<% end %>
@@ -101,14 +116,20 @@ defmodule LogflareWeb.AccessTokensLive do