Skip to content

Commit 929db53

Browse files
fix(plumber): check if DT permits user to partially rebuild pipeline (#427)
## 📝 Description Refreshed protos, in partial_rebuild action checks if deployment target policy allows user to run the pipeline ## ✅ Checklist - [x] I have tested this change - [ ] This change requires documentation update
1 parent c4c4ab2 commit 929db53

29 files changed

+2800
-1745
lines changed

plumber/gofer_client/lib/gofer_client.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,11 @@ defmodule GoferClient do
3636
|> GrpcClient.pipeline_done()
3737
|> ResponseParser.process_pipeline_done_response()
3838
end
39+
40+
def verify_deployment_target_access(target_id, triggerer, git_ref_type, git_ref_label) do
41+
target_id
42+
|> RequestFormatter.form_verify_request(triggerer, git_ref_type, git_ref_label)
43+
|> GrpcClient.verify_deployment_target_access()
44+
|> ResponseParser.process_verify_response()
45+
end
3946
end

plumber/gofer_client/lib/gofer_client/grpc_client.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule GoferClient.GrpcClient do
66
"""
77

88
alias InternalApi.Gofer.Switch
9+
alias InternalApi.Gofer.DeploymentTargets
910
alias Util.Metrics
1011
alias LogTee, as: LT
1112

@@ -54,6 +55,26 @@ defmodule GoferClient.GrpcClient do
5455
end)
5556
end
5657

58+
# Verify Deployment Target Access
59+
60+
def verify_deployment_target_access({:ok, verify_request}) do
61+
result = Wormhole.capture(__MODULE__, :verify_deployment_target_access_, [verify_request], stacktrace: true, timeout: 2_345)
62+
case result do
63+
{:ok, result} -> result
64+
error -> error
65+
end
66+
end
67+
def verify_deployment_target_access(error), do: error
68+
69+
def verify_deployment_target_access_(verify_request) do
70+
{:ok, channel} = GRPC.Stub.connect(url())
71+
Metrics.benchmark("Ppl.gofer_client.grpc_client", "verify_deployment_target", fn ->
72+
channel
73+
|> DeploymentTargets.DeploymentTargets.Stub.verify(verify_request, opts())
74+
|> is_ok?("verify_deployment_target")
75+
end)
76+
end
77+
5778
# Utility
5879

5980
defp is_ok?(response = {:ok, _rsp}, _method), do: response

plumber/gofer_client/lib/gofer_client/request_formatter.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule GoferClient.RequestFormatter do
55
"""
66

77
alias InternalApi.Gofer.{CreateRequest, PipelineDoneRequest, GitRefType}
8+
alias InternalApi.Gofer.DeploymentTargets.VerifyRequest
89
alias Util.{ToTuple, Proto}
910

1011
# Create
@@ -79,4 +80,18 @@ defmodule GoferClient.RequestFormatter do
7980
"One or more of these params: #{inspect switch_id}, #{inspect result} and #{inspect result_reason} is not string."
8081
|> ToTuple.error()
8182
end
83+
84+
# Verify
85+
86+
def form_verify_request(target_id, triggerer, git_ref_type, git_ref_label)
87+
when is_binary(target_id) and is_binary(triggerer) and is_binary(git_ref_type) and is_binary(git_ref_label) do
88+
verify_params = %{target_id: target_id, triggerer: triggerer, git_ref_type: git_ref_type, git_ref_label: git_ref_label}
89+
90+
Proto.deep_new(VerifyRequest, verify_params,
91+
transformations: %{VerifyRequest.GitRefType => {__MODULE__, :string_to_enum_atom}})
92+
end
93+
def form_verify_request(target_id, triggerer, git_ref_type, git_ref_label) do
94+
"One or more of these params: #{inspect target_id}, #{inspect triggerer}, #{inspect git_ref_type} and #{inspect git_ref_label} is not in the expected format."
95+
|> ToTuple.error()
96+
end
8297
end

plumber/gofer_client/lib/gofer_client/response_parser.ex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule GoferClient.ResponseParser do
55
"""
66

77
alias InternalApi.Gofer.ResponseStatus.ResponseCode
8+
alias InternalApi.Gofer.DeploymentTargets.VerifyResponse.Status
89
alias LogTee, as: LT
910
alias Util.ToTuple
1011

@@ -47,15 +48,44 @@ defmodule GoferClient.ResponseParser do
4748

4849
def process_pipeline_done_response(error), do: error
4950

51+
# Verify
52+
53+
def process_verify_response({:ok, response}) do
54+
with true <- is_map(response),
55+
{:ok, status} <- Map.fetch(response, :status),
56+
status_atom <- verify_status_value(status)
57+
do
58+
handle_verify_status(status_atom, response)
59+
else
60+
_ -> log_invalid_response(response, "verify")
61+
end
62+
end
63+
64+
def process_verify_response(error), do: error
65+
5066
# Util
5167

68+
defp handle_verify_status(:ACCESS_GRANTED, _response), do: {:ok, :access_granted}
69+
defp handle_verify_status(:SYNCING_TARGET, _response), do: {:error, :syncing_target}
70+
defp handle_verify_status(:BANNED_SUBJECT, _response), do: {:error, :banned_subject}
71+
defp handle_verify_status(:BANNED_OBJECT, _response), do: {:error, :banned_object}
72+
defp handle_verify_status(:CORDONED_TARGET, _response), do: {:error, :cordoned_target}
73+
defp handle_verify_status(:CORRUPTED_TARGET, _response), do: {:error, :corrupted_target}
74+
defp handle_verify_status(_status_atom, response), do: log_invalid_response(response, "verify")
75+
5276
defp response_code_value(%{code: code}) do
5377
ResponseCode.key(code)
5478
rescue _ ->
5579
nil
5680
end
5781
defp response_code_value(_), do: nil
5882

83+
defp verify_status_value(status) do
84+
Status.key(status)
85+
rescue _ ->
86+
nil
87+
end
88+
5989
defp log_invalid_response(response, rpc_method) do
6090
response
6191
|> LT.error("Gofer service responded to #{rpc_method} with :ok and invalid data:")

plumber/ppl/lib/ppl/grpc/server.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,17 @@ defmodule Ppl.Grpc.Server do
418418
{:ok, false} <- project_deleted?(ppl.project_id),
419419
{"done", result} when result != "passed"
420420
<- {ppl.state, ppl.result},
421+
{:ok, ppl_req} <- PplRequestsQueries.get_by_id(request.ppl_id),
422+
{:ok} <- verify_deployment_target_permission(ppl_req, request.user_id),
421423
{:ok, ppl_id} <- Actions.partial_rebuild(request)
422424
do
423425
Proto.deep_new!(PartialRebuildResponse,
424426
%{ppl_id: ppl_id, response_status: %{code: :OK}})
425427
else
426428
{:error, {:project_deleted, project_id}} ->
427429
responed_refused(PartialRebuildResponse, "Project with id #{project_id} was deleted.")
430+
{:error, {:deployment_target_permission_denied, reason}} ->
431+
rebuild_error_resp("Access to deployment target denied: #{inspect reason}")
428432
{:error, message} ->
429433
rebuild_error_resp("#{inspect message}")
430434
{"done", "passed"} ->
@@ -491,6 +495,22 @@ defmodule Ppl.Grpc.Server do
491495
defp limit_status(message),
492496
do: ResponseStatus.new(code: ResponseCode.value(:LIMIT_EXCEEDED), message: to_str(message))
493497

498+
defp verify_deployment_target_permission(%{request_args: %{"deployment_target_id" => ""}}, _user_id), do: {:ok}
499+
defp verify_deployment_target_permission(%{request_args: %{"deployment_target_id" => nil}}, _user_id), do: {:ok}
500+
defp verify_deployment_target_permission(%{
501+
request_args: %{"deployment_target_id" => deployment_target_id, "label" => label},
502+
source_args: %{"git_ref_type" => git_ref_type}
503+
}, user_id) when is_binary(git_ref_type) and is_binary(label) and label != "" do
504+
case GoferClient.verify_deployment_target_access(deployment_target_id, user_id, git_ref_type, label) do
505+
{:ok, :access_granted} -> {:ok}
506+
{:error, reason} -> {:error, {:deployment_target_permission_denied, reason}}
507+
error -> {:error, {:deployment_target_permission_denied, error}}
508+
end
509+
end
510+
defp verify_deployment_target_permission(%{request_args: %{"deployment_target_id" => deployment_target_id}}, _user_id) when is_binary(deployment_target_id) and deployment_target_id != "",
511+
do: {:error, {:deployment_target_permission_denied, "Missing label or git_ref_type"}}
512+
defp verify_deployment_target_permission(_, _), do: {:ok}
513+
494514
defp string_keys(map), do: map |> Poison.encode!() |> Poison.decode!()
495515

496516
defp to_str(term) when is_binary(term), do: term

plumber/ppl/test/grpc/server_test.exs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule Ppl.Grpc.Server.Test do
22
use Ppl.IntegrationCase
33
@moduletag capture_log: true
4+
import Mock
45
alias Test.Helpers
56
alias Util.{ToTuple, Proto}
67
alias Ppl.PplRequests.Model.PplRequestsQueries
@@ -2208,6 +2209,85 @@ defmodule Ppl.Grpc.Server.Test do
22082209
assert new_ppl_id_1 == new_ppl_id_2
22092210
end
22102211

2212+
@tag :integration
2213+
test "gRPC partial_rebuild() - succeeds when no deployment target is specified" do
2214+
{:ok, %{ppl_id: ppl_id}} =
2215+
%{"repo_name" => "14_free_topology_failing_block"}
2216+
|> Test.Helpers.schedule_request_factory(:local)
2217+
|> Actions.schedule()
2218+
2219+
loopers = Test.Helpers.start_all_loopers()
2220+
{:ok, _ppl} = Test.Helpers.wait_for_ppl_state(ppl_id, "done", 20_000)
2221+
Test.Helpers.stop_all_loopers(loopers)
2222+
2223+
request_token = UUID.uuid4()
2224+
new_ppl_id = assert_partial_rebuild(ppl_id, request_token, :ok)
2225+
assert is_binary(new_ppl_id)
2226+
end
2227+
2228+
@tag :integration
2229+
test "gRPC partial_rebuild() - fails when deployment target permission is denied" do
2230+
deployment_target_id = UUID.uuid4()
2231+
{:ok, %{ppl_id: ppl_id}} = create_pipeline_with_deployment_target(deployment_target_id)
2232+
2233+
# Mock GoferClient to return access denied
2234+
with_mock GoferClient, [
2235+
verify_deployment_target_access: fn(_, _, _, _) -> {:error, :banned_subject} end
2236+
] do
2237+
expected_message = "Access to deployment target denied: :banned_subject"
2238+
assert_partial_rebuild(ppl_id, UUID.uuid4(), :error, expected_message)
2239+
end
2240+
end
2241+
2242+
@tag :integration
2243+
test "gRPC partial_rebuild() - succeeds when deployment target permission is granted" do
2244+
deployment_target_id = UUID.uuid4()
2245+
{:ok, %{ppl_id: ppl_id}} = create_pipeline_with_deployment_target(deployment_target_id)
2246+
2247+
# Mock GoferClient to return access granted
2248+
with_mock GoferClient, [
2249+
verify_deployment_target_access: fn(_, _, _, _) -> {:ok, :access_granted} end
2250+
] do
2251+
request_token = UUID.uuid4()
2252+
new_ppl_id = assert_partial_rebuild(ppl_id, request_token, :ok)
2253+
assert is_binary(new_ppl_id)
2254+
end
2255+
end
2256+
2257+
@tag :integration
2258+
test "gRPC partial_rebuild() - fails when deployment target verification returns error" do
2259+
deployment_target_id = UUID.uuid4()
2260+
{:ok, %{ppl_id: ppl_id}} = create_pipeline_with_deployment_target(deployment_target_id)
2261+
2262+
# Mock GoferClient to return syncing target error
2263+
with_mock GoferClient, [
2264+
verify_deployment_target_access: fn(_, _, _, _) -> {:error, :syncing_target} end
2265+
] do
2266+
expected_message = "Access to deployment target denied: :syncing_target"
2267+
assert_partial_rebuild(ppl_id, UUID.uuid4(), :error, expected_message)
2268+
end
2269+
end
2270+
2271+
defp create_pipeline_with_deployment_target(deployment_target_id) do
2272+
source_args = Test.Support.RequestFactory.source_args(%{})
2273+
2274+
%{
2275+
"repo_name" => "14_free_topology_failing_block",
2276+
"deployment_target_id" => deployment_target_id
2277+
}
2278+
|> Test.Helpers.schedule_request_factory(:local)
2279+
|> Map.put("source_args", source_args)
2280+
|> Actions.schedule()
2281+
|> case do
2282+
{:ok, %{ppl_id: ppl_id}} = result ->
2283+
loopers = Test.Helpers.start_all_loopers()
2284+
{:ok, _ppl} = Test.Helpers.wait_for_ppl_state(ppl_id, "done", 20_000)
2285+
Test.Helpers.stop_all_loopers(loopers)
2286+
result
2287+
error -> error
2288+
end
2289+
end
2290+
22112291
defp assert_partial_rebuild(ppl_id, request_token, expected_status, expected_message \\ "") do
22122292
request =
22132293
%{ppl_id: ppl_id, request_token: request_token, user_id: "rebuild_user"}

plumber/proto/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ endif
3131
docker run --rm -v $(PWD):/home/protoc/code -v $(TMP_INTERNAL_REPO_DIR):/home/protoc/source renderedtext/protoc:$(RT_PROTOC_IMG_VSN) protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:$(RELATIVE_INTERNAL_PB_OUTPUT_DIR) --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/include/google/rpc/status.proto
3232
docker run --rm -v $(PWD):/home/protoc/code -v $(TMP_INTERNAL_REPO_DIR):/home/protoc/source renderedtext/protoc:$(RT_PROTOC_IMG_VSN) protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:$(RELATIVE_INTERNAL_PB_OUTPUT_DIR) --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/include/google/rpc/code.proto
3333
docker run --rm -v $(PWD):/home/protoc/code -v $(TMP_INTERNAL_REPO_DIR):/home/protoc/source renderedtext/protoc:$(RT_PROTOC_IMG_VSN) protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:$(RELATIVE_INTERNAL_PB_OUTPUT_DIR) --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/gofer.switch.proto
34+
docker run --rm -v $(PWD):/home/protoc/code -v $(TMP_INTERNAL_REPO_DIR):/home/protoc/source renderedtext/protoc:$(RT_PROTOC_IMG_VSN) protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:$(RELATIVE_INTERNAL_PB_OUTPUT_DIR) --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/gofer.dt.proto
3435
docker run --rm -v $(PWD):/home/protoc/code -v $(TMP_INTERNAL_REPO_DIR):/home/protoc/source renderedtext/protoc:$(RT_PROTOC_IMG_VSN) protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:$(RELATIVE_INTERNAL_PB_OUTPUT_DIR) --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/organization.proto
3536
docker run --rm -v $(PWD):/home/protoc/code -v $(TMP_INTERNAL_REPO_DIR):/home/protoc/source renderedtext/protoc:$(RT_PROTOC_IMG_VSN) protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:$(RELATIVE_INTERNAL_PB_OUTPUT_DIR) --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/paparazzo.snapshot.proto
3637
docker run --rm -v $(PWD):/home/protoc/code -v $(TMP_INTERNAL_REPO_DIR):/home/protoc/source renderedtext/protoc:$(RT_PROTOC_IMG_VSN) protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:$(RELATIVE_INTERNAL_PB_OUTPUT_DIR) --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/plumber.pipeline.proto

0 commit comments

Comments
 (0)