Skip to content

Commit 94afa9b

Browse files
committed
feat: add DescribeMany and remove role assignment from service_account service
1 parent 8b1d24f commit 94afa9b

File tree

7 files changed

+384
-35
lines changed

7 files changed

+384
-35
lines changed

guard/lib/guard/grpc_servers/service_account_server.ex

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ defmodule Guard.GrpcServers.ServiceAccountServer do
1818
org_id: org_id,
1919
name: name,
2020
description: description,
21-
creator_id: creator_id,
22-
role_id: role_id
21+
creator_id: creator_id
2322
},
2423
_stream
2524
) do
@@ -29,13 +28,11 @@ defmodule Guard.GrpcServers.ServiceAccountServer do
2928
org_id: org_id,
3029
name: name,
3130
description: description,
32-
creator_id: creator_id,
33-
role_id: role_id
31+
creator_id: creator_id
3432
},
3533
fn ->
3634
validate_uuid!(org_id)
3735
validate_uuid!(creator_id)
38-
validate_uuid!(role_id)
3936

4037
if String.trim(name) == "" do
4138
grpc_error!(:invalid_argument, "Service account name cannot be empty")
@@ -50,8 +47,7 @@ defmodule Guard.GrpcServers.ServiceAccountServer do
5047
org_id: org_id,
5148
name: String.trim(name),
5249
description: String.trim(description || ""),
53-
creator_id: creator_id,
54-
role_id: role_id
50+
creator_id: creator_id
5551
}
5652

5753
case Guard.ServiceAccount.Actions.create(params) do
@@ -136,6 +132,30 @@ defmodule Guard.GrpcServers.ServiceAccountServer do
136132
)
137133
end
138134

135+
@spec describe_many(ServiceAccountPB.DescribeManyRequest.t(), GRPC.Server.Stream.t()) ::
136+
ServiceAccountPB.DescribeManyResponse.t()
137+
def describe_many(%ServiceAccountPB.DescribeManyRequest{sa_ids: sa_ids}, _stream) do
138+
observe_and_log(
139+
"grpc.service_account.describe_many",
140+
%{sa_ids: sa_ids, count: length(sa_ids)},
141+
fn ->
142+
# Validate all UUIDs
143+
Enum.each(sa_ids, &validate_uuid!/1)
144+
145+
case ServiceAccount.find_many(sa_ids) do
146+
{:ok, service_accounts} ->
147+
ServiceAccountPB.DescribeManyResponse.new(
148+
service_accounts: Enum.map(service_accounts, &map_service_account/1)
149+
)
150+
151+
{:error, reason} ->
152+
Logger.error("Failed to describe many service accounts: #{inspect(reason)}")
153+
grpc_error!(:internal, "Failed to describe service accounts")
154+
end
155+
end
156+
)
157+
end
158+
139159
@spec update(ServiceAccountPB.UpdateRequest.t(), GRPC.Server.Stream.t()) ::
140160
ServiceAccountPB.UpdateResponse.t()
141161
def update(

guard/lib/guard/service_account/actions.ex

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ defmodule Guard.ServiceAccount.Actions do
1616
org_id: String.t(),
1717
name: String.t(),
1818
description: String.t(),
19-
creator_id: String.t(),
20-
role_id: String.t()
19+
creator_id: String.t()
2120
}
2221

