Skip to content

Commit e44bc27

Browse files
[multicast] implicit group lifecycle with IP pool integration
This PR also addresses permission models, object deletion, and error handling questions related to reserved addresses presented in @askfongjojo's testing Google Doc (default IP Pools are covered in a follow-up, stacked PR). In thinking through the *Groups* API, permission scopes, and flexibility, @rcgoodfellow mentioned this consideration: > Do we need an explicit notion of a group object at all? Or can > instances simply allocate/deallocate group IPs from pools, and there is > no explicit management of group objects. With Fleet admins having access control to create pools and link silos to a pool, we arrived at the idea of replacing the current explicit multicast group CRUD with an implicit lifecycle, where groups are created upon the first member join and deleted when the last member leaves. **Note**: Most of the PR's changes are test-related due to moving away from the explicit multicast group(s) lifecycle. Auth Model: - Discovery (fleet-scoped): - Read/list groups and list members: any authenticated user in the same fleet. - Membership (project-scoped): - Join/leave requires Instance::Modify on the specific instance. - Creation control: - Implicit group creation only when the s silo is linked to a suitable multicast pool (by name or by explicit IP in that pool). Behavior: - Implicit lifecycle: - Create on first join (idempotent); delete when last member leaves (atomic mark-for-removal, reconciler schedules cleanup). - Addressing and validation: - Implicit allocation from the s linked multicast pools. - SSM/ASM semantics enforced: - IPv4 SSM 232/8 and IPv6 ff3x::/32 - Error handling: - Reserved/invalid multicast ranges rejected at pool/range add time. API: - Primary flows: - Group-centric member management: POST/DELETE /v1/multicast-groups/{group}/members - Instance-centric join/leave: PUT/DELETE /v1/instances/{instance}/multicast-groups/{group} - Discovery endpoints remain for list/view; there is no explicit group create/update/delete. - This is a *breaking* change, but multicast is not yet enabled or available in production Key changes: - Implicit group model; groups exist while they have members. - IP pool integration for multicast allocation with silo link gating. - Simplified API centered on join/leave flows. - Add multicast_ip to the member table for responses. - For consistency, move to `Instant` type over `SystemTime` for mcast-related caches Follow-ups (stacked PRs) - [ ] Remove MVLAN from group data model. - [ ] Default IP pool support (IPv4/IPv6 Followrequire unicast/multicast). - [ ] Dendrite: use omicron-common constants for validation.
1 parent 64b40cb commit e44bc27

File tree

53 files changed

+7614
-6459
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+7614
-6459
lines changed

common/src/address.rs

Lines changed: 130 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,52 +25,70 @@ pub const SLED_PREFIX: u8 = 64;
2525

2626
// Multicast constants
2727

28-
/// IPv4 Source-Specific Multicast (SSM) subnet as defined in RFC 4607:
29-
/// <https://tools.ietf.org/html/rfc4607>.
28+
/// IPv4 Source-Specific Multicast (SSM) subnet.
3029
///
31-
/// RFC 4607 Section 3 allocates 232.0.0.0/8 as the IPv4 SSM address range.
30+
/// See [RFC 4607 §3] for the IPv4 SSM address range allocation (232.0.0.0/8).
3231
/// This is a single contiguous block, unlike IPv6 which has per-scope ranges.
33-
pub const IPV4_SSM_SUBNET: oxnet::Ipv4Net =
34-
oxnet::Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8);
32+
///
33+
/// [RFC 4607 §3]: https://www.rfc-editor.org/rfc/rfc4607#section-3
34+
pub const IPV4_SSM_SUBNET: Ipv4Net =
35+
Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8);
3536

