Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bdc9a3b
feat(superlink): add generic federation can_execute hook
chongshenng Mar 30, 2026
820adb9
feat(superlink): add generic federation can_execute hook
chongshenng Mar 30, 2026
b3eec03
feat(superlink): enforce start run entitlement via can_execute
chongshenng Mar 30, 2026
c83f24f
refactor(superlink): type can_execute action with ActionType
chongshenng Mar 30, 2026
a77c2b0
Format code
chongshenng Mar 30, 2026
2739fd2
refactor(superlink): type can_execute action with ActionType
chongshenng Mar 30, 2026
73503d5
refactor(superlink): use typed can_execute args in StartRun
chongshenng Mar 30, 2026
9e270ae
Format
chongshenng Mar 31, 2026
bcbad42
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Mar 31, 2026
828afdc
Merge branch 'main' into feat/pr1-can-execute-hook
chongshenng Mar 31, 2026
2fd8a3f
Rebase
chongshenng Mar 31, 2026
a7d1052
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Mar 31, 2026
c515134
Disable too many public methods
chongshenng Mar 31, 2026
9a5960e
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Mar 31, 2026
fc2f1a9
Merge branch 'main' into feat/pr1-can-execute-hook
chongshenng Apr 1, 2026
7946f43
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
2a20769
Update
chongshenng Apr 1, 2026
d4dfb1d
Revert
chongshenng Apr 1, 2026
37e7a9c
Fix
chongshenng Apr 1, 2026
cf65e6d
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
99806fb
Improve
chongshenng Apr 1, 2026
1b4c70d
Fix
chongshenng Apr 1, 2026
8568361
Merge branch 'main' into feat/pr1-can-execute-hook
chongshenng Apr 1, 2026
9865ed5
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
8c2278f
Set typing
chongshenng Apr 1, 2026
b0b9a85
Sort
chongshenng Apr 1, 2026
ed8ae0d
Fix
chongshenng Apr 1, 2026
23ce56d
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
4d78827
Rebase
chongshenng Apr 1, 2026
d1a7c18
Fix
chongshenng Apr 1, 2026
9eaa04f
Merge branch 'main' into feat/pr2-start-run-entitlement
chongshenng Apr 1, 2026
dace39a
Update framework/py/flwr/superlink/servicer/control/control_servicer.py
chongshenng Apr 1, 2026
2eed4ed
Update framework/py/flwr/superlink/servicer/control/control_servicer.py
chongshenng Apr 1, 2026
2fcc1d8
Update framework/py/flwr/superlink/servicer/control/control_servicer.py
chongshenng Apr 1, 2026
ee963cb
Add simulation test coverage
chongshenng Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions framework/py/flwr/supercore/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,16 @@ class RunType(str, Enum):

SERVER_APP = "serverapp"
SIMULATION = "simulation"


class RunTime(str, Enum):
"""Supported runtimes."""

DEPLOYMENT = "deployment"
SIMULATION = "simulation"


class ActionType(str, Enum):
"""Supported control action types."""

START_RUN = "start_run"
41 changes: 41 additions & 0 deletions framework/py/flwr/supercore/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2026 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower SuperCore type definitions."""


from dataclasses import dataclass

from flwr.supercore.constant import RunTime


@dataclass(frozen=True)
class ActionContext:
"""Base context for authorization checks in ``can_execute``."""


@dataclass(frozen=True)
class StartRunContext(ActionContext):
"""Context for the `ActionType.START_RUN` action.

Attributes
----------
federation : str
Target federation name.
runtime : RunTime
The runtime relevant to the action.
"""

federation: str
runtime: RunTime
24 changes: 24 additions & 0 deletions framework/py/flwr/superlink/federation/federation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
from flwr.common.typing import Federation
from flwr.proto.federation_config_pb2 import SimulationConfig # pylint: disable=E0611
from flwr.proto.federation_pb2 import Invitation # pylint: disable=E0611
from flwr.supercore.constant import ActionType
from flwr.supercore.typing import ActionContext

if TYPE_CHECKING:
from flwr.server.superlink.linkstate.linkstate import LinkState


# pylint: disable=too-many-public-methods
class FederationManager(ABC):
"""Abstract base class for FederationManager."""

Expand Down Expand Up @@ -329,3 +332,24 @@ def report_run_usage(self) -> None:
This method is called on successful run status transition to FINISHED and when
runs are marked as failed due to expired tokens.
"""

@abstractmethod
def can_execute(
self, flwr_aid: str, action: ActionType, context: ActionContext
) -> bool:
"""Check if an account can execute an action under a given context.

Parameters
----------
flwr_aid : str
Flower account ID of the subject.
action : ActionType
The action to authorize.
context : ActionContext
Action-specific context required for authorization.

Returns
-------
bool
``True`` if the action is allowed, otherwise ``False``.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
DEFAULT_SIMULATION_CONFIG,
NOOP_FEDERATION,
NOOP_FEDERATION_DESCRIPTION,
ActionType,
)
from flwr.supercore.error import ApiErrorCode, FlowerError
from flwr.supercore.typing import ActionContext

from .federation_manager import FederationManager

Expand Down Expand Up @@ -222,3 +224,10 @@ def report_run_usage(self) -> None:
This method is called on successful run status transition to FINISHED and when
runs are marked as failed due to expired tokens.
"""

