flwr-nightly 1.15.0.dev20250121__py3-none-any.whl → 1.15.0.dev20250122__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/cli/config_utils.py +9 -145
- flwr/cli/install.py +1 -1
- flwr/client/clientapp/utils.py +1 -1
- flwr/client/grpc_rere_client/client_interceptor.py +19 -125
- flwr/common/config.py +132 -6
- flwr/common/constant.py +6 -0
- flwr/common/exit/exit_code.py +8 -5
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -88
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +95 -169
- flwr/simulation/app.py +4 -6
- {flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/METADATA +1 -1
- {flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/RECORD +15 -15
- {flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/entry_points.txt +0 -0
flwr/cli/config_utils.py
CHANGED
@@ -15,53 +15,13 @@
|
|
15
15
|
"""Utility to validate the `pyproject.toml` file."""
|
16
16
|
|
17
17
|
|
18
|
-
import zipfile
|
19
|
-
from io import BytesIO
|
20
18
|
from pathlib import Path
|
21
|
-
from typing import
|
19
|
+
from typing import Any, Optional, Union
|
22
20
|
|
23
21
|
import tomli
|
24
22
|
import typer
|
25
23
|
|
26
|
-
from flwr.common import
|
27
|
-
from flwr.common.typing import UserConfigValue
|
28
|
-
|
29
|
-
|
30
|
-
def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
|
31
|
-
"""Extract the config from a FAB file or path.
|
32
|
-
|
33
|
-
Parameters
|
34
|
-
----------
|
35
|
-
fab_file : Union[Path, bytes]
|
36
|
-
The Flower App Bundle file to validate and extract the metadata from.
|
37
|
-
It can either be a path to the file or the file itself as bytes.
|
38
|
-
|
39
|
-
Returns
|
40
|
-
-------
|
41
|
-
Dict[str, Any]
|
42
|
-
The `config` of the given Flower App Bundle.
|
43
|
-
"""
|
44
|
-
fab_file_archive: Union[Path, IO[bytes]]
|
45
|
-
if isinstance(fab_file, bytes):
|
46
|
-
fab_file_archive = BytesIO(fab_file)
|
47
|
-
elif isinstance(fab_file, Path):
|
48
|
-
fab_file_archive = fab_file
|
49
|
-
else:
|
50
|
-
raise ValueError("fab_file must be either a Path or bytes")
|
51
|
-
|
52
|
-
with zipfile.ZipFile(fab_file_archive, "r") as zipf:
|
53
|
-
with zipf.open("pyproject.toml") as file:
|
54
|
-
toml_content = file.read().decode("utf-8")
|
55
|
-
|
56
|
-
conf = load_from_string(toml_content)
|
57
|
-
if conf is None:
|
58
|
-
raise ValueError("Invalid TOML content in pyproject.toml")
|
59
|
-
|
60
|
-
is_valid, errors, _ = validate(conf, check_module=False)
|
61
|
-
if not is_valid:
|
62
|
-
raise ValueError(errors)
|
63
|
-
|
64
|
-
return conf
|
24
|
+
from flwr.common.config import get_fab_config, get_metadata_from_config, validate_config
|
65
25
|
|
66
26
|
|
67
27
|
def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
|
@@ -78,12 +38,7 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
|
|
78
38
|
Tuple[str, str]
|
79
39
|
The `fab_id` and `fab_version` of the given Flower App Bundle.
|
80
40
|
"""
|
81
|
-
|
82
|
-
|
83
|
-
return (
|
84
|
-
f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}",
|
85
|
-
conf["project"]["version"],
|
86
|
-
)
|
41
|
+
return get_metadata_from_config(get_fab_config(fab_file))
|
87
42
|
|
88
43
|
|
89
44
|
def load_and_validate(
|
@@ -120,7 +75,7 @@ def load_and_validate(
|
|
120
75
|
]
|
121
76
|
return (None, errors, [])
|
122
77
|
|
123
|
-
is_valid, errors, warnings =
|
78
|
+
is_valid, errors, warnings = validate_config(config, check_module, path.parent)
|
124
79
|
|
125
80
|
if not is_valid:
|
126
81
|
return (None, errors, warnings)
|
@@ -133,102 +88,11 @@ def load(toml_path: Path) -> Optional[dict[str, Any]]:
|
|
133
88
|
if not toml_path.is_file():
|
134
89
|
return None
|
135
90
|
|
136
|
-
with toml_path.open(
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
for key, value in config_dict.items():
|
142
|
-
if isinstance(value, dict):
|
143
|
-
_validate_run_config(config_dict[key], errors)
|
144
|
-
elif not isinstance(value, get_args(UserConfigValue)):
|
145
|
-
raise ValueError(
|
146
|
-
f"The value for key {key} needs to be of type `int`, `float`, "
|
147
|
-
"`bool, `str`, or a `dict` of those.",
|
148
|
-
)
|
149
|
-
|
150
|
-
|
151
|
-
# pylint: disable=too-many-branches
|
152
|
-
def validate_fields(config: dict[str, Any]) -> tuple[bool, list[str], list[str]]:
|
153
|
-
"""Validate pyproject.toml fields."""
|
154
|
-
errors = []
|
155
|
-
warnings = []
|
156
|
-
|
157
|
-
if "project" not in config:
|
158
|
-
errors.append("Missing [project] section")
|
159
|
-
else:
|
160
|
-
if "name" not in config["project"]:
|
161
|
-
errors.append('Property "name" missing in [project]')
|
162
|
-
if "version" not in config["project"]:
|
163
|
-
errors.append('Property "version" missing in [project]')
|
164
|
-
if "description" not in config["project"]:
|
165
|
-
warnings.append('Recommended property "description" missing in [project]')
|
166
|
-
if "license" not in config["project"]:
|
167
|
-
warnings.append('Recommended property "license" missing in [project]')
|
168
|
-
if "authors" not in config["project"]:
|
169
|
-
warnings.append('Recommended property "authors" missing in [project]')
|
170
|
-
|
171
|
-
if (
|
172
|
-
"tool" not in config
|
173
|
-
or "flwr" not in config["tool"]
|
174
|
-
or "app" not in config["tool"]["flwr"]
|
175
|
-
):
|
176
|
-
errors.append("Missing [tool.flwr.app] section")
|
177
|
-
else:
|
178
|
-
if "publisher" not in config["tool"]["flwr"]["app"]:
|
179
|
-
errors.append('Property "publisher" missing in [tool.flwr.app]')
|
180
|
-
if "config" in config["tool"]["flwr"]["app"]:
|
181
|
-
_validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
|
182
|
-
if "components" not in config["tool"]["flwr"]["app"]:
|
183
|
-
errors.append("Missing [tool.flwr.app.components] section")
|
184
|
-
else:
|
185
|
-
if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
|
186
|
-
errors.append(
|
187
|
-
'Property "serverapp" missing in [tool.flwr.app.components]'
|
188
|
-
)
|
189
|
-
if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
|
190
|
-
errors.append(
|
191
|
-
'Property "clientapp" missing in [tool.flwr.app.components]'
|
192
|
-
)
|
193
|
-
|
194
|
-
return len(errors) == 0, errors, warnings
|
195
|
-
|
196
|
-
|
197
|
-
def validate(
|
198
|
-
config: dict[str, Any],
|
199
|
-
check_module: bool = True,
|
200
|
-
project_dir: Optional[Union[str, Path]] = None,
|
201
|
-
) -> tuple[bool, list[str], list[str]]:
|
202
|
-
"""Validate pyproject.toml."""
|
203
|
-
is_valid, errors, warnings = validate_fields(config)
|
204
|
-
|
205
|
-
if not is_valid:
|
206
|
-
return False, errors, warnings
|
207
|
-
|
208
|
-
# Validate serverapp
|
209
|
-
serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
|
210
|
-
is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
|
211
|
-
|
212
|
-
if not is_valid and isinstance(reason, str):
|
213
|
-
return False, [reason], []
|
214
|
-
|
215
|
-
# Validate clientapp
|
216
|
-
clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
217
|
-
is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
|
218
|
-
|
219
|
-
if not is_valid and isinstance(reason, str):
|
220
|
-
return False, [reason], []
|
221
|
-
|
222
|
-
return True, [], []
|
223
|
-
|
224
|
-
|
225
|
-
def load_from_string(toml_content: str) -> Optional[dict[str, Any]]:
|
226
|
-
"""Load TOML content from a string and return as dict."""
|
227
|
-
try:
|
228
|
-
data = tomli.loads(toml_content)
|
229
|
-
return data
|
230
|
-
except tomli.TOMLDecodeError:
|
231
|
-
return None
|
91
|
+
with toml_path.open("rb") as toml_file:
|
92
|
+
try:
|
93
|
+
return tomli.load(toml_file)
|
94
|
+
except tomli.TOMLDecodeError:
|
95
|
+
return None
|
232
96
|
|
233
97
|
|
234
98
|
def process_loaded_project_config(
|
flwr/cli/install.py
CHANGED
@@ -154,7 +154,7 @@ def validate_and_install(
|
|
154
154
|
)
|
155
155
|
raise typer.Exit(code=1)
|
156
156
|
|
157
|
-
|
157
|
+
fab_id, version = get_metadata_from_config(config)
|
158
158
|
publisher, project_name = fab_id.split("/")
|
159
159
|
config_metadata = (publisher, project_name, version, fab_hash)
|
160
160
|
|
flwr/client/clientapp/utils.py
CHANGED
@@ -66,7 +66,7 @@ def get_load_client_app_fn(
|
|
66
66
|
# `fab_hash` is not required since the app is loaded from `runtime_app_dir`.
|
67
67
|
elif app_path is not None:
|
68
68
|
config = get_project_config(runtime_app_dir)
|
69
|
-
|
69
|
+
this_fab_id, this_fab_version = get_metadata_from_config(config)
|
70
70
|
|
71
71
|
if this_fab_version != fab_version or this_fab_id != fab_id:
|
72
72
|
raise LoadClientAppError(
|
@@ -15,71 +15,18 @@
|
|
15
15
|
"""Flower client interceptor."""
|
16
16
|
|
17
17
|
|
18
|
-
import
|
19
|
-
import collections
|
20
|
-
from collections.abc import Sequence
|
21
|
-
from logging import WARNING
|
22
|
-
from typing import Any, Callable, Optional, Union
|
18
|
+
from typing import Any, Callable
|
23
19
|
|
24
20
|
import grpc
|
25
21
|
from cryptography.hazmat.primitives.asymmetric import ec
|
22
|
+
from google.protobuf.message import Message as GrpcMessage
|
26
23
|
|
27
|
-
from flwr.common
|
24
|
+
from flwr.common import now
|
25
|
+
from flwr.common.constant import PUBLIC_KEY_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER
|
28
26
|
from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
|
29
|
-
bytes_to_public_key,
|
30
|
-
compute_hmac,
|
31
|
-
generate_shared_key,
|
32
27
|
public_key_to_bytes,
|
28
|
+
sign_message,
|
33
29
|
)
|
34
|
-
from flwr.proto.fab_pb2 import GetFabRequest # pylint: disable=E0611
|
35
|
-
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
36
|
-
CreateNodeRequest,
|
37
|
-
DeleteNodeRequest,
|
38
|
-
PingRequest,
|
39
|
-
PullMessagesRequest,
|
40
|
-
PullTaskInsRequest,
|
41
|
-
PushMessagesRequest,
|
42
|
-
PushTaskResRequest,
|
43
|
-
)
|
44
|
-
from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611
|
45
|
-
|
46
|
-
_PUBLIC_KEY_HEADER = "public-key"
|
47
|
-
_AUTH_TOKEN_HEADER = "auth-token"
|
48
|
-
|
49
|
-
Request = Union[
|
50
|
-
CreateNodeRequest,
|
51
|
-
DeleteNodeRequest,
|
52
|
-
PullTaskInsRequest,
|
53
|
-
PushTaskResRequest,
|
54
|
-
GetRunRequest,
|
55
|
-
PingRequest,
|
56
|
-
GetFabRequest,
|
57
|
-
PullMessagesRequest,
|
58
|
-
PushMessagesRequest,
|
59
|
-
]
|
60
|
-
|
61
|
-
|
62
|
-
def _get_value_from_tuples(
|
63
|
-
key_string: str, tuples: Sequence[tuple[str, Union[str, bytes]]]
|
64
|
-
) -> bytes:
|
65
|
-
value = next((value for key, value in tuples if key == key_string), "")
|
66
|
-
if isinstance(value, str):
|
67
|
-
return value.encode()
|
68
|
-
|
69
|
-
return value
|
70
|
-
|
71
|
-
|
72
|
-
class _ClientCallDetails(
|
73
|
-
collections.namedtuple(
|
74
|
-
"_ClientCallDetails", ("method", "timeout", "metadata", "credentials")
|
75
|
-
),
|
76
|
-
grpc.ClientCallDetails, # type: ignore
|
77
|
-
):
|
78
|
-
"""Details for each client call.
|
79
|
-
|
80
|
-
The class will be passed on as the first argument in continuation function.
|
81
|
-
In our case, `AuthenticateClientInterceptor` adds new metadata to the construct.
|
82
|
-
"""
|
83
30
|
|
84
31
|
|
85
32
|
class AuthenticateClientInterceptor(grpc.UnaryUnaryClientInterceptor): # type: ignore
|
@@ -91,86 +38,33 @@ class AuthenticateClientInterceptor(grpc.UnaryUnaryClientInterceptor): # type:
|
|
91
38
|
public_key: ec.EllipticCurvePublicKey,
|
92
39
|
):
|
93
40
|
self.private_key = private_key
|
94
|
-
self.
|
95
|
-
self.shared_secret: Optional[bytes] = None
|
96
|
-
self.server_public_key: Optional[ec.EllipticCurvePublicKey] = None
|
97
|
-
self.encoded_public_key = base64.urlsafe_b64encode(
|
98
|
-
public_key_to_bytes(self.public_key)
|
99
|
-
)
|
41
|
+
self.public_key_bytes = public_key_to_bytes(public_key)
|
100
42
|
|
101
43
|
def intercept_unary_unary(
|
102
44
|
self,
|
103
45
|
continuation: Callable[[Any, Any], Any],
|
104
46
|
client_call_details: grpc.ClientCallDetails,
|
105
|
-
request:
|
47
|
+
request: GrpcMessage,
|
106
48
|
) -> grpc.Call:
|
107
49
|
"""Flower client interceptor.
|
108
50
|
|
109
51
|
Intercept unary call from client and add necessary authentication header in the
|
110
52
|
RPC metadata.
|
111
53
|
"""
|
112
|
-
metadata = []
|
113
|
-
postprocess = False
|
114
|
-
if client_call_details.metadata is not None:
|
115
|
-
metadata = list(client_call_details.metadata)
|
116
|
-
|
117
|
-
# Always add the public key header
|
118
|
-
metadata.append(
|
119
|
-
(
|
120
|
-
_PUBLIC_KEY_HEADER,
|
121
|
-
self.encoded_public_key,
|
122
|
-
)
|
123
|
-
)
|
124
|
-
|
125
|
-
if isinstance(request, CreateNodeRequest):
|
126
|
-
postprocess = True
|
127
|
-
elif isinstance(
|
128
|
-
request,
|
129
|
-
(
|
130
|
-
DeleteNodeRequest,
|
131
|
-
PullTaskInsRequest,
|
132
|
-
PushTaskResRequest,
|
133
|
-
GetRunRequest,
|
134
|
-
PingRequest,
|
135
|
-
GetFabRequest,
|
136
|
-
PullMessagesRequest,
|
137
|
-
PushMessagesRequest,
|
138
|
-
),
|
139
|
-
):
|
140
|
-
if self.shared_secret is None:
|
141
|
-
raise RuntimeError("Failure to compute hmac")
|
142
|
-
|
143
|
-
message_bytes = request.SerializeToString(deterministic=True)
|
144
|
-
metadata.append(
|
145
|
-
(
|
146
|
-
_AUTH_TOKEN_HEADER,
|
147
|
-
base64.urlsafe_b64encode(
|
148
|
-
compute_hmac(self.shared_secret, message_bytes)
|
149
|
-
),
|
150
|
-
)
|
151
|
-
)
|
54
|
+
metadata = list(client_call_details.metadata or [])
|
152
55
|
|
153
|
-
|
154
|
-
|
155
|
-
client_call_details.timeout,
|
156
|
-
metadata,
|
157
|
-
client_call_details.credentials,
|
158
|
-
)
|
56
|
+
# Add the public key
|
57
|
+
metadata.append((PUBLIC_KEY_HEADER, self.public_key_bytes))
|
159
58
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
_get_value_from_tuples(_PUBLIC_KEY_HEADER, response.initial_metadata())
|
164
|
-
)
|
59
|
+
# Add timestamp
|
60
|
+
timestamp = now().isoformat()
|
61
|
+
metadata.append((TIMESTAMP_HEADER, timestamp))
|
165
62
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
log(WARNING, "Can't get server public key, SuperLink may be offline")
|
63
|
+
# Sign and add the signature
|
64
|
+
signature = sign_message(self.private_key, timestamp.encode("ascii"))
|
65
|
+
metadata.append((SIGNATURE_HEADER, signature))
|
170
66
|
|
171
|
-
|
172
|
-
|
173
|
-
self.private_key, self.server_public_key
|
174
|
-
)
|
67
|
+
# Overwrite the metadata
|
68
|
+
details = client_call_details._replace(metadata=metadata)
|
175
69
|
|
176
|
-
return
|
70
|
+
return continuation(details, request)
|
flwr/common/config.py
CHANGED
@@ -17,13 +17,13 @@
|
|
17
17
|
|
18
18
|
import os
|
19
19
|
import re
|
20
|
+
import zipfile
|
21
|
+
from io import BytesIO
|
20
22
|
from pathlib import Path
|
21
|
-
from typing import Any, Optional, Union, cast, get_args
|
23
|
+
from typing import IO, Any, Optional, Union, cast, get_args
|
22
24
|
|
23
25
|
import tomli
|
24
26
|
|
25
|
-
from flwr.cli.config_utils import get_fab_config, validate_fields
|
26
|
-
from flwr.common import ConfigsRecord
|
27
27
|
from flwr.common.constant import (
|
28
28
|
APP_DIR,
|
29
29
|
FAB_CONFIG_FILE,
|
@@ -33,6 +33,8 @@ from flwr.common.constant import (
|
|
33
33
|
)
|
34
34
|
from flwr.common.typing import Run, UserConfig, UserConfigValue
|
35
35
|
|
36
|
+
from . import ConfigsRecord, object_ref
|
37
|
+
|
36
38
|
|
37
39
|
def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
|
38
40
|
"""Return the Flower home directory based on env variables."""
|
@@ -80,7 +82,7 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
|
|
80
82
|
config = tomli.loads(toml_file.read())
|
81
83
|
|
82
84
|
# Validate pyproject.toml fields
|
83
|
-
is_valid, errors, _ =
|
85
|
+
is_valid, errors, _ = validate_fields_in_config(config)
|
84
86
|
if not is_valid:
|
85
87
|
error_msg = "\n".join([f" - {error}" for error in errors])
|
86
88
|
raise ValueError(
|
@@ -227,10 +229,10 @@ def parse_config_args(
|
|
227
229
|
|
228
230
|
|
229
231
|
def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
|
230
|
-
"""Extract `
|
232
|
+
"""Extract `fab_id` and `fab_version` from a project config."""
|
231
233
|
return (
|
232
|
-
config["project"]["version"],
|
233
234
|
f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
|
235
|
+
config["project"]["version"],
|
234
236
|
)
|
235
237
|
|
236
238
|
|
@@ -241,3 +243,127 @@ def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
|
|
241
243
|
c_record[k] = v
|
242
244
|
|
243
245
|
return c_record
|
246
|
+
|
247
|
+
|
248
|
+
def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
|
249
|
+
"""Extract the config from a FAB file or path.
|
250
|
+
|
251
|
+
Parameters
|
252
|
+
----------
|
253
|
+
fab_file : Union[Path, bytes]
|
254
|
+
The Flower App Bundle file to validate and extract the metadata from.
|
255
|
+
It can either be a path to the file or the file itself as bytes.
|
256
|
+
|
257
|
+
Returns
|
258
|
+
-------
|
259
|
+
Dict[str, Any]
|
260
|
+
The `config` of the given Flower App Bundle.
|
261
|
+
"""
|
262
|
+
fab_file_archive: Union[Path, IO[bytes]]
|
263
|
+
if isinstance(fab_file, bytes):
|
264
|
+
fab_file_archive = BytesIO(fab_file)
|
265
|
+
elif isinstance(fab_file, Path):
|
266
|
+
fab_file_archive = fab_file
|
267
|
+
else:
|
268
|
+
raise ValueError("fab_file must be either a Path or bytes")
|
269
|
+
|
270
|
+
with zipfile.ZipFile(fab_file_archive, "r") as zipf:
|
271
|
+
with zipf.open("pyproject.toml") as file:
|
272
|
+
toml_content = file.read().decode("utf-8")
|
273
|
+
try:
|
274
|
+
conf = tomli.loads(toml_content)
|
275
|
+
except tomli.TOMLDecodeError:
|
276
|
+
raise ValueError("Invalid TOML content in pyproject.toml") from None
|
277
|
+
|
278
|
+
is_valid, errors, _ = validate_config(conf, check_module=False)
|
279
|
+
if not is_valid:
|
280
|
+
raise ValueError(errors)
|
281
|
+
|
282
|
+
return conf
|
283
|
+
|
284
|
+
|
285
|
+
def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
|
286
|
+
for key, value in config_dict.items():
|
287
|
+
if isinstance(value, dict):
|
288
|
+
_validate_run_config(config_dict[key], errors)
|
289
|
+
elif not isinstance(value, get_args(UserConfigValue)):
|
290
|
+
raise ValueError(
|
291
|
+
f"The value for key {key} needs to be of type `int`, `float`, "
|
292
|
+
"`bool, `str`, or a `dict` of those.",
|
293
|
+
)
|
294
|
+
|
295
|
+
|
296
|
+
# pylint: disable=too-many-branches
|
297
|
+
def validate_fields_in_config(
|
298
|
+
config: dict[str, Any]
|
299
|
+
) -> tuple[bool, list[str], list[str]]:
|
300
|
+
"""Validate pyproject.toml fields."""
|
301
|
+
errors = []
|
302
|
+
warnings = []
|
303
|
+
|
304
|
+
if "project" not in config:
|
305
|
+
errors.append("Missing [project] section")
|
306
|
+
else:
|
307
|
+
if "name" not in config["project"]:
|
308
|
+
errors.append('Property "name" missing in [project]')
|
309
|
+
if "version" not in config["project"]:
|
310
|
+
errors.append('Property "version" missing in [project]')
|
311
|
+
if "description" not in config["project"]:
|
312
|
+
warnings.append('Recommended property "description" missing in [project]')
|
313
|
+
if "license" not in config["project"]:
|
314
|
+
warnings.append('Recommended property "license" missing in [project]')
|
315
|
+
if "authors" not in config["project"]:
|
316
|
+
warnings.append('Recommended property "authors" missing in [project]')
|
317
|
+
|
318
|
+
if (
|
319
|
+
"tool" not in config
|
320
|
+
or "flwr" not in config["tool"]
|
321
|
+
or "app" not in config["tool"]["flwr"]
|
322
|
+
):
|
323
|
+
errors.append("Missing [tool.flwr.app] section")
|
324
|
+
else:
|
325
|
+
if "publisher" not in config["tool"]["flwr"]["app"]:
|
326
|
+
errors.append('Property "publisher" missing in [tool.flwr.app]')
|
327
|
+
if "config" in config["tool"]["flwr"]["app"]:
|
328
|
+
_validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
|
329
|
+
if "components" not in config["tool"]["flwr"]["app"]:
|
330
|
+
errors.append("Missing [tool.flwr.app.components] section")
|
331
|
+
else:
|
332
|
+
if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
|
333
|
+
errors.append(
|
334
|
+
'Property "serverapp" missing in [tool.flwr.app.components]'
|
335
|
+
)
|
336
|
+
if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
|
337
|
+
errors.append(
|
338
|
+
'Property "clientapp" missing in [tool.flwr.app.components]'
|
339
|
+
)
|
340
|
+
|
341
|
+
return len(errors) == 0, errors, warnings
|
342
|
+
|
343
|
+
|
344
|
+
def validate_config(
|
345
|
+
config: dict[str, Any],
|
346
|
+
check_module: bool = True,
|
347
|
+
project_dir: Optional[Union[str, Path]] = None,
|
348
|
+
) -> tuple[bool, list[str], list[str]]:
|
349
|
+
"""Validate pyproject.toml."""
|
350
|
+
is_valid, errors, warnings = validate_fields_in_config(config)
|
351
|
+
|
352
|
+
if not is_valid:
|
353
|
+
return False, errors, warnings
|
354
|
+
|
355
|
+
# Validate serverapp
|
356
|
+
serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
|
357
|
+
is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
|
358
|
+
|
359
|
+
if not is_valid and isinstance(reason, str):
|
360
|
+
return False, [reason], []
|
361
|
+
|
362
|
+
# Validate clientapp
|
363
|
+
clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
|
364
|
+
is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
|
365
|
+
|
366
|
+
if not is_valid and isinstance(reason, str):
|
367
|
+
return False, [reason], []
|
368
|
+
|
369
|
+
return True, [], []
|
flwr/common/constant.py
CHANGED
@@ -112,6 +112,12 @@ AUTH_TYPE = "auth_type"
|
|
112
112
|
ACCESS_TOKEN_KEY = "access_token"
|
113
113
|
REFRESH_TOKEN_KEY = "refresh_token"
|
114
114
|
|
115
|
+
# Constants for node authentication
|
116
|
+
PUBLIC_KEY_HEADER = "public-key-bin" # Must end with "-bin" for binary data
|
117
|
+
SIGNATURE_HEADER = "signature-bin" # Must end with "-bin" for binary data
|
118
|
+
TIMESTAMP_HEADER = "timestamp"
|
119
|
+
TIMESTAMP_TOLERANCE = 10 # Tolerance for timestamp verification
|
120
|
+
|
115
121
|
|
116
122
|
class MessageType:
|
117
123
|
"""Message type."""
|
flwr/common/exit/exit_code.py
CHANGED
@@ -39,10 +39,12 @@ class ExitCode:
|
|
39
39
|
|
40
40
|
# ClientApp-specific exit codes (400-499)
|
41
41
|
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
# Simulation-specific exit codes (500-599)
|
43
|
+
|
44
|
+
# Common exit codes (600-)
|
45
|
+
COMMON_ADDRESS_INVALID = 600
|
46
|
+
COMMON_MISSING_EXTRA_REST = 601
|
47
|
+
COMMON_TLS_NOT_SUPPORTED = 602
|
46
48
|
|
47
49
|
def __new__(cls) -> ExitCode:
|
48
50
|
"""Prevent instantiation."""
|
@@ -75,7 +77,8 @@ EXIT_CODE_HELP = {
|
|
75
77
|
"file and try again."
|
76
78
|
),
|
77
79
|
# ClientApp-specific exit codes (400-499)
|
78
|
-
#
|
80
|
+
# Simulation-specific exit codes (500-599)
|
81
|
+
# Common exit codes (600-)
|
79
82
|
ExitCode.COMMON_ADDRESS_INVALID: (
|
80
83
|
"Please provide a valid URL, IPv4 or IPv6 address."
|
81
84
|
),
|
@@ -15,7 +15,7 @@
|
|
15
15
|
"""Fleet API gRPC adapter servicer."""
|
16
16
|
|
17
17
|
|
18
|
-
from logging import DEBUG
|
18
|
+
from logging import DEBUG
|
19
19
|
from typing import Callable, TypeVar
|
20
20
|
|
21
21
|
import grpc
|
@@ -31,35 +31,30 @@ from flwr.common.constant import (
|
|
31
31
|
from flwr.common.logger import log
|
32
32
|
from flwr.common.version import package_name, package_version
|
33
33
|
from flwr.proto import grpcadapter_pb2_grpc # pylint: disable=E0611
|
34
|
-
from flwr.proto.fab_pb2 import GetFabRequest
|
34
|
+
from flwr.proto.fab_pb2 import GetFabRequest # pylint: disable=E0611
|
35
35
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
36
36
|
CreateNodeRequest,
|
37
|
-
CreateNodeResponse,
|
38
37
|
DeleteNodeRequest,
|
39
|
-
DeleteNodeResponse,
|
40
38
|
PingRequest,
|
41
|
-
|
42
|
-
|
43
|
-
PullTaskInsResponse,
|
44
|
-
PushTaskResRequest,
|
45
|
-
PushTaskResResponse,
|
39
|
+
PullMessagesRequest,
|
40
|
+
PushMessagesRequest,
|
46
41
|
)
|
47
42
|
from flwr.proto.grpcadapter_pb2 import MessageContainer # pylint: disable=E0611
|
48
|
-
from flwr.proto.run_pb2 import GetRunRequest
|
49
|
-
|
50
|
-
from
|
51
|
-
from flwr.server.superlink.linkstate import LinkStateFactory
|
43
|
+
from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611
|
44
|
+
|
45
|
+
from ..grpc_rere.fleet_servicer import FleetServicer
|
52
46
|
|
53
47
|
T = TypeVar("T", bound=GrpcMessage)
|
54
48
|
|
55
49
|
|
56
50
|
def _handle(
|
57
51
|
msg_container: MessageContainer,
|
52
|
+
context: grpc.ServicerContext,
|
58
53
|
request_type: type[T],
|
59
|
-
handler: Callable[[T], GrpcMessage],
|
54
|
+
handler: Callable[[T, grpc.ServicerContext], GrpcMessage],
|
60
55
|
) -> MessageContainer:
|
61
56
|
req = request_type.FromString(msg_container.grpc_message_content)
|
62
|
-
res = handler(req)
|
57
|
+
res = handler(req, context)
|
63
58
|
res_cls = res.__class__
|
64
59
|
return MessageContainer(
|
65
60
|
metadata={
|
@@ -74,89 +69,26 @@ def _handle(
|
|
74
69
|
)
|
75
70
|
|
76
71
|
|
77
|
-
class GrpcAdapterServicer(grpcadapter_pb2_grpc.GrpcAdapterServicer):
|
72
|
+
class GrpcAdapterServicer(grpcadapter_pb2_grpc.GrpcAdapterServicer, FleetServicer):
|
78
73
|
"""Fleet API via GrpcAdapter servicer."""
|
79
74
|
|
80
|
-
def __init__(
|
81
|
-
self, state_factory: LinkStateFactory, ffs_factory: FfsFactory
|
82
|
-
) -> None:
|
83
|
-
self.state_factory = state_factory
|
84
|
-
self.ffs_factory = ffs_factory
|
85
|
-
|
86
75
|
def SendReceive( # pylint: disable=too-many-return-statements
|
87
76
|
self, request: MessageContainer, context: grpc.ServicerContext
|
88
77
|
) -> MessageContainer:
|
89
78
|
"""."""
|
90
79
|
log(DEBUG, "GrpcAdapterServicer.SendReceive")
|
91
80
|
if request.grpc_message_name == CreateNodeRequest.__qualname__:
|
92
|
-
return _handle(request, CreateNodeRequest, self.
|
81
|
+
return _handle(request, context, CreateNodeRequest, self.CreateNode)
|
93
82
|
if request.grpc_message_name == DeleteNodeRequest.__qualname__:
|
94
|
-
return _handle(request, DeleteNodeRequest, self.
|
83
|
+
return _handle(request, context, DeleteNodeRequest, self.DeleteNode)
|
95
84
|
if request.grpc_message_name == PingRequest.__qualname__:
|
96
|
-
return _handle(request, PingRequest, self.
|
97
|
-
if request.grpc_message_name == PullTaskInsRequest.__qualname__:
|
98
|
-
return _handle(request, PullTaskInsRequest, self._pull_task_ins)
|
99
|
-
if request.grpc_message_name == PushTaskResRequest.__qualname__:
|
100
|
-
return _handle(request, PushTaskResRequest, self._push_task_res)
|
85
|
+
return _handle(request, context, PingRequest, self.Ping)
|
101
86
|
if request.grpc_message_name == GetRunRequest.__qualname__:
|
102
|
-
return _handle(request, GetRunRequest, self.
|
87
|
+
return _handle(request, context, GetRunRequest, self.GetRun)
|
103
88
|
if request.grpc_message_name == GetFabRequest.__qualname__:
|
104
|
-
return _handle(request, GetFabRequest, self.
|
89
|
+
return _handle(request, context, GetFabRequest, self.GetFab)
|
90
|
+
if request.grpc_message_name == PullMessagesRequest.__qualname__:
|
91
|
+
return _handle(request, context, PullMessagesRequest, self.PullMessages)
|
92
|
+
if request.grpc_message_name == PushMessagesRequest.__qualname__:
|
93
|
+
return _handle(request, context, PushMessagesRequest, self.PushMessages)
|
105
94
|
raise ValueError(f"Invalid grpc_message_name: {request.grpc_message_name}")
|
106
|
-
|
107
|
-
def _create_node(self, request: CreateNodeRequest) -> CreateNodeResponse:
|
108
|
-
"""."""
|
109
|
-
log(INFO, "GrpcAdapter.CreateNode")
|
110
|
-
return message_handler.create_node(
|
111
|
-
request=request,
|
112
|
-
state=self.state_factory.state(),
|
113
|
-
)
|
114
|
-
|
115
|
-
def _delete_node(self, request: DeleteNodeRequest) -> DeleteNodeResponse:
|
116
|
-
"""."""
|
117
|
-
log(INFO, "GrpcAdapter.DeleteNode")
|
118
|
-
return message_handler.delete_node(
|
119
|
-
request=request,
|
120
|
-
state=self.state_factory.state(),
|
121
|
-
)
|
122
|
-
|
123
|
-
def _ping(self, request: PingRequest) -> PingResponse:
|
124
|
-
"""."""
|
125
|
-
log(DEBUG, "GrpcAdapter.Ping")
|
126
|
-
return message_handler.ping(
|
127
|
-
request=request,
|
128
|
-
state=self.state_factory.state(),
|
129
|
-
)
|
130
|
-
|
131
|
-
def _pull_task_ins(self, request: PullTaskInsRequest) -> PullTaskInsResponse:
|
132
|
-
"""Pull TaskIns."""
|
133
|
-
log(INFO, "GrpcAdapter.PullTaskIns")
|
134
|
-
return message_handler.pull_task_ins(
|
135
|
-
request=request,
|
136
|
-
state=self.state_factory.state(),
|
137
|
-
)
|
138
|
-
|
139
|
-
def _push_task_res(self, request: PushTaskResRequest) -> PushTaskResResponse:
|
140
|
-
"""Push TaskRes."""
|
141
|
-
log(INFO, "GrpcAdapter.PushTaskRes")
|
142
|
-
return message_handler.push_task_res(
|
143
|
-
request=request,
|
144
|
-
state=self.state_factory.state(),
|
145
|
-
)
|
146
|
-
|
147
|
-
def _get_run(self, request: GetRunRequest) -> GetRunResponse:
|
148
|
-
"""Get run information."""
|
149
|
-
log(INFO, "GrpcAdapter.GetRun")
|
150
|
-
return message_handler.get_run(
|
151
|
-
request=request,
|
152
|
-
state=self.state_factory.state(),
|
153
|
-
)
|
154
|
-
|
155
|
-
def _get_fab(self, request: GetFabRequest) -> GetFabResponse:
|
156
|
-
"""Get FAB."""
|
157
|
-
log(INFO, "GrpcAdapter.GetFab")
|
158
|
-
return message_handler.get_fab(
|
159
|
-
request=request,
|
160
|
-
ffs=self.ffs_factory.ffs(),
|
161
|
-
state=self.state_factory.state(),
|
162
|
-
)
|
@@ -15,91 +15,54 @@
|
|
15
15
|
"""Flower server interceptor."""
|
16
16
|
|
17
17
|
|
18
|
-
import
|
19
|
-
from
|
20
|
-
from logging import INFO, WARNING
|
21
|
-
from typing import Any, Callable, Optional, Union
|
18
|
+
import datetime
|
19
|
+
from typing import Any, Callable, Optional, cast
|
22
20
|
|
23
21
|
import grpc
|
24
|
-
from
|
25
|
-
|
26
|
-
from flwr.common
|
22
|
+
from google.protobuf.message import Message as GrpcMessage
|
23
|
+
|
24
|
+
from flwr.common import now
|
25
|
+
from flwr.common.constant import (
|
26
|
+
PUBLIC_KEY_HEADER,
|
27
|
+
SIGNATURE_HEADER,
|
28
|
+
TIMESTAMP_HEADER,
|
29
|
+
TIMESTAMP_TOLERANCE,
|
30
|
+
)
|
27
31
|
from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
|
28
|
-
bytes_to_private_key,
|
29
32
|
bytes_to_public_key,
|
30
|
-
|
31
|
-
verify_hmac,
|
33
|
+
verify_signature,
|
32
34
|
)
|
33
|
-
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
34
35
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
35
36
|
CreateNodeRequest,
|
36
37
|
CreateNodeResponse,
|
37
|
-
DeleteNodeRequest,
|
38
|
-
DeleteNodeResponse,
|
39
|
-
PingRequest,
|
40
|
-
PingResponse,
|
41
|
-
PullTaskInsRequest,
|
42
|
-
PullTaskInsResponse,
|
43
|
-
PushTaskResRequest,
|
44
|
-
PushTaskResResponse,
|
45
38
|
)
|
46
|
-
from flwr.proto.node_pb2 import Node # pylint: disable=E0611
|
47
|
-
from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
|
48
39
|
from flwr.server.superlink.linkstate import LinkStateFactory
|
49
40
|
|
50
|
-
_PUBLIC_KEY_HEADER = "public-key"
|
51
|
-
_AUTH_TOKEN_HEADER = "auth-token"
|
52
|
-
|
53
|
-
Request = Union[
|
54
|
-
CreateNodeRequest,
|
55
|
-
DeleteNodeRequest,
|
56
|
-
PullTaskInsRequest,
|
57
|
-
PushTaskResRequest,
|
58
|
-
GetRunRequest,
|
59
|
-
PingRequest,
|
60
|
-
GetFabRequest,
|
61
|
-
]
|
62
|
-
|
63
|
-
Response = Union[
|
64
|
-
CreateNodeResponse,
|
65
|
-
DeleteNodeResponse,
|
66
|
-
PullTaskInsResponse,
|
67
|
-
PushTaskResResponse,
|
68
|
-
GetRunResponse,
|
69
|
-
PingResponse,
|
70
|
-
GetFabResponse,
|
71
|
-
]
|
72
|
-
|
73
41
|
|
74
|
-
def
|
75
|
-
|
76
|
-
)
|
77
|
-
|
78
|
-
if isinstance(value, str):
|
79
|
-
return value.encode()
|
42
|
+
def _unary_unary_rpc_terminator(message: str) -> grpc.RpcMethodHandler:
|
43
|
+
def terminate(_request: GrpcMessage, context: grpc.ServicerContext) -> GrpcMessage:
|
44
|
+
context.abort(grpc.StatusCode.UNAUTHENTICATED, message)
|
45
|
+
raise RuntimeError("Should not reach this point") # Make mypy happy
|
80
46
|
|
81
|
-
return
|
47
|
+
return grpc.unary_unary_rpc_method_handler(terminate)
|
82
48
|
|
83
49
|
|
84
50
|
class AuthenticateServerInterceptor(grpc.ServerInterceptor): # type: ignore
|
85
|
-
"""Server interceptor for node authentication.
|
86
|
-
|
87
|
-
|
51
|
+
"""Server interceptor for node authentication.
|
52
|
+
|
53
|
+
Parameters
|
54
|
+
----------
|
55
|
+
state_factory : LinkStateFactory
|
56
|
+
A factory for creating new instances of LinkState.
|
57
|
+
auto_auth : bool (default: False)
|
58
|
+
If True, nodes are authenticated without requiring their public keys to be
|
59
|
+
pre-stored in the LinkState. If False, only nodes with pre-stored public keys
|
60
|
+
can be authenticated.
|
61
|
+
"""
|
62
|
+
|
63
|
+
def __init__(self, state_factory: LinkStateFactory, auto_auth: bool = False):
|
88
64
|
self.state_factory = state_factory
|
89
|
-
|
90
|
-
|
91
|
-
self.node_public_keys = state.get_node_public_keys()
|
92
|
-
if len(self.node_public_keys) == 0:
|
93
|
-
log(WARNING, "Authentication enabled, but no known public keys configured")
|
94
|
-
|
95
|
-
private_key = state.get_server_private_key()
|
96
|
-
public_key = state.get_server_public_key()
|
97
|
-
|
98
|
-
if private_key is None or public_key is None:
|
99
|
-
raise ValueError("Error loading authentication keys")
|
100
|
-
|
101
|
-
self.server_private_key = bytes_to_private_key(private_key)
|
102
|
-
self.encoded_server_public_key = base64.urlsafe_b64encode(public_key)
|
65
|
+
self.auto_auth = auto_auth
|
103
66
|
|
104
67
|
def intercept_service(
|
105
68
|
self,
|
@@ -112,117 +75,80 @@ class AuthenticateServerInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
112
75
|
metadata sent by the node. Continue RPC call if node is authenticated, else,
|
113
76
|
terminate RPC call by setting context to abort.
|
114
77
|
"""
|
78
|
+
state = self.state_factory.state()
|
79
|
+
metadata_dict = dict(handler_call_details.invocation_metadata)
|
80
|
+
|
81
|
+
# Retrieve info from the metadata
|
82
|
+
try:
|
83
|
+
node_pk_bytes = cast(bytes, metadata_dict[PUBLIC_KEY_HEADER])
|
84
|
+
timestamp_iso = cast(str, metadata_dict[TIMESTAMP_HEADER])
|
85
|
+
signature = cast(bytes, metadata_dict[SIGNATURE_HEADER])
|
86
|
+
except KeyError:
|
87
|
+
return _unary_unary_rpc_terminator("Missing authentication metadata")
|
88
|
+
|
89
|
+
if not self.auto_auth:
|
90
|
+
# Abort the RPC call if the node public key is not found
|
91
|
+
if node_pk_bytes not in state.get_node_public_keys():
|
92
|
+
return _unary_unary_rpc_terminator("Public key not recognized")
|
93
|
+
|
94
|
+
# Verify the signature
|
95
|
+
node_pk = bytes_to_public_key(node_pk_bytes)
|
96
|
+
if not verify_signature(node_pk, timestamp_iso.encode("ascii"), signature):
|
97
|
+
return _unary_unary_rpc_terminator("Invalid signature")
|
98
|
+
|
99
|
+
# Verify the timestamp
|
100
|
+
current = now()
|
101
|
+
time_diff = current - datetime.datetime.fromisoformat(timestamp_iso)
|
102
|
+
# Abort the RPC call if the timestamp is too old or in the future
|
103
|
+
if not 0 < time_diff.total_seconds() < TIMESTAMP_TOLERANCE:
|
104
|
+
return _unary_unary_rpc_terminator("Invalid timestamp")
|
105
|
+
|
106
|
+
# Continue the RPC call
|
107
|
+
expected_node_id = state.get_node_id(node_pk_bytes)
|
108
|
+
if not handler_call_details.method.endswith("CreateNode"):
|
109
|
+
if expected_node_id is None:
|
110
|
+
return _unary_unary_rpc_terminator("Invalid node ID")
|
115
111
|
# One of the method handlers in
|
116
112
|
# `flwr.server.superlink.fleet.grpc_rere.fleet_server.FleetServicer`
|
117
113
|
method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
|
118
|
-
return self.
|
114
|
+
return self._wrap_method_handler(
|
115
|
+
method_handler, expected_node_id, node_pk_bytes
|
116
|
+
)
|
119
117
|
|
120
|
-
def
|
121
|
-
self,
|
118
|
+
def _wrap_method_handler(
|
119
|
+
self,
|
120
|
+
method_handler: grpc.RpcMethodHandler,
|
121
|
+
expected_node_id: Optional[int],
|
122
|
+
node_public_key: bytes,
|
122
123
|
) -> grpc.RpcMethodHandler:
|
123
124
|
def _generic_method_handler(
|
124
|
-
request:
|
125
|
+
request: GrpcMessage,
|
125
126
|
context: grpc.ServicerContext,
|
126
|
-
) ->
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
_get_value_from_tuples(
|
149
|
-
_AUTH_TOKEN_HEADER, context.invocation_metadata()
|
150
|
-
)
|
151
|
-
)
|
152
|
-
public_key = bytes_to_public_key(node_public_key_bytes)
|
153
|
-
|
154
|
-
if not self._verify_hmac(public_key, request, hmac_value):
|
155
|
-
context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied")
|
156
|
-
|
157
|
-
# Verify node_id
|
158
|
-
node_id = self.state_factory.state().get_node_id(node_public_key_bytes)
|
159
|
-
|
160
|
-
if not self._verify_node_id(node_id, request):
|
161
|
-
context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied")
|
162
|
-
|
163
|
-
return method_handler.unary_unary(request, context) # type: ignore
|
127
|
+
) -> GrpcMessage:
|
128
|
+
# Verify the node ID
|
129
|
+
if not isinstance(request, CreateNodeRequest):
|
130
|
+
try:
|
131
|
+
if request.node.node_id != expected_node_id: # type: ignore
|
132
|
+
raise ValueError
|
133
|
+
except (AttributeError, ValueError):
|
134
|
+
context.abort(grpc.StatusCode.UNAUTHENTICATED, "Invalid node ID")
|
135
|
+
|
136
|
+
response: GrpcMessage = method_handler.unary_unary(request, context)
|
137
|
+
|
138
|
+
# Set the public key after a successful CreateNode request
|
139
|
+
if isinstance(response, CreateNodeResponse):
|
140
|
+
state = self.state_factory.state()
|
141
|
+
try:
|
142
|
+
state.set_node_public_key(response.node.node_id, node_public_key)
|
143
|
+
except ValueError as e:
|
144
|
+
# Remove newly created node if setting the public key fails
|
145
|
+
state.delete_node(response.node.node_id)
|
146
|
+
context.abort(grpc.StatusCode.UNAUTHENTICATED, str(e))
|
147
|
+
|
148
|
+
return response
|
164
149
|
|
165
150
|
return grpc.unary_unary_rpc_method_handler(
|
166
151
|
_generic_method_handler,
|
167
152
|
request_deserializer=method_handler.request_deserializer,
|
168
153
|
response_serializer=method_handler.response_serializer,
|
169
154
|
)
|
170
|
-
|
171
|
-
def _verify_node_id(
|
172
|
-
self,
|
173
|
-
node_id: Optional[int],
|
174
|
-
request: Union[
|
175
|
-
DeleteNodeRequest,
|
176
|
-
PullTaskInsRequest,
|
177
|
-
PushTaskResRequest,
|
178
|
-
GetRunRequest,
|
179
|
-
PingRequest,
|
180
|
-
GetFabRequest,
|
181
|
-
],
|
182
|
-
) -> bool:
|
183
|
-
if node_id is None:
|
184
|
-
return False
|
185
|
-
if isinstance(request, PushTaskResRequest):
|
186
|
-
if len(request.task_res_list) == 0:
|
187
|
-
return False
|
188
|
-
return request.task_res_list[0].task.producer.node_id == node_id
|
189
|
-
if isinstance(request, GetRunRequest):
|
190
|
-
return node_id in self.state_factory.state().get_nodes(request.run_id)
|
191
|
-
return request.node.node_id == node_id
|
192
|
-
|
193
|
-
def _verify_hmac(
|
194
|
-
self, public_key: ec.EllipticCurvePublicKey, request: Request, hmac_value: bytes
|
195
|
-
) -> bool:
|
196
|
-
shared_secret = generate_shared_key(self.server_private_key, public_key)
|
197
|
-
message_bytes = request.SerializeToString(deterministic=True)
|
198
|
-
return verify_hmac(shared_secret, message_bytes, hmac_value)
|
199
|
-
|
200
|
-
def _create_authenticated_node(
|
201
|
-
self,
|
202
|
-
public_key_bytes: bytes,
|
203
|
-
request: CreateNodeRequest,
|
204
|
-
context: grpc.ServicerContext,
|
205
|
-
) -> CreateNodeResponse:
|
206
|
-
context.send_initial_metadata(
|
207
|
-
(
|
208
|
-
(
|
209
|
-
_PUBLIC_KEY_HEADER,
|
210
|
-
self.encoded_server_public_key,
|
211
|
-
),
|
212
|
-
)
|
213
|
-
)
|
214
|
-
state = self.state_factory.state()
|
215
|
-
node_id = state.get_node_id(public_key_bytes)
|
216
|
-
|
217
|
-
# Handle `CreateNode` here instead of calling the default method handler
|
218
|
-
# Return previously assigned `node_id` for the provided `public_key`
|
219
|
-
if node_id is not None:
|
220
|
-
state.acknowledge_ping(node_id, request.ping_interval)
|
221
|
-
return CreateNodeResponse(node=Node(node_id=node_id))
|
222
|
-
|
223
|
-
# No `node_id` exists for the provided `public_key`
|
224
|
-
# Handle `CreateNode` here instead of calling the default method handler
|
225
|
-
# Note: the innermost `CreateNode` method will never be called
|
226
|
-
node_id = state.create_node(request.ping_interval)
|
227
|
-
state.set_node_public_key(node_id, public_key_bytes)
|
228
|
-
return CreateNodeResponse(node=Node(node_id=node_id))
|
flwr/simulation/app.py
CHANGED
@@ -16,7 +16,6 @@
|
|
16
16
|
|
17
17
|
|
18
18
|
import argparse
|
19
|
-
import sys
|
20
19
|
from logging import DEBUG, ERROR, INFO
|
21
20
|
from queue import Queue
|
22
21
|
from time import sleep
|
@@ -39,6 +38,7 @@ from flwr.common.constant import (
|
|
39
38
|
Status,
|
40
39
|
SubStatus,
|
41
40
|
)
|
41
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
42
42
|
from flwr.common.logger import (
|
43
43
|
log,
|
44
44
|
mirror_output_to_queue,
|
@@ -81,12 +81,10 @@ def flwr_simulation() -> None:
|
|
81
81
|
log(INFO, "Starting Flower Simulation")
|
82
82
|
|
83
83
|
if not args.insecure:
|
84
|
-
|
85
|
-
|
86
|
-
"`flwr-simulation` does not support TLS yet. "
|
87
|
-
"Please use the '--insecure' flag.",
|
84
|
+
flwr_exit(
|
85
|
+
ExitCode.COMMON_TLS_NOT_SUPPORTED,
|
86
|
+
"`flwr-simulation` does not support TLS yet. ",
|
88
87
|
)
|
89
|
-
sys.exit(1)
|
90
88
|
|
91
89
|
log(
|
92
90
|
DEBUG,
|
{flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/RECORD
RENAMED
@@ -3,9 +3,9 @@ flwr/cli/__init__.py,sha256=cZJVgozlkC6Ni2Hd_FAIrqefrkCGOV18fikToq-6iLw,720
|
|
3
3
|
flwr/cli/app.py,sha256=UeXrW5gxrUnFViDjAMIxGNZZKwu3a1oAj83v53IWIWM,1382
|
4
4
|
flwr/cli/build.py,sha256=4P70i_FnUs0P21aTwjTXtFQSAfY-C04hUDF-2npfJdo,6345
|
5
5
|
flwr/cli/cli_user_auth_interceptor.py,sha256=aZepPA298s-HjGmkJGMvI_uZe72O5aLC3jri-ilG53o,3126
|
6
|
-
flwr/cli/config_utils.py,sha256=
|
6
|
+
flwr/cli/config_utils.py,sha256=u4VMNgNTj1mGgCVzV4KfBz3Nyn0j46KJ-Ii8dUgZ4OM,7196
|
7
7
|
flwr/cli/example.py,sha256=uk5CoD0ZITgpY_ffsTbEKf8XOOCSUzByjHPcMSPqV18,2216
|
8
|
-
flwr/cli/install.py,sha256
|
8
|
+
flwr/cli/install.py,sha256=-RnrYGejN_zyXXp_CoddSQwoQfRTWWyt9WYlxphJzyU,8180
|
9
9
|
flwr/cli/log.py,sha256=O7MBpsJp114PIZb-7Cru-KM6fqyneFQkqoQbQsqQmZU,6121
|
10
10
|
flwr/cli/login/__init__.py,sha256=6_9zOzbPOAH72K2wX3-9dXTAbS7Mjpa5sEn2lA6eHHI,800
|
11
11
|
flwr/cli/login/login.py,sha256=VaBPQBdLYmSfxXEJWVyu8U5dXztQgIv6rfTJkvz3zV8,3025
|
@@ -75,14 +75,14 @@ flwr/client/client_app.py,sha256=cTig-N00YzTucbo9zNi6I21J8PlbflU_8J_f5CI-Wpw,103
|
|
75
75
|
flwr/client/clientapp/__init__.py,sha256=kZqChGnTChQ1WGSUkIlW2S5bc0d0mzDubCAmZUGRpEY,800
|
76
76
|
flwr/client/clientapp/app.py,sha256=O2dghU6PBXeU6kK5Ihj2-8cKAzXIC1efyZv4aulqHU4,8952
|
77
77
|
flwr/client/clientapp/clientappio_servicer.py,sha256=5L6bjw_j3Mnx9kRFwYwxDNABKurBO5q1jZOWE_X11wQ,8522
|
78
|
-
flwr/client/clientapp/utils.py,sha256=
|
78
|
+
flwr/client/clientapp/utils.py,sha256=qqTw9PKPCldGnnbAbMhtS-Qs_GcqADE1eOtVPXeKYAo,4344
|
79
79
|
flwr/client/dpfedavg_numpy_client.py,sha256=4KsEvzavDKyVDU1V0kMqffTwu1lNdUCYQN-i0DTYVN8,7404
|
80
80
|
flwr/client/grpc_adapter_client/__init__.py,sha256=QyNWIbsq9DpyMk7oemiO1P3TBFfkfkctnJ1JoAkTl3s,742
|
81
81
|
flwr/client/grpc_adapter_client/connection.py,sha256=nV-hPd5q5Eblg6PgUrGGYj74mbE1a0qjfN8G3wzJVAc,4006
|
82
82
|
flwr/client/grpc_client/__init__.py,sha256=LsnbqXiJhgQcB0XzAlUQgPx011Uf7Y7yabIC1HxivJ8,735
|
83
83
|
flwr/client/grpc_client/connection.py,sha256=Y6VDDDEHCPlGxSf-bEyBua_khnY_F1OoXD-8gpIcYhE,9171
|
84
84
|
flwr/client/grpc_rere_client/__init__.py,sha256=MK-oSoV3kwUEQnIwl0GN4OpiHR7eLOrMA8ikunET130,752
|
85
|
-
flwr/client/grpc_rere_client/client_interceptor.py,sha256=
|
85
|
+
flwr/client/grpc_rere_client/client_interceptor.py,sha256=8yX2jhwfX9r1PO76ZdME4tPefutnQqWPi7kELriBMUo,2451
|
86
86
|
flwr/client/grpc_rere_client/connection.py,sha256=9XBcTn4myN_pgAGM6QFQ2Q35clSqxfgY8irrChQuF6I,11668
|
87
87
|
flwr/client/grpc_rere_client/grpc_adapter.py,sha256=VrSqHosRcWv8xDLKEabuzyHpVnRhjAEJf_MUFQxhDh8,6155
|
88
88
|
flwr/client/heartbeat.py,sha256=cx37mJBH8LyoIN4Lks85wtqT1mnU5GulQnr4pGCvAq0,2404
|
@@ -113,8 +113,8 @@ flwr/common/address.py,sha256=9KNYE69WW_QVcyumsux3Qn1wmn4J7f13Y9nHASpvzbA,3018
|
|
113
113
|
flwr/common/args.py,sha256=bCvtG0hhh_hVjl9NoWsY_g7kLMIN3jCN7B883HvZ7hg,6223
|
114
114
|
flwr/common/auth_plugin/__init__.py,sha256=1Y8Oj3iB49IHDu9tvDih1J74Ygu7k85V9s2A4WORPyA,887
|
115
115
|
flwr/common/auth_plugin/auth_plugin.py,sha256=wgDorBUB4IkK6twQ8vNawRVz7BDPmKdXZBNLqhU9RSs,3871
|
116
|
-
flwr/common/config.py,sha256=
|
117
|
-
flwr/common/constant.py,sha256
|
116
|
+
flwr/common/config.py,sha256=GEnOmW1vw-0IF-NkekyXQLYqstSqetZFEtT_SShyBgA,12693
|
117
|
+
flwr/common/constant.py,sha256=FLqav6UCcdCG61XZr31fmAFqOu4WRFG8zcbnwUyYJ4w,6202
|
118
118
|
flwr/common/context.py,sha256=uJ-mnoC_8y_udEb3kAX-r8CPphNTWM72z1AlsvQEu54,2403
|
119
119
|
flwr/common/date.py,sha256=NHHpESce5wYqEwoDXf09gp9U9l_5Bmlh2BsOcwS-kDM,1554
|
120
120
|
flwr/common/differential_privacy.py,sha256=XwcJ3rWr8S8BZUocc76vLSJAXIf6OHnWkBV6-xlIRuw,6106
|
@@ -122,7 +122,7 @@ flwr/common/differential_privacy_constants.py,sha256=c7b7tqgvT7yMK0XN9ndiTBs4mQf
|
|
122
122
|
flwr/common/dp.py,sha256=vddkvyjV2FhRoN4VuU2LeAM1UBn7dQB8_W-Qdiveal8,1978
|
123
123
|
flwr/common/exit/__init__.py,sha256=-ZOJYLaNnR729a7VzZiFsLiqngzKQh3xc27svYStZ_Q,826
|
124
124
|
flwr/common/exit/exit.py,sha256=DmZFyksp-w1sFDQekq5Z-qfnr-ivCAv78aQkqj-TDps,3458
|
125
|
-
flwr/common/exit/exit_code.py,sha256=
|
125
|
+
flwr/common/exit/exit_code.py,sha256=PNEnCrZfOILjfDAFu5m-2YWEJBrk97xglq4zCUlqV7E,3470
|
126
126
|
flwr/common/exit_handlers.py,sha256=Dke87CC6d6b6kqkC2mF0I4JsP4mHhlQTFxkS4sKKgyw,3308
|
127
127
|
flwr/common/grpc.py,sha256=GCdiTCppW-clhzOo7OIJbsKIWKnJ9pqNTsAKhj7y4So,9646
|
128
128
|
flwr/common/logger.py,sha256=UPyI_98EDibqgf3epgWxFHxdXgYReSWtaKFf9Mj0hd0,12306
|
@@ -269,7 +269,7 @@ flwr/server/superlink/ffs/ffs.py,sha256=qLI1UfosJugu2BKOJWqHIhafTm-YiuKqGf3OGWPH
|
|
269
269
|
flwr/server/superlink/ffs/ffs_factory.py,sha256=N_eMuUZggotdGiDQ5r_Tf21xsu_ob0e3jyM6ag7d3kk,1490
|
270
270
|
flwr/server/superlink/fleet/__init__.py,sha256=76od-HhYjOUoZFLFDFCFnNHI4JLAmaXQEAyp7LWlQpc,711
|
271
271
|
flwr/server/superlink/fleet/grpc_adapter/__init__.py,sha256=spBQQJeYz8zPOBOfyMLv87kqWPASGB73AymcLXdFaYA,742
|
272
|
-
flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py,sha256=
|
272
|
+
flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py,sha256=RLYHmuNBtv7DUyxrUu6hmjqkum_WEsTP3RMpbqnVGic,4160
|
273
273
|
flwr/server/superlink/fleet/grpc_bidi/__init__.py,sha256=dkSKQMuMTYh1qSnuN87cAPv_mcdLg3f0PqTABHs8gUE,735
|
274
274
|
flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py,sha256=ud08wi9j8OYRYVTIioL1xenOgrEbtS7afyr8MnQEk4I,6021
|
275
275
|
flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py,sha256=JkAH_nIZaqe_9kntrg26od_jaz5XdLFuvNMgGu8xk9Q,6485
|
@@ -277,7 +277,7 @@ flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py,sha256=h3EhqgelegVC4E
|
|
277
277
|
flwr/server/superlink/fleet/grpc_bidi/grpc_server.py,sha256=mxPxyEF0IW0vV41Bqk1zfKOdRDEvXPwzJyMiRMg7nTI,5173
|
278
278
|
flwr/server/superlink/fleet/grpc_rere/__init__.py,sha256=j2hyC342am-_Hgp1g80Y3fGDzfTI6n8QOOn2PyWf4eg,758
|
279
279
|
flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=EVx3rHX8WjUXVX7Svki5ihsA1aIZkpOMLv0aQv9Rjjw,6656
|
280
|
-
flwr/server/superlink/fleet/grpc_rere/server_interceptor.py,sha256=
|
280
|
+
flwr/server/superlink/fleet/grpc_rere/server_interceptor.py,sha256=zf58FXJ4S-k4kUh-LWcz6O6AWRcxs_ZGNQtUDDM7FVw,6307
|
281
281
|
flwr/server/superlink/fleet/message_handler/__init__.py,sha256=h8oLD7uo5lKICPy0rRdKRjTYe62u8PKkT_fA4xF5JPA,731
|
282
282
|
flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=5QRqE0w8Kb-M2cEiPNIdKkPc17CEGHvNYjMpGOfgOlE,6886
|
283
283
|
flwr/server/superlink/fleet/rest_rere/__init__.py,sha256=5jbYbAn75sGv-gBwOPDySE0kz96F6dTYLeMrGqNi4lM,735
|
@@ -308,7 +308,7 @@ flwr/server/workflow/secure_aggregation/__init__.py,sha256=3XlgDOjD_hcukTGl6Bc1B
|
|
308
308
|
flwr/server/workflow/secure_aggregation/secagg_workflow.py,sha256=l2IdMdJjs1bgHs5vQgLSOVzar7v2oxUn46oCrnVE1rM,5839
|
309
309
|
flwr/server/workflow/secure_aggregation/secaggplus_workflow.py,sha256=rfn2etO1nb7u-1oRl-H9q3enJZz3shMINZaBB7rPsC4,29671
|
310
310
|
flwr/simulation/__init__.py,sha256=5UcDVJNjFoSwWqHbGM1hKfTTUUNdwAtuoNvNrfvdkUY,1556
|
311
|
-
flwr/simulation/app.py,sha256=
|
311
|
+
flwr/simulation/app.py,sha256=xRVSJBnTXQUqWIYOzENfTnJlZ24CSNhWkhVEFxIu4I0,9758
|
312
312
|
flwr/simulation/legacy_app.py,sha256=qpZI4Vvzr5TyWSLTRrMP-jN4rH2C25JI9nVSSjhFwSQ,15861
|
313
313
|
flwr/simulation/ray_transport/__init__.py,sha256=wzcEEwUUlulnXsg6raCA1nGpP3LlAQDtJ8zNkCXcVbA,734
|
314
314
|
flwr/simulation/ray_transport/ray_actor.py,sha256=k11yoAPQzFGQU-KnCCP0ZrfPPdUPXXrBe-1DKM5VdW4,18997
|
@@ -324,8 +324,8 @@ flwr/superexec/exec_servicer.py,sha256=X10ILT-AoGMrB3IgI2mBe9i-QcIVUAl9bucuqVOPY
|
|
324
324
|
flwr/superexec/exec_user_auth_interceptor.py,sha256=K06OU-l4LnYhTDg071hGJuOaQWEJbZsYi5qxUmmtiG0,3704
|
325
325
|
flwr/superexec/executor.py,sha256=_B55WW2TD1fBINpabSSDRenVHXYmvlfhv-k8hJKU4lQ,3115
|
326
326
|
flwr/superexec/simulation.py,sha256=WQDon15oqpMopAZnwRZoTICYCfHqtkvFSqiTQ2hLD_g,4088
|
327
|
-
flwr_nightly-1.15.0.
|
328
|
-
flwr_nightly-1.15.0.
|
329
|
-
flwr_nightly-1.15.0.
|
330
|
-
flwr_nightly-1.15.0.
|
331
|
-
flwr_nightly-1.15.0.
|
327
|
+
flwr_nightly-1.15.0.dev20250122.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
328
|
+
flwr_nightly-1.15.0.dev20250122.dist-info/METADATA,sha256=HNrdO4R0GWS2WA5_9eQjJrKDJhObz0BZJKPsVYErnso,15864
|
329
|
+
flwr_nightly-1.15.0.dev20250122.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
330
|
+
flwr_nightly-1.15.0.dev20250122.dist-info/entry_points.txt,sha256=JlNxX3qhaV18_2yj5a3kJW1ESxm31cal9iS_N_pf1Rk,538
|
331
|
+
flwr_nightly-1.15.0.dev20250122.dist-info/RECORD,,
|
{flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/LICENSE
RENAMED
File without changes
|
{flwr_nightly-1.15.0.dev20250121.dist-info → flwr_nightly-1.15.0.dev20250122.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|