flwr-nightly 1.23.0.dev20251001__py3-none-any.whl → 1.23.0.dev20251003__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 +13 -0
- flwr/cli/auth_plugin/__init__.py +7 -4
- flwr/cli/auth_plugin/auth_plugin.py +23 -11
- flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +15 -25
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +4 -4
- flwr/cli/login/login.py +34 -14
- flwr/{client/clientapp → cli/supernode}/__init__.py +11 -1
- flwr/cli/supernode/create.py +58 -0
- flwr/cli/supernode/delete.py +58 -0
- flwr/cli/supernode/ls.py +51 -0
- flwr/cli/utils.py +36 -22
- flwr/client/__init__.py +2 -1
- flwr/clientapp/__init__.py +1 -2
- flwr/{client/clientapp → clientapp}/utils.py +1 -1
- flwr/common/constant.py +14 -8
- flwr/common/typing.py +6 -6
- flwr/compat/client/app.py +1 -1
- flwr/proto/control_pb2.py +48 -35
- flwr/proto/control_pb2.pyi +67 -4
- flwr/proto/control_pb2_grpc.py +102 -0
- flwr/proto/control_pb2_grpc.pyi +39 -0
- flwr/proto/node_pb2.py +3 -1
- flwr/proto/node_pb2.pyi +33 -0
- flwr/server/app.py +34 -16
- 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 +2 -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 +1 -1
- flwr/superlink/auth_plugin/__init__.py +5 -2
- flwr/superlink/auth_plugin/auth_plugin.py +16 -12
- 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 +9 -9
- flwr/superlink/servicer/control/control_servicer.py +57 -29
- flwr/supernode/runtime/run_clientapp.py +2 -2
- {flwr_nightly-1.23.0.dev20251001.dist-info → flwr_nightly-1.23.0.dev20251003.dist-info}/METADATA +1 -1
- {flwr_nightly-1.23.0.dev20251001.dist-info → flwr_nightly-1.23.0.dev20251003.dist-info}/RECORD +44 -39
- /flwr/{client → clientapp}/client_app.py +0 -0
- {flwr_nightly-1.23.0.dev20251001.dist-info → flwr_nightly-1.23.0.dev20251003.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.23.0.dev20251001.dist-info → flwr_nightly-1.23.0.dev20251003.dist-info}/entry_points.txt +0 -0
flwr/proto/control_pb2_grpc.pyi
CHANGED
@@ -44,6 +44,21 @@ class ControlStub:
|
|
44
44
|
flwr.proto.control_pb2.PullArtifactsResponse]
|
45
45
|
"""Pull artifacts generated during a run (flwr pull)"""
|
46
46
|
|
47
|
+
CreateNodeCli: grpc.UnaryUnaryMultiCallable[
|
48
|
+
flwr.proto.control_pb2.CreateNodeCliRequest,
|
49
|
+
flwr.proto.control_pb2.CreateNodeCliResponse]
|
50
|
+
"""Add SuperNode"""
|
51
|
+
|
52
|
+
DeleteNodeCli: grpc.UnaryUnaryMultiCallable[
|
53
|
+
flwr.proto.control_pb2.DeleteNodeCliRequest,
|
54
|
+
flwr.proto.control_pb2.DeleteNodeCliResponse]
|
55
|
+
"""Remove SuperNode"""
|
56
|
+
|
57
|
+
ListNodesCli: grpc.UnaryUnaryMultiCallable[
|
58
|
+
flwr.proto.control_pb2.ListNodesCliRequest,
|
59
|
+
flwr.proto.control_pb2.ListNodesCliResponse]
|
60
|
+
"""List SuperNodes"""
|
61
|
+
|
47
62
|
|
48
63
|
class ControlServicer(metaclass=abc.ABCMeta):
|
49
64
|
@abc.abstractmethod
|
@@ -102,5 +117,29 @@ class ControlServicer(metaclass=abc.ABCMeta):
|
|
102
117
|
"""Pull artifacts generated during a run (flwr pull)"""
|
103
118
|
pass
|
104
119
|
|
120
|
+
@abc.abstractmethod
|
121
|
+
def CreateNodeCli(self,
|
122
|
+
request: flwr.proto.control_pb2.CreateNodeCliRequest,
|
123
|
+
context: grpc.ServicerContext,
|
124
|
+
) -> flwr.proto.control_pb2.CreateNodeCliResponse:
|
125
|
+
"""Add SuperNode"""
|
126
|
+
pass
|
127
|
+
|
128
|
+
@abc.abstractmethod
|
129
|
+
def DeleteNodeCli(self,
|
130
|
+
request: flwr.proto.control_pb2.DeleteNodeCliRequest,
|
131
|
+
context: grpc.ServicerContext,
|
132
|
+
) -> flwr.proto.control_pb2.DeleteNodeCliResponse:
|
133
|
+
"""Remove SuperNode"""
|
134
|
+
pass
|
135
|
+
|
136
|
+
@abc.abstractmethod
|
137
|
+
def ListNodesCli(self,
|
138
|
+
request: flwr.proto.control_pb2.ListNodesCliRequest,
|
139
|
+
context: grpc.ServicerContext,
|
140
|
+
) -> flwr.proto.control_pb2.ListNodesCliResponse:
|
141
|
+
"""List SuperNodes"""
|
142
|
+
pass
|
143
|
+
|
105
144
|
|
106
145
|
def add_ControlServicer_to_server(servicer: ControlServicer, server: grpc.Server) -> None: ...
|
flwr/proto/node_pb2.py
CHANGED
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
|
|
14
14
|
|
15
15
|
|
16
16
|
|
17
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x62\x06proto3')
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\xb6\x01\n\x08NodeInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x12\x11\n\towner_aid\x18\x02 \x01(\t\x12\x12\n\ncreated_at\x18\x03 \x01(\t\x12\x14\n\x0c\x61\x63tivated_at\x18\x04 \x01(\t\x12\x16\n\x0e\x64\x65\x61\x63tivated_at\x18\x05 \x01(\t\x12\x12\n\ndeleted_at\x18\x06 \x01(\t\x12\x14\n\x0conline_until\x18\x07 \x01(\x02\x12\x1a\n\x12heartbeat_interval\x18\x08 \x01(\x02\x62\x06proto3')
|
18
18
|
|
19
19
|
_globals = globals()
|
20
20
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
@@ -23,4 +23,6 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
23
|
DESCRIPTOR._options = None
|
24
24
|
_globals['_NODE']._serialized_start=37
|
25
25
|
_globals['_NODE']._serialized_end=60
|
26
|
+
_globals['_NODEINFO']._serialized_start=63
|
27
|
+
_globals['_NODEINFO']._serialized_end=245
|
26
28
|
# @@protoc_insertion_point(module_scope)
|
flwr/proto/node_pb2.pyi
CHANGED
@@ -5,6 +5,7 @@ isort:skip_file
|
|
5
5
|
import builtins
|
6
6
|
import google.protobuf.descriptor
|
7
7
|
import google.protobuf.message
|
8
|
+
import typing
|
8
9
|
import typing_extensions
|
9
10
|
|
10
11
|
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
|
@@ -19,3 +20,35 @@ class Node(google.protobuf.message.Message):
|
|
19
20
|
) -> None: ...
|
20
21
|
def ClearField(self, field_name: typing_extensions.Literal["node_id",b"node_id"]) -> None: ...
|
21
22
|
global___Node = Node
|
23
|
+
|
24
|
+
class NodeInfo(google.protobuf.message.Message):
|
25
|
+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
26
|
+
NODE_ID_FIELD_NUMBER: builtins.int
|
27
|
+
OWNER_AID_FIELD_NUMBER: builtins.int
|
28
|
+
CREATED_AT_FIELD_NUMBER: builtins.int
|
29
|
+
ACTIVATED_AT_FIELD_NUMBER: builtins.int
|
30
|
+
DEACTIVATED_AT_FIELD_NUMBER: builtins.int
|
31
|
+
DELETED_AT_FIELD_NUMBER: builtins.int
|
32
|
+
ONLINE_UNTIL_FIELD_NUMBER: builtins.int
|
33
|
+
HEARTBEAT_INTERVAL_FIELD_NUMBER: builtins.int
|
34
|
+
node_id: builtins.int
|
35
|
+
owner_aid: typing.Text
|
36
|
+
created_at: typing.Text
|
37
|
+
activated_at: typing.Text
|
38
|
+
deactivated_at: typing.Text
|
39
|
+
deleted_at: typing.Text
|
40
|
+
online_until: builtins.float
|
41
|
+
heartbeat_interval: builtins.float
|
42
|
+
def __init__(self,
|
43
|
+
*,
|
44
|
+
node_id: builtins.int = ...,
|
45
|
+
owner_aid: typing.Text = ...,
|
46
|
+
created_at: typing.Text = ...,
|
47
|
+
activated_at: typing.Text = ...,
|
48
|
+
deactivated_at: typing.Text = ...,
|
49
|
+
deleted_at: typing.Text = ...,
|
50
|
+
online_until: builtins.float = ...,
|
51
|
+
heartbeat_interval: builtins.float = ...,
|
52
|
+
) -> None: ...
|
53
|
+
def ClearField(self, field_name: typing_extensions.Literal["activated_at",b"activated_at","created_at",b"created_at","deactivated_at",b"deactivated_at","deleted_at",b"deleted_at","heartbeat_interval",b"heartbeat_interval","node_id",b"node_id","online_until",b"online_until","owner_aid",b"owner_aid"]) -> None: ...
|
54
|
+
global___NodeInfo = NodeInfo
|
flwr/server/app.py
CHANGED
@@ -38,7 +38,7 @@ from flwr.common.address import parse_address
|
|
38
38
|
from flwr.common.args import try_obtain_server_certificates
|
39
39
|
from flwr.common.config import get_flwr_dir
|
40
40
|
from flwr.common.constant import (
|
41
|
-
|
41
|
+
AUTHN_TYPE_YAML_KEY,
|
42
42
|
AUTHZ_TYPE_YAML_KEY,
|
43
43
|
CLIENT_OCTET,
|
44
44
|
CONTROL_API_DEFAULT_SERVER_ADDRESS,
|
@@ -71,7 +71,7 @@ from flwr.supercore.ffs import FfsFactory
|
|
71
71
|
from flwr.supercore.grpc_health import add_args_health, run_health_server_grpc_no_tls
|
72
72
|
from flwr.supercore.object_store import ObjectStoreFactory
|
73
73
|
from flwr.superlink.artifact_provider import ArtifactProvider
|
74
|
-
from flwr.superlink.auth_plugin import
|
74
|
+
from flwr.superlink.auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
|
75
75
|
from flwr.superlink.servicer.control import run_control_api_grpc
|
76
76
|
|
77
77
|
from .superlink.fleet.grpc_adapter.grpc_adapter_servicer import GrpcAdapterServicer
|
@@ -83,13 +83,13 @@ from .superlink.simulation.simulationio_grpc import run_simulationio_api_grpc
|
|
83
83
|
|
84
84
|
DATABASE = ":flwr-in-memory-state:"
|
85
85
|
BASE_DIR = get_flwr_dir() / "superlink" / "ffs"
|
86
|
-
P = TypeVar("P",
|
86
|
+
P = TypeVar("P", ControlAuthnPlugin, ControlAuthzPlugin)
|
87
87
|
|
88
88
|
|
89
89
|
try:
|
90
90
|
from flwr.ee import (
|
91
91
|
add_ee_args_superlink,
|
92
|
-
|
92
|
+
get_control_authn_plugins,
|
93
93
|
get_control_authz_plugins,
|
94
94
|
get_control_event_log_writer_plugins,
|
95
95
|
get_ee_artifact_provider,
|
@@ -101,7 +101,7 @@ except ImportError:
|
|
101
101
|
def add_ee_args_superlink(parser: argparse.ArgumentParser) -> None:
|
102
102
|
"""Add EE-specific arguments to the parser."""
|
103
103
|
|
104
|
-
def
|
104
|
+
def get_control_authn_plugins() -> dict[str, type[ControlAuthnPlugin]]:
|
105
105
|
"""Return all Control API authentication plugins."""
|
106
106
|
raise NotImplementedError("No authentication plugins are currently supported.")
|
107
107
|
|
@@ -189,16 +189,23 @@ def run_superlink() -> None:
|
|
189
189
|
# Obtain certificates
|
190
190
|
certificates = try_obtain_server_certificates(args)
|
191
191
|
|
192
|
-
# Disable the
|
192
|
+
# Disable the account auth TLS check if args.disable_oidc_tls_cert_verification is
|
193
193
|
# provided
|
194
194
|
verify_tls_cert = not getattr(args, "disable_oidc_tls_cert_verification", None)
|
195
195
|
|
196
|
-
|
196
|
+
authn_plugin: Optional[ControlAuthnPlugin] = None
|
197
197
|
authz_plugin: Optional[ControlAuthzPlugin] = None
|
198
198
|
event_log_plugin: Optional[EventLogWriterPlugin] = None
|
199
|
-
# Load the auth plugin if the args.
|
199
|
+
# Load the auth plugin if the args.account_auth_config is provided
|
200
200
|
if cfg_path := getattr(args, "user_auth_config", None):
|
201
|
-
|
201
|
+
log(
|
202
|
+
WARN,
|
203
|
+
"The `--user-auth-config` flag is deprecated and will be removed in a "
|
204
|
+
"future release. Please use `--account-auth-config` instead.",
|
205
|
+
)
|
206
|
+
args.account_auth_config = cfg_path
|
207
|
+
if cfg_path := getattr(args, "account_auth_config", None):
|
208
|
+
authn_plugin, authz_plugin = _try_obtain_control_auth_plugins(
|
202
209
|
Path(cfg_path), verify_tls_cert
|
203
210
|
)
|
204
211
|
# Enable event logging if the args.enable_event_log is True
|
@@ -229,7 +236,7 @@ def run_superlink() -> None:
|
|
229
236
|
objectstore_factory=objectstore_factory,
|
230
237
|
certificates=certificates,
|
231
238
|
is_simulation=is_simulation,
|
232
|
-
|
239
|
+
authn_plugin=authn_plugin,
|
233
240
|
authz_plugin=authz_plugin,
|
234
241
|
event_log_plugin=event_log_plugin,
|
235
242
|
artifact_provider=artifact_provider,
|
@@ -444,7 +451,7 @@ def _try_load_public_keys_node_authentication(
|
|
444
451
|
|
445
452
|
def _try_obtain_control_auth_plugins(
|
446
453
|
config_path: Path, verify_tls_cert: bool
|
447
|
-
) -> tuple[
|
454
|
+
) -> tuple[ControlAuthnPlugin, ControlAuthzPlugin]:
|
448
455
|
"""Obtain Control API authentication and authorization plugins."""
|
449
456
|
# Load YAML file
|
450
457
|
with config_path.open("r", encoding="utf-8") as file:
|
@@ -459,7 +466,7 @@ def _try_obtain_control_auth_plugins(
|
|
459
466
|
plugins: dict[str, type[P]] = loader()
|
460
467
|
plugin_cls: type[P] = plugins[auth_plugin_name]
|
461
468
|
return plugin_cls(
|
462
|
-
|
469
|
+
account_auth_config_path=config_path, verify_tls_cert=verify_tls_cert
|
463
470
|
)
|
464
471
|
except KeyError:
|
465
472
|
if auth_plugin_name:
|
@@ -471,11 +478,22 @@ def _try_obtain_control_auth_plugins(
|
|
471
478
|
except NotImplementedError:
|
472
479
|
sys.exit(f"No {section} plugins are currently supported.")
|
473
480
|
|
481
|
+
# Warn deprecated authn_type key
|
482
|
+
if "authn_type" in config["authentication"]:
|
483
|
+
log(
|
484
|
+
WARN,
|
485
|
+
"The `authn_type` key in the authentication configuration is deprecated. "
|
486
|
+
"Use `%s` instead.",
|
487
|
+
AUTHN_TYPE_YAML_KEY,
|
488
|
+
)
|
489
|
+
authn_type = config["authentication"].pop("authn_type")
|
490
|
+
config["authentication"][AUTHN_TYPE_YAML_KEY] = authn_type
|
491
|
+
|
474
492
|
# Load authentication plugin
|
475
|
-
|
493
|
+
authn_plugin = _load_plugin(
|
476
494
|
section="authentication",
|
477
|
-
yaml_key=
|
478
|
-
loader=
|
495
|
+
yaml_key=AUTHN_TYPE_YAML_KEY,
|
496
|
+
loader=get_control_authn_plugins,
|
479
497
|
)
|
480
498
|
|
481
499
|
# Load authorization plugin
|
@@ -485,7 +503,7 @@ def _try_obtain_control_auth_plugins(
|
|
485
503
|
loader=get_control_authz_plugins,
|
486
504
|
)
|
487
505
|
|
488
|
-
return
|
506
|
+
return authn_plugin, authz_plugin
|
489
507
|
|
490
508
|
|
491
509
|
def _try_obtain_control_event_log_writer_plugin() -> Optional[EventLogWriterPlugin]:
|
@@ -18,7 +18,7 @@
|
|
18
18
|
from abc import ABC, abstractmethod
|
19
19
|
from typing import Callable
|
20
20
|
|
21
|
-
from flwr.
|
21
|
+
from flwr.clientapp.client_app import ClientApp
|
22
22
|
from flwr.common.context import Context
|
23
23
|
from flwr.common.message import Message
|
24
24
|
from flwr.common.typing import ConfigRecordValues
|
@@ -21,7 +21,7 @@ from typing import Callable, Optional, Union
|
|
21
21
|
|
22
22
|
import ray
|
23
23
|
|
24
|
-
from flwr.
|
24
|
+
from flwr.clientapp.client_app import ClientApp
|
25
25
|
from flwr.common.constant import PARTITION_ID_KEY
|
26
26
|
from flwr.common.context import Context
|
27
27
|
from flwr.common.logger import log
|
@@ -27,9 +27,9 @@ from typing import Callable, Optional
|
|
27
27
|
from uuid import uuid4
|
28
28
|
|
29
29
|
from flwr.app.error import Error
|
30
|
-
from flwr.client.client_app import ClientApp, ClientAppException, LoadClientAppError
|
31
|
-
from flwr.client.clientapp.utils import get_load_client_app_fn
|
32
30
|
from flwr.client.run_info_store import DeprecatedRunInfoStore
|
31
|
+
from flwr.clientapp.client_app import ClientApp, ClientAppException, LoadClientAppError
|
32
|
+
from flwr.clientapp.utils import get_load_client_app_fn
|
33
33
|
from flwr.common import Message
|
34
34
|
from flwr.common.constant import (
|
35
35
|
HEARTBEAT_MAX_INTERVAL,
|
@@ -24,7 +24,7 @@ import ray
|
|
24
24
|
from ray import ObjectRef
|
25
25
|
from ray.util.actor_pool import ActorPool
|
26
26
|
|
27
|
-
from flwr.
|
27
|
+
from flwr.clientapp.client_app import ClientApp, ClientAppException, LoadClientAppError
|
28
28
|
from flwr.common import Context, Message
|
29
29
|
from flwr.common.logger import log
|
30
30
|
|
@@ -21,8 +21,8 @@ from typing import Optional
|
|
21
21
|
|
22
22
|
from flwr import common
|
23
23
|
from flwr.client import ClientFnExt
|
24
|
-
from flwr.client.client_app import ClientApp
|
25
24
|
from flwr.client.run_info_store import DeprecatedRunInfoStore
|
25
|
+
from flwr.clientapp.client_app import ClientApp
|
26
26
|
from flwr.common import DEFAULT_TTL, Message, Metadata, RecordDict, now
|
27
27
|
from flwr.common.constant import (
|
28
28
|
NUM_PARTITIONS_KEY,
|
@@ -30,7 +30,7 @@ from typing import Any, Optional
|
|
30
30
|
|
31
31
|
from flwr.cli.config_utils import load_and_validate
|
32
32
|
from flwr.cli.utils import get_sha256_hash
|
33
|
-
from flwr.
|
33
|
+
from flwr.clientapp import ClientApp
|
34
34
|
from flwr.common import Context, EventType, RecordDict, event, log, now
|
35
35
|
from flwr.common.config import get_fused_config_from_dir, parse_config_args
|
36
36
|
from flwr.common.constant import RUN_ID_NUM_BYTES, Status
|
@@ -15,9 +15,12 @@
|
|
15
15
|
"""Account auth plugin for ControlServicer."""
|
16
16
|
|
17
17
|
|
18
|
-
from .auth_plugin import
|
18
|
+
from .auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
|
19
|
+
from .noop_auth_plugin import NoOpControlAuthnPlugin, NoOpControlAuthzPlugin
|
19
20
|
|
20
21
|
__all__ = [
|
21
|
-
"
|
22
|
+
"ControlAuthnPlugin",
|
22
23
|
"ControlAuthzPlugin",
|
24
|
+
"NoOpControlAuthnPlugin",
|
25
|
+
"NoOpControlAuthzPlugin",
|
23
26
|
]
|
@@ -12,7 +12,7 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
# ==============================================================================
|
15
|
-
"""Abstract classes for Flower
|
15
|
+
"""Abstract classes for Flower account auth plugins."""
|
16
16
|
|
17
17
|
|
18
18
|
from abc import ABC, abstractmethod
|
@@ -20,15 +20,19 @@ from collections.abc import Sequence
|
|
20
20
|
from pathlib import Path
|
21
21
|
from typing import Optional, Union
|
22
22
|
|
23
|
-
from flwr.common.typing import
|
23
|
+
from flwr.common.typing import (
|
24
|
+
AccountAuthCredentials,
|
25
|
+
AccountAuthLoginDetails,
|
26
|
+
AccountInfo,
|
27
|
+
)
|
24
28
|
|
25
29
|
|
26
|
-
class
|
27
|
-
"""Abstract Flower
|
30
|
+
class ControlAuthnPlugin(ABC):
|
31
|
+
"""Abstract Flower Authentication Plugin class for ControlServicer.
|
28
32
|
|
29
33
|
Parameters
|
30
34
|
----------
|
31
|
-
|
35
|
+
account_auth_config_path : Path
|
32
36
|
Path to the YAML file containing the authentication configuration.
|
33
37
|
verify_tls_cert : bool
|
34
38
|
Boolean indicating whether to verify the TLS certificate
|
@@ -38,13 +42,13 @@ class ControlAuthPlugin(ABC):
|
|
38
42
|
@abstractmethod
|
39
43
|
def __init__(
|
40
44
|
self,
|
41
|
-
|
45
|
+
account_auth_config_path: Path,
|
42
46
|
verify_tls_cert: bool,
|
43
47
|
):
|
44
48
|
"""Abstract constructor."""
|
45
49
|
|
46
50
|
@abstractmethod
|
47
|
-
def get_login_details(self) -> Optional[
|
51
|
+
def get_login_details(self) -> Optional[AccountAuthLoginDetails]:
|
48
52
|
"""Get the login details."""
|
49
53
|
|
50
54
|
@abstractmethod
|
@@ -54,7 +58,7 @@ class ControlAuthPlugin(ABC):
|
|
54
58
|
"""Validate authentication tokens in the provided metadata."""
|
55
59
|
|
56
60
|
@abstractmethod
|
57
|
-
def get_auth_tokens(self, device_code: str) -> Optional[
|
61
|
+
def get_auth_tokens(self, device_code: str) -> Optional[AccountAuthCredentials]:
|
58
62
|
"""Get authentication tokens."""
|
59
63
|
|
60
64
|
@abstractmethod
|
@@ -71,7 +75,7 @@ class ControlAuthzPlugin(ABC): # pylint: disable=too-few-public-methods
|
|
71
75
|
|
72
76
|
Parameters
|
73
77
|
----------
|
74
|
-
|
78
|
+
account_auth_config_path : Path
|
75
79
|
Path to the YAML file containing the authorization configuration.
|
76
80
|
verify_tls_cert : bool
|
77
81
|
Boolean indicating whether to verify the TLS certificate
|
@@ -79,9 +83,9 @@ class ControlAuthzPlugin(ABC): # pylint: disable=too-few-public-methods
|
|
79
83
|
"""
|
80
84
|
|
81
85
|
@abstractmethod
|
82
|
-
def __init__(self,
|
86
|
+
def __init__(self, account_auth_config_path: Path, verify_tls_cert: bool):
|
83
87
|
"""Abstract constructor."""
|
84
88
|
|
85
89
|
@abstractmethod
|
86
|
-
def
|
87
|
-
"""Verify
|
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
|
@@ -31,7 +31,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
31
31
|
StreamLogsRequest,
|
32
32
|
StreamLogsResponse,
|
33
33
|
)
|
34
|
-
from flwr.superlink.auth_plugin import
|
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
|
@@ -31,12 +31,12 @@ from flwr.supercore.ffs import FfsFactory
|
|
31
31
|
from flwr.supercore.license_plugin import LicensePlugin
|
32
32
|
from flwr.supercore.object_store import ObjectStoreFactory
|
33
33
|
from flwr.superlink.artifact_provider import ArtifactProvider
|
34
|
-
from flwr.superlink.auth_plugin import
|
34
|
+
from flwr.superlink.auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
|
35
35
|
|
36
|
+
from .control_account_auth_interceptor import ControlAccountAuthInterceptor
|
36
37
|
from .control_event_log_interceptor import ControlEventLogInterceptor
|
37
38
|
from .control_license_interceptor import ControlLicenseInterceptor
|
38
39
|
from .control_servicer import ControlServicer
|
39
|
-
from .control_user_auth_interceptor import ControlUserAuthInterceptor
|
40
40
|
|
41
41
|
try:
|
42
42
|
from flwr.ee import get_license_plugin
|
@@ -54,7 +54,7 @@ def run_control_api_grpc(
|
|
54
54
|
objectstore_factory: ObjectStoreFactory,
|
55
55
|
certificates: Optional[tuple[bytes, bytes, bytes]],
|
56
56
|
is_simulation: bool,
|
57
|
-
|
57
|
+
authn_plugin: Optional[ControlAuthnPlugin] = None,
|
58
58
|
authz_plugin: Optional[ControlAuthzPlugin] = None,
|
59
59
|
event_log_plugin: Optional[EventLogWriterPlugin] = None,
|
60
60
|
artifact_provider: Optional[ArtifactProvider] = None,
|
@@ -69,15 +69,15 @@ def run_control_api_grpc(
|
|
69
69
|
ffs_factory=ffs_factory,
|
70
70
|
objectstore_factory=objectstore_factory,
|
71
71
|
is_simulation=is_simulation,
|
72
|
-
|
72
|
+
authn_plugin=authn_plugin,
|
73
73
|
artifact_provider=artifact_provider,
|
74
74
|
)
|
75
75
|
interceptors: list[grpc.ServerInterceptor] = []
|
76
76
|
if license_plugin is not None:
|
77
77
|
interceptors.append(ControlLicenseInterceptor(license_plugin))
|
78
|
-
if
|
79
|
-
interceptors.append(
|
80
|
-
# Event log interceptor must be added after
|
78
|
+
if authn_plugin is not None and authz_plugin is not None:
|
79
|
+
interceptors.append(ControlAccountAuthInterceptor(authn_plugin, authz_plugin))
|
80
|
+
# Event log interceptor must be added after account auth interceptor
|
81
81
|
if event_log_plugin is not None:
|
82
82
|
interceptors.append(ControlEventLogInterceptor(event_log_plugin))
|
83
83
|
log(INFO, "Flower event logging enabled")
|
@@ -90,12 +90,12 @@ def run_control_api_grpc(
|
|
90
90
|
interceptors=interceptors or None,
|
91
91
|
)
|
92
92
|
|
93
|
-
if
|
93
|
+
if authn_plugin is None:
|
94
94
|
log(INFO, "Flower Deployment Runtime: Starting Control API on %s", address)
|
95
95
|
else:
|
96
96
|
log(
|
97
97
|
INFO,
|
98
|
-
"Flower Deployment Runtime: Starting Control API with
|
98
|
+
"Flower Deployment Runtime: Starting Control API with account "
|
99
99
|
"authentication on %s",
|
100
100
|
address,
|
101
101
|
)
|