flwr 1.22.0__py3-none-any.whl → 1.23.0__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/app.py +15 -1
- flwr/cli/auth_plugin/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +95 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
- flwr/cli/build.py +118 -47
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
- flwr/cli/log.py +2 -2
- flwr/cli/login/login.py +34 -23
- flwr/cli/ls.py +13 -9
- flwr/cli/new/new.py +187 -35
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
- flwr/cli/pull.py +2 -2
- flwr/cli/run/run.py +11 -7
- flwr/cli/stop.py +2 -2
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +260 -0
- flwr/cli/supernode/register.py +185 -0
- flwr/cli/supernode/unregister.py +138 -0
- flwr/cli/utils.py +92 -69
- flwr/client/__init__.py +2 -1
- flwr/client/grpc_adapter_client/connection.py +6 -8
- flwr/client/grpc_rere_client/connection.py +59 -31
- flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
- flwr/client/rest_client/connection.py +82 -37
- flwr/clientapp/__init__.py +1 -2
- flwr/{client/clientapp → clientapp}/utils.py +1 -1
- flwr/common/constant.py +53 -13
- flwr/common/exit/exit_code.py +20 -10
- flwr/common/inflatable_utils.py +10 -10
- flwr/common/record/array.py +3 -3
- flwr/common/record/arrayrecord.py +10 -1
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
- flwr/common/serde.py +4 -2
- flwr/common/typing.py +7 -6
- flwr/compat/client/app.py +1 -1
- flwr/compat/client/grpc_client/connection.py +2 -2
- flwr/proto/control_pb2.py +48 -35
- flwr/proto/control_pb2.pyi +71 -5
- flwr/proto/control_pb2_grpc.py +102 -0
- flwr/proto/control_pb2_grpc.pyi +39 -0
- flwr/proto/fab_pb2.py +11 -7
- flwr/proto/fab_pb2.pyi +21 -1
- flwr/proto/fleet_pb2.py +31 -23
- flwr/proto/fleet_pb2.pyi +63 -23
- flwr/proto/fleet_pb2_grpc.py +98 -28
- flwr/proto/fleet_pb2_grpc.pyi +45 -13
- flwr/proto/node_pb2.py +3 -1
- flwr/proto/node_pb2.pyi +48 -0
- flwr/server/app.py +139 -114
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
- flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
- flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
- flwr/server/superlink/fleet/vce/vce_api.py +18 -5
- flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
- flwr/server/superlink/linkstate/linkstate.py +107 -24
- flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
- flwr/server/superlink/linkstate/utils.py +3 -54
- flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
- flwr/server/utils/validator.py +2 -3
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
- flwr/simulation/ray_transport/ray_actor.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
- flwr/simulation/run_simulation.py +3 -2
- flwr/supercore/constant.py +22 -0
- flwr/supercore/object_store/in_memory_object_store.py +0 -4
- flwr/supercore/object_store/object_store_factory.py +26 -6
- flwr/supercore/object_store/sqlite_object_store.py +252 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
- flwr/supercore/sqlite_mixin.py +156 -0
- flwr/supercore/utils.py +20 -0
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +91 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
- flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
- flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
- flwr/superlink/servicer/control/control_grpc.py +13 -11
- flwr/superlink/servicer/control/control_servicer.py +152 -60
- flwr/supernode/cli/flower_supernode.py +19 -26
- flwr/supernode/runtime/run_clientapp.py +2 -2
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
- flwr/supernode/start_client_internal.py +17 -9
- {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/METADATA +1 -1
- {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/RECORD +107 -96
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- /flwr/{client → clientapp}/client_app.py +0 -0
- {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
- {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Ed25519-only asymmetric cryptography utilities."""
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
|
|
19
|
+
from cryptography.exceptions import InvalidSignature
|
|
20
|
+
from cryptography.hazmat.primitives import serialization
|
|
21
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_key_pair() -> tuple[ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey]:
|
|
25
|
+
"""Generate an Ed25519 private/public key pair.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
Tuple[Ed25519PrivateKey, Ed25519PublicKey]
|
|
30
|
+
Private and public key pair.
|
|
31
|
+
"""
|
|
32
|
+
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
33
|
+
return private_key, private_key.public_key()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def private_key_to_bytes(private_key: ed25519.Ed25519PrivateKey) -> bytes:
|
|
37
|
+
"""Serialize an Ed25519 private key to PEM bytes.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
private_key : Ed25519PrivateKey
|
|
42
|
+
The private key to serialize.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
bytes
|
|
47
|
+
PEM-encoded private key.
|
|
48
|
+
"""
|
|
49
|
+
return private_key.private_bytes(
|
|
50
|
+
encoding=serialization.Encoding.PEM,
|
|
51
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
52
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def bytes_to_private_key(private_key_bytes: bytes) -> ed25519.Ed25519PrivateKey:
|
|
57
|
+
"""Deserialize an Ed25519 private key from PEM bytes.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
private_key_bytes : bytes
|
|
62
|
+
PEM-encoded private key.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
Ed25519PrivateKey
|
|
67
|
+
Deserialized private key.
|
|
68
|
+
"""
|
|
69
|
+
return serialization.load_pem_private_key(
|
|
70
|
+
private_key_bytes, password=None
|
|
71
|
+
) # type: ignore[return-value]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def public_key_to_bytes(public_key: ed25519.Ed25519PublicKey) -> bytes:
|
|
75
|
+
"""Serialize an Ed25519 public key to PEM bytes.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
public_key : Ed25519PublicKey
|
|
80
|
+
The public key to serialize.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
bytes
|
|
85
|
+
PEM-encoded public key.
|
|
86
|
+
"""
|
|
87
|
+
return public_key.public_bytes(
|
|
88
|
+
encoding=serialization.Encoding.PEM,
|
|
89
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def bytes_to_public_key(public_key_bytes: bytes) -> ed25519.Ed25519PublicKey:
|
|
94
|
+
"""Deserialize an Ed25519 public key from PEM bytes.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
public_key_bytes : bytes
|
|
99
|
+
PEM-encoded public key.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
Ed25519PublicKey
|
|
104
|
+
Deserialized public key.
|
|
105
|
+
"""
|
|
106
|
+
return serialization.load_pem_public_key(public_key_bytes) # type: ignore[return-value]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def sign_message(private_key: ed25519.Ed25519PrivateKey, message: bytes) -> bytes:
|
|
110
|
+
"""Sign a message using an Ed25519 private key.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
private_key : Ed25519PrivateKey
|
|
115
|
+
The private key used for signing.
|
|
116
|
+
message : bytes
|
|
117
|
+
The message to sign.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
bytes
|
|
122
|
+
The signature of the message.
|
|
123
|
+
"""
|
|
124
|
+
return private_key.sign(message)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def verify_signature(
|
|
128
|
+
public_key: ed25519.Ed25519PublicKey, message: bytes, signature: bytes
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""Verify a signature using an Ed25519 public key.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
public_key : Ed25519PublicKey
|
|
135
|
+
The public key used for verification.
|
|
136
|
+
message : bytes
|
|
137
|
+
The original message.
|
|
138
|
+
signature : bytes
|
|
139
|
+
The signature to verify.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
bool
|
|
144
|
+
True if the signature is valid, False otherwise.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
public_key.verify(signature, message)
|
|
148
|
+
return True
|
|
149
|
+
except InvalidSignature:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def create_signed_message(fab_digest: bytes, timestamp: int) -> bytes:
|
|
154
|
+
"""Create a canonical message:
|
|
155
|
+
timestamp (8 bytes big-endian) + fab_digest.
|
|
156
|
+
"""
|
|
157
|
+
timestamp_bytes = timestamp.to_bytes(8, byteorder="big")
|
|
158
|
+
return timestamp_bytes + fab_digest
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def decode_base64url(sig: str) -> bytes:
|
|
162
|
+
"""Convert signature to b64 format."""
|
|
163
|
+
# add missing padding (=) to a multiple of 4
|
|
164
|
+
pad = (-len(sig)) % 4
|
|
165
|
+
return base64.urlsafe_b64decode(sig + ("=" * pad))
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Mixin providing common SQLite connection and initialization logic."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import sqlite3
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from logging import DEBUG, ERROR
|
|
23
|
+
from typing import Any, Optional, Union
|
|
24
|
+
|
|
25
|
+
from flwr.common.logger import log
|
|
26
|
+
|
|
27
|
+
DictOrTuple = Union[tuple[Any, ...], dict[str, Any]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SqliteMixin(ABC):
|
|
31
|
+
"""Mixin providing common SQLite connection and initialization logic."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, database_path: str) -> None:
|
|
34
|
+
self.database_path = database_path
|
|
35
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def conn(self) -> sqlite3.Connection:
|
|
39
|
+
"""Get the SQLite connection."""
|
|
40
|
+
if self._conn is None:
|
|
41
|
+
raise AttributeError("Database not initialized. Call initialize() first.")
|
|
42
|
+
return self._conn
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
46
|
+
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
log_queries : bool
|
|
51
|
+
Log each query which is executed.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
list[tuple[str]]
|
|
56
|
+
The list of all tables in the DB.
|
|
57
|
+
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
Implement in subclass:
|
|
61
|
+
|
|
62
|
+
.. code:: python
|
|
63
|
+
|
|
64
|
+
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
65
|
+
return self._ensure_initialized(
|
|
66
|
+
SQL_CREATE_TABLE_FOO,
|
|
67
|
+
SQL_CREATE_TABLE_BAR,
|
|
68
|
+
log_queries=log_queries
|
|
69
|
+
)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def _ensure_initialized(
|
|
73
|
+
self,
|
|
74
|
+
*create_statements: str,
|
|
75
|
+
log_queries: bool = False,
|
|
76
|
+
) -> list[tuple[str]]:
|
|
77
|
+
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
78
|
+
|
|
79
|
+
Subclasses should call this with their own CREATE TABLE/INDEX statements in
|
|
80
|
+
their `.initialize()` methods.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
create_statements : str
|
|
85
|
+
SQL statements to create tables and indexes.
|
|
86
|
+
log_queries : bool
|
|
87
|
+
Log each query which is executed.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
list[tuple[str]]
|
|
92
|
+
The list of all tables in the DB.
|
|
93
|
+
"""
|
|
94
|
+
self._conn = sqlite3.connect(self.database_path)
|
|
95
|
+
# Enable Write-Ahead Logging (WAL) for better concurrency
|
|
96
|
+
self._conn.execute("PRAGMA journal_mode = WAL;")
|
|
97
|
+
self._conn.execute("PRAGMA synchronous = NORMAL;")
|
|
98
|
+
self._conn.execute("PRAGMA foreign_keys = ON;")
|
|
99
|
+
self._conn.row_factory = dict_factory
|
|
100
|
+
|
|
101
|
+
if log_queries:
|
|
102
|
+
self._conn.set_trace_callback(lambda q: log(DEBUG, q))
|
|
103
|
+
|
|
104
|
+
# Create tables and indexes
|
|
105
|
+
cur = self._conn.cursor()
|
|
106
|
+
for sql in create_statements:
|
|
107
|
+
cur.execute(sql)
|
|
108
|
+
res = cur.execute("SELECT name FROM sqlite_schema;")
|
|
109
|
+
return res.fetchall()
|
|
110
|
+
|
|
111
|
+
def query(
|
|
112
|
+
self,
|
|
113
|
+
query: str,
|
|
114
|
+
data: Optional[Union[Sequence[DictOrTuple], DictOrTuple]] = None,
|
|
115
|
+
) -> list[dict[str, Any]]:
|
|
116
|
+
"""Execute a SQL query and return the results as list of dicts."""
|
|
117
|
+
if self._conn is None:
|
|
118
|
+
raise AttributeError("LinkState is not initialized.")
|
|
119
|
+
|
|
120
|
+
if data is None:
|
|
121
|
+
data = []
|
|
122
|
+
|
|
123
|
+
# Clean up whitespace to make the logs nicer
|
|
124
|
+
query = re.sub(r"\s+", " ", query)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
with self._conn:
|
|
128
|
+
if (
|
|
129
|
+
len(data) > 0
|
|
130
|
+
and isinstance(data, (tuple, list))
|
|
131
|
+
and isinstance(data[0], (tuple, dict))
|
|
132
|
+
):
|
|
133
|
+
rows = self._conn.executemany(query, data)
|
|
134
|
+
else:
|
|
135
|
+
rows = self._conn.execute(query, data)
|
|
136
|
+
|
|
137
|
+
# Extract results before committing to support
|
|
138
|
+
# INSERT/UPDATE ... RETURNING
|
|
139
|
+
# style queries
|
|
140
|
+
result = rows.fetchall()
|
|
141
|
+
except KeyError as exc:
|
|
142
|
+
log(ERROR, {"query": query, "data": data, "exception": exc})
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def dict_factory(
|
|
148
|
+
cursor: sqlite3.Cursor,
|
|
149
|
+
row: sqlite3.Row,
|
|
150
|
+
) -> dict[str, Any]:
|
|
151
|
+
"""Turn SQLite results into dicts.
|
|
152
|
+
|
|
153
|
+
Less efficent for retrival of large amounts of data but easier to use.
|
|
154
|
+
"""
|
|
155
|
+
fields = [column[0] for column in cursor.description]
|
|
156
|
+
return dict(zip(fields, row))
|
flwr/supercore/utils.py
CHANGED
|
@@ -30,3 +30,23 @@ def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
|
|
|
30
30
|
if len(value) <= head + tail:
|
|
31
31
|
return value
|
|
32
32
|
return f"{value[:head]}...{value[-tail:]}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def uint64_to_int64(unsigned: int) -> int:
|
|
36
|
+
"""Convert a uint64 integer to a sint64 with the same bit pattern.
|
|
37
|
+
|
|
38
|
+
For values >= 2^63, wraps around by subtracting 2^64.
|
|
39
|
+
"""
|
|
40
|
+
if unsigned >= (1 << 63):
|
|
41
|
+
return unsigned - (1 << 64)
|
|
42
|
+
return unsigned
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def int64_to_uint64(signed: int) -> int:
|
|
46
|
+
"""Convert a sint64 integer to a uint64 with the same bit pattern.
|
|
47
|
+
|
|
48
|
+
For negative values, wraps around by adding 2^64.
|
|
49
|
+
"""
|
|
50
|
+
if signed < 0:
|
|
51
|
+
return signed + (1 << 64)
|
|
52
|
+
return signed
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""
|
|
15
|
+
"""Account auth plugin for ControlServicer."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from .auth_plugin import
|
|
19
|
-
from .
|
|
20
|
-
from .auth_plugin import ControlAuthzPlugin as ControlAuthzPlugin
|
|
18
|
+
from .auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
|
|
19
|
+
from .noop_auth_plugin import NoOpControlAuthnPlugin, NoOpControlAuthzPlugin
|
|
21
20
|
|
|
22
21
|
__all__ = [
|
|
23
|
-
"
|
|
24
|
-
"ControlAuthPlugin",
|
|
22
|
+
"ControlAuthnPlugin",
|
|
25
23
|
"ControlAuthzPlugin",
|
|
24
|
+
"NoOpControlAuthnPlugin",
|
|
25
|
+
"NoOpControlAuthzPlugin",
|
|
26
26
|
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Abstract classes for Flower account auth plugins."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional, Union
|
|
22
|
+
|
|
23
|
+
from flwr.common.typing import (
|
|
24
|
+
AccountAuthCredentials,
|
|
25
|
+
AccountAuthLoginDetails,
|
|
26
|
+
AccountInfo,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ControlAuthnPlugin(ABC):
|
|
31
|
+
"""Abstract Flower Authentication Plugin class for ControlServicer.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
account_auth_config_path : Path
|
|
36
|
+
Path to the YAML file containing the authentication configuration.
|
|
37
|
+
verify_tls_cert : bool
|
|
38
|
+
Boolean indicating whether to verify the TLS certificate
|
|
39
|
+
when making requests to the server.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
account_auth_config_path: Path,
|
|
46
|
+
verify_tls_cert: bool,
|
|
47
|
+
):
|
|
48
|
+
"""Abstract constructor."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_login_details(self) -> Optional[AccountAuthLoginDetails]:
|
|
52
|
+
"""Get the login details."""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def validate_tokens_in_metadata(
|
|
56
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
|
57
|
+
) -> tuple[bool, Optional[AccountInfo]]:
|
|
58
|
+
"""Validate authentication tokens in the provided metadata."""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_auth_tokens(self, device_code: str) -> Optional[AccountAuthCredentials]:
|
|
62
|
+
"""Get authentication tokens."""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def refresh_tokens(
|
|
66
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
|
67
|
+
) -> tuple[
|
|
68
|
+
Optional[Sequence[tuple[str, Union[str, bytes]]]], Optional[AccountInfo]
|
|
69
|
+
]:
|
|
70
|
+
"""Refresh authentication tokens in the provided metadata."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ControlAuthzPlugin(ABC): # pylint: disable=too-few-public-methods
|
|
74
|
+
"""Abstract Flower Authorization Plugin class for ControlServicer.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
account_auth_config_path : Path
|
|
79
|
+
Path to the YAML file containing the authorization configuration.
|
|
80
|
+
verify_tls_cert : bool
|
|
81
|
+
Boolean indicating whether to verify the TLS certificate
|
|
82
|
+
when making requests to the server.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def __init__(self, account_auth_config_path: Path, verify_tls_cert: bool):
|
|
87
|
+
"""Abstract constructor."""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def authorize(self, account_info: AccountInfo) -> bool:
|
|
91
|
+
"""Verify account authorization request."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Concrete NoOp implementation for Servicer-side account authentication and
|
|
16
|
+
authorization plugins."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional, Union
|
|
22
|
+
|
|
23
|
+
from flwr.common.constant import NOOP_ACCOUNT_NAME, NOOP_FLWR_AID, AuthnType
|
|
24
|
+
from flwr.common.typing import (
|
|
25
|
+
AccountAuthCredentials,
|
|
26
|
+
AccountAuthLoginDetails,
|
|
27
|
+
AccountInfo,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
|
|
31
|
+
|
|
32
|
+
NOOP_ACCOUNT_INFO = AccountInfo(
|
|
33
|
+
flwr_aid=NOOP_FLWR_AID,
|
|
34
|
+
account_name=NOOP_ACCOUNT_NAME,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NoOpControlAuthnPlugin(ControlAuthnPlugin):
|
|
39
|
+
"""No-operation implementation of ControlAuthnPlugin."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
account_auth_config_path: Path,
|
|
44
|
+
verify_tls_cert: bool,
|
|
45
|
+
):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def get_login_details(self) -> Optional[AccountAuthLoginDetails]:
|
|
49
|
+
"""Get the login details."""
|
|
50
|
+
# This allows the `flwr login` command to load the NoOp plugin accordingly,
|
|
51
|
+
# which then raises a LoginError when attempting to login.
|
|
52
|
+
return AccountAuthLoginDetails(
|
|
53
|
+
authn_type=AuthnType.NOOP, # No operation authn type
|
|
54
|
+
device_code="",
|
|
55
|
+
verification_uri_complete="",
|
|
56
|
+
expires_in=0,
|
|
57
|
+
interval=0,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def validate_tokens_in_metadata(
|
|
61
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
|
62
|
+
) -> tuple[bool, Optional[AccountInfo]]:
|
|
63
|
+
"""Return valid for no-op plugin."""
|
|
64
|
+
return True, NOOP_ACCOUNT_INFO
|
|
65
|
+
|
|
66
|
+
def get_auth_tokens(self, device_code: str) -> Optional[AccountAuthCredentials]:
|
|
67
|
+
"""Get authentication tokens."""
|
|
68
|
+
raise RuntimeError("NoOp plugin does not support getting auth tokens.")
|
|
69
|
+
|
|
70
|
+
def refresh_tokens(
|
|
71
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
|
72
|
+
) -> tuple[
|
|
73
|
+
Optional[Sequence[tuple[str, Union[str, bytes]]]], Optional[AccountInfo]
|
|
74
|
+
]:
|
|
75
|
+
"""Refresh authentication tokens in the provided metadata."""
|
|
76
|
+
return metadata, NOOP_ACCOUNT_INFO
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class NoOpControlAuthzPlugin(ControlAuthzPlugin):
|
|
80
|
+
"""No-operation implementation of ControlAuthzPlugin."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, account_auth_config_path: Path, verify_tls_cert: bool):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def authorize(self, account_info: AccountInfo) -> bool:
|
|
86
|
+
"""Return True for no-op plugin."""
|
|
87
|
+
return True
|
|
@@ -20,7 +20,6 @@ from typing import Any, Callable, Union
|
|
|
20
20
|
|
|
21
21
|
import grpc
|
|
22
22
|
|
|
23
|
-
from flwr.common.auth_plugin import ControlAuthPlugin, ControlAuthzPlugin
|
|
24
23
|
from flwr.common.typing import AccountInfo
|
|
25
24
|
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
26
25
|
GetAuthTokensRequest,
|
|
@@ -32,6 +31,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
|
32
31
|
StreamLogsRequest,
|
|
33
32
|
StreamLogsResponse,
|
|
34
33
|
)
|
|
34
|
+
from flwr.superlink.auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
|
|
35
35
|
|
|
36
36
|
Request = Union[
|
|
37
37
|
StartRunRequest,
|
|
@@ -50,15 +50,15 @@ shared_account_info: contextvars.ContextVar[AccountInfo] = contextvars.ContextVa
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
class
|
|
54
|
-
"""Control API interceptor for
|
|
53
|
+
class ControlAccountAuthInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
54
|
+
"""Control API interceptor for account authentication."""
|
|
55
55
|
|
|
56
56
|
def __init__(
|
|
57
57
|
self,
|
|
58
|
-
|
|
58
|
+
authn_plugin: ControlAuthnPlugin,
|
|
59
59
|
authz_plugin: ControlAuthzPlugin,
|
|
60
60
|
):
|
|
61
|
-
self.
|
|
61
|
+
self.authn_plugin = authn_plugin
|
|
62
62
|
self.authz_plugin = authz_plugin
|
|
63
63
|
|
|
64
64
|
def intercept_service(
|
|
@@ -96,45 +96,45 @@ class ControlUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
96
96
|
if isinstance(request, (GetLoginDetailsRequest, GetAuthTokensRequest)):
|
|
97
97
|
return call(request, context) # type: ignore
|
|
98
98
|
|
|
99
|
-
# For other requests, check if the
|
|
100
|
-
valid_tokens, account_info = self.
|
|
99
|
+
# For other requests, check if the account is authenticated
|
|
100
|
+
valid_tokens, account_info = self.authn_plugin.validate_tokens_in_metadata(
|
|
101
101
|
metadata
|
|
102
102
|
)
|
|
103
103
|
if valid_tokens:
|
|
104
104
|
if account_info is None:
|
|
105
105
|
context.abort(
|
|
106
106
|
grpc.StatusCode.UNAUTHENTICATED,
|
|
107
|
-
"Tokens validated, but
|
|
107
|
+
"Tokens validated, but account info not found",
|
|
108
108
|
)
|
|
109
109
|
raise grpc.RpcError()
|
|
110
|
-
# Store
|
|
110
|
+
# Store account info in contextvars for authenticated accounts
|
|
111
111
|
shared_account_info.set(account_info)
|
|
112
|
-
# Check if the
|
|
113
|
-
if not self.authz_plugin.
|
|
112
|
+
# Check if the account is authorized
|
|
113
|
+
if not self.authz_plugin.authorize(account_info):
|
|
114
114
|
context.abort(
|
|
115
115
|
grpc.StatusCode.PERMISSION_DENIED,
|
|
116
|
-
"❗️
|
|
116
|
+
"❗️ Account not authorized. "
|
|
117
117
|
"Please contact the SuperLink administrator.",
|
|
118
118
|
)
|
|
119
119
|
raise grpc.RpcError()
|
|
120
120
|
return call(request, context) # type: ignore
|
|
121
121
|
|
|
122
|
-
# If the
|
|
123
|
-
tokens, account_info = self.
|
|
122
|
+
# If the account is not authenticated, refresh tokens
|
|
123
|
+
tokens, account_info = self.authn_plugin.refresh_tokens(metadata)
|
|
124
124
|
if tokens is not None:
|
|
125
125
|
if account_info is None:
|
|
126
126
|
context.abort(
|
|
127
127
|
grpc.StatusCode.UNAUTHENTICATED,
|
|
128
|
-
"Tokens refreshed, but
|
|
128
|
+
"Tokens refreshed, but account info not found",
|
|
129
129
|
)
|
|
130
130
|
raise grpc.RpcError()
|
|
131
|
-
# Store
|
|
131
|
+
# Store account info in contextvars for authenticated accounts
|
|
132
132
|
shared_account_info.set(account_info)
|
|
133
|
-
# Check if the
|
|
134
|
-
if not self.authz_plugin.
|
|
133
|
+
# Check if the account is authorized
|
|
134
|
+
if not self.authz_plugin.authorize(account_info):
|
|
135
135
|
context.abort(
|
|
136
136
|
grpc.StatusCode.PERMISSION_DENIED,
|
|
137
|
-
"❗️
|
|
137
|
+
"❗️ Account not authorized. "
|
|
138
138
|
"Please contact the SuperLink administrator.",
|
|
139
139
|
)
|
|
140
140
|
raise grpc.RpcError()
|
|
@@ -24,7 +24,7 @@ from google.protobuf.message import Message as GrpcMessage
|
|
|
24
24
|
from flwr.common.event_log_plugin.event_log_plugin import EventLogWriterPlugin
|
|
25
25
|
from flwr.common.typing import LogEntry
|
|
26
26
|
|
|
27
|
-
from .
|
|
27
|
+
from .control_account_auth_interceptor import shared_account_info
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class ControlEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
|