36-
/// IPv6 Source-Specific Multicast (SSM) subnet as defined in RFC 4607:
37-
/// <https://tools.ietf.org/html/rfc4607>.
37+
/// IPv6 Source-Specific Multicast (SSM) subnet.
3838
///
39-
/// RFC 4607 Section 3 specifies "FF3x::/32 for each scope x" - meaning one
40-
/// /32 block per scope (FF30::/32, FF31::/32, ..., FF3F::/32).
39+
/// See [RFC 4607 §3] for SSM scope allocation. The RFC specifies "ff3x::/32
40+
/// for each scope x" - meaning one /32 block per scope (ff30::/32, ff31::/32,
41+
/// ..., ff3f::/32).
4142
///
4243
/// We use /12 as an implementation convenience to match all these blocks with
4344
/// a single subnet. This works because all SSM addresses share the same first
4445
/// 12 bits:
45-
/// - Bits 0-7: 11111111 (0xFF, multicast prefix)
46+
/// - Bits 0-7: 11111111 (0xff, multicast prefix)
4647
/// - Bits 8-11: 0011 (flag field = 3, indicating SSM)
47-
/// - Bits 12-15: xxxx (scope field, any value 0-F)
48+
/// - Bits 12-15: xxxx (scope field, any value 0-f)
4849
///
49-
/// Thus FF30::/12 efficiently matches FF30:: through FF3F:FFFF:...:FFFF,
50+
/// Thus ff30::/12 efficiently matches ff30:: through ff3f:ffff:...:ffff,
5051
/// covering all SSM scopes.
51-
pub const IPV6_SSM_SUBNET: oxnet::Ipv6Net = oxnet::Ipv6Net::new_unchecked(
52-
Ipv6Addr::new(0xff30, 0, 0, 0, 0, 0, 0, 0),
53-
12,
54-
);
52+
///
53+
/// This superset is used only for contains-based classification and validation
54+
/// (e.g., `contains()` checks). It is not an allocation boundary.
55+
///
56+
/// [RFC 4607 §3]: https://www.rfc-editor.org/rfc/rfc4607#section-3
57+
pub const IPV6_SSM_SUBNET: Ipv6Net =
58+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff30, 0, 0, 0, 0, 0, 0, 0), 12);
59+
60+
/// Maximum source IPs per SSM group member (per [RFC 3376] IGMPv3).
61+
///
62+
/// [RFC 3376]: https://www.rfc-editor.org/rfc/rfc3376
63+
pub const MAX_SSM_SOURCE_IPS: usize = 64;
5564

5665
/// IPv4 multicast address range (224.0.0.0/4).
57-
/// See RFC 5771 (IPv4 Multicast Address Assignments):
58-
/// <https://www.rfc-editor.org/rfc/rfc5771>
66+
///
67+
/// See [RFC 5771] for IPv4 multicast address assignments.
68+
///
69+
/// [RFC 5771]: https://www.rfc-editor.org/rfc/rfc5771
5970
pub const IPV4_MULTICAST_RANGE: Ipv4Net =
6071
Ipv4Net::new_unchecked(Ipv4Addr::new(224, 0, 0, 0), 4);
6172

6273
/// IPv4 link-local multicast subnet (224.0.0.0/24).
74+
///
6375
/// This range is reserved for local network control protocols and should not
6476
/// be routed beyond the local link. Includes addresses for protocols like
6577
/// OSPF (224.0.0.5), RIPv2 (224.0.0.9), and other local routing protocols.
66-
/// See RFC 5771 Section 4:
67-
/// <https://www.rfc-editor.org/rfc/rfc5771#section-4>
78+
///
79+
/// See [RFC 5771 §4] for link-local multicast address assignments. The IANA
80+
/// IPv4 Multicast Address Space registry is the canonical source for
81+
/// assignments.
82+
///
83+
/// [RFC 5771 §4]: https://www.rfc-editor.org/rfc/rfc5771#section-4
6884
pub const IPV4_LINK_LOCAL_MULTICAST_SUBNET: Ipv4Net =
6985
Ipv4Net::new_unchecked(Ipv4Addr::new(224, 0, 0, 0), 24);
7086

7187
/// IPv6 multicast address range (ff00::/8).
72-
/// See RFC 4291 (IPv6 Addressing Architecture):
73-
/// <https://www.rfc-editor.org/rfc/rfc4291>
88+
///
89+
/// See [RFC 4291] for IPv6 addressing architecture.
90+
///
91+
/// [RFC 4291]: https://www.rfc-editor.org/rfc/rfc4291
7492
pub const IPV6_MULTICAST_RANGE: Ipv6Net =
7593
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 8);
7694

