Skip to content

Commit fd67c54

Browse files
Merge pull request #1068 from Annopaolo/reliable-public-key-retrieval
Backends: fix corner case error in public key retrieval
2 parents b39e088 + 7386077 commit fd67c54

File tree

8 files changed

+193
-100
lines changed

8 files changed

+193
-100
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2222
- [astarte_pairing] Correctly handle Cassandra `varchar`s.
2323
- [astarte_realm_management] Correctly handle Cassandra `varchar`s.
2424
- [astarte_trigger_engine] Correctly handle Cassandra `varchar`s.
25+
- [astarte_pairing] Fix a corner case in the realm public key retrieval
26+
when connection to the database might fail.
27+
- [astarte_realm_management] Fix a corner case in the realm public key retrieval
28+
when connection to the database might fail.
29+
- [astarte_appengine_api] Fix a corner case in the realm public key retrieval
30+
when connection to the database might fail.
2531

2632
## [1.2.0] - 2024-07-02
2733
### Fixed

apps/astarte_appengine_api/lib/astarte_appengine_api/auth/auth.ex

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,14 @@
1818

1919
defmodule Astarte.AppEngine.API.Auth do
2020
alias Astarte.AppEngine.API.Queries
21-
alias Astarte.DataAccess.Database
21+
alias Astarte.AppEngine.API.Config
22+
alias Astarte.Core.CQLUtils
2223

2324
require Logger
2425

2526
def fetch_public_key(realm) do
26-
with {:ok, client} <- Database.connect(realm: realm),
27-
{:ok, public_key} <- Queries.fetch_public_key(client) do
28-
{:ok, public_key}
29-
else
30-
{:error, :public_key_not_found} ->
31-
_ = Logger.warning("No public key found in realm #{realm}.", tag: "no_public_key_found")
32-
{:error, :public_key_not_found}
27+
keyspace = CQLUtils.realm_name_to_keyspace_name(realm, Config.astarte_instance_id!())
3328

34-
{:error, :database_connection_error} ->
35-
_ = Logger.info("Auth request for unexisting realm #{realm}.", tag: "unexisting_realm")
36-
# TODO: random busy wait here to prevent realm enumeration
37-
{:error, :not_existing_realm}
38-
end
29+
Queries.fetch_public_key(keyspace)
3930
end
4031
end

apps/astarte_appengine_api/lib/astarte_appengine_api/queries.ex

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,68 @@ defmodule Astarte.AppEngine.API.Queries do
2323

2424
require Logger
2525

26-
def fetch_public_key(client) do
27-
query =
28-
DatabaseQuery.new()
29-
|> DatabaseQuery.statement(
30-
"SELECT blobAsVarchar(value) FROM kv_store WHERE group='auth' AND key='jwt_public_key_pem'"
31-
)
26+
@keyspace_does_not_exist_regex ~r/Keyspace (.*) does not exist/
27+
28+
def fetch_public_key(keyspace_name) do
29+
case Xandra.Cluster.run(:xandra, &do_fetch_public_key(keyspace_name, &1)) do
30+
{:ok, pem} ->
31+
{:ok, pem}
32+
33+
{:error, %Xandra.ConnectionError{} = err} ->
34+
_ =
35+
Logger.warning("Database connection error #{Exception.message(err)}.",
36+
tag: "database_connection_error"
37+
)
3238

33-
result =
34-
DatabaseQuery.call!(client, query)
35-
|> DatabaseResult.head()
39+
{:error, :database_connection_error}
40+
41+
{:error, %Xandra.Error{} = err} ->
42+
handle_xandra_error(err)
43+
end
44+
end
45+
46+
defp do_fetch_public_key(keyspace_name, conn) do
47+
query = """
48+
SELECT blobAsVarchar(value)
49+
FROM #{keyspace_name}.kv_store
50+
WHERE group='auth' AND key='jwt_public_key_pem';
51+
"""
3652

