Skip to content

Commit 83a8684

Browse files
authored
fix(guard): fix gitlab caching null credentials (#69) (#77)
1 parent b4c65f3 commit 83a8684

10 files changed

+253
-16
lines changed

guard/config/config.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ if System.get_env("AMQP_URL") != nil do
111111
authorization: [connection: :amqp],
112112
user: [connection: :amqp],
113113
organization: [connection: :amqp],
114-
project: [connection: :amqp]
114+
project: [connection: :amqp],
115+
instance_config: [connection: :amqp]
115116
]
116117
end
117118

guard/config/runtime.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ if System.get_env("AMQP_URL") != nil do
179179
authorization: [connection: :amqp],
180180
user: [connection: :amqp],
181181
organization: [connection: :amqp],
182-
project: [connection: :amqp]
182+
project: [connection: :amqp],
183+
instance_config: [connection: :amqp]
183184
]
184185
end

guard/lib/guard/application.ex

+5-11
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ defmodule Guard.Application do
2424
services = [Guard.Repo, Guard.FrontRepo] ++ start_instance_config()
2525

2626
children = add_api_servers(services)
27-
children = children ++ workers()
2827
children = children ++ init_feature_provider()
2928

3029
children = children ++ caches()
@@ -66,15 +65,6 @@ defmodule Guard.Application do
6665
end
6766
end
6867

69-
defp workers do
70-
select_active([
71-
%{
72-
worker: {Guard.Workers.SyncOidcUsers, []},
73-
active: System.get_env("START_SYNC_OIDC_USERS_WORKER") == "true"
74-
}
75-
])
76-
end
77-
7868
defp add_api_servers(services) do
7969
grpc = System.get_env("GRPC_API") || "true"
8070
rabbit = System.get_env("RABBIT_CONSUMER") || "true"
@@ -112,7 +102,11 @@ defmodule Guard.Application do
112102
defp add_test_grpc_service(services, _, _), do: services
113103

114104
defp add_id_service(services, "true") do
115-
services ++ [{Plug.Cowboy, scheme: :http, plug: Guard.Id.Api, options: [port: 4003]}]
105+
services ++
106+
[
107+
{Plug.Cowboy, scheme: :http, plug: Guard.Id.Api, options: [port: 4003]},
108+
{Services.InstanceConfigInvalidatorWorker, []}
109+
]
116110
end
117111

118112
defp add_id_service(services, _), do: services
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule Guard.Events.ConfigModified do
2+
@moduledoc """
3+
Event emitted when an instance config is modified.
4+
"""
5+
6+
@spec publish(InternalApi.InstanceConfig.ConfigType.t()) :: :ok
7+
def publish(type) do
8+
event =
9+
InternalApi.InstanceConfig.ConfigModified.new(
10+
type: type,
11+
timestamp:
12+
Google.Protobuf.Timestamp.new(seconds: DateTime.utc_now() |> DateTime.to_unix(:second))
13+
)
14+
15+
message = InternalApi.InstanceConfig.ConfigModified.encode(event)
16+
17+
exchange_name = "instance_config_exchange"
18+
routing_key = "modified"
19+
20+
{:ok, channel} = AMQP.Application.get_channel(:instance_config)
21+
Tackle.Exchange.create(channel, exchange_name)
22+
23+
:ok = Tackle.Exchange.publish(channel, exchange_name, message, routing_key)
24+
25+
:ok
26+
end
27+
end

guard/lib/guard/git_provider_credentials.ex

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ defmodule Guard.GitProviderCredentials do
4747
cache_key = "#{provider}_credentials"
4848

4949
case Cachex.get(:config_cache, cache_key) do
50-
{:ok, credentials} when not is_nil(credentials) ->
50+
{:ok, %{client_id: client_id, client_secret: client_secret} = credentials}
51+
when not is_nil(client_id) and not is_nil(client_secret) ->
5152
{:ok, credentials}
5253

5354
_ ->

guard/lib/guard/grpc_servers/instance_config_server.ex

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ defmodule Guard.GrpcServers.InstanceConfigServer do
3434