@@ -82,25 +100,98 @@ pub const IPV6_MULTICAST_PREFIX: u16 = 0xff00;
82100
pub const IPV6_ADMIN_SCOPED_MULTICAST_PREFIX: u16 = 0xff04;
83101

84102
/// IPv6 interface-local multicast subnet (ff01::/16).
103+
///
85104
/// These addresses are not routable and should not be added to IP pools.
86-
/// See RFC 4291 Section 2.7 (multicast scope field):
87-
/// <https://www.rfc-editor.org/rfc/rfc4291#section-2.7>
88-
pub const IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET: oxnet::Ipv6Net =
89-
oxnet::Ipv6Net::new_unchecked(
90-
Ipv6Addr::new(0xff01, 0, 0, 0, 0, 0, 0, 0),
91-
16,
92-
);
105+
///
106+
/// See [RFC 4291 §2.7] for multicast scope field definitions.
107+
///
108+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
109+
pub const IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET: Ipv6Net =
110+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff01, 0, 0, 0, 0, 0, 0, 0), 16);
111+
112+
/// Last address in the IPv6 interface-local multicast subnet.
113+
pub const IPV6_INTERFACE_LOCAL_MULTICAST_LAST: Ipv6Addr = Ipv6Addr::new(
114+
0xff01, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
115+
);
93116

94117
/// IPv6 link-local multicast subnet (ff02::/16).
118+
///
95119
/// These addresses are not routable beyond the local link and should not be
96120
/// added to IP pools.
97-
/// See RFC 4291 Section 2.7 (multicast scope field):
98-
/// <https://www.rfc-editor.org/rfc/rfc4291#section-2.7>
99-
pub const IPV6_LINK_LOCAL_MULTICAST_SUBNET: oxnet::Ipv6Net =
100-
oxnet::Ipv6Net::new_unchecked(
101-
Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0),
102-
16,
103-
);
121+
///
122+
/// See [RFC 4291 §2.7] for multicast scope field definitions.
123+
///
124+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
125+
pub const IPV6_LINK_LOCAL_MULTICAST_SUBNET: Ipv6Net =
126+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0), 16);
127+
128+
/// Last address in the IPv6 link-local multicast subnet.
129+
pub const IPV6_LINK_LOCAL_MULTICAST_LAST: Ipv6Addr = Ipv6Addr::new(
130+
0xff02, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
131+
);
132+
133+
/// IPv6 reserved-scope multicast subnet (ff00::/16).
134+
///
135+
/// Scope 0 is reserved - packets with this scope must not be originated and
136+
/// must be silently dropped if received. These addresses should not be added
137+
/// to IP pools.
138+
///
139+
/// See [RFC 4291 §2.7] for multicast scope field definitions.
140+
///
141+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
142+
pub const IPV6_RESERVED_SCOPE_MULTICAST_SUBNET: Ipv6Net =
143+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 16);
144+
145+
/// Last address in the IPv6 reserved-scope multicast subnet.
146+
pub const IPV6_RESERVED_SCOPE_MULTICAST_LAST: Ipv6Addr = Ipv6Addr::new(
147+
0xff00, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
148+
);
149+
150+
/// IPv4 GLOP addressing block (233.0.0.0/8).
151+
///
152+
/// This range is reserved for GLOP addressing and should not be allocated from
153+
/// IP pools for general multicast use.
154+
///
155+
/// See [RFC 3180] for GLOP address allocation.
156+
///
157+
/// [RFC 3180]: https://www.rfc-editor.org/rfc/rfc3180
158+
pub const IPV4_GLOP_MULTICAST_SUBNET: Ipv4Net =
159+
Ipv4Net::new_unchecked(Ipv4Addr::new(233, 0, 0, 0), 8);
160+
161+
/// IPv4 administratively scoped multicast subnet (239.0.0.0/8).
162+
///
163+
/// This range is reserved for organization-local administrative scoping and
164+
/// should not be allocated from IP pools for general multicast use.
165+
///
166+
/// See [RFC 2365] for administratively scoped IP multicast.
167+
///
168+
/// [RFC 2365]: https://www.rfc-editor.org/rfc/rfc2365
169+
pub const IPV4_ADMIN_SCOPED_MULTICAST_SUBNET: Ipv4Net =
170+
Ipv4Net::new_unchecked(Ipv4Addr::new(239, 0, 0, 0), 8);
171+
172+
/// Specifically reserved IPv4 multicast addresses.
173+
///
174+
/// These addresses are reserved for specific protocols and should not be
175+
/// allocated from IP pools. They fall outside the link-local range
176+
/// (224.0.0.0/24) but are still reserved.
177+
///
178+
/// - 224.0.1.1: NTP (Network Time Protocol, RFC 5905)
179+
/// - 224.0.1.39: Cisco Auto-RP-Announce
180+
/// - 224.0.1.40: Cisco Auto-RP-Discovery
181+
/// - 224.0.1.129-132: PTP (Precision Time Protocol, IEEE 1588)
182+
///
183+
/// See [IANA IPv4 Multicast Address Space Registry] for complete assignments.
184+
///
185+
/// [IANA IPv4 Multicast Address Space Registry]: https://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml
186+
pub const IPV4_SPECIFIC_RESERVED_MULTICAST_ADDRS: [Ipv4Addr; 7] = [
187+
Ipv4Addr::new(224, 0, 1, 1), // NTP
188+
Ipv4Addr::new(224, 0, 1, 39), // Cisco Auto-RP-Announce
189+
Ipv4Addr::new(224, 0, 1, 40), // Cisco Auto-RP-Discovery
190+
Ipv4Addr::new(224, 0, 1, 129), // PTP-primary
191+
Ipv4Addr::new(224, 0, 1, 130), // PTP-alternate1
192+
Ipv4Addr::new(224, 0, 1, 131), // PTP-alternate2
193+
Ipv4Addr::new(224, 0, 1, 132), // PTP-alternate3
194+
];
104195

