Skip to content

Commit 281a070

Browse files
authored
Move network interface authz to the data store (#778)
* Move network interface authz to the data store - Adds a module-private function for actually inserting the database record, after performing authz checks. This is used in the publicly-available method and in tests. - Adds authz objects to the `DataStore::instance_create_network_interface` method and does authz checks inside them. - Reorders the instance-creation saga. This moves the instance DB record creation before the NIC creation, since the latter can only be attached to an existing instance record. This also allows uniform authz checks inside the `DataStore` method, which wouldn't be possible if the instance record were not yet in the database. Note that this also requires a small change to the data the instance-record-creation saga node serializes. It previously contained the NICs, but these are no longer available at that time. Instead, the NICs are deserialized from the saga node that creates them and used to instantiate the instance runtime object only inside the `sic_instance_ensure` saga node. - Moves authz check for listing NICs for an instance into `DataStore` method - Moves authz check for fetching a single NIC for an instance into the `DataStore` method - Adds the `network_interface_fetch` method, for returning an authz interface and the database record, after checking read access. This uses a `*_noauthz` method as well, both of which are analogous to the other similarly-named methods. Note there's no lookup by ID or path at this point, since they're not really needed yet. - Moves the check for deleting an interface into the `DataStore` method. - Changes how deletion of a previously-deleted NIC works. We used to return a success, but we now return a not-found error, in line with the rest of the API. * Bring NIC create/delete permission in line with other containers
1 parent 1954ce2 commit 281a070

File tree

6 files changed

+195
-119
lines changed

6 files changed

+195
-119
lines changed

nexus/src/authz/api_resources.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,3 +516,4 @@ pub type Instance = ProjectChild;
516516
pub type Vpc = ProjectChild;
517517
pub type VpcRouter = ProjectChild;
518518
pub type VpcSubnet = ProjectChild;
519+
pub type NetworkInterface = ProjectChild;

nexus/src/authz/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ pub use api_resources::Disk;
167167
pub use api_resources::Fleet;
168168
pub use api_resources::FleetChild;
169169
pub use api_resources::Instance;
170+
pub use api_resources::NetworkInterface;
170171
pub use api_resources::Organization;
171172
pub use api_resources::Project;
172173
pub use api_resources::Vpc;

nexus/src/db/datastore.rs

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,8 +1674,29 @@ impl DataStore {
16741674
}
16751675

16761676
// Network interfaces
1677+
1678+
/// Create a network interface attached to the provided instance.
16771679
pub async fn instance_create_network_interface(
16781680
&self,
1681+
opctx: &OpContext,
1682+
authz_subnet: &authz::VpcSubnet,
1683+
authz_instance: &authz::Instance,
1684+
interface: IncompleteNetworkInterface,
1685+
) -> Result<NetworkInterface, NetworkInterfaceError> {
1686+
opctx
1687+
.authorize(authz::Action::CreateChild, authz_instance)
1688+
.await
1689+
.map_err(NetworkInterfaceError::External)?;
1690+
opctx
1691+
.authorize(authz::Action::CreateChild, authz_subnet)
1692+
.await
1693+
.map_err(NetworkInterfaceError::External)?;
1694+
self.instance_create_network_interface_raw(&opctx, interface).await
1695+
}
1696+
1697+
pub(super) async fn instance_create_network_interface_raw(
1698+
&self,
1699+
opctx: &OpContext,
16791700
interface: IncompleteNetworkInterface,
16801701
) -> Result<NetworkInterface, NetworkInterfaceError> {
16811702
use db::schema::network_interface::dsl;
@@ -1686,7 +1707,11 @@ impl DataStore {
16861707
diesel::insert_into(dsl::network_interface)
16871708
.values(query)
16881709
.returning(NetworkInterface::as_returning())
1689-
.get_result_async(self.pool())
1710+
.get_result_async(
1711+
self.pool_authorized(opctx)
1712+
.await
1713+
.map_err(NetworkInterfaceError::External)?,
1714+
)
16901715
.await
16911716
.map_err(|e| NetworkInterfaceError::from_pool(e, &interface))
16921717
}
@@ -1718,109 +1743,124 @@ impl DataStore {
17181743
Ok(())
17191744
}
17201745

1721-
pub async fn instance_delete_network_interface(
1746+
/// Fetches a `NetworkInterface` from the database and returns both the
1747+
/// database row and an [`authz::NetworkInterface`] for doing authz checks.
1748+
///
1749+
/// See [`DataStore::organization_lookup_noauthz()`] for intended use cases
1750+
/// and caveats.
1751+
// TODO-security See the note on organization_lookup_noauthz().
1752+
async fn network_interface_lookup_noauthz(
17221753
&self,
1723-
interface_id: &Uuid,
1724-
) -> DeleteResult {
1754+
authz_instance: &authz::Instance,
1755+
interface_name: &Name,
1756+
) -> LookupResult<(authz::NetworkInterface, NetworkInterface)> {
17251757
use db::schema::network_interface::dsl;
1726-
let now = Utc::now();
1727-
let result = diesel::update(dsl::network_interface)
1728-
.filter(dsl::id.eq(*interface_id))
1758+
dsl::network_interface
17291759
.filter(dsl::time_deleted.is_null())
1730-
.set((dsl::time_deleted.eq(now),))
1731-
.check_if_exists::<db::model::NetworkInterface>(*interface_id)
1732-
.execute_and_check(self.pool())
1760+
.filter(dsl::instance_id.eq(authz_instance.id()))
1761+
.filter(dsl::name.eq(interface_name.clone()))
1762+
.select(NetworkInterface::as_select())
1763+
.first_async(self.pool())
17331764
.await
17341765
.map_err(|e| {
17351766
public_error_from_diesel_pool(
17361767
e,
17371768
ErrorHandler::NotFoundByLookup(
17381769
ResourceType::NetworkInterface,
1739-
LookupType::ById(*interface_id),
1770+
LookupType::ByName(interface_name.as_str().to_owned()),
17401771
),
17411772
)
1742-
})?;
1743-
match result.status {
1744-
UpdateStatus::Updated => Ok(()),
1745-
UpdateStatus::NotUpdatedButExists => {
1746-
let interface = &result.found;
1747-
if interface.time_deleted().is_some() {
1748-
// Already deleted
1749-
Ok(())
1750-
} else {
1751-
Err(Error::internal_error(&format!(
1752-
"failed to delete network interface: {}",
1753-
interface_id
1754-
)))
1755-
}
1756-
}
1757-
}
1773+
})
1774+
.map(|d| {
1775+
(
1776+
authz_instance.child_generic(
1777+
ResourceType::NetworkInterface,
1778+
d.id(),
1779+
LookupType::from(&interface_name.0),
1780+
),
1781+
d,
1782+
)
1783+
})
17581784
}
17591785

1760-
pub async fn subnet_lookup_network_interface(
1786+
/// Lookup a `NetworkInterface` by name and return the full database record,
1787+
/// along with an [`authz::NetworkInterface`] for subsequent authorization
1788+
/// checks.
1789+
pub async fn network_interface_fetch(
17611790
&self,
1762-
subnet_id: &Uuid,
1791+
opctx: &OpContext,
1792+
authz_instance: &authz::Instance,
1793+
name: &Name,
1794+
) -> LookupResult<(authz::NetworkInterface, NetworkInterface)> {
1795+
let (authz_interface, db_interface) =
1796+
self.network_interface_lookup_noauthz(authz_instance, name).await?;
1797+
opctx.authorize(authz::Action::Read, &authz_interface).await?;
1798+
Ok((authz_interface, db_interface))
1799+
}
1800+
1801+
/// Delete a `NetworkInterface` attached to a provided instance.
1802+
pub async fn instance_delete_network_interface(
1803+
&self,
1804+
opctx: &OpContext,
1805+
authz_instance: &authz::Instance,
17631806
interface_name: &Name,
1764-
) -> LookupResult<db::model::NetworkInterface> {
1765-
use db::schema::network_interface::dsl;
1807+
) -> DeleteResult {
1808+
let (authz_interface, _) = self
1809+
.network_interface_fetch(opctx, &authz_instance, interface_name)
1810+
.await?;
1811+
opctx.authorize(authz::Action::Delete, &authz_interface).await?;
17661812

1767-
dsl::network_interface
1768-
.filter(dsl::subnet_id.eq(*subnet_id))
1813+
use db::schema::network_interface::dsl;
1814+
let now = Utc::now();
1815+
let interface_id = authz_interface.id();
1816+
diesel::update(dsl::network_interface)
1817+
.filter(dsl::id.eq(interface_id))
17691818
.filter(dsl::time_deleted.is_null())
1770-
.filter(dsl::name.eq(interface_name.clone()))
1771-
.select(db::model::NetworkInterface::as_select())
1772-
.get_result_async(self.pool())
1819+
.set((dsl::time_deleted.eq(now),))
1820+
.execute_async(self.pool_authorized(opctx).await?)
17731821
.await
17741822
.map_err(|e| {
17751823
public_error_from_diesel_pool(
17761824
e,
17771825
ErrorHandler::NotFoundByLookup(
17781826
ResourceType::NetworkInterface,
1779-
LookupType::ByName(interface_name.to_string()),
1827+
LookupType::ById(interface_id),
17801828
),
17811829
)
1782-
})
1830+
})?;
1831+
Ok(())
17831832
}
17841833

17851834
/// List network interfaces associated with a given instance.
17861835
pub async fn instance_list_network_interfaces(
17871836
&self,
1788-
instance_id: &Uuid,
1837+
opctx: &OpContext,
1838+
authz_instance: &authz::Instance,
17891839
pagparams: &DataPageParams<'_, Name>,
17901840
) -> ListResultVec<NetworkInterface> {
1841+
opctx.authorize(authz::Action::ListChildren, authz_instance).await?;
1842+
17911843
use db::schema::network_interface::dsl;
17921844
paginated(dsl::network_interface, dsl::name, &pagparams)
17931845
.filter(dsl::time_deleted.is_null())
1794-
.filter(dsl::instance_id.eq(*instance_id))
1846+
.filter(dsl::instance_id.eq(authz_instance.id()))
17951847
.select(NetworkInterface::as_select())
1796-
.load_async::<NetworkInterface>(self.pool())
1848+
.load_async::<NetworkInterface>(self.pool_authorized(opctx).await?)
17971849
.await
17981850
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
17991851
}
18001852

18011853
/// Get a network interface by name attached to an instance
18021854
pub async fn instance_lookup_network_interface(
18031855
&self,
1804-
instance_id: &Uuid,
1856+
opctx: &OpContext,
1857+
authz_instance: &authz::Instance,
18051858
interface_name: &Name,
18061859
) -> LookupResult<NetworkInterface> {
1807-
use db::schema::network_interface::dsl;
1808-
dsl::network_interface
1809-
.filter(dsl::instance_id.eq(*instance_id))
1810-
.filter(dsl::name.eq(interface_name.clone()))
1811-
.filter(dsl::time_deleted.is_null())
1812-
.select(NetworkInterface::as_select())
1813-
.get_result_async(self.pool())
1814-
.await
1815-
.map_err(|e| {
1816-
public_error_from_diesel_pool(
1817-
e,
1818-
ErrorHandler::NotFoundByLookup(
1819-
ResourceType::NetworkInterface,
1820-
LookupType::ByName(interface_name.to_string()),
1821-
),
1822-
)
1823-
})
1860+
Ok(self
1861+
.network_interface_fetch(opctx, &authz_instance, interface_name)
1862+
.await?
1863+
.1)
18241864
}
18251865

18261866
// Create a record for a new Oximeter instance

nexus/src/db/subnet_allocation.rs

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,7 @@ impl QueryFragment<Pg> for InsertNetworkInterfaceQueryValues {
11341134
mod test {
11351135
use super::NetworkInterfaceError;
11361136
use super::SubnetError;
1137+
use crate::context::OpContext;
11371138
use crate::db::model::{
11381139
self, IncompleteNetworkInterface, NetworkInterface, VpcSubnet,
11391140
};
@@ -1298,6 +1299,7 @@ mod test {
12981299
let pool = Arc::new(crate::db::Pool::new(&cfg));
12991300
let db_datastore =
13001301
Arc::new(crate::db::DataStore::new(Arc::clone(&pool)));
1302+
let opctx = OpContext::for_tests(log.new(o!()), db_datastore.clone());
13011303

13021304
// Two test VpcSubnets, in different VPCs. The IPv4 range has space for
13031305
// 16 addresses, less the 6 that are reserved.
@@ -1352,7 +1354,7 @@ mod test {
13521354
)
13531355
.unwrap();
13541356
let inserted_interface = db_datastore
1355-
.instance_create_network_interface(interface.clone())
1357+
.instance_create_network_interface_raw(&opctx, interface.clone())
13561358
.await
13571359
.expect("Failed to insert interface with known-good IP address");
13581360
assert_interfaces_eq(&interface, &inserted_interface);
@@ -1380,7 +1382,7 @@ mod test {
13801382
)
13811383
.unwrap();
13821384
let inserted_interface = db_datastore
1383-
.instance_create_network_interface(interface.clone())
1385+
.instance_create_network_interface_raw(&opctx, interface.clone())
13841386
.await
13851387
.expect("Failed to insert interface with known-good IP address");
13861388
assert_interfaces_eq(&interface, &inserted_interface);
@@ -1404,8 +1406,9 @@ mod test {
14041406
Some(requested_ip),
14051407
)
14061408
.unwrap();
1407-
let result =
1408-
db_datastore.instance_create_network_interface(interface).await;
1409+
let result = db_datastore
1410+
.instance_create_network_interface_raw(&opctx, interface)
1411+
.await;
14091412
assert!(
14101413
matches!(
14111414
result,
@@ -1429,8 +1432,9 @@ mod test {
14291432
None,
14301433
)
14311434
.unwrap();
1432-
let result =
1433-
db_datastore.instance_create_network_interface(interface).await;
1435+
let result = db_datastore
1436+
.instance_create_network_interface_raw(&opctx, interface)
1437+
.await;
14341438
assert!(
14351439
matches!(
14361440
result,
@@ -1455,8 +1459,9 @@ mod test {
14551459
addr,
14561460
)
14571461
.unwrap();
1458-
let result =
1459-
db_datastore.instance_create_network_interface(interface).await;
1462+
let result = db_datastore
1463+
.instance_create_network_interface_raw(&opctx, interface)
1464+
.await;
14601465
assert!(
14611466
matches!(result, Err(NetworkInterfaceError::InstanceSpansMultipleVpcs(_))),
14621467
"Attaching an interface to an instance which already has one in a different VPC should fail"
@@ -1481,8 +1486,9 @@ mod test {
14811486
None,
14821487
)
14831488
.unwrap();
1484-
let result =
1485-
db_datastore.instance_create_network_interface(interface).await;
1489+
let result = db_datastore
1490+
.instance_create_network_interface_raw(&opctx, interface)
1491+
.await;
14861492
assert!(
14871493
result.is_ok(),
14881494
"We should be able to allocate 8 more interfaces successfully",
@@ -1501,8 +1507,9 @@ mod test {
15011507
None,
15021508
)
15031509
.unwrap();
1504-
let result =
1505-
db_datastore.instance_create_network_interface(interface).await;
1510+
let result = db_datastore
1511+
.instance_create_network_interface_raw(&opctx, interface)
1512+
.await;
15061513
assert!(
15071514
matches!(
15081515
result,
@@ -1528,8 +1535,9 @@ mod test {
15281535
None,
15291536
)
15301537
.unwrap();
1531-
let result =
1532-
db_datastore.instance_create_network_interface(interface).await;
1538+
let result = db_datastore
1539+
.instance_create_network_interface_raw(&opctx, interface)
1540+
.await;
15331541
assert!(
15341542
result.is_ok(),
15351543
concat!(
@@ -1577,6 +1585,7 @@ mod test {
15771585
let pool = Arc::new(crate::db::Pool::new(&cfg));
15781586
let db_datastore =
15791587
Arc::new(crate::db::DataStore::new(Arc::clone(&pool)));
1588+
let opctx = OpContext::for_tests(log.new(o!()), db_datastore.clone());
15801589
let ipv4_block = Ipv4Net("172.30.0.0/28".parse().unwrap());
15811590
let ipv6_block = Ipv6Net("fd12:3456:7890::/64".parse().unwrap());
15821591
let subnet_name = "subnet-a".to_string().try_into().unwrap();
@@ -1610,13 +1619,13 @@ mod test {
16101619
)
16111620
.unwrap();
16121621
let inserted_interface = db_datastore
1613-
.instance_create_network_interface(interface.clone())
1622+
.instance_create_network_interface_raw(&opctx, interface.clone())
16141623
.await
16151624
.expect("Failed to insert interface with known-good IP address");
16161625

16171626
// Attempt to insert the exact same record again.
16181627
let result = db_datastore
1619-
.instance_create_network_interface(interface.clone())
1628+
.instance_create_network_interface_raw(&opctx, interface.clone())
16201629
.await;
16211630
if let Err(NetworkInterfaceError::DuplicatePrimaryKey(key)) = result {
16221631
assert_eq!(key, inserted_interface.identity.id);

0 commit comments

Comments
 (0)