Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ee/rbac/lib/internal_api/rbac.pb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ defmodule InternalApi.RBAC.Permission do
field(:scope, 4, type: InternalApi.RBAC.Scope, enum: true)
end

defmodule InternalApi.RBAC.ListSubjectsRequest do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"

field(:org_id, 1, type: :string, json_name: "orgId")
field(:subject_ids, 2, repeated: true, type: :string, json_name: "subjectIds")
end

defmodule InternalApi.RBAC.ListSubjectsResponse do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"

field(:subjects, 1, repeated: true, type: InternalApi.RBAC.Subject)
end

defmodule InternalApi.RBAC.RBAC.Service do
@moduledoc false

Expand Down Expand Up @@ -436,6 +453,8 @@ defmodule InternalApi.RBAC.RBAC.Service do
InternalApi.RBAC.RefreshCollaboratorsRequest,
InternalApi.RBAC.RefreshCollaboratorsResponse
)

rpc(:ListSubjects, InternalApi.RBAC.ListSubjectsRequest, InternalApi.RBAC.ListSubjectsResponse)
end

defmodule InternalApi.RBAC.RBAC.Stub do
Expand Down
22 changes: 22 additions & 0 deletions ee/rbac/lib/rbac/grpc_servers/rbac_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ defmodule Rbac.GrpcServers.RbacServer do
end)
end

def list_subjects(%RBAC.ListSubjectsRequest{} = req, _stream) do
Watchman.benchmark("list_subjects.duration", fn ->
validate_uuid!(req.org_id)

subjects = Rbac.Repo.Subject.find_by_ids_and_org(req.subject_ids, req.org_id)

%RBAC.ListSubjectsResponse{
subjects: Enum.map(subjects, &construct_grpc_subject/1)
}
end)
end

###
### Helper functions
###
Expand Down Expand Up @@ -443,6 +455,16 @@ defmodule Rbac.GrpcServers.RbacServer do
}
end

defp construct_grpc_subject(subject) do
subject_type = subject.type |> String.upcase() |> String.to_existing_atom()

%RBAC.Subject{
subject_type: subject_type,
subject_id: subject.id,
display_name: subject.name
}
end

defp scope_name_to_grpc_enum(name) do
case name do
"org_scope" -> :SCOPE_ORG
Expand Down
11 changes: 10 additions & 1 deletion ee/rbac/lib/rbac/repo/subject.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Rbac.Repo.Subject do
use Rbac.Repo.Schema
import Ecto.Query, only: [where: 3]
import Ecto.Query

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

@spec find_by_ids_and_org([String.t()], String.t()) :: [%__MODULE__{}]
def find_by_ids_and_org(subject_ids, org_id) do
__MODULE__
|> join(:inner, [s], srb in assoc(s, :role_bindings))
|> where([s, srb], s.id in ^subject_ids and srb.org_id == ^org_id)
|> distinct([s], s.id)
|> Rbac.Repo.all()
end

def changeset(subject, params \\ %{}) do
subject
|> cast(params, [:id, :name, :type])
Expand Down
78 changes: 78 additions & 0 deletions ee/rbac/test/rbac/grpc_servers/rbac_server_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,84 @@ defmodule Rbac.GrpcServers.RbacServer.Test do
end
end

describe "list_subjects" do
alias InternalApi.RBAC.ListSubjectsRequest, as: Request

test "invalid org_id returns error", state do
req = %Request{org_id: "invalid-uuid", subject_ids: []}
{:error, grpc_error} = state.grpc_channel |> Stub.list_subjects(req)
assert grpc_error.message =~ "Invalid uuid"
end

test "returns subjects that are part of the organization", state do
user1_id = UUID.generate()
user2_id = UUID.generate()
user3_id = UUID.generate()

Support.Factories.RbacUser.insert(user1_id, "User One")
Support.Factories.RbacUser.insert(user2_id, "User Two")
Support.Factories.RbacUser.insert(user3_id, "User Three")

Support.Rbac.assign_org_role_by_name(@org_id, user1_id, "Admin")
Support.Rbac.assign_org_role_by_name(@org_id, user2_id, "Member")

req = %Request{org_id: @org_id, subject_ids: [user1_id, user2_id, user3_id]}
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)

assert length(subjects) == 2
subject_ids = Enum.map(subjects, & &1.subject_id)
assert user1_id in subject_ids
assert user2_id in subject_ids
refute user3_id in subject_ids
end

test "returns empty list when no subjects match", state do
user_id = UUID.generate()
Support.Factories.RbacUser.insert(user_id, "User One")