def can_execute(
self, flwr_aid: str, action: ActionType, context: ActionContext
) -> bool:
"""Check if an account can execute an action under a given context."""
_ = (flwr_aid, action, context)
return True
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
DEFAULT_SIMULATION_CONFIG,
NOOP_FEDERATION,
NOOP_FEDERATION_DESCRIPTION,
ActionType,
)
from flwr.supercore.error import ApiErrorCode, FlowerError
from flwr.supercore.typing import ActionContext

from .noop_federation_manager import NoOpFederationManager

Expand Down Expand Up @@ -234,6 +236,15 @@ def test_has_node() -> None:
manager.has_node(999, "any_federation")


def test_can_execute() -> None:
"""Test can_execute method returns True for NOOP_FEDERATION."""
manager = NoOpFederationManager()

allowed = manager.can_execute(NOOP_FLWR_AID, ActionType.START_RUN, ActionContext())

assert allowed is True


def test_get_federations() -> None:
"""Test get_federations method returns NOOP_FEDERATION."""
# Prepare
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,18 @@
from flwr.proto.federation_pb2 import Federation # pylint: disable=E0611
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
from flwr.supercore.constant import NOOP_FEDERATION, PLATFORM_API_URL, RunType
from flwr.supercore.constant import (
NOOP_FEDERATION,
PLATFORM_API_URL,
ActionType,
RunTime,
RunType,
)
from flwr.supercore.error import ApiErrorCode, FlowerError, rpc_error_translator
from flwr.supercore.ffs import FfsFactory
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
from flwr.supercore.typing import StartRunContext
from flwr.supercore.utils import parse_app_spec, request_download_link
from flwr.superlink.artifact_provider import ArtifactProvider
from flwr.superlink.auth_plugin import ControlAuthnPlugin
Expand Down Expand Up @@ -198,6 +205,18 @@ def StartRun( # pylint: disable=too-many-locals, too-many-branches, too-many-st
resolved_federation_config.CopyFrom(sim_cfg)
resolved_federation_config.MergeFrom(request.override_federation_config)

runtime = RunTime.SIMULATION if sim_cfg else RunTime.DEPLOYMENT
if not state.federation_manager.can_execute(
flwr_aid,
ActionType.START_RUN,
StartRunContext(federation=federation, runtime=runtime),
):
raise FlowerError(
ApiErrorCode.NO_PERMISSIONS,
f"'{ActionType.START_RUN}' action cannot be executed on federation "
f"'{federation}'.",
)

try:
# Validate user config overrides matches keys in run config in FAB
fab_config = get_fab_config(fab_file)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@
)
from flwr.proto.federation_pb2 import Account, Member # pylint: disable=E0611
from flwr.server.superlink.linkstate import LinkStateFactory
from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME, NOOP_FEDERATION, RunType
from flwr.supercore.constant import (
FLWR_IN_MEMORY_DB_NAME,
NOOP_FEDERATION,
ActionType,
RunTime,
RunType,
StartRunContext,
)
from flwr.supercore.error import ApiErrorCode, FlowerError
from flwr.supercore.error.catalog import API_ERROR_MAP
from flwr.supercore.ffs import FfsFactory
Expand Down Expand Up @@ -253,6 +260,61 @@ def test_start_run_rejects_unknown_override_keys(self) -> None:
self.assertEqual(status_code, grpc.StatusCode.FAILED_PRECONDITION)
self.assertIn("unknown.key", details)

def test_start_run_denied_when_not_entitled(self) -> None:
"""Test StartRun aborts when federation manager denies execution."""
request = StartRunRequest()
request.fab.hash_str = hashlib.sha256(b"test FAB content").hexdigest()
request.fab.content = b"test FAB content"
request.federation = NOOP_FEDERATION

context = Mock()
context.abort.side_effect = grpc.RpcError()

with (
patch.object(
self.state.federation_manager,
"can_execute",
return_value=False,
),
self.assertRaises(grpc.RpcError),
):
self.servicer.StartRun(request, context)

_assert_abort_with_flwr_err(context, ApiErrorCode.NO_PERMISSIONS)

def test_start_run_calls_can_execute_with_expected_args(self) -> None:
"""Test StartRun calls can_execute with expected typed arguments."""
fab_content = b"test FAB content 777"
request = StartRunRequest()
request.fab.hash_str = hashlib.sha256(fab_content).hexdigest()
request.fab.content = fab_content
request.federation = NOOP_FEDERATION

with (
patch(
"flwr.superlink.servicer.control.control_servicer.get_fab_config"
) as mock_get_fab_config,
patch(
"flwr.superlink.servicer.control.control_servicer.get_metadata_from_config"
) as mock_get_metadata_from_config,
patch.object(
self.state.federation_manager,
"can_execute",
return_value=True,
) as mock_can_execute,
):
mock_get_fab_config.return_value = {
"tool": {"flwr": {"app": {"config": {"train": {"lr": 0.1}}}}}
}
mock_get_metadata_from_config.return_value = ("flwr/demo", "v1.0.0")
_ = self.servicer.StartRun(request, Mock())

mock_can_execute.assert_called_once_with(
self.aid,
ActionType.START_RUN,
StartRunContext(federation=NOOP_FEDERATION, runtime=RunTime.DEPLOYMENT),
)

@parameterized.expand([(None,), (1,), (2,), (3,), (9,)]) # type: ignore
def test_list_runs(self, limit: int | None) -> None:
"""Test List method of ControlServicer with --runs option."""
Expand Down
Loading