Skip to content

Commit fc80d28

Browse files
authored
Add RBAC endpoint for listing subjects (#622)
## πŸ“ Description Adds endpoint to RBAC service for listing organization subjects and their types. ## βœ… Checklist - [x] I have tested this change - [ ] This change requires documentation update
1 parent 55bd5c0 commit fc80d28

File tree

8 files changed

+316
-1
lines changed

8 files changed

+316
-1
lines changed

β€Žee/rbac/lib/internal_api/rbac.pb.exβ€Ž

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,23 @@ defmodule InternalApi.RBAC.Permission do
380380
field(:scope, 4, type: InternalApi.RBAC.Scope, enum: true)
381381
end
382382

383+
defmodule InternalApi.RBAC.ListSubjectsRequest do
384+
@moduledoc false
385+
386+
use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"
387+
388+
field(:org_id, 1, type: :string, json_name: "orgId")
389+
field(:subject_ids, 2, repeated: true, type: :string, json_name: "subjectIds")
390+
end
391+
392+
defmodule InternalApi.RBAC.ListSubjectsResponse do
393+
@moduledoc false
394+
395+
use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"
396+
397+
field(:subjects, 1, repeated: true, type: InternalApi.RBAC.Subject)
398+
end
399+
383400
defmodule InternalApi.RBAC.RBAC.Service do
384401
@moduledoc false
385402

@@ -436,6 +453,8 @@ defmodule InternalApi.RBAC.RBAC.Service do
436453
InternalApi.RBAC.RefreshCollaboratorsRequest,
437454
InternalApi.RBAC.RefreshCollaboratorsResponse
438455
)
456+
457+
rpc(:ListSubjects, InternalApi.RBAC.ListSubjectsRequest, InternalApi.RBAC.ListSubjectsResponse)
439458
end
440459

441460
defmodule InternalApi.RBAC.RBAC.Stub do

β€Žee/rbac/lib/rbac/grpc_servers/rbac_server.exβ€Ž

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,18 @@ defmodule Rbac.GrpcServers.RbacServer do
264264
end)
265265
end
266266

267+
def list_subjects(%RBAC.ListSubjectsRequest{} = req, _stream) do
268+
Watchman.benchmark("list_subjects.duration", fn ->
269+
validate_uuid!(req.org_id)
270+
271+
subjects = Rbac.Repo.Subject.find_by_ids_and_org(req.subject_ids, req.org_id)
272+
273+
%RBAC.ListSubjectsResponse{
274+
subjects: Enum.map(subjects, &construct_grpc_subject/1)
275+
}
276+
end)
277+
end
278+
267279
###
268280
### Helper functions
269281
###
@@ -443,6 +455,16 @@ defmodule Rbac.GrpcServers.RbacServer do
443455
}
444456
end
445457

458+
defp construct_grpc_subject(subject) do
459+
subject_type = subject.type |> String.upcase() |> String.to_existing_atom()
460+
461+
%RBAC.Subject{
462+
subject_type: subject_type,
463+
subject_id: subject.id,
464+
display_name: subject.name
465+
}
466+
end
467+
446468
defp scope_name_to_grpc_enum(name) do
447469
case name do
448470
"org_scope" -> :SCOPE_ORG

β€Žee/rbac/lib/rbac/repo/subject.exβ€Ž

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule Rbac.Repo.Subject do
22
use Rbac.Repo.Schema
3-
import Ecto.Query, only: [where: 3]
3+
import Ecto.Query
44

55
schema "subjects" do
66
has_many(:role_bindings, Rbac.Repo.SubjectRoleBinding)
@@ -15,6 +15,15 @@ defmodule Rbac.Repo.Subject do
1515
__MODULE__ |> where([s], s.id == ^id) |> Rbac.Repo.one()
1616
end
1717