105196
/// maximum possible value for a tcp or udp port
106197
pub const MAX_PORT: u16 = u16::MAX;
@@ -254,8 +345,9 @@ pub static NTP_OPTE_IPV6_SUBNET: LazyLock<Ipv6Net> = LazyLock::new(|| {
254345
// Anycast is a mechanism in which a single IP address is shared by multiple
255346
// devices, and the destination is located based on routing distance.
256347
//
257-
// This is covered by RFC 4291 in much more detail:
258-
// <https://datatracker.ietf.org/doc/html/rfc4291#section-2.6>
348+
// See [RFC 4291 §2.6] for anycast address allocation.
349+
//
350+
// [RFC 4291 §2.6]: https://www.rfc-editor.org/rfc/rfc4291#section-2.6
259351
//
260352
// Anycast addresses are always the "zeroeth" address within a subnet. We
261353
// always explicitly skip these addresses within our network.

dev-tools/omdb/tests/successes.out

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ task: "multicast_reconciler"
713713
configured period: every <REDACTED_DURATION>m
714714
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
715715
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
716-
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
716+
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "empty_groups_marked": Number(0), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
717717

718718
task: "phantom_disks"
719719
configured period: every <REDACTED_DURATION>s
@@ -1281,7 +1281,7 @@ task: "multicast_reconciler"
12811281
configured period: every <REDACTED_DURATION>m
12821282
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
12831283
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
1284-
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
1284+
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "empty_groups_marked": Number(0), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
12851285

12861286
task: "phantom_disks"
12871287
configured period: every <REDACTED_DURATION>s

illumos-utils/src/opte/port_manager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,7 @@ impl PortManager {
836836
///
837837
/// TODO: Once OPTE kernel module supports multicast group APIs, this
838838
/// method should be updated to configure OPTE port-level multicast
839-
/// group membership. Note: multicast groups are fleet-wide and can span
839+
/// group membership. Note: multicast groups are fleet-scoped and can span
840840
/// across VPCs.
841841
pub fn multicast_groups_ensure(
842842
&self,

nexus/auth/src/authz/api_resources.rs

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//! accept these `authz` types.
1313
//!
1414
//! The `authz` types can be passed to
15-
//! [`crate::context::OpContext::authorize()`] to do an authorization check --
15+
//! [`OpContext::authorize()`] to do an authorization check --
1616
//! is the caller allowed to perform some action on the resource? This is the
1717
//! primary way of doing authz checks in Nexus.
1818
//!
@@ -153,7 +153,7 @@ where
153153
/// Fleets.
154154
///
155155
/// This object is used for authorization checks on a Fleet by passing it as the
156-
/// `resource` argument to [`crate::context::OpContext::authorize()`]. You
156+
/// `resource` argument to [`OpContext::authorize()`]. You
157157
/// don't construct a `Fleet` yourself -- use the global [`FLEET`].
158158
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
159159
pub struct Fleet;
@@ -475,18 +475,16 @@ impl AuthorizedResource for IpPoolList {
475475
/// collection.
476476
///
477477
/// **Authorization Model:**
478-
/// - Multicast groups are fleet-wide resources (similar to IP pools).
479-
/// - Any authenticated user within a silo in the fleet can create, list, read,
480-
/// and modify groups. This includes project collaborators, silo collaborators,
481-
/// and silo admins.
482-
/// - Cross-silo multicast communication is enabled by fleet-wide access.
478+
/// - Multicast groups are fleet-scoped resources.
479+
/// - Groups are created when the first instance joins and deleted when the last
480+
/// member leaves (implicit lifecycle).
481+
/// - **List**: Any authenticated user in the fleet (for discovery).
483482
///
484483
/// The fleet-level collection endpoint (`/v1/multicast-groups`) allows:
485-
/// - Any authenticated user within the fleet's silos to create and list groups.
486-
/// - Instances from different projects and silos can join the same multicast groups.
484+
/// - Fleet-wide listing for all authenticated users (discovery).
485+
/// - Instances from different projects and silos can join the same groups.
487486
///
488-
/// See `omicron.polar` for the detailed policy rules that grant fleet-wide
489-
/// access to authenticated silo users for multicast group operations.
487+
/// See `omicron.polar` for the detailed policy rules.
490488
#[derive(Clone, Copy, Debug)]
491489
pub struct MulticastGroupList;
492490

@@ -1393,35 +1391,21 @@ authz_resource! {
13931391

13941392
// MulticastGroup Authorization
13951393
//
1396-
// MulticastGroups are **fleet-scoped resources** (parent = "Fleet"), similar to
1397-
// IP pools, to enable efficient cross-project and cross-silo multicast
1398-
// communication.
1394+
// MulticastGroups are **fleet-scoped resources** with an implicit lifecycle:
1395+
// created when the first instance joins and deleted when the last member leaves.
13991396
//
14001397
// Authorization rules:
1401-
// - Creating/modifying groups: Any authenticated user within a silo in the fleet.
1402-
// This includes project collaborators, silo collaborators, and silo admins.
1403-
// - Listing groups: Any authenticated user within a silo in the fleet
1404-
// - Viewing individual groups: Any authenticated user within a silo in the fleet
1405-
// - Attaching instances to groups: only requires Instance::Modify permission
1406-
// (users can attach their own instances to any fleet-scoped group)
1398+
// - List/Read: Any authenticated user in their fleet
1399+
// - Attach/detach: Instance::Modify permission on the instance being attached
14071400
//
1408-
// Fleet::Admin role can also perform all operations via the parent Fleet relation.
1409-
//
1410-
// See omicron.polar for the special `has_permission` rules that grant create/modify/
1411-
// list/read access to authenticated silo users (including project collaborators),
1412-
// enabling cross-project and cross-silo multicast communication without requiring
1413-
// Fleet::Admin or Fleet::Viewer roles.
1414-
//
1415-
// Member management: `MulticastGroup` member attachments/detachments (instances
1416-
// joining/leaving groups) use the existing `MulticastGroup` and `Instance`
1417-
// authz resources rather than creating a separate `MulticastGroupMember` authz
1418-
// resource.
1401+
// See omicron.polar for the custom authorization rules.
1402+
14191403
authz_resource! {
14201404
name = "MulticastGroup",
14211405
parent = "Fleet",
14221406
primary_key = Uuid,
14231407
roles_allowed = false,
1424-
polar_snippet = FleetChild,
1408+
polar_snippet = Custom,
14251409
}
14261410

14271411
// Customer network integration resources nested below "Fleet"

nexus/auth/src/authz/omicron.polar

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -493,45 +493,40 @@ has_permission(actor: AuthenticatedActor, "create_child", ip_pool: IpPool)
493493
if actor.is_user and silo in actor.silo and silo.fleet = ip_pool.fleet;
494494

495495
# Describes the policy for accessing "/v1/multicast-groups" in the API
496+
# Groups are created when the first instance joins and deleted when the last leaves.
496497
resource MulticastGroupList {
497-
permissions = [
498-
"list_children",
499-
"create_child",
500-
];
498+
permissions = [ "list_children" ];
501499

502500
relations = { parent_fleet: Fleet };
503-
# Fleet Administrators can create multicast groups
504-
"create_child" if "admin" on "parent_fleet";
505501

506502
# Fleet Viewers can list multicast groups
507503
"list_children" if "viewer" on "parent_fleet";
508504
}
509505
has_relation(fleet: Fleet, "parent_fleet", multicast_group_list: MulticastGroupList)
510506
if multicast_group_list.fleet = fleet;
511507

512-
# Any authenticated user can create multicast groups in their fleet.
513-
# This is necessary to allow silo users to create multicast groups for
514-
# cross-project and cross-silo communication without requiring Fleet::Admin.
515-
has_permission(actor: AuthenticatedActor, "create_child", multicast_group_list: MulticastGroupList)
516-
if silo in actor.silo and silo.fleet = multicast_group_list.fleet;
517-
518508
# Any authenticated user can list multicast groups in their fleet.
519-
# This is necessary because multicast groups are fleet-scoped resources that
520-
# silo users need to discover and attach their instances to, without requiring
521-
# Fleet::Viewer role.
509+
# This enables silo users to discover groups for attaching instances,
510+
# without requiring the Fleet::Viewer role.
522511
has_permission(actor: AuthenticatedActor, "list_children", multicast_group_list: MulticastGroupList)
523-
if silo in actor.silo and silo.fleet = multicast_group_list.fleet;
512+
if actor.is_user and silo in actor.silo and silo.fleet = multicast_group_list.fleet;
513+
514+
# MulticastGroup is a fleet-level discovery resource.
515+
# Join/leave authorization is gated by Instance::Modify, not the group itself.
516+
resource MulticastGroup {
517+
permissions = [ "read", "list_children" ];
518+
relations = { parent_fleet: Fleet };
519+
}
520+
has_relation(fleet: Fleet, "parent_fleet", multicast_group: MulticastGroup)
521+
if multicast_group.fleet = fleet;
524522

525-
# Any authenticated user can read and modify individual multicast groups in their fleet.
526-
# Users can create, modify, and consume (attach instances to) multicast groups.
527-
# This enables cross-project and cross-silo multicast while maintaining
528-
# appropriate security boundaries via API authorization and underlay group
529-
# membership validation.
523+
# Any authenticated user can read multicast groups in their fleet
530524
has_permission(actor: AuthenticatedActor, "read", multicast_group: MulticastGroup)
531-
if silo in actor.silo and silo.fleet = multicast_group.fleet;
525+
if actor.is_user and silo in actor.silo and silo.fleet = multicast_group.fleet;
532526

533-
has_permission(actor: AuthenticatedActor, "modify", multicast_group: MulticastGroup)
534-
if silo in actor.silo and silo.fleet = multicast_group.fleet;
527+
# Any authenticated user can list members of multicast groups in their fleet
528+
has_permission(actor: AuthenticatedActor, "list_children", multicast_group: MulticastGroup)
529+
if actor.is_user and silo in actor.silo and silo.fleet = multicast_group.fleet;
535530

536531
# Describes the policy for reading and writing the audit log
537532
resource AuditLog {

0 commit comments

Comments
 (0)