37-
case result do
38-
["system.blobasvarchar(value)": public_key] ->
39-
{:ok, public_key}
53+
with {:ok, prepared} <- Xandra.prepare(conn, query),
54+
{:ok, page} <-
55+
Xandra.execute(conn, prepared, %{},
56+
uuid_format: :binary,
57+
consistency: :quorum
58+
) do
59+
case Enum.to_list(page) do
60+
[%{"system.blobasvarchar(value)" => pem}] ->
61+
{:ok, pem}
62+
63+
[] ->
64+
{:error, :public_key_not_found}
65+
end
66+
end
67+
end
68+
69+
defp handle_xandra_error(error) do
70+
%Xandra.Error{message: message} = error
71+
72+
case Regex.run(@keyspace_does_not_exist_regex, message) do
73+
[_message, keyspace] ->
74+
Logger.warning("Keyspace #{keyspace} does not exist.",
75+
tag: "realm_not_found"
76+
)
77+
78+
{:error, :not_existing_realm}
79+
80+
nil ->
81+
_ =
82+
Logger.warning(
83+
"Database error, cannot get realm public key: #{Exception.message(error)}.",
84+
tag: "database_error"
85+
)
4086

41-
:empty_dataset ->
42-
{:error, :public_key_not_found}
87+
{:error, :database_error}
4388
end
4489
end
4590

apps/astarte_pairing/lib/astarte_pairing/engine.ex

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,27 +56,10 @@ defmodule Astarte.Pairing.Engine do
5656
end
5757
end
5858

59-
def get_agent_public_key_pems(realm) do
60-
keyspace = CQLUtils.realm_name_to_keyspace_name(realm, Config.astarte_instance_id!())
59+
def get_agent_public_key_pems(realm_name) do
60+
keyspace = CQLUtils.realm_name_to_keyspace_name(realm_name, Config.astarte_instance_id!())
6161

62-
cqex_options =
63-
Config.cqex_options!()
64-
|> Keyword.put(:keyspace, keyspace)
65-
66-
with {:ok, client} <-
67-
Client.new(
68-
Config.cassandra_node!(),
69-
cqex_options
70-
),
71-
{:ok, jwt_pems} <- Queries.get_agent_public_key_pems(client) do
72-
{:ok, jwt_pems}
73-
else
74-
{:error, :shutdown} ->
75-
{:error, :realm_not_found}
76-
77-
{:error, reason} ->
78-
{:error, reason}
79-
end
62+
Queries.get_agent_public_key_pems(keyspace)
8063
end
8164

