Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
3752d57
add restrict_networking_actions code
charliepark Sep 12, 2025
420ed2b
More development of Polar-based change to permissions
charliepark Sep 15, 2025
ad106d6
Polar working, perhaps; lots of permission rules
charliepark Sep 16, 2025
9966d8e
refactor; add a few tests that might still need a bit of tweaking
charliepark Sep 17, 2025
a06feda
clean up migration files
charliepark Sep 19, 2025
062d441
small cleanup
charliepark Sep 19, 2025
9450227
fix clippy issues
charliepark Sep 19, 2025
9c709fd
safer migratino file
charliepark Sep 19, 2025
37f1e1e
merge main and resolve conflicts
charliepark Sep 19, 2025
91d7856
Update nexus, tests
charliepark Sep 19, 2025
759f3a6
formatting
charliepark Sep 19, 2025
1f54b26
remove unused method
charliepark Oct 2, 2025
01434b2
Move logic from silo to project
charliepark Oct 15, 2025
56af8c8
Remove accidentally committed .bak files
charliepark Oct 15, 2025
ced5348
cargo fmt
charliepark Oct 15, 2025
d8764d3
Merge main
charliepark Oct 15, 2025
6cad94e
fix clippy issues
charliepark Oct 15, 2025
7737776
cargo fmt again
charliepark Oct 15, 2025
ed3e5f4
Update tests
charliepark Oct 16, 2025
f3b34a9
Merge branch 'main' into restrict_networking_actions_4
charliepark Oct 16, 2025
6f41131
Update version number in dbint.sql
charliepark Oct 17, 2025
937d15e
Merge branch 'main' into restrict_networking_actions_4
charliepark Oct 17, 2025
51207ca
remove redundant Silo query
charliepark Oct 17, 2025
f3605a5
Update tests
charliepark Oct 17, 2025
d9b8bcd
cargo fmt
charliepark Oct 17, 2025
c0e922d
Merge branch 'main' into restrict_networking_actions_4
charliepark Oct 18, 2025
388e903
Move restriction check to actor silo policy, rather than project silo
charliepark Oct 20, 2025
4b4c392
cargo fmt
charliepark Oct 20, 2025
901e241
Merge branch 'main' into restrict_networking_actions_4
charliepark Oct 21, 2025
2dae549
Add test back in
charliepark Oct 21, 2025
109b966
Update checks for VPC update, more tests
charliepark Oct 21, 2025
4768df9
cargo fmt
charliepark Oct 21, 2025
15713f5
Add VPC subnet restriction and tests
charliepark Oct 21, 2025
7bb9e35
Add routers and router route checks and tests
charliepark Oct 21, 2025
bbf0c19
Add networking restrictions check to Internet Gateways and Firewall R…
charliepark Oct 21, 2025
1320eb2
Refactor tests
charliepark Oct 21, 2025
49cbce0
Add internet gateway attach/detach restrictions
charliepark Oct 21, 2025
7ce7cc1
Add tests for IP Pools / Addresses
charliepark Oct 21, 2025
a948699
Merge branch 'main' into restrict_networking_actions_4
charliepark Oct 21, 2025
2daa80e
Add bypass on VPC creation saga in restricted environments
charliepark Oct 22, 2025
5d2b21c
cargo fmt
charliepark Oct 22, 2025
49488e7
merge main and resolve conflicts
charliepark Oct 22, 2025
1ef5e62
Merge branch 'main' into restrict_networking_actions_4
charliepark Oct 22, 2025
680a1a1
Update dbint.sql version again
charliepark Oct 22, 2025
479a696
remove pub from method
charliepark Oct 22, 2025
b9b5465
Add missing Polar rules
charliepark Oct 22, 2025
28ba5f3
Use InProjectNetworking snippet instead of permissive InProject snippet
charliepark Oct 22, 2025
fefa847
Adjust VPC deletion
charliepark Oct 22, 2025
b56ff84
Use project:createChild check for VPC creation in lieu of creating a …
charliepark Oct 22, 2025
2331d3f
Comment out callsites for check_networking_restrictions; enable for V…
charliepark Oct 22, 2025
acc70af
Remove unneeded Rust checks and add missing Polar rules
charliepark Oct 23, 2025
0b82612
Add tests
charliepark Oct 23, 2025
79a849e
cargo fmt
charliepark Oct 23, 2025
57c79b6
Merge main and resolve conflicts
charliepark Oct 23, 2025
40fd6ba
Fix compilation errors
charliepark Oct 23, 2025
c0ac662
Refactor Polar rules
charliepark Oct 23, 2025
134af5a
Remove empty lines
charliepark Oct 23, 2025
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
19 changes: 14 additions & 5 deletions nexus/auth/src/authn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ impl Context {
Details { actor: Actor::Scim { silo_id } },
// This should never be non-empty, we don't want the SCIM user
// to ever have associated roles.
Some(SiloAuthnPolicy::new(BTreeMap::default())),
Some(SiloAuthnPolicy::new(BTreeMap::default(), false)),
),
schemes_tried: Vec::new(),
}
Expand All @@ -289,20 +289,28 @@ pub struct SiloAuthnPolicy {
/// Describes which fleet-level roles are automatically conferred by which
/// silo-level roles.
mapped_fleet_roles: BTreeMap<SiloRole, BTreeSet<FleetRole>>,

/// When true, restricts networking actions to Silo Admins only
restrict_network_actions: bool,
Comment on lines +293 to +294
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest making this an enum with two explicit values, like:

enum ProjectRolesConfigureNetworking {
    Allowed,
    Disallowed,
}

}

