Skip to content

Commit 9b2ea5a

Browse files
authored
fix(guard): differentiate between various types of GRPC errors (#522)
1 parent 597d235 commit 9b2ea5a

File tree

7 files changed

+83
-4
lines changed

7 files changed

+83
-4
lines changed

guard/lib/guard/front_repo/user.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ defmodule Guard.FrontRepo.User do
9494
u.authentication_token == ^token and is_nil(u.blocked_at) and
9595
(is_nil(u.deactivated) or u.deactivated == false)
9696
)
97+
# Preload service account to avoid N+1 queries when checking if user is a service account
98+
|> preload(:service_account)
9799
) do
98100
nil -> {:error, :not_found}
99101
user -> {:ok, user}

guard/lib/guard/grpc_servers/auth_server.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ defmodule Guard.GrpcServers.AuthServer do
1111
observe("grpc.authentication.authenticate", fn ->
1212
case find_user_by_token(token) do
1313
{:ok, user} ->
14-
respond_with_user(user, "API_TOKEN", "", "")
14+
user
15+
|> tap(&log_service_account_access(&1))
16+
|> respond_with_user("API_TOKEN", "", "")
1517

1618
{:error, :user, :not_found} ->
1719
respond_false()
@@ -217,4 +219,11 @@ defmodule Guard.GrpcServers.AuthServer do
217219
end
218220
end)
219221
end
222+
223+
defp log_service_account_access(%{service_account: nil} = _user), do: :ok
224+
225+
defp log_service_account_access(%{service_account: _} = user) do
226+
Watchman.increment({"service_account.access", [user.org_id]})
227+
:ok
228+
end
220229
end

guard/lib/guard/grpc_servers/utils.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ defmodule Guard.GrpcServers.Utils do
1818
Watchman.increment({name, ["OK"]})
1919
result
2020
rescue
21+
# Some GRPC errors are expected and we don't need to do anything with them(validation, not found, etc).
22+
e in GRPC.RPCError ->
23+
Logger.error(
24+
"Service #{name} - request: #{inspect(request)} - Exited with a failure: #{inspect(e)}"
25+
)
26+
27+
error_category = categorize_grpc_error(e.status)
28+
Watchman.increment({name, [error_category]})
29+
reraise e, __STACKTRACE__
30+
2131
e ->
2232
Logger.error(
2333
"Service #{name} - request: #{inspect(request)} - Exited with an error: #{inspect(e)}"
@@ -28,4 +38,19 @@ defmodule Guard.GrpcServers.Utils do
2838
end
2939
end)
3040
end
41+
42+
defp categorize_grpc_error(status) do
43+
case status do
44+
# Client errors (expected)
45+
status when status in [3, :invalid_argument] -> "CLIENT_ERROR"
46+
status when status in [5, :not_found] -> "CLIENT_ERROR"
47+
status when status in [6, :already_exists] -> "CLIENT_ERROR"
48+
status when status in [7, :permission_denied] -> "CLIENT_ERROR"
49+
status when status in [8, :resource_exhausted] -> "CLIENT_ERROR"
50+
status when status in [9, :failed_precondition] -> "CLIENT_ERROR"
51+
status when status in [11, :out_of_range] -> "CLIENT_ERROR"
52+
# Server errors (unexpected)
53+
_ -> "SERVER_ERROR"
54+
end
55+
end
3156
end

guard/test/guard/api/bitbucket_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule Guard.Api.BitbucketTest do
2-
use Guard.RepoCase
2+
use Guard.RepoCase, async: false
33

44
alias Guard.Api.Bitbucket
55

guard/test/guard/api/github_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule Guard.Api.GithubTest do
2-
use Guard.RepoCase
2+
use Guard.RepoCase, async: false
33

44
alias Guard.Api.Github
55

guard/test/guard/api/gitlab_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule Guard.Api.GitlabTest do
2-
use Guard.RepoCase
2+
use Guard.RepoCase, async: false
33

44
alias Guard.Api.Gitlab
55

guard/test/guard/grpc_servers/auth_server_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule Guard.GrpcServers.AuthServerTest do
22
use Guard.RepoCase, async: false
33

4+
import Mock
5+
46
alias InternalApi.Auth
57
alias InternalApi.Auth.Authentication.Stub
68
alias InternalApi.Auth.AuthenticateRequest
@@ -190,6 +192,47 @@ defmodule Guard.GrpcServers.AuthServerTest do
190192

191193
assert_response(response, user)
192194
end
195+
196+
test "logs service account access metric for service account authentication" do
197+
org_id = Ecto.UUID.generate()
198+
199+
{:ok, %{user: sa_user}} = Support.Factories.ServiceAccountFactory.insert(org_id: org_id)
200+
201+
token = Guard.AuthenticationToken.new()
202+
token_hash = Guard.AuthenticationToken.hash_token(token)
203+
204+
sa_user
205+
|> Ecto.Changeset.change(authentication_token: token_hash)
206+
|> Guard.FrontRepo.update()
207+
208+
with_mock Watchman, [:passthrough], increment: fn _ -> :ok end do
209+
# Authenticate with service account token
210+
request = AuthenticateRequest.new(token: token)
211+
{:ok, channel} = GRPC.Stub.connect("localhost:50051")
212+
{:ok, response} = channel |> Stub.authenticate(request)
213+
214+
assert response.authenticated == true
215+
assert response.user_id == sa_user.id
216+
217+
# Verify service account access metric was logged
218+
assert called(Watchman.increment({"service_account.access", [org_id]}))
219+
end
220+
end
221+
222+
test "does not log service account metric for regular users", %{token: token, user: user} do
223+
with_mock Watchman, [:passthrough], increment: fn _ -> :ok end do
224+
# Authenticate with regular user token
225+
request = AuthenticateRequest.new(token: token)
226+
{:ok, channel} = GRPC.Stub.connect("localhost:50051")
227+
{:ok, response} = channel |> Stub.authenticate(request)
228+
229+
assert response.authenticated == true
230+
assert response.user_id == user.id
231+
232+
# Verify no service account metric was logged
233+
assert_not_called(Watchman.increment({"service_account.access", :_}))
234+
end
235+
end
193236
end
194237

195238
describe "authenticate_with_cookie" do

0 commit comments

Comments
 (0)