req = %Request{org_id: @org_id, subject_ids: [user_id]}
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)

assert subjects == []
end

test "returns subjects with correct type and display name", state do
user_id = UUID.generate()
Support.Factories.RbacUser.insert(user_id, "Test User")
Support.Rbac.assign_org_role_by_name(@org_id, user_id, "Admin")

req = %Request{org_id: @org_id, subject_ids: [user_id]}
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)

assert length(subjects) == 1
subject = hd(subjects)
assert subject.subject_id == user_id
assert subject.display_name == "Test User"
assert subject.subject_type == :USER
end

test "returns empty list when subject_ids is empty", state do
req = %Request{org_id: @org_id, subject_ids: []}
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)

assert subjects == []
end

test "filters subjects by organization correctly", state do
other_org_id = UUID.generate()
user_id = UUID.generate()

Support.Factories.RbacUser.insert(user_id, "User One")
Support.Rbac.create_org_roles(other_org_id)
Support.Rbac.assign_org_role_by_name(other_org_id, user_id, "Admin")

req = %Request{org_id: @org_id, subject_ids: [user_id]}
{:ok, %{subjects: subjects}} = state.grpc_channel |> Stub.list_subjects(req)

assert subjects == []
end
end

###
### Helper functions
###
Expand Down
19 changes: 19 additions & 0 deletions rbac/ce/lib/internal_api/rbac.pb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ defmodule InternalApi.RBAC.Permission do
field(:scope, 4, type: InternalApi.RBAC.Scope, enum: true)
end

defmodule InternalApi.RBAC.ListSubjectsRequest do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"

field(:org_id, 1, type: :string, json_name: "orgId")
field(:subject_ids, 2, repeated: true, type: :string, json_name: "subjectIds")
end

defmodule InternalApi.RBAC.ListSubjectsResponse do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0"

field(:subjects, 1, repeated: true, type: InternalApi.RBAC.Subject)
end

defmodule InternalApi.RBAC.RBAC.Service do
@moduledoc false

Expand Down Expand Up @@ -436,6 +453,8 @@ defmodule InternalApi.RBAC.RBAC.Service do
InternalApi.RBAC.RefreshCollaboratorsRequest,
InternalApi.RBAC.RefreshCollaboratorsResponse
)

rpc(:ListSubjects, InternalApi.RBAC.ListSubjectsRequest, InternalApi.RBAC.ListSubjectsResponse)
end

defmodule InternalApi.RBAC.RBAC.Stub do
Expand Down
25 changes: 25 additions & 0 deletions rbac/ce/lib/rbac/grpc_servers/rbac_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,21 @@ defmodule Rbac.GrpcServers.RbacServer do
end)
end

@spec list_subjects(RBAC.ListSubjectsRequest.t(), GRPC.Server.Stream.t()) ::
RBAC.ListSubjectsResponse.t()
def list_subjects(%RBAC.ListSubjectsRequest{} = req, _stream) do
Log.observe("grpc.rbac.list_subjects", fn ->
validate_uuid!(req.org_id)

role_assignments = RoleAssignment.find_by_ids_and_org(req.subject_ids, req.org_id)
display_names_by_id = fetch_display_names(role_assignments)

%RBAC.ListSubjectsResponse{
subjects: Enum.map(role_assignments, &construct_grpc_subject(&1, display_names_by_id))
}
end)
end

# ----------------
# Helper functions
# ----------------
Expand Down Expand Up @@ -539,4 +554,14 @@ defmodule Rbac.GrpcServers.RbacServer do
_ -> "user"
end
end

defp construct_grpc_subject(assignment, display_names_by_id) do
subject_type = assignment.subject_type |> String.upcase() |> String.to_existing_atom()

%RBAC.Subject{
subject_type: subject_type,
subject_id: assignment.user_id,
display_name: display_names_by_id[assignment.user_id] || ""
}
end
end
12 changes: 12 additions & 0 deletions rbac/ce/lib/rbac/models/role_assignment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ defmodule Rbac.Models.RoleAssignment do
|> Repo.all()
end

@doc """
Finds role assignments by subject IDs and organization ID.
Returns distinct assignments filtered by org_id.
"""
def find_by_ids_and_org(subject_ids, org_id) do
from(r in __MODULE__,
where: r.user_id in ^subject_ids and r.org_id == ^org_id,
distinct: r.user_id
)
|> Repo.all()
end

@doc """
Get user ids that are owner or admin in the given organization
"""
Expand Down
Loading