impl SiloAuthnPolicy {
pub fn new(
mapped_fleet_roles: BTreeMap<SiloRole, BTreeSet<FleetRole>>,
restrict_network_actions: bool,
) -> SiloAuthnPolicy {
SiloAuthnPolicy { mapped_fleet_roles }
SiloAuthnPolicy { mapped_fleet_roles, restrict_network_actions }
}

pub fn mapped_fleet_roles(
&self,
) -> &BTreeMap<SiloRole, BTreeSet<FleetRole>> {
&self.mapped_fleet_roles
}

pub fn restrict_network_actions(&self) -> bool {
self.restrict_network_actions
}
}

impl TryFrom<&nexus_db_model::Silo> for SiloAuthnPolicy {
Expand All @@ -311,9 +319,10 @@ impl TryFrom<&nexus_db_model::Silo> for SiloAuthnPolicy {
fn try_from(
value: &nexus_db_model::Silo,
) -> Result<Self, omicron_common::api::external::Error> {
value
.mapped_fleet_roles()
.map(|mapped_fleet_roles| SiloAuthnPolicy { mapped_fleet_roles })
value.mapped_fleet_roles().map(|mapped_fleet_roles| SiloAuthnPolicy {
mapped_fleet_roles,
restrict_network_actions: value.restrict_network_actions,
})
}
}

Expand Down
13 changes: 13 additions & 0 deletions nexus/auth/src/authz/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ impl AuthenticatedActor {
})
.collect()
}

/// Returns whether this actor's Silo restricts networking actions to Silo
/// Admins only
pub fn silo_restricts_networking(&self) -> bool {
self.silo_policy
.as_ref()
.map(|policy| policy.restrict_network_actions())
.unwrap_or(false)
}
}

impl PartialEq for AuthenticatedActor {
Expand Down Expand Up @@ -164,5 +173,9 @@ impl oso::PolarClass for AuthenticatedActor {
authn::Actor::Scim { .. } => false,
},
)
.add_method(
"silo_restricts_networking",
|a: &AuthenticatedActor| a.silo_restricts_networking(),
)
}
}
14 changes: 7 additions & 7 deletions nexus/auth/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1142,55 +1142,55 @@ authz_resource! {
parent = "Project",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
polar_snippet = InProjectNetworking,
}

authz_resource! {
name = "VpcRouter",
parent = "Vpc",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
polar_snippet = InProjectNetworking,
}

authz_resource! {
name = "RouterRoute",
parent = "VpcRouter",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
polar_snippet = InProjectNetworking,
}

authz_resource! {
name = "VpcSubnet",
parent = "Vpc",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
polar_snippet = InProjectNetworking,
}

authz_resource! {
name = "InternetGateway",
parent = "Vpc",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
polar_snippet = InProjectNetworking,
}

authz_resource! {
name = "InternetGatewayIpPool",
parent = "InternetGateway",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
polar_snippet = InProjectNetworking,
}

authz_resource! {
name = "InternetGatewayIpAddress",
parent = "InternetGateway",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
polar_snippet = InProjectNetworking,
}