3535
def modify_config(request, _stream) do
3636
Watchman.benchmark("modify_config.duration", fn ->
37-
request = request |> request_atom_type()
37+
parsed_request = request |> request_atom_type()
38+
response = modify_config_(parsed_request.config)
3839

39-
modify_config_(request.config)
40+
Guard.Events.ConfigModified.publish(request.config.type)
41+
response
4042
end)
4143
end
4244

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
defmodule Guard.Services.InstanceConfigInvalidatorWorker do
2+
require Logger
3+
4+
@doc """
5+
This module consumes RabbitMQ instance config change events
6+
and invalidates instance config caches.
7+
"""
8+
9+
use Tackle.Consumer,
10+
url: Application.get_env(:guard, :amqp_url),
11+
exchange: "instance_config_exchange",
12+
routing_key: "modified",
13+
service: "guard",
14+
queue: :dynamic,
15+
queue_opts: [
16+
durable: false,
17+
auto_delete: true,
18+
exclusive: true
19+
]
20+
21+
def handle_message(message) do
22+
event = InternalApi.InstanceConfig.ConfigModified.decode(message)
23+
24+
cache_key =
25+
InternalApi.InstanceConfig.ConfigType.key(event.type)
26+
|> cache_key()
27+
28+
if cache_key, do: Cachex.del(:config_cache, cache_key)
29+
Logger.info("Invalidated Instance Config Cache with key: #{inspect(cache_key)}")
30+
31+
:ok
32+
end
33+
34+
defp cache_key(:CONFIG_TYPE_GITHUB_APP), do: "github_credentials"
35+
defp cache_key(:CONFIG_TYPE_BITBUCKET_APP), do: "bitbucket_credentials"
36+
defp cache_key(:CONFIG_TYPE_GITLAB_APP), do: "gitlab_credentials"
37+
defp cache_key(_), do: nil
38+
end

guard/lib/internal_api/instance_config.pb.ex

+14
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ defmodule InternalApi.InstanceConfig.ConfigType do
101101
field(:CONFIG_TYPE_GITLAB_APP, 4)
102102
end
103103

