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.
Files changed (108) hide show
  1. flwr/cli/app.py +15 -1
  2. flwr/cli/auth_plugin/__init__.py +15 -6
  3. flwr/cli/auth_plugin/auth_plugin.py +95 -0
  4. flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
  5. flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
  6. flwr/cli/build.py +118 -47
  7. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
  8. flwr/cli/log.py +2 -2
  9. flwr/cli/login/login.py +34 -23
  10. flwr/cli/ls.py +13 -9
  11. flwr/cli/new/new.py +187 -35
  12. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  13. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  14. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  15. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  16. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  17. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  18. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  19. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  20. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  21. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  22. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  23. flwr/cli/pull.py +2 -2
  24. flwr/cli/run/run.py +11 -7
  25. flwr/cli/stop.py +2 -2
  26. flwr/cli/supernode/__init__.py +25 -0
  27. flwr/cli/supernode/ls.py +260 -0
  28. flwr/cli/supernode/register.py +185 -0
  29. flwr/cli/supernode/unregister.py +138 -0
  30. flwr/cli/utils.py +92 -69
  31. flwr/client/__init__.py +2 -1
  32. flwr/client/grpc_adapter_client/connection.py +6 -8
  33. flwr/client/grpc_rere_client/connection.py +59 -31
  34. flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
  35. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
  36. flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
  37. flwr/client/rest_client/connection.py +82 -37
  38. flwr/clientapp/__init__.py +1 -2
  39. flwr/{client/clientapp → clientapp}/utils.py +1 -1
  40. flwr/common/constant.py +53 -13
  41. flwr/common/exit/exit_code.py +20 -10
  42. flwr/common/inflatable_utils.py +10 -10
  43. flwr/common/record/array.py +3 -3
  44. flwr/common/record/arrayrecord.py +10 -1
  45. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  46. flwr/common/serde.py +4 -2
  47. flwr/common/typing.py +7 -6
  48. flwr/compat/client/app.py +1 -1
  49. flwr/compat/client/grpc_client/connection.py +2 -2
  50. flwr/proto/control_pb2.py +48 -35
  51. flwr/proto/control_pb2.pyi +71 -5
  52. flwr/proto/control_pb2_grpc.py +102 -0
  53. flwr/proto/control_pb2_grpc.pyi +39 -0
  54. flwr/proto/fab_pb2.py +11 -7
  55. flwr/proto/fab_pb2.pyi +21 -1
  56. flwr/proto/fleet_pb2.py +31 -23
  57. flwr/proto/fleet_pb2.pyi +63 -23
  58. flwr/proto/fleet_pb2_grpc.py +98 -28
  59. flwr/proto/fleet_pb2_grpc.pyi +45 -13
  60. flwr/proto/node_pb2.py +3 -1
  61. flwr/proto/node_pb2.pyi +48 -0
  62. flwr/server/app.py +139 -114
  63. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
  64. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
  65. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
  66. flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
  67. flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
  68. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  69. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
  70. flwr/server/superlink/fleet/vce/vce_api.py +18 -5
  71. flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
  72. flwr/server/superlink/linkstate/linkstate.py +107 -24
  73. flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
  74. flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
  75. flwr/server/superlink/linkstate/utils.py +3 -54
  76. flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
  77. flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
  78. flwr/server/utils/validator.py +2 -3
  79. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
  80. flwr/simulation/ray_transport/ray_actor.py +1 -1
  81. flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
  82. flwr/simulation/run_simulation.py +3 -2
  83. flwr/supercore/constant.py +22 -0
  84. flwr/supercore/object_store/in_memory_object_store.py +0 -4
  85. flwr/supercore/object_store/object_store_factory.py +26 -6
  86. flwr/supercore/object_store/sqlite_object_store.py +252 -0
  87. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  88. flwr/supercore/primitives/asymmetric.py +117 -0
  89. flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
  90. flwr/supercore/sqlite_mixin.py +156 -0
  91. flwr/supercore/utils.py +20 -0
  92. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  93. flwr/superlink/auth_plugin/auth_plugin.py +91 -0
  94. flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
  95. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
  96. flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
  97. flwr/superlink/servicer/control/control_grpc.py +13 -11
  98. flwr/superlink/servicer/control/control_servicer.py +152 -60
  99. flwr/supernode/cli/flower_supernode.py +19 -26
  100. flwr/supernode/runtime/run_clientapp.py +2 -2
  101. flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
  102. flwr/supernode/start_client_internal.py +17 -9
  103. {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/METADATA +1 -1
  104. {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/RECORD +107 -96
  105. flwr/common/auth_plugin/auth_plugin.py +0 -149
  106. /flwr/{client → clientapp}/client_app.py +0 -0
  107. {flwr-1.22.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
  108. {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
- """Auth plugin components."""
15
+ """Account auth plugin for ControlServicer."""
16
16
 
17
17
 
18
- from .auth_plugin import CliAuthPlugin as CliAuthPlugin
19
- from .auth_plugin import ControlAuthPlugin as ControlAuthPlugin
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
- "CliAuthPlugin",
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 ControlUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
54
- """Control API interceptor for user authentication."""
53
+ class ControlAccountAuthInterceptor(grpc.ServerInterceptor): # type: ignore
54
+ """Control API interceptor for account authentication."""
55
55
 
56
56
  def __init__(
57
57
  self,
58
- auth_plugin: ControlAuthPlugin,
58
+ authn_plugin: ControlAuthnPlugin,
59
59
  authz_plugin: ControlAuthzPlugin,
60
60
  ):
61
- self.auth_plugin = auth_plugin
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 user is authenticated
100
- valid_tokens, account_info = self.auth_plugin.validate_tokens_in_metadata(
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 user info not found",
107
+ "Tokens validated, but account info not found",
108
108
  )
109
109
  raise grpc.RpcError()
110
- # Store user info in contextvars for authenticated users
110
+ # Store account info in contextvars for authenticated accounts
111
111
  shared_account_info.set(account_info)
112
- # Check if the user is authorized
113
- if not self.authz_plugin.verify_user_authorization(account_info):
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
- "❗️ User not authorized. "
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 user is not authenticated, refresh tokens
123
- tokens, account_info = self.auth_plugin.refresh_tokens(metadata)
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 user info not found",
128
+ "Tokens refreshed, but account info not found",
129
129
  )
130
130
  raise grpc.RpcError()
131
- # Store user info in contextvars for authenticated users
131
+ # Store account info in contextvars for authenticated accounts
132
132
  shared_account_info.set(account_info)
133
- # Check if the user is authorized
134
- if not self.authz_plugin.verify_user_authorization(account_info):
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
- "❗️ User not authorized. "
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 .control_user_auth_interceptor import shared_account_info
27
+ from .control_account_auth_interceptor import shared_account_info
28
28
 
29
29
 
30
30
  class ControlEventLogInterceptor(grpc.ServerInterceptor): # type: ignore