authz_resource! {
Expand Down
55 changes: 55 additions & 0 deletions nexus/auth/src/authz/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,61 @@ mod test {
Context::new(Arc::new(authn), Arc::new(authz), datastore)
}

#[tokio::test]
async fn test_networking_restrictions_structure() {
// This test verifies that our networking restrictions compile and can be instantiated
let logctx =
dev::test_setup_log("test_networking_restrictions_structure");

// Test that SiloAuthnPolicy with networking restrictions can be created
let restricted_policy = authn::SiloAuthnPolicy::new(
std::collections::BTreeMap::new(),
true, // restrict_network_actions
);

let normal_policy = authn::SiloAuthnPolicy::new(
std::collections::BTreeMap::new(),
false, // restrict_network_actions
);

// Verify that the restricts_networking method works
assert_eq!(restricted_policy.restrict_network_actions(), true);
assert_eq!(normal_policy.restrict_network_actions(), false);

// Test that we can create auth contexts with these policies
let authn_restricted = authn::Context::for_test_user(
omicron_uuid_kinds::SiloUserUuid::new_v4(),
Uuid::new_v4(),
restricted_policy,
);
let authn_normal = authn::Context::for_test_user(
omicron_uuid_kinds::SiloUserUuid::new_v4(),
Uuid::new_v4(),
normal_policy,
);

// Verify the policies are accessible
assert_eq!(
authn_restricted
.silo_authn_policy()
.unwrap()
.restrict_network_actions(),
true
);
assert_eq!(
authn_normal
.silo_authn_policy()
.unwrap()
.restrict_network_actions(),
false
);

println!(
"Networking restrictions structure test completed successfully"
);
logctx.cleanup_successful();
}

#[tokio::test]
async fn test_unregistered_resource() {
let logctx = dev::test_setup_log("test_unregistered_resource");
Expand Down
73 changes: 73 additions & 0 deletions nexus/auth/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -748,3 +748,76 @@ has_relation(silo: Silo, "parent_silo", scim_client_bearer_token_list: ScimClien
if scim_client_bearer_token_list.silo = silo;
has_relation(fleet: Fleet, "parent_fleet", collection: ScimClientBearerTokenList)
if collection.silo.fleet = fleet;

# NETWORKING RESTRICTIONS BASED ON SILO SETTINGS
#
# These rules enforce networking restrictions when a silo has restrict_network_actions = true.
# For silos with this restriction, only Silo Admins can perform networking create/modify/delete actions,
# while read/list actions remain available to all project collaborators.

# Determine if the actor has permissions to modify networking resources
can_modify_networking_resource(actor: AuthenticatedActor, project: Project) if
# Always allow silo admins to update networking resources
has_role(actor, "admin", project.silo) or
# Allow project collaborators to update networking resources if the actor's silo allows it
# Note that the restriction is checked on the actor's silo, not embedded in the project
(has_role(actor, "collaborator", project) and not actor.silo_restricts_networking());

# Helper predicates to reduce duplication across networking resources
networking_write_perm(actor: AuthenticatedActor, action: String, project: Project) if
action in ["create_child", "modify", "delete"] and
can_modify_networking_resource(actor, project);

networking_read_perm(actor: AuthenticatedActor, action: String, project: Project) if
action in ["read", "list_children"] and
has_role(actor, "viewer", project);

# Apply networking restrictions to all networking resources
# VPCs (project path: vpc.project)
has_permission(actor: AuthenticatedActor, action: String, vpc: Vpc) if
networking_write_perm(actor, action, vpc.project);

has_permission(actor: AuthenticatedActor, action: String, vpc: Vpc) if
networking_read_perm(actor, action, vpc.project);

# VPC Routers (project path: router.vpc.project)
has_permission(actor: AuthenticatedActor, action: String, router: VpcRouter) if
networking_write_perm(actor, action, router.vpc.project);

has_permission(actor: AuthenticatedActor, action: String, router: VpcRouter) if
networking_read_perm(actor, action, router.vpc.project);

# VPC Subnets (project path: subnet.vpc.project)
has_permission(actor: AuthenticatedActor, action: String, subnet: VpcSubnet) if
networking_write_perm(actor, action, subnet.vpc.project);

has_permission(actor: AuthenticatedActor, action: String, subnet: VpcSubnet) if
networking_read_perm(actor, action, subnet.vpc.project);

# Internet Gateways (project path: gateway.vpc.project)
has_permission(actor: AuthenticatedActor, action: String, gateway: InternetGateway) if
networking_write_perm(actor, action, gateway.vpc.project);

has_permission(actor: AuthenticatedActor, action: String, gateway: InternetGateway) if
networking_read_perm(actor, action, gateway.vpc.project);

# Router Routes (project path: route.vpc_router.vpc.project)
has_permission(actor: AuthenticatedActor, action: String, route: RouterRoute) if
networking_write_perm(actor, action, route.vpc_router.vpc.project);

has_permission(actor: AuthenticatedActor, action: String, route: RouterRoute) if
networking_read_perm(actor, action, route.vpc_router.vpc.project);

# Internet Gateway IP Pool attachments (project path: pool.internet_gateway.vpc.project)
has_permission(actor: AuthenticatedActor, action: String, pool: InternetGatewayIpPool) if
networking_write_perm(actor, action, pool.internet_gateway.vpc.project);

has_permission(actor: AuthenticatedActor, action: String, pool: InternetGatewayIpPool) if
networking_read_perm(actor, action, pool.internet_gateway.vpc.project);

# Internet Gateway IP Address attachments (project path: addr.internet_gateway.vpc.project)
has_permission(actor: AuthenticatedActor, action: String, addr: InternetGatewayIpAddress) if
networking_write_perm(actor, action, addr.internet_gateway.vpc.project);

has_permission(actor: AuthenticatedActor, action: String, addr: InternetGatewayIpAddress) if
networking_read_perm(actor, action, addr.internet_gateway.vpc.project);
66 changes: 66 additions & 0 deletions nexus/authz-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ enum PolarSnippet {
/// Generate it as a resource nested within a Project (either directly or
/// indirectly)
InProject,

/// Generate it as a networking resource nested within a Project
/// (like InProject, but without default permission rules - all rules
/// defined in omicron.polar for networking restrictions)
InProjectNetworking,
}

/// Implementation of [`authz_resource!`]
Expand Down Expand Up @@ -433,6 +438,67 @@ fn do_authz_resource(
resource_name,
parent_as_snake,
),

// InProjectNetworking: Like InProject, but NO default permission rules.
// All permission rules are defined in omicron.polar to enforce
// networking restrictions. Only defines resource structure + relations.
(PolarSnippet::InProjectNetworking, "Project") => format!(
r#"
resource {} {{
permissions = [
"list_children",
"modify",
"read",
"create_child",
"delete",
];

relations = {{ containing_project: Project }};
# NOTE: No permission rules defined here!
# All permissions controlled by custom networking restriction
# rules in omicron.polar (can_modify_networking_resource)
Comment on lines +457 to +459
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why you can't put the stuff that's copy/pasted for each networking resource in omicron.polar in here instead.

}}

has_relation(parent: Project, "containing_project", child: {})
if child.project = parent;
"#,
resource_name, resource_name,
),

(PolarSnippet::InProjectNetworking, _) => format!(
r#"
resource {} {{
permissions = [
"list_children",
"modify",
"read",
"create_child",
"delete",
];

relations = {{
containing_project: Project,
parent: {}
}};
# NOTE: No permission rules defined here!
# All permissions controlled by custom networking restriction
# rules in omicron.polar (can_modify_networking_resource)
}}

has_relation(project: Project, "containing_project", child: {})
if has_relation(project, "containing_project", child.{});

has_relation(parent: {}, "parent", child: {})
if child.{} = parent;
"#,
resource_name,
parent_resource_name,
resource_name,
parent_as_snake,
parent_resource_name,
resource_name,
parent_as_snake,
),
};

let doc_struct = format!(
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-fixed-data/src/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub static DEFAULT_SILO: LazyLock<model::Silo> = LazyLock::new(|| {
admin_group_name: None,
tls_certificates: vec![],
mapped_fleet_roles: Default::default(),
restrict_network_actions: None,
},
)
.unwrap()
Expand All @@ -55,6 +56,7 @@ pub static INTERNAL_SILO: LazyLock<model::Silo> = LazyLock::new(|| {
admin_group_name: None,
tls_certificates: vec![],
mapped_fleet_roles: Default::default(),
restrict_network_actions: None,
},
)
.unwrap()
Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(201, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(202, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(202, "restrict-network-actions"),
KnownVersion::new(201, "scim-client-bearer-token"),
KnownVersion::new(200, "dual-stack-network-interfaces"),
KnownVersion::new(199, "multicast-pool-support"),
Expand Down
Loading
Loading