104+
defmodule InternalApi.InstanceConfig.ConfigModified do
105+
@moduledoc false
106+
use Protobuf, syntax: :proto3
107+
108+
@type t :: %__MODULE__{
109+
type: integer,
110+
timestamp: Google.Protobuf.Timestamp.t() | nil
111+
}
112+
113+
defstruct [:type, :timestamp]
114+
field(:type, 1, type: InternalApi.InstanceConfig.ConfigType, enum: true)
115+
field(:timestamp, 2, type: Google.Protobuf.Timestamp)
116+
end
117+
104118
defmodule InternalApi.InstanceConfig.InstanceConfigService.Service do
105119
@moduledoc false
106120
use GRPC.Service, name: "InternalApi.InstanceConfig.InstanceConfigService"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
defmodule Guard.GitProviderCredentialsTest do
2+
use Guard.RepoCase, async: false
3+
alias Guard.InstanceConfig.Store
4+
5+
setup do
6+
Application.put_env(:guard, :include_instance_config, true)
7+
Guard.Mocks.GithubAppApi.github_app_api()
8+
Guard.InstanceConfigRepo.delete_all(Guard.InstanceConfig.Models.Config)
9+
Cachex.clear(:config_cache)
10+
11+
:ok
12+
end
13+
14+
describe "get/1" do
15+
test "should retrieve valid credentials from each provider when instance config is configured" do
16+
setup_github_app_integration()
17+
setup_gitlab_app_integration()
18+
setup_bitbucket_app_integration()
19+
20+
assert {:ok, {"client_id", "client_secret"}} == Guard.GitProviderCredentials.get(:github)
21+
assert {:ok, {"client_id", "client_secret"}} == Guard.GitProviderCredentials.get(:gitlab)
22+
assert {:ok, {"client_id", "client_secret"}} == Guard.GitProviderCredentials.get(:bitbucket)
23+
end
24+
25+
test "should return error when provider is not configured" do
26+
assert {:error, :not_found} == Guard.GitProviderCredentials.get(:github)
27+
assert {:error, :not_found} == Guard.GitProviderCredentials.get(:gitlab)
28+
assert {:error, :not_found} == Guard.GitProviderCredentials.get(:bitbucket)
29+
end
30+
31+
test "should refetch from instance config when cache is wrong" do
32+
setup_github_app_integration()
33+
setup_gitlab_app_integration()
34+
setup_bitbucket_app_integration()
35+
36+
Cachex.put(:config_cache, "github_credentials", %{client_id: nil, client_secret: nil})
37+
Cachex.put(:config_cache, "gitlab_credentials", %{client_id: nil, client_secret: nil})
38+
Cachex.put(:config_cache, "bitbucket_credentials", %{client_id: nil, client_secret: nil})
39+
40+
assert {:ok, {"client_id", "client_secret"}} == Guard.GitProviderCredentials.get(:github)
41+
assert {:ok, {"client_id", "client_secret"}} == Guard.GitProviderCredentials.get(:gitlab)
42+
assert {:ok, {"client_id", "client_secret"}} == Guard.GitProviderCredentials.get(:bitbucket)
43+
end
44+
45+
test "should get credentials from cache if it contains valid credentials" do
46+
Cachex.put(:config_cache, "github_credentials", %{
47+
client_id: "cached_client_id",
48+
client_secret: "cached_secret"
49+
})
50+
51+
Cachex.put(:config_cache, "gitlab_credentials", %{
52+
client_id: "cached_client_id",
53+
client_secret: "cached_secret"
54+
})
55+
56+
Cachex.put(:config_cache, "bitbucket_credentials", %{
57+
client_id: "cached_client_id",
58+
client_secret: "cached_secret"
59+
})
60+
61+
assert {:ok, {"cached_client_id", "cached_secret"}} ==
62+
Guard.GitProviderCredentials.get(:github)
63+
64+
assert {:ok, {"cached_client_id", "cached_secret"}} ==
65+
Guard.GitProviderCredentials.get(:gitlab)
66+
67+
assert {:ok, {"cached_client_id", "cached_secret"}} ==
68+
Guard.GitProviderCredentials.get(:bitbucket)
69+
end
70+
end
71+
72+
defp setup_github_app_integration do
73+
private_key = JOSE.JWK.generate_key({:rsa, 1024})
74+
{_, pem_private_key} = JOSE.JWK.to_pem(private_key)
75+
76+
Guard.InstanceConfig.Models.Config.changeset(%Guard.InstanceConfig.Models.Config{}, %{
77+
name: :CONFIG_TYPE_GITHUB_APP |> Atom.to_string(),
78+
config: %{
79+
app_id: "11111111111111111111111",
80+
slug: "slug",
81+
name: "name",
82+
client_id: "client_id",
83+
client_secret: "client_secret",
84+
pem: pem_private_key,
85+
html_url: "https://github.com",
86+
webhook_secret: "webhook_secret"
87+
}
88+
})
89+
|> Store.set()
90+
end
91+
92+
defp setup_gitlab_app_integration do
93+
Guard.InstanceConfig.Models.Config.changeset(%Guard.InstanceConfig.Models.Config{}, %{
94+
name: :CONFIG_TYPE_GITLAB_APP |> Atom.to_string(),
95+
config: %{
96+
client_id: "client_id",
97+
client_secret: "client_secret"
98+
}
99+
})
100+
|> Store.set()
101+
end
102+
103+
defp setup_bitbucket_app_integration do
104+
Guard.InstanceConfig.Models.Config.changeset(%Guard.InstanceConfig.Models.Config{}, %{
105+
name: :CONFIG_TYPE_BITBUCKET_APP |> Atom.to_string(),
106+
config: %{
107+
client_id: "client_id",
108+
client_secret: "client_secret"
109+
}
110+
})
111+
|> Store.set()
112+
end
113+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
defmodule Guard.Services.InstanceConfigInvalidatorWorker.Test do
2+
use Guard.RepoCase
3+
4+
setup do
5+
Support.Guard.Store.clear!()
6+
7+
:ok
8+
end
9+
10+
describe ".handle_message" do
11+
test "Message processes and invalidates the cache" do
12+
default_credentials = %{
13+
client_id: "client_id",
14+
client_secret: "client_secret"
15+
}
16+
17+
Cachex.put(:config_cache, "github_credentials", default_credentials)
18+
Cachex.put(:config_cache, "gitlab_credentials", default_credentials)
19+
Cachex.put(:config_cache, "bitbucket_credentials", default_credentials)
20+
21+
[
22+
:CONFIG_TYPE_GITHUB_APP,
23+
:CONFIG_TYPE_BITBUCKET_APP,
24+
:CONFIG_TYPE_GITLAB_APP
25+
]
26+
|> Enum.each(fn type ->
27+
InternalApi.InstanceConfig.ConfigType.value(type)
28+
|> publish_event()
29+
end)
30+
31+
:timer.sleep(1000)
32+
33+
assert {:ok, false} == Cachex.exists?(:config_cache, "github_credentials")
34+
assert {:ok, false} == Cachex.exists?(:config_cache, "gitlab_credentials")
35+
assert {:ok, false} == Cachex.exists?(:config_cache, "bitbucket_credentials")
36+
end
37+
end
38+
39+
#
40+
# Helpers
41+
#
42+
43+
defp publish_event(type) do
44+
Guard.Events.ConfigModified.publish(type)
45+
end
46+
end

0 commit comments

Comments
 (0)