18+
@spec find_by_ids_and_org([String.t()], String.t()) :: [%__MODULE__{}]
19+
def find_by_ids_and_org(subject_ids, org_id) do
20+
__MODULE__
21+
|> join(:inner, [s], srb in assoc(s, :role_bindings))
22+
|> where([s, srb], s.id in ^subject_ids and srb.org_id == ^org_id)
23+
|> distinct([s], s.id)
24+
|> Rbac.Repo.all()
25+
end
26+
1827
def changeset(subject, params \\ %{}) do
1928
subject
2029
|> cast(params, [:id, :name, :type])

β€Žee/rbac/test/rbac/grpc_servers/rbac_server_test.exsβ€Ž

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,84 @@ defmodule Rbac.GrpcServers.RbacServer.Test do
10681068
end
10691069
end
10701070

1071+
describe "list_subjects" do
1072+
alias InternalApi.RBAC.ListSubjectsRequest, as: Request
1073+
1074+
test "invalid org_id returns error", state do
1075+
req = %Request{org_id: "invalid-uuid", subject_ids: []}
1076+
{:error, grpc_error} = state.grpc_channel |> Stub.list_subjects(req)
1077+
assert grpc_error.message =~ "Invalid uuid"
1078+
end
1079+
1080+
test "returns subjects that are part of the organization", state do
1081+
user1_id = UUID.generate()
1082+
user2_id = UUID.generate()
1083+
user3_id = UUID.generate()
1084+
1085+
Support.Factories.RbacUser.insert(user1_id, "User One")
1086+
Support.Factories.RbacUser.insert(user2_id, "User Two")
1087+
Support.Factories.RbacUser.insert(user3_id, "User Three")
1088+
1089+
Support.Rbac.assign_org_role_by_name(@org_id, user1_id, "Admin")
1090+
Support.Rbac.assign_org_role_by_name(@org_id, user2_id, "Member")
1091+
1092+
req = %Request{org_id: @org_id, subject_ids: [user1_id, user2_id, user3_id]}
1093+
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)
1094+
1095+
assert length(subjects) == 2
1096+
subject_ids = Enum.map(subjects, & &1.subject_id)
1097+
assert user1_id in subject_ids
1098+
assert user2_id in subject_ids
1099+
refute user3_id in subject_ids
1100+
end
1101+
1102+
test "returns empty list when no subjects match", state do
1103+
user_id = UUID.generate()
1104+
Support.Factories.RbacUser.insert(user_id, "User One")
1105+
1106+
req = %Request{org_id: @org_id, subject_ids: [user_id]}
1107+
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)
1108+
1109+
assert subjects == []
1110+
end
1111+
1112+
test "returns subjects with correct type and display name", state do
1113+
user_id = UUID.generate()
1114+
Support.Factories.RbacUser.insert(user_id, "Test User")
1115+
Support.Rbac.assign_org_role_by_name(@org_id, user_id, "Admin")
1116+
1117+
req = %Request{org_id: @org_id, subject_ids: [user_id]}
1118+
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)
1119+
1120+
assert length(subjects) == 1
1121+
subject = hd(subjects)
1122+
assert subject.subject_id == user_id
1123+
assert subject.display_name == "Test User"
1124+
assert subject.subject_type == :USER
1125+
end
1126+
1127+
test "returns empty list when subject_ids is empty", state do
1128+
req = %Request{org_id: @org_id, subject_ids: []}
1129+
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)
1130+
1131+
assert subjects == []
1132+
end
1133+
1134+
test "filters subjects by organization correctly", state do
1135+
other_org_id = UUID.generate()
1136+
user_id = UUID.generate()
1137+
1138+
Support.Factories.RbacUser.insert(user_id, "User One")
1139+
Support.Rbac.create_org_roles(other_org_id)
1140+
Support.Rbac.assign_org_role_by_name(other_org_id, user_id, "Admin")
1141+
1142+
req = %Request{org_id: @org_id, subject_ids: [user_id]}
1143+
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)
1144+
1145+
assert subjects == []
1146+
end
1147+
end
1148+
10711149
###
10721150
### Helper functions
10731151
###

