Skip to content

Commit 23c0b69

Browse files
authored
feat: managed service accounts for BYOB users (#2508)
* feat: managed service accounts for BYOB users * chore: update staging and prod secrets with managed SA count * fix: add handling of when system is enabled but byob is disabled * chore: fix failing tests * chore: simplify logic, formatting * fix: update function head to match :query * chore: version bump * chore: fix failing test * feat: add iam policy updating on config change
1 parent bac028e commit 23c0b69

File tree

20 files changed

+479
-39
lines changed

20 files changed

+479
-39
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.16.0
1+
1.16.1

cloudbuild/.prod.env.enc

37 Bytes
Binary file not shown.

cloudbuild/.staging.env.enc

35 Bytes
Binary file not shown.

docs/docs.logflare.com/docs/backends/bigquery/index.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ Assign `BigQuery Data Owner` and `BigQuery Job User` permissions to the Logflare
9898

9999
![BigQuery Job User Permissions](./bq-job-user-permissions.png)
100100

101+
:::note
102+
If using [managed service accounts](#managed-service-accounts), include the following additional roles:
103+
104+
- `roles/resourcemanager.projectIamAdmin`
105+
- `roles/iam.serviceAccountCreator`
106+
- `roles/iam.serviceAccountTokenCreator`
107+
108+
:::
109+
101110
#### Step 3: Update Account Settings in Logflare
102111

103112
Find the GCP project ID in the [dashboard](https://console.cloud.google.com/home/dashboard)
@@ -118,6 +127,24 @@ You can also optionally update your sources' TTL to tweak how long you want to r
118127
The steps for setting up self-hosted Logflare requires different BigQuery configurations, please refer to the [self-hosting](/self-hosting) documentation for more details.
119128
:::
120129

130+
### Managed Service Accounts
131+
132+
When query volume for your instance is high, you may experience BigQuery rate limiting for their REST API. This occurs when BigQuery receives over 100 requests per second per user. This adversely affects the Logflare Endpoints functionality as well as Logflare Search functionality.
133+
134+
By default, all BYOB users have managed service accounts disabled. In order to enable routing requests through mangaged service accounts, go to **Accounts > BigQuery Backend > Managed Service Accounts** and enable the setting by ticking the checkbox.
135+
136+
Ensure that the Logflare service account has the following roles:
137+
138+
- `roles/bigquery.admin` **(no change)**
139+
- `roles/resourcemanager.projectIamAdmin` **(new)** - used to set the IAM policy with the managed service accounts
140+
- `roles/iam.serviceAccountCreator` **(new)** - used to create new managed service accounts
141+
- `roles/iam.serviceAccountTokenCreator` **(new)** - used to generate short-lived tokens for authenticating with BigQuery REST API
142+
143+
Enabling this option will have the following effects:
144+
145+
- BigQuery REST API requests made will be routed through multiple managed service accounts. Each managed service account has the `logflare-managed-` prefix followed by the index (for example `logflare-managed-0`).
146+
- IAM policy for this project will be completely managed by Logflare, and permissions for managed service accounts will be handled in a non-destructive manner.
147+
121148
## Querying in BigQuery
122149

123150
You can directly execute SQL queries in BigQuery instead of through the Logflare UI. This would be helpful for generating reports that require aggregations, or to perform queries across multiple BigQuery tables.

lib/logflare/backends/adaptor/bigquery_adaptor.ex

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule Logflare.Backends.Adaptor.BigQueryAdaptor do
1414
alias Logflare.Backends
1515
alias Logflare.Google.BigQuery.GenUtils
1616
alias Logflare.Google.CloudResourceManager
17+
alias Logflare.Google
1718
use Supervisor
1819
require Logger
1920

@@ -161,6 +162,14 @@ defmodule Logflare.Backends.Adaptor.BigQueryAdaptor do
161162
"#{@service_account_prefix}-#{service_account_index}"
162163
end
163164

165+
@doc """
166+
Returns a list of all managed service account ids
167+
"""
168+
def managed_service_account_ids() do
169+
for i <- 0..(managed_service_account_pool_size() - 1),
170+
do: managed_service_account_id(i)
171+
end
172+
164173
@doc """
165174
Lists all managed service accounts.
166175
@@ -395,7 +404,19 @@ defmodule Logflare.Backends.Adaptor.BigQueryAdaptor do
395404
iex> update_iam_policy()
396405
:ok
397406
"""
398-
def update_iam_policy() do
407+
def update_iam_policy(user \\ nil) do
399408
CloudResourceManager.set_iam_policy(async: false)
409+
410+
if Map.get(user || %{}, :bigquery_project_id) do
411+
# byob project, maybe append managed SA to policy
412+
append_managed_sa_to_iam_policy(user) |> dbg()
413+
end
400414
end
415+
416+
defdelegate get_iam_policy(user), to: CloudResourceManager
417+
defdelegate append_managed_sa_to_iam_policy(user), to: CloudResourceManager
418+
defdelegate append_managed_service_accounts(project_id, policy), to: CloudResourceManager
419+
defdelegate patch_dataset_access(user), to: Google.BigQuery
420+
421+
defdelegate get_conn(conn_type), to: GenUtils
401422
end

lib/logflare/ecto/bigquery/bq_repo.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ defmodule Logflare.BqRepo do
5252
|> then(fn map -> struct(QueryRequest, map) end)
5353

5454
result =
55-
GenUtils.get_conn(:query)
55+
GenUtils.get_conn({:query, user})
5656
|> Api.Jobs.bigquery_jobs_query(project_id, body: query_request)
5757
|> GenUtils.maybe_parse_google_api_result()
5858

lib/logflare/google/bigquery/gen_utils/gen_utils.ex

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,30 @@ defmodule Logflare.Google.BigQuery.GenUtils do
7878
7979
Uses `Logflare.FinchDefault` by default
8080
"""
81-
@typep conn_type :: :ingest | :query | :default
81+
@typep conn_type :: :ingest | {:query, User.t()} | :default
8282
@spec get_conn(conn_type()) :: Tesla.Env.client()
8383
def get_conn(conn_type \\ :default) do
84+
system_managed_sa_enabled = BigQueryAdaptor.managed_service_accounts_enabled?()
8485
# use pid as the partition hash
85-
partition_count =
86-
if conn_type == :query do
87-
BigQueryAdaptor.managed_service_account_partition_count()
88-
else
89-
BigQueryAdaptor.ingest_service_account_partition_count()
86+
{use_managed_sa?, partition_count} =
87+
case conn_type do
88+
{:query,
89+
%_{bigquery_project_id: project_id, bigquery_enable_managed_service_accounts: true}}
90+
when system_managed_sa_enabled == true and project_id != nil ->
91+
{true, BigQueryAdaptor.managed_service_account_partition_count()}
92+
93+
{:query, %_{bigquery_project_id: nil}}
94+
when system_managed_sa_enabled == true ->
95+
{true, BigQueryAdaptor.managed_service_account_partition_count()}
96+
97+
_ ->
98+
{false, BigQueryAdaptor.ingest_service_account_partition_count()}
9099
end
91100

92101
partition = :erlang.phash2(self(), partition_count)
93102

94103
{name, metadata} =
95-
if conn_type == :query && BigQueryAdaptor.managed_service_accounts_enabled?() do
104+
if use_managed_sa? == true do
96105
pool_size = BigQueryAdaptor.managed_service_account_pool_size()
97106

98107
sa_index = :erlang.phash2(self(), pool_size)
@@ -144,7 +153,7 @@ defmodule Logflare.Google.BigQuery.GenUtils do
144153
).adapter
145154
end
146155

147-
defp build_tesla_adapter_call(:query) do
156+
defp build_tesla_adapter_call({:query, _}) do
148157
Tesla.client(
149158
[],
150159
{Tesla.Adapter.Finch, name: Logflare.FinchQuery, receive_timeout: 60_000}

lib/logflare/google/resource_manager.ex

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Logflare.Google.CloudResourceManager do
66
alias GoogleApi.CloudResourceManager.V1.Model
77
alias Logflare.Google.BigQuery.GenUtils
88
alias Logflare.Users
9+
alias Logflare.User
910
alias Logflare.TeamUsers
1011
alias Logflare.Utils.Tasks
1112
alias Logflare.Backends.Adaptor.BigQueryAdaptor
@@ -27,6 +28,80 @@ defmodule Logflare.Google.CloudResourceManager do
2728
)
2829
end
2930

31+
def get_iam_policy(%User{bigquery_project_id: nil}), do: {:error, :no_project_id}
32+
33+
def get_iam_policy(user) do
34+
conn = GenUtils.get_conn()
35+
36+
Api.Projects.cloudresourcemanager_projects_get_iam_policy(
37+
conn,
38+
user.bigquery_project_id,
39+
body: %Model.GetIamPolicyRequest{}
40+
)
41+
end
42+
43+
def append_managed_sa_to_iam_policy(%User{bigquery_project_id: nil}),
44+
do: {:error, :no_project_id}
45+
46+
def append_managed_sa_to_iam_policy(%User{bigquery_enable_managed_service_accounts: false}),
47+
do: {:error, :managed_service_accounts_disabled}
48+
49+
def append_managed_sa_to_iam_policy(user) do
50+
with {:enabled?, true} <- {:enabled?, BigQueryAdaptor.managed_service_accounts_enabled?()},
51+
{:ok, policy} <- get_iam_policy(user),
52+
{:contains?, _policy, false} <-
53+
{:contains?, policy, contains_managed_service_accounts?(policy)} do
54+
append_managed_service_accounts(user.bigquery_project_id, policy)
55+
else
56+
{:contains?, policy, _} -> {:ok, policy}
57+
{:enabled?, false} -> {:error, :managed_service_accounts_disabled}
58+
{:error, _err} = err -> err
59+
end
60+
end
61+
62+
# returns false if missing any of the managed service accounts
63+
defp contains_managed_service_accounts?(%Model.Policy{bindings: bindings}) do
64+
ids = BigQueryAdaptor.managed_service_account_ids()
65+
members = Enum.flat_map(bindings, fn %{members: members} -> members end)
66+
67+
Enum.all?(ids, fn id ->
68+
Enum.any?(members, fn member ->
69+
member =~ id
70+
end)
71+
end)
72+
end
73+
74+
def append_managed_service_accounts(project_id, %Model.Policy{bindings: bindings})
75+
when project_id != nil do
76+
conn = GenUtils.get_conn()
77+
78+
members =
79+
for %{email: name} <- BigQueryAdaptor.list_managed_service_accounts() do
80+
"serviceAccount:" <> name
81+
end
82+
83+
new_binding = %Model.Binding{members: members, role: "roles/bigquery.admin"}
84+
85+
policy = %Model.Policy{bindings: [new_binding | bindings]}
86+
body = %Model.SetIamPolicyRequest{policy: policy}
87+
88+
case Api.Projects.cloudresourcemanager_projects_set_iam_policy(conn, project_id, body: body) do
89+
{:ok, response} ->
90+
Logger.info(
91+
"Appended managed service accounts to IAM policy for #{project_id} successful"
92+
)
93+
94+
{:ok, response}
95+
96+
{:error, err} ->
97+
Logger.error(
98+
"Append managed service accounts to IAM policy for #{project_id} unknown error: #{inspect(err)}"
99+
)
100+
101+
{:error, err}
102+
end
103+
end
104+
30105
def set_iam_policy(opts \\ [async: true])
31106

32107
def set_iam_policy(async: true) do

lib/logflare/user.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ defmodule Logflare.User do
7878
field :bigquery_dataset_id, :string
7979
field :bigquery_udfs_hash, :string
8080
field :bigquery_processed_bytes_limit, :integer
81+
field :bigquery_enable_managed_service_accounts, :boolean, default: false
8182
field :api_quota, :integer, default: @default_user_api_quota
8283
field :valid_google_account, :boolean
8384
field :provider_uid, :string
@@ -113,6 +114,7 @@ defmodule Logflare.User do
113114
:bigquery_dataset_location,
114115
:bigquery_dataset_id,
115116
:bigquery_processed_bytes_limit,
117+
:bigquery_enable_managed_service_accounts,
116118
:valid_google_account,
117119
:provider_uid,
118120
:company,

lib/logflare/users/users.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Logflare.Users do
33

44
import Ecto.Query
55
alias Logflare.Google.BigQuery
6-
alias Logflare.Google.CloudResourceManager
6+
alias Logflare.Backends.Adaptor.BigQueryAdaptor
77
alias Logflare.Repo
88
alias Logflare.Source.Supervisor
99
alias Logflare.Sources
@@ -219,7 +219,7 @@ defmodule Logflare.Users do
219219
case Repo.delete(user) do
220220
{:ok, _user} = response ->
221221
BigQuery.delete_dataset(user)
222-
CloudResourceManager.set_iam_policy()
222+
BigQueryAdaptor.update_iam_policy()
223223
response
224224

225225
{:error, _reason} = response ->

0 commit comments

Comments
 (0)