8265
def get_credentials(

apps/astarte_pairing/lib/astarte_pairing/queries.ex

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,68 @@ defmodule Astarte.Pairing.Queries do
2828
require Logger
2929

3030
@protocol_revision 1
31+
@keyspace_does_not_exist_regex ~r/Keyspace (.*) does not exist/
3132

32-
def get_agent_public_key_pems(client) do
33-
get_jwt_public_key_pem = """
33+
def get_agent_public_key_pems(keyspace_name) do
34+
case Xandra.Cluster.run(:xandra, &do_get_agent_public_key_pems(keyspace_name, &1)) do
35+
{:ok, pems} ->
36+
{:ok, pems}
37+
38+
{:error, %Xandra.ConnectionError{} = err} ->
39+
_ =
40+
Logger.warning("Database connection error #{Exception.message(err)}.",
41+
tag: "database_connection_error"
42+
)
43+
44+
{:error, :database_connection_error}
45+
46+
{:error, %Xandra.Error{} = err} ->
47+
handle_xandra_error(err)
48+
end
49+
end
50+
51+
defp handle_xandra_error(error) do
52+
%Xandra.Error{message: message} = error
53+
54+
case Regex.run(@keyspace_does_not_exist_regex, message) do
55+
[_message, keyspace] ->
56+
Logger.warning("Keyspace #{keyspace} does not exist.",
57+
tag: "realm_not_found"
58+
)
59+
60+
{:error, :realm_not_found}
61+
62+
nil ->
63+
_ =
64+
Logger.warning(
65+
"Database error, cannot get realm public key: #{Exception.message(error)}.",
66+
tag: "database_error"
67+
)
68+
69+
{:error, :database_error}
70+
end
71+
end
72+
73+
defp do_get_agent_public_key_pems(keyspace_name, conn) do
74+
query = """
3475
SELECT blobAsVarchar(value)
35-
FROM kv_store
76+
FROM #{keyspace_name}.kv_store
3677
WHERE group='auth' AND key='jwt_public_key_pem';
3778
"""
3879

39-
# TODO: add additional keys
40-
query =
41-
Query.new()
42-
|> Query.statement(get_jwt_public_key_pem)
43-
44-
with {:ok, res} <- Query.call(client, query),
45-
["system.blobasvarchar(value)": pem] <- Result.head(res) do
46-
{:ok, [pem]}
47-
else
48-
:empty_dataset ->
49-
{:error, :public_key_not_found}
80+
with {:ok, prepared} <- Xandra.prepare(conn, query),
81+
{:ok, page} <-
82+
Xandra.execute(conn, prepared, %{},
83+
uuid_format: :binary,
84+
consistency: :quorum
85+
) do
86+
case Enum.to_list(page) do
87+
[%{"system.blobasvarchar(value)" => pem}] ->
88+
{:ok, [pem]}
5089

51-
error ->
52-
Logger.warning("DB error: #{inspect(error)}")
53-
{:error, :database_error}
90+
[] ->
91+
{:error, :public_key_not_found}
92+
end
5493
end
5594
end
5695

apps/astarte_realm_management/lib/astarte_realm_management/engine.ex

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -477,20 +477,7 @@ defmodule Astarte.RealmManagement.Engine do
477477
keyspace_name =
478478
CQLUtils.realm_name_to_keyspace_name(realm_name, Config.astarte_instance_id!())
479479

480-
cqex_options =
481-
Config.cqex_options!()
482-
|> Keyword.put(:keyspace, keyspace_name)
483-
484-
with {:ok, client} <-
485-
DatabaseClient.new(
486-
Config.cassandra_node!(),
487-
cqex_options
488-
) do
489-
Queries.get_jwt_public_key_pem(client)
490-
else
491-
{:error, :shutdown} ->
492-
{:error, :realm_not_found}
493-
end
480+
Queries.get_jwt_public_key_pem(keyspace_name)
494481
end
495482

496483
def update_jwt_public_key_pem(realm_name, jwt_public_key_pem) do

apps/astarte_realm_management/lib/astarte_realm_management/queries.ex

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,13 @@ defmodule Astarte.RealmManagement.Queries do
9393
)
9494
"""
9595

96-
@query_jwt_public_key_pem """
97-
SELECT blobAsVarchar(value)
98-
FROM kv_store
99-
WHERE group='auth' AND key='jwt_public_key_pem';
100-
"""
101-
10296
@query_insert_jwt_public_key_pem """
10397
INSERT INTO kv_store (group, key, value)
10498
VALUES ('auth', 'jwt_public_key_pem', varcharAsBlob(:pem));
10599
"""
106100

101+
@keyspace_does_not_exist_regex ~r/Keyspace (.*) does not exist/
102+
107103
defp create_one_object_columns_for_mappings(mappings) do
108104
for %Mapping{endpoint: endpoint, value_type: value_type} <- mappings do
109105
column_name = CQLUtils.endpoint_to_db_column_name(endpoint)
@@ -1091,17 +1087,66 @@ defmodule Astarte.RealmManagement.Queries do
10911087
end
10921088
end
10931089

1094-
def get_jwt_public_key_pem(client) do
1095-
query =
1096-
DatabaseQuery.new()
1097-
|> DatabaseQuery.statement(@query_jwt_public_key_pem)
1090+
def get_jwt_public_key_pem(keyspace) do
1091+
case Xandra.Cluster.run(:xandra, &do_get_jwt_public_key_pem(keyspace, &1)) do
1092+
{:ok, pem} ->
1093+
{:ok, pem}
10981094

1099-
with {:ok, result} <- DatabaseQuery.call(client, query),
1100-
["system.blobasvarchar(value)": pem] <- DatabaseResult.head(result) do
1101-
{:ok, pem}
1102-
else
1103-
_ ->
1104-
{:error, :public_key_not_found}
1095+
{:error, %Xandra.ConnectionError{} = err} ->
1096+
_ =
1097+
Logger.warning("Database connection error #{Exception.message(err)}.",
1098+
tag: "database_connection_error"
1099+
)
1100+
1101+
{:error, :database_connection_error}
1102+
1103+
{:error, %Xandra.Error{} = err} ->
1104+
handle_xandra_error(err)
1105+
end
1106+
end
1107+
1108+
defp do_get_jwt_public_key_pem(keyspace_name, conn) do
1109+
query = """
1110+
SELECT blobAsVarchar(value)
1111+
FROM #{keyspace_name}.kv_store
1112+
WHERE group='auth' AND key='jwt_public_key_pem';
1113+
"""
1114+
1115+
with {:ok, prepared} <- Xandra.prepare(conn, query),
1116+
{:ok, page} <-
1117+
Xandra.execute(conn, prepared, %{},
1118+
uuid_format: :binary,
1119+
consistency: :quorum
1120+
) do
1121+
case Enum.to_list(page) do
1122+
[%{"system.blobasvarchar(value)": pem}] ->
1123+
{:ok, pem}
1124+
1125+
[] ->
1126+
{:error, :public_key_not_found}
1127+
end
1128+
end
1129+
end
1130+
1131+
defp handle_xandra_error(error) do
1132+
%Xandra.Error{message: message} = error
1133+
1134+
case Regex.run(@keyspace_does_not_exist_regex, message) do
1135+
[_message, keyspace] ->
1136+
Logger.warning("Keyspace #{keyspace} does not exist.",
1137+
tag: "realm_not_found"
1138+
)
1139+
1140+
{:error, :realm_not_found}
1141+
1142+
nil ->
1143+
_ =
1144+
Logger.warning(
1145+
"Database error, cannot get realm public key: #{Exception.message(error)}.",
1146+
tag: "database_error"
1147+
)
1148+
1149+
{:error, :database_error}
11051150
end
11061151
end
11071152

apps/astarte_realm_management/test/astarte_realm_management/queries_test.exs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -560,10 +560,7 @@ defmodule Astarte.RealmManagement.QueriesTest do
560560
end
561561

562562
test "get JWT public key PEM" do
563-
DatabaseTestHelper.connect_to_test_database()
564-
client = connect_to_test_realm("autotestrealm")
565-
566-
assert Queries.get_jwt_public_key_pem(client) ==
563+
assert Queries.get_jwt_public_key_pem("autotestrealm") ==
567564
{:ok, DatabaseTestHelper.jwt_public_key_pem_fixture()}
568565
end
569566

@@ -573,15 +570,15 @@ defmodule Astarte.RealmManagement.QueriesTest do
573570

574571
new_pem = "not_exactly_a_PEM_but_will_do"
575572
assert Queries.update_jwt_public_key_pem(client, new_pem) == :ok
576-
assert Queries.get_jwt_public_key_pem(client) == {:ok, new_pem}
573+
assert Queries.get_jwt_public_key_pem("autotestrealm") == {:ok, new_pem}
577574

578575
# Put the PEM fixture back
579576
assert Queries.update_jwt_public_key_pem(
580577
client,
581578
DatabaseTestHelper.jwt_public_key_pem_fixture()
582579
) == :ok
583580

584-
assert Queries.get_jwt_public_key_pem(client) ==
581+
assert Queries.get_jwt_public_key_pem("autotestrealm") ==
585582
{:ok, DatabaseTestHelper.jwt_public_key_pem_fixture()}
586583
end
587584

0 commit comments

Comments
 (0)