2322
@doc """
@@ -29,7 +28,6 @@ defmodule Guard.ServiceAccount.Actions do
2928
3. Creates a service_account record
3029
4. Generates an API token
3130
5. Creates RBAC user record
32-
6. Assigns the specified role to the service account
3331
7. Publishes UserCreated event
3432
3533
Returns {:ok, %{service_account: service_account, api_token: api_token}} or {:error, reason}
@@ -41,8 +39,7 @@ defmodule Guard.ServiceAccount.Actions do
4139
org_id: _org_id,
4240
name: _name,
4341
description: _description,
44-
creator_id: _creator_id,
45-
role_id: _role_id
42+
creator_id: _creator_id
4643
} = params
4744
) do
4845
case _create(params) do
@@ -165,13 +162,7 @@ defmodule Guard.ServiceAccount.Actions do
165162
defp _create(params) do
166163
FrontRepo.transaction(fn ->
167164
with {:ok, result} <- ServiceAccount.create(params),
168-
{:ok, _rbac_user} <- create_rbac_user(result.service_account),
169-
:ok <-
170-
Guard.Api.Rbac.assign_role(
171-
result.service_account.org_id,
172-
result.service_account.id,
173-
params.role_id
174-
) do
165+
{:ok, _rbac_user} <- create_rbac_user(result.service_account) do
175166
# Return the service account data and API token
176167
{result.service_account, result.api_token}
177168
else

guard/lib/guard/store/service_account.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ defmodule Guard.Store.ServiceAccount do
3838
end
3939
end
4040

41+
@doc """
42+
Find multiple service accounts by IDs.
43+
44+
Returns a list of service accounts for the given IDs.
45+
Invalid or non-existent IDs are filtered out.
46+
"""
47+
@spec find_many([String.t()]) :: {:ok, [map()]} | {:error, term()}
48+
def find_many(service_account_ids) when is_list(service_account_ids) do
49+
# Filter out invalid UUIDs
50+
valid_ids = Enum.filter(service_account_ids, &valid_uuid?/1)
51+
52+
if length(valid_ids) > 0 do
53+
query =
54+
build_service_account_query()
55+
|> where([sa, u], sa.id in ^valid_ids)
56+
|> where([sa, u], is_nil(u.blocked_at))
57+
|> order_by([sa, u], asc: u.created_at, asc: sa.id)
58+
59+
service_accounts = FrontRepo.all(query)
60+
{:ok, service_accounts}
61+
else
62+
{:ok, []}
63+
end
64+
rescue
65+
e ->
66+
Logger.error("Error during find_many for service accounts #{inspect(service_account_ids)}: #{inspect(e)}")
67+
{:error, :internal_error}
68+
end
69+
4170
@doc """
4271
Find service accounts by organization with pagination.
4372

guard/test/guard/front_repo/service_account_test.exs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,16 @@ defmodule Guard.FrontRepo.ServiceAccountTest do
319319
# Helper functions
320320