β€Žrbac/ce/lib/internal_api/rbac.pb.exβ€Ž

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,23 @@ defmodule InternalApi.RBAC.Permission do
380380
field(:scope, 4, type: InternalApi.RBAC.Scope, enum: true)
381381
end
382382

383+
defmodule InternalApi.RBAC.ListSubjectsRequest do
384+
@moduledoc false
385+
386+
use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"
387+
388+
field(:org_id, 1, type: :string, json_name: "orgId")
389+
field(:subject_ids, 2, repeated: true, type: :string, json_name: "subjectIds")
390+
end
391+
392+
defmodule InternalApi.RBAC.ListSubjectsResponse do
393+
@moduledoc false
394+
395+
use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"
396+
397+
field(:subjects, 1, repeated: true, type: InternalApi.RBAC.Subject)
398+
end
399+
383400
defmodule InternalApi.RBAC.RBAC.Service do
384401
@moduledoc false
385402

@@ -436,6 +453,8 @@ defmodule InternalApi.RBAC.RBAC.Service do
436453
InternalApi.RBAC.RefreshCollaboratorsRequest,
437454
InternalApi.RBAC.RefreshCollaboratorsResponse
438455
)
456+
457+
rpc(:ListSubjects, InternalApi.RBAC.ListSubjectsRequest, InternalApi.RBAC.ListSubjectsResponse)
439458
end
440459

441460
defmodule InternalApi.RBAC.RBAC.Stub do

β€Žrbac/ce/lib/rbac/grpc_servers/rbac_server.exβ€Ž

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,21 @@ defmodule Rbac.GrpcServers.RbacServer do
261261
end)
262262
end
263263

264+
@spec list_subjects(RBAC.ListSubjectsRequest.t(), GRPC.Server.Stream.t()) ::
265+
RBAC.ListSubjectsResponse.t()
266+
def list_subjects(%RBAC.ListSubjectsRequest{} = req, _stream) do
267+
Log.observe("grpc.rbac.list_subjects", fn ->
268+
validate_uuid!(req.org_id)
269+
270+
role_assignments = RoleAssignment.find_by_ids_and_org(req.subject_ids, req.org_id)
271+
display_names_by_id = fetch_display_names(role_assignments)
272+
273+
%RBAC.ListSubjectsResponse{
274+
subjects: Enum.map(role_assignments, &construct_grpc_subject(&1, display_names_by_id))
275+
}
276+
end)
277+
end
278+
264279
# ----------------
265280
# Helper functions
266281
# ----------------
@@ -539,4 +554,14 @@ defmodule Rbac.GrpcServers.RbacServer do
539554
_ -> "user"
540555
end
541556
end
557+
558+
defp construct_grpc_subject(assignment, display_names_by_id) do
559+
subject_type = assignment.subject_type |> String.upcase() |> String.to_existing_atom()
560+
561+
%RBAC.Subject{
562+
subject_type: subject_type,
563+
subject_id: assignment.user_id,
564+
display_name: display_names_by_id[assignment.user_id] || ""
565+
}
566+
end
542567
end

β€Žrbac/ce/lib/rbac/models/role_assignment.exβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ defmodule Rbac.Models.RoleAssignment do
7373
|> Repo.all()
7474
end
7575

76+
@doc """
77+
Finds role assignments by subject IDs and organization ID.
78+
Returns distinct assignments filtered by org_id.
79+
"""
80+
def find_by_ids_and_org(subject_ids, org_id) do
81+
from(r in __MODULE__,
82+
where: r.user_id in ^subject_ids and r.org_id == ^org_id,
83+
distinct: r.user_id
84+
)
85+
|> Repo.all()
86+
end
87+
7688
@doc """
7789
Get user ids that are owner or admin in the given organization
7890
"""

0 commit comments

Comments
Β (0)