Skip to content

Commit 5a071cd

Browse files
authored
authz: protect endpoints for firewall rules (#782)
1 parent 4e60f4f commit 5a071cd

File tree

5 files changed

+138
-94
lines changed

5 files changed

+138
-94
lines changed

nexus/src/db/datastore.rs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,41 +2336,47 @@ impl DataStore {
23362336

23372337
pub async fn vpc_list_firewall_rules(
23382338
&self,
2339-
vpc_id: &Uuid,
2339+
opctx: &OpContext,
2340+
authz_vpc: &authz::Vpc,
23402341
) -> ListResultVec<VpcFirewallRule> {
2342+
// Firewall rules are modeled in the API as a single resource under the
2343+
// Vpc (rather than individual child resources with their own CRUD
2344+
// endpoints). You cannot look them up individually, create them,
2345+
// remove them, or update them. You can only modify the whole set. So
2346+
// for authz, we treat them as part of the Vpc itself.
2347+
opctx.authorize(authz::Action::Read, authz_vpc).await?;
23412348
use db::schema::vpc_firewall_rule::dsl;
23422349

23432350
dsl::vpc_firewall_rule
23442351
.filter(dsl::time_deleted.is_null())
2345-
.filter(dsl::vpc_id.eq(*vpc_id))
2352+
.filter(dsl::vpc_id.eq(authz_vpc.id()))
23462353
.order(dsl::name.asc())
23472354
.select(VpcFirewallRule::as_select())
2348-
.load_async(self.pool())
2355+
.load_async(self.pool_authorized(opctx).await?)
23492356
.await
23502357
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
23512358
}
23522359

23532360
pub async fn vpc_delete_all_firewall_rules(
23542361
&self,
2355-
vpc_id: &Uuid,
2362+
opctx: &OpContext,
2363+
authz_vpc: &authz::Vpc,
23562364
) -> DeleteResult {
2365+
opctx.authorize(authz::Action::Modify, authz_vpc).await?;
23572366
use db::schema::vpc_firewall_rule::dsl;
23582367

23592368
let now = Utc::now();
23602369
// TODO-performance: Paginate this update to avoid long queries
23612370
diesel::update(dsl::vpc_firewall_rule)
23622371
.filter(dsl::time_deleted.is_null())
2363-
.filter(dsl::vpc_id.eq(*vpc_id))
2372+
.filter(dsl::vpc_id.eq(authz_vpc.id()))
23642373
.set(dsl::time_deleted.eq(now))
2365-
.execute_async(self.pool())
2374+
.execute_async(self.pool_authorized(opctx).await?)
23662375
.await
23672376
.map_err(|e| {
23682377
public_error_from_diesel_pool(
23692378
e,
2370-
ErrorHandler::NotFoundByLookup(
2371-
ResourceType::Vpc,
2372-
LookupType::ById(*vpc_id),
2373-
),
2379+
ErrorHandler::NotFoundByResource(authz_vpc),
23742380
)
23752381
})?;
23762382
Ok(())
@@ -2379,19 +2385,31 @@ impl DataStore {
23792385
/// Replace all firewall rules with the given rules
23802386
pub async fn vpc_update_firewall_rules(
23812387
&self,
2382-
vpc_id: &Uuid,
2383-
rules: Vec<VpcFirewallRule>,
2388+
opctx: &OpContext,
2389+
authz_vpc: &authz::Vpc,
2390+
mut rules: Vec<VpcFirewallRule>,
23842391
) -> UpdateResult<Vec<VpcFirewallRule>> {
2392+
opctx.authorize(authz::Action::Modify, authz_vpc).await?;
2393+
for r in &rules {
2394+
assert_eq!(r.vpc_id, authz_vpc.id());
2395+
}
2396+
2397+
// Sort the rules in the same order that we would return them when
2398+
// listing them. This is because we're going to use RETURNING to return
2399+
// the inserted rows from the database and we want them to come back in
2400+
// the same order that we would normally list them.
2401+
rules.sort_by_key(|r| r.name().to_string());
2402+
23852403
use db::schema::vpc_firewall_rule::dsl;
23862404

23872405
let now = Utc::now();
23882406
let delete_old_query = diesel::update(dsl::vpc_firewall_rule)
23892407
.filter(dsl::time_deleted.is_null())
2390-
.filter(dsl::vpc_id.eq(*vpc_id))
2408+
.filter(dsl::vpc_id.eq(authz_vpc.id()))
23912409
.set(dsl::time_deleted.eq(now));
23922410

23932411
let insert_new_query = Vpc::insert_resource(
2394-
*vpc_id,
2412+
authz_vpc.id(),
23952413
diesel::insert_into(dsl::vpc_firewall_rule).values(rules),
23962414
);
23972415

@@ -2405,7 +2423,8 @@ impl DataStore {
24052423
// hold a transaction open across multiple roundtrips from the database,
24062424
// but for now we're using a transaction due to the severely decreased
24072425
// legibility of CTEs via diesel right now.
2408-
self.pool()
2426+
self.pool_authorized(opctx)
2427+
.await?
24092428
.transaction(move |conn| {
24102429
delete_old_query.execute(conn)?;
24112430

@@ -2427,13 +2446,10 @@ impl DataStore {
24272446
.map_err(|e| match e {
24282447
TxnError::CustomError(
24292448
FirewallUpdateError::CollectionNotFound,
2430-
) => Error::not_found_by_id(ResourceType::Vpc, vpc_id),
2449+
) => Error::not_found_by_id(ResourceType::Vpc, &authz_vpc.id()),
24312450
TxnError::Pool(e) => public_error_from_diesel_pool(
24322451
e,
2433-
ErrorHandler::NotFoundByLookup(
2434-
ResourceType::VpcFirewallRule,
2435-
LookupType::ById(*vpc_id),
2436-
),
2452+
ErrorHandler::NotFoundByResource(authz_vpc),
24372453
),
24382454
})
24392455
}

nexus/src/external_api/http_entrypoints.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,8 +1719,10 @@ async fn vpc_firewall_rules_get(
17191719
let nexus = &apictx.nexus;
17201720
let path = path_params.into_inner();
17211721
let handler = async {
1722+
let opctx = OpContext::for_external_api(&rqctx).await?;
17221723
let rules = nexus
17231724
.vpc_list_firewall_rules(
1725+
&opctx,
17241726
&path.organization_name,
17251727
&path.project_name,
17261728
&path.vpc_name,
@@ -1750,8 +1752,10 @@ async fn vpc_firewall_rules_put(
17501752
let nexus = &apictx.nexus;
17511753
let path = path_params.into_inner();
17521754
let handler = async {
1755+
let opctx = OpContext::for_external_api(&rqctx).await?;
17531756
let rules = nexus
17541757
.vpc_update_firewall_rules(
1758+
&opctx,
17551759
&path.organization_name,
17561760
&path.project_name,
17571761
&path.vpc_name,

nexus/src/nexus.rs

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,20 +1808,14 @@ impl Nexus {
18081808
}
18091809
SubnetError::External(e) => e,
18101810
})?;
1811-
self.create_default_vpc_firewall(&vpc_id).await?;
1812-
Ok(db_vpc)
1813-
}
1814-
1815-
async fn create_default_vpc_firewall(
1816-
&self,
1817-
vpc_id: &Uuid,
1818-
) -> CreateResult<()> {
18191811
let rules = db::model::VpcFirewallRule::vec_from_params(
1820-
*vpc_id,
1812+
authz_vpc.id(),
18211813
defaults::DEFAULT_FIREWALL_RULES.clone(),
18221814
);
1823-
self.db_datastore.vpc_update_firewall_rules(&vpc_id, rules).await?;
1824-
Ok(())
1815+
self.db_datastore
1816+
.vpc_update_firewall_rules(opctx, &authz_vpc, rules)
1817+
.await?;
1818+
Ok(db_vpc)
18251819
}
18261820

18271821
pub async fn vpc_fetch(
@@ -1842,23 +1836,6 @@ impl Nexus {
18421836
.1)
18431837
}
18441838

1845-
// TODO-security TODO-cleanup Remove this function. Callers should use
1846-
// vpc_lookup_by_path() / vpc_fetch() instead, or we should create a more
1847-
// useful pattern for looking up records by path (e.g., *_fetch_by_path()).
1848-
pub async fn project_lookup_vpc(
1849-
&self,
1850-
organization_name: &Name,
1851-
project_name: &Name,
1852-
vpc_name: &Name,
1853-
) -> LookupResult<db::model::Vpc> {
1854-
let project_id = self
1855-
.db_datastore
1856-
.project_lookup_by_path(organization_name, project_name)
1857-
.await?
1858-
.id();
1859-
Ok(self.db_datastore.vpc_fetch_by_name(&project_id, vpc_name).await?)
1860-
}
1861-
18621839
pub async fn project_update_vpc(
18631840
&self,
18641841
opctx: &OpContext,
@@ -1904,38 +1881,48 @@ impl Nexus {
19041881

19051882
// Delete all firewall rules after deleting the VPC, to ensure no
19061883
// firewall rules get added between rules deletion and VPC deletion.
1907-
self.db_datastore.vpc_delete_all_firewall_rules(&authz_vpc.id()).await
1884+
self.db_datastore
1885+
.vpc_delete_all_firewall_rules(&opctx, &authz_vpc)
1886+
.await
19081887
}
19091888

19101889
pub async fn vpc_list_firewall_rules(
19111890
&self,
1891+
opctx: &OpContext,
19121892
organization_name: &Name,
19131893
project_name: &Name,
19141894
vpc_name: &Name,
19151895
) -> ListResultVec<db::model::VpcFirewallRule> {
1916-
let vpc = self
1917-
.project_lookup_vpc(organization_name, project_name, vpc_name)
1896+
let authz_vpc = self
1897+
.db_datastore
1898+
.vpc_lookup_by_path(organization_name, project_name, vpc_name)
1899+
.await?;
1900+
let rules = self
1901+
.db_datastore
1902+
.vpc_list_firewall_rules(&opctx, &authz_vpc)
19181903
.await?;
1919-
let rules =
1920-
self.db_datastore.vpc_list_firewall_rules(&vpc.id()).await?;
19211904
Ok(rules)
19221905
}
19231906

19241907
pub async fn vpc_update_firewall_rules(
19251908
&self,
1909+
opctx: &OpContext,
19261910
organization_name: &Name,
19271911
project_name: &Name,
19281912
vpc_name: &Name,
19291913
params: &VpcFirewallRuleUpdateParams,
19301914
) -> UpdateResult<Vec<db::model::VpcFirewallRule>> {
1931-
let vpc = self
1932-
.project_lookup_vpc(organization_name, project_name, vpc_name)
1915+
let authz_vpc = self
1916+
.db_datastore
1917+
.vpc_lookup_by_path(organization_name, project_name, vpc_name)
19331918
.await?;
19341919
let rules = db::model::VpcFirewallRule::vec_from_params(
1935-
vpc.id(),
1920+
authz_vpc.id(),
19361921
params.clone(),
19371922
);
1938-
self.db_datastore.vpc_update_firewall_rules(&vpc.id(), rules).await
1923+
self.db_datastore
1924+
.vpc_update_firewall_rules(opctx, &authz_vpc, rules)
1925+
.await
19391926
}
19401927

19411928
pub async fn vpc_list_subnets(

nexus/tests/integration_tests/unauthorized.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use omicron_common::api::external::RouteDestination;
3131
use omicron_common::api::external::RouteTarget;
3232
use omicron_common::api::external::RouterRouteCreateParams;
3333
use omicron_common::api::external::RouterRouteUpdateParams;
34+
use omicron_common::api::external::VpcFirewallRuleUpdateParams;
3435
use omicron_nexus::authn;
3536
use omicron_nexus::authn::external::spoof;
3637
use omicron_nexus::external_api::params;
@@ -182,10 +183,12 @@ lazy_static! {
182183
static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap();
183184
static ref DEMO_VPC_URL: String =
184185
format!("{}/{}", *DEMO_PROJECT_URL_VPCS, *DEMO_VPC_NAME);
185-
static ref DEMO_VPC_URL_SUBNETS: String =
186-
format!("{}/subnets", *DEMO_VPC_URL);
186+
static ref DEMO_VPC_URL_FIREWALL_RULES: String =
187+
format!("{}/firewall/rules", *DEMO_VPC_URL);
187188
static ref DEMO_VPC_URL_ROUTERS: String =
188189
format!("{}/routers", *DEMO_VPC_URL);
190+
static ref DEMO_VPC_URL_SUBNETS: String =
191+
format!("{}/subnets", *DEMO_VPC_URL);
189192
static ref DEMO_VPC_CREATE: params::VpcCreate =
190193
params::VpcCreate {
191194
identity: IdentityMetadataCreateParams {
@@ -473,6 +476,20 @@ lazy_static! {
473476
],
474477
},
475478

479+
/* Firewall rules */
480+
VerifyEndpoint {
481+
url: &*DEMO_VPC_URL_FIREWALL_RULES,
482+
visibility: Visibility::Protected,
483+
allowed_methods: vec![
484+
AllowedMethod::Get,
485+
AllowedMethod::Put(
486+
serde_json::to_value(VpcFirewallRuleUpdateParams {
487+
rules: vec![],
488+
}).unwrap()
489+
),
490+
],
491+
},
492+
476493
/* VPC Subnets */
477494
VerifyEndpoint {
478495
url: &*DEMO_VPC_URL_SUBNETS,

0 commit comments

Comments
 (0)