321321
defp create_test_user do
322+
# Generate a unique suffix using current timestamp in microseconds + random number
323+
# This ensures better test isolation by making each user truly unique
324+
timestamp = System.system_time(:microsecond)
325+
random_suffix = :rand.uniform(999_999)
326+
unique_suffix = "#{timestamp}-#{random_suffix}"
327+
322328
user_attrs = %{
323329
id: Ecto.UUID.generate(),
324-
email: "test-#{:rand.uniform(10000)}@example.com",
325-
name: "Test User",
330+
email: "test-#{unique_suffix}@example.com",
331+
name: "Test User #{unique_suffix}",
326332
org_id: Ecto.UUID.generate(),
327333
creation_source: :service_account,
328334
single_org_user: true,

guard/test/guard/grpc_servers/service_account_server_test.exs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,232 @@ defmodule Guard.GrpcServers.ServiceAccountServerTest do
382382
end
383383
end
384384

385+
describe "describe_many/2" do
386+
test "describes multiple service accounts successfully", %{grpc_channel: channel} do
387+
sa1_id = Ecto.UUID.generate()
388+
sa2_id = Ecto.UUID.generate()
389+
sa3_id = Ecto.UUID.generate()
390+
391+
with_mocks([
392+
{Guard.Utils, [:passthrough], [validate_uuid!: fn _ -> :ok end]},
393+
{Guard.Store.ServiceAccount, [:passthrough],
394+
[
395+
find_many: fn ids ->
396+
assert length(ids) == 3
397+
assert sa1_id in ids
398+
assert sa2_id in ids
399+
assert sa3_id in ids
400+
401+
{:ok,
402+
[
403+
%{
404+
id: sa1_id,
405+
name: "Service Account 1",
406+
description: "Description 1",
407+
org_id: "org-id-1",
408+
creator_id: "creator-1",
409+
created_at: DateTime.utc_now(),
410+
updated_at: DateTime.utc_now(),
411+
deactivated: false
412+
},
413+
%{
414+
id: sa2_id,
415+
name: "Service Account 2",
416+
description: "Description 2",
417+
org_id: "org-id-2",
418+
creator_id: "creator-2",
419+
created_at: DateTime.utc_now(),
420+
updated_at: DateTime.utc_now(),
421+
deactivated: true
422+
},
423+
%{
424+
id: sa3_id,
425+
name: "Service Account 3",
426+
description: nil,
427+
org_id: "org-id-3",
428+
creator_id: nil,
429+
created_at: DateTime.utc_now(),
430+
updated_at: DateTime.utc_now(),
431+
deactivated: false
432+
}
433+
]}
434+
end
435+
]}
436+
]) do
437+
request = ServiceAccount.DescribeManyRequest.new(sa_ids: [sa1_id, sa2_id, sa3_id])
438+
439+
{:ok, response} = channel |> Stub.describe_many(request)
440+
441+
assert length(response.service_accounts) == 3
442+
443+
# Verify first service account
444+
sa1 = Enum.find(response.service_accounts, &(&1.id == sa1_id))
445+
assert sa1.name == "Service Account 1"
446+
assert sa1.description == "Description 1"
447+
assert sa1.org_id == "org-id-1"
448+
assert sa1.creator_id == "creator-1"
449+
assert sa1.deactivated == false
450+
451+
# Verify second service account (deactivated)
452+
sa2 = Enum.find(response.service_accounts, &(&1.id == sa2_id))
453+
assert sa2.name == "Service Account 2"
454+
assert sa2.deactivated == true
455+
456+
# Verify third service account (nil handling)
457+
sa3 = Enum.find(response.service_accounts, &(&1.id == sa3_id))
458+
assert sa3.name == "Service Account 3"
459+
assert sa3.description == ""
460+
assert sa3.creator_id == ""
461+
assert sa3.deactivated == false
462+
end
463+
end
464+
465+
test "returns empty list when no service accounts found", %{grpc_channel: channel} do
466+
sa1_id = Ecto.UUID.generate()
467+
sa2_id = Ecto.UUID.generate()
468+
469+
with_mocks([
470+
{Guard.Utils, [:passthrough], [validate_uuid!: fn _ -> :ok end]},
471+
{Guard.Store.ServiceAccount, [:passthrough],
472+
[
473+
find_many: fn ids ->
474+
assert length(ids) == 2
475+
{:ok, []}
476+
end
477+
]}
478+
]) do
479+
request = ServiceAccount.DescribeManyRequest.new(sa_ids: [sa1_id, sa2_id])
480+
481+
{:ok, response} = channel |> Stub.describe_many(request)
482+
483+
assert response.service_accounts == []
484+
end
485+
end
486+
487+
test "handles partial matches correctly", %{grpc_channel: channel} do
488+
existing_id = Ecto.UUID.generate()
489+
non_existent_id = Ecto.UUID.generate()
490+
491+
with_mocks([
492+
{Guard.Utils, [:passthrough], [validate_uuid!: fn _ -> :ok end]},
493+
{Guard.Store.ServiceAccount, [:passthrough],
494+
[
495+
find_many: fn ids ->
496+
assert length(ids) == 2
497+
assert existing_id in ids
498+
assert non_existent_id in ids
499+
500+
# Return only the existing one
501+
{:ok,
502+
[
503+
%{
504+
id: existing_id,
505+
name: "Existing SA",
506+
description: "Exists",
507+
org_id: "org-id",
508+
creator_id: "creator-id",
509+
created_at: DateTime.utc_now(),
510+
updated_at: DateTime.utc_now(),
511+
deactivated: false
512+
}
513+
]}
514+
end
515+
]}
516+
]) do
517+
request = ServiceAccount.DescribeManyRequest.new(sa_ids: [existing_id, non_existent_id])
518+
519+
{:ok, response} = channel |> Stub.describe_many(request)
520+
521+
assert length(response.service_accounts) == 1
522+
assert hd(response.service_accounts).id == existing_id
523+
end
524+
end
525+
526+
test "handles empty input list", %{grpc_channel: channel} do
527+
with_mocks([
528+
{Guard.Store.ServiceAccount, [:passthrough],
529+
[
530+
find_many: fn ids ->
531+
assert ids == []
532+
{:ok, []}
533+
end
534+
]}
535+
]) do
536+
request = ServiceAccount.DescribeManyRequest.new(sa_ids: [])
537+
538+
{:ok, response} = channel |> Stub.describe_many(request)
539+
540+
assert response.service_accounts == []
541+
end
542+
end
543+
544+
test "validates UUID format for all IDs", %{grpc_channel: channel} do
545+
valid_id = Ecto.UUID.generate()
546+
547+
request = ServiceAccount.DescribeManyRequest.new(sa_ids: [valid_id, "invalid-uuid"])
548+
549+
{:error, %GRPC.RPCError{status: 3}} = channel |> Stub.describe_many(request)
550+
end
551+
552+
test "handles internal errors", %{grpc_channel: channel} do
553+
sa_id = Ecto.UUID.generate()
554+
555+
with_mocks([
556+
{Guard.Utils, [:passthrough], [validate_uuid!: fn _ -> :ok end]},
557+
{Guard.Store.ServiceAccount, [:passthrough],
558+
[
559+
find_many: fn _ -> {:error, :database_error} end
560+
]}
561+
]) do
562+
request = ServiceAccount.DescribeManyRequest.new(sa_ids: [sa_id])
563+
564+
{:error, %GRPC.RPCError{status: 13, message: message}} =
565+
channel |> Stub.describe_many(request)
566+
567+
assert String.contains?(message, "Failed to describe service accounts")
568+
end
569+
end
570+
571+
test "handles large number of IDs", %{grpc_channel: channel} do
572+
# Generate 50 IDs to test batch processing
573+
ids = for _ <- 1..50, do: Ecto.UUID.generate()
574+
575+
with_mocks([
576+
{Guard.Utils, [:passthrough], [validate_uuid!: fn _ -> :ok end]},
577+
{Guard.Store.ServiceAccount, [:passthrough],
578+
[
579+
find_many: fn received_ids ->
580+
assert length(received_ids) == 50
581+
# Return only the first 10 to simulate partial results
582+
service_accounts =
583+
received_ids
584+
|> Enum.take(10)
585+
|> Enum.map(fn id ->
586+
%{
587+
id: id,
588+
name: "SA #{id}",
589+
description: "Description",
590+
org_id: "org-id",
591+
creator_id: "creator-id",
592+
created_at: DateTime.utc_now(),
593+
updated_at: DateTime.utc_now(),
594+
deactivated: false
595+
}
596+
end)
597+
598+
{:ok, service_accounts}
599+
end
600+
]}
601+
]) do
602+
request = ServiceAccount.DescribeManyRequest.new(sa_ids: ids)
603+
604+
{:ok, response} = channel |> Stub.describe_many(request)
605+
606+
assert length(response.service_accounts) == 10
607+
end
608+
end
609+
end
610+
385611
describe "update/2" do
386612
test "updates service account successfully", %{grpc_channel: channel} do
387613
service_account_id = Ecto.UUID.generate()

0 commit comments

Comments
 (0)