From 4610d1a657474589906992c57be6b35ad7b88e20 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sat, 19 Mar 2022 19:57:34 -0700 Subject: [PATCH 01/59] first cut --- nexus/src/db/lookup.rs | 203 +++++++++++++++++++++++++++++++++++++++++ nexus/src/db/mod.rs | 1 + 2 files changed, 204 insertions(+) create mode 100644 nexus/src/db/lookup.rs diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs new file mode 100644 index 00000000000..e68ddb8e412 --- /dev/null +++ b/nexus/src/db/lookup.rs @@ -0,0 +1,203 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Facilities for looking up API resources from the database + +// XXX-dap TODO status: +// - I've got the skeleton of an API here that looks pretty promising +// - Next step is probably to implement some of the `Fetch` interfaces. The +// challenge I'm facing here is that I need access to the `Lookup` at the top +// of the tree in order to use the datastore's methods. +// - idea: pass the Lookup down so it's only present at the leaf +// problem: doesn't that mean every node needs two versions: one as a leaf, +// and one as a node in the tree? (maybe every internal node can be the +// same class? but probably not because we're going to wind up using +// their `Fetch` impls recursively). Maybe instead of two versions, +// each internal node could be an enum with two variants, one as a leaf +// (which has the Lookup) and one as an internal node (which has the +// rest)? But then how will its impl of Fetch work -- it will have _no_ +// way to get the datastore out. +// - idea: use Arc on the Lookup -- this would probably work but feels cheesy +// - idea: put a reference to the Lookup at each node +// problem: _somebody_ has to own it. Who will that be? Root? +// - idea: have every resource impl a trait that gets its own key out. Then +// we can impl `key.lookup()` in terms of the parent key. +// problem: lots of boilerplate +// - idea: just use Key instead of structs like Project, etc. Then we can +// impl `key.lookup()` more easily? +// problem: then we can't have different methods (like "instance_name") at +// each node along the way. +// +// Most promising right now looks like putting a reference to the Lookup in +// every node. + +use super::datastore::DataStore; +use super::model; +use crate::{authz, context::OpContext}; +use futures::future::BoxFuture; +use omicron_common::api::external::{LookupResult, Name}; +use uuid::Uuid; + +pub trait Fetch { + type FetchType; + fn fetch(&self) -> BoxFuture<'_, LookupResult>; +} + +enum Key<'a, P> { + Name(P, &'a Name), + Id(Lookup<'a>, Uuid), +} + +pub struct Lookup<'a> { + opctx: &'a OpContext, + datastore: &'a DataStore, +} + +struct Root<'a> { + lookup: Lookup<'a>, +} + +impl<'a> Lookup<'a> { + pub fn new<'b, 'c>( + opctx: &'b OpContext, + datastore: &'c DataStore, + ) -> Lookup<'a> + where + 'b: 'a, + 'c: 'a, + { + Lookup { opctx, datastore } + } + + pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> + where + 'a: 'c, + 'b: 'c, + { + Organization { key: Key::Name(Root { lookup: self }, name) } + } + + pub fn organization_id(self, id: Uuid) -> Organization<'a> { + Organization { key: Key::Id(self, id) } + } + + pub fn project_id(self, id: Uuid) -> Project<'a> { + Project { key: Key::Id(self, id) } + } + + pub fn instance_id(self, id: Uuid) -> Instance<'a> { + Instance { key: Key::Id(self, id) } + } +} + +pub struct Organization<'a> { + key: Key<'a, Root<'a>>, +} + +impl<'a> Organization<'a> { + fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> + where + 'a: 'c, + 'b: 'c, + { + Project { key: Key::Name(self, name) } + } +} + +impl Fetch for Organization<'_> { + type FetchType = (authz::Organization, model::Organization); + + fn fetch(&self) -> BoxFuture<'_, LookupResult> { + todo!() + } +} + +pub struct Project<'a> { + key: Key<'a, Organization<'a>>, +} + +impl Fetch for Project<'_> { + type FetchType = (authz::Organization, authz::Project, model::Project); + + fn fetch(&self) -> BoxFuture<'_, LookupResult> { + todo!() + } +} + +impl<'a> Project<'a> { + fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> + where + 'a: 'c, + 'b: 'c, + { + Instance { key: Key::Name(self, name) } + } +} + +impl Fetch for Instance<'_> { + type FetchType = + (authz::Organization, authz::Project, authz::Instance, model::Instance); + + fn fetch(&self) -> BoxFuture<'_, LookupResult> { + todo!() + } +} + +pub struct Instance<'a> { + key: Key<'a, Project<'a>>, +} + +#[cfg(test)] +mod test { + use super::Instance; + use super::Key; + use super::Lookup; + use super::Organization; + use super::Project; + use super::Root; + use crate::context::OpContext; + use nexus_test_utils::db::test_setup_database; + use omicron_common::api::external::Name; + use omicron_test_utils::dev; + use std::sync::Arc; + + #[tokio::test] + async fn test_lookup() { + let logctx = dev::test_setup_log("test_lookup"); + let mut db = test_setup_database(&logctx.log).await; + let (_, datastore) = + crate::db::datastore::datastore_test(&logctx, &db).await; + let opctx = + OpContext::for_tests(logctx.log.new(o!()), Arc::clone(&datastore)); + let org_name: Name = "my-org".parse().unwrap(); + let project_name: Name = "my-project".parse().unwrap(); + let instance_name: Name = "my-instance".parse().unwrap(); + + let leaf = Lookup::new(&opctx, &datastore) + .organization_name(&org_name) + .project_name(&project_name) + .instance_name(&instance_name); + assert!(matches!(&leaf, + Instance { + key: Key::Name(Project { + key: Key::Name(Organization { + key: Key::Name(Root { .. }, o) + }, p) + }, i) + } + if **o == org_name && **p == project_name && **i == instance_name)); + + let org_id = "006f29d9-0ff0-e2d2-a022-87e152440122".parse().unwrap(); + let leaf = Lookup::new(&opctx, &datastore) + .organization_id(org_id) + .project_name(&project_name); + assert!(matches!(&leaf, Project { + key: Key::Name(Organization { + key: Key::Id(Lookup { .. }, o) + }, p) + } if *o == org_id && **p == project_name)); + + db.cleanup().await.unwrap(); + } +} diff --git a/nexus/src/db/mod.rs b/nexus/src/db/mod.rs index 341c023c9cb..b6d503b360f 100644 --- a/nexus/src/db/mod.rs +++ b/nexus/src/db/mod.rs @@ -14,6 +14,7 @@ pub mod datastore; mod error; mod explain; pub mod fixed_data; +mod lookup; mod pagination; mod pool; mod saga_recovery; From ef932a189ed39ceaeb052a98ca36153b7311a37b Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sat, 19 Mar 2022 20:04:23 -0700 Subject: [PATCH 02/59] rename Lookup --- nexus/src/db/lookup.rs | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index e68ddb8e412..bb93f6421bc 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -7,19 +7,19 @@ // XXX-dap TODO status: // - I've got the skeleton of an API here that looks pretty promising // - Next step is probably to implement some of the `Fetch` interfaces. The -// challenge I'm facing here is that I need access to the `Lookup` at the top -// of the tree in order to use the datastore's methods. -// - idea: pass the Lookup down so it's only present at the leaf +// challenge I'm facing here is that I need access to the `LookupPath` at the +// top of the tree in order to use the datastore's methods. +// - idea: pass the LookupPath down so it's only present at the leaf // problem: doesn't that mean every node needs two versions: one as a leaf, // and one as a node in the tree? (maybe every internal node can be the // same class? but probably not because we're going to wind up using // their `Fetch` impls recursively). Maybe instead of two versions, // each internal node could be an enum with two variants, one as a leaf -// (which has the Lookup) and one as an internal node (which has the +// (which has the LookupPath) and one as an internal node (which has the // rest)? But then how will its impl of Fetch work -- it will have _no_ // way to get the datastore out. -// - idea: use Arc on the Lookup -- this would probably work but feels cheesy -// - idea: put a reference to the Lookup at each node +// - idea: use Arc on the LookupPath -- this would probably work but feels +// cheesy - idea: put a reference to the LookupPath at each node // problem: _somebody_ has to own it. Who will that be? Root? // - idea: have every resource impl a trait that gets its own key out. Then // we can impl `key.lookup()` in terms of the parent key. @@ -29,7 +29,7 @@ // problem: then we can't have different methods (like "instance_name") at // each node along the way. // -// Most promising right now looks like putting a reference to the Lookup in +// Most promising right now looks like putting a reference to the LookupPath in // every node. use super::datastore::DataStore; @@ -46,28 +46,26 @@ pub trait Fetch { enum Key<'a, P> { Name(P, &'a Name), - Id(Lookup<'a>, Uuid), + Id(Root, Uuid), } -pub struct Lookup<'a> { +pub struct LookupPath<'a> { opctx: &'a OpContext, datastore: &'a DataStore, } -struct Root<'a> { - lookup: Lookup<'a>, -} +struct Root<'a> {} -impl<'a> Lookup<'a> { +impl<'a> LookupPath<'a> { pub fn new<'b, 'c>( opctx: &'b OpContext, datastore: &'c DataStore, - ) -> Lookup<'a> + ) -> LookupPath<'a> where 'b: 'a, 'c: 'a, { - Lookup { opctx, datastore } + LookupPath { opctx, datastore } } pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> @@ -152,7 +150,7 @@ pub struct Instance<'a> { mod test { use super::Instance; use super::Key; - use super::Lookup; + use super::LookupPath; use super::Organization; use super::Project; use super::Root; @@ -174,7 +172,7 @@ mod test { let project_name: Name = "my-project".parse().unwrap(); let instance_name: Name = "my-instance".parse().unwrap(); - let leaf = Lookup::new(&opctx, &datastore) + let leaf = LookupPath::new(&opctx, &datastore) .organization_name(&org_name) .project_name(&project_name) .instance_name(&instance_name); @@ -189,12 +187,12 @@ mod test { if **o == org_name && **p == project_name && **i == instance_name)); let org_id = "006f29d9-0ff0-e2d2-a022-87e152440122".parse().unwrap(); - let leaf = Lookup::new(&opctx, &datastore) + let leaf = LookupPath::new(&opctx, &datastore) .organization_id(org_id) .project_name(&project_name); assert!(matches!(&leaf, Project { key: Key::Name(Organization { - key: Key::Id(Lookup { .. }, o) + key: Key::Id(LookupPath { .. }, o) }, p) } if *o == org_id && **p == project_name)); From 9cc52b0c9dd65a8bf3d67e5d17ecc54e669b2b8e Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sat, 19 Mar 2022 20:04:56 -0700 Subject: [PATCH 03/59] formatting nit --- nexus/src/db/lookup.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index bb93f6421bc..73990d0cac5 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -19,8 +19,9 @@ // rest)? But then how will its impl of Fetch work -- it will have _no_ // way to get the datastore out. // - idea: use Arc on the LookupPath -- this would probably work but feels -// cheesy - idea: put a reference to the LookupPath at each node -// problem: _somebody_ has to own it. Who will that be? Root? +// cheesy +// - idea: put a reference to the LookupPath at each node problem: _somebody_ +// has to own it. Who will that be? Root? // - idea: have every resource impl a trait that gets its own key out. Then // we can impl `key.lookup()` in terms of the parent key. // problem: lots of boilerplate From 584514929678297ecd28b8e39dc8390fbaca70c7 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sat, 19 Mar 2022 20:05:33 -0700 Subject: [PATCH 04/59] formatting --- nexus/src/db/lookup.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 73990d0cac5..da5a6a8ed8f 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -20,8 +20,8 @@ // way to get the datastore out. // - idea: use Arc on the LookupPath -- this would probably work but feels // cheesy -// - idea: put a reference to the LookupPath at each node problem: _somebody_ -// has to own it. Who will that be? Root? +// - idea: put a reference to the LookupPath at each node +// problem: _somebody_ has to own it. Who will that be? Root? // - idea: have every resource impl a trait that gets its own key out. Then // we can impl `key.lookup()` in terms of the parent key. // problem: lots of boilerplate From 49a10e3b2e9f5bf539d34c94355c41de8ccf953e Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sat, 19 Mar 2022 20:13:34 -0700 Subject: [PATCH 05/59] fix --- nexus/src/db/lookup.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index da5a6a8ed8f..cd9d76e6ee7 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -47,7 +47,7 @@ pub trait Fetch { enum Key<'a, P> { Name(P, &'a Name), - Id(Root, Uuid), + Id(LookupPath<'a>, Uuid), } pub struct LookupPath<'a> { @@ -55,7 +55,9 @@ pub struct LookupPath<'a> { datastore: &'a DataStore, } -struct Root<'a> {} +struct Root<'a> { + lookup: LookupPath<'a>, +} impl<'a> LookupPath<'a> { pub fn new<'b, 'c>( From 491f30c427b6c71fa7a422a7b1e06e6b7e149ff0 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sun, 20 Mar 2022 14:02:20 -0700 Subject: [PATCH 06/59] some notes, PoC diesel impl --- nexus/src/db/datastore.rs | 2 +- nexus/src/db/lookup.rs | 117 +++++++++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 08050cab5ca..fb39848db49 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -122,7 +122,7 @@ impl DataStore { self.pool.pool() } - async fn pool_authorized( + pub(super) async fn pool_authorized( &self, opctx: &OpContext, ) -> Result<&bb8::Pool>, Error> { diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index cd9d76e6ee7..6bba22f76cf 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -30,14 +30,43 @@ // problem: then we can't have different methods (like "instance_name") at // each node along the way. // -// Most promising right now looks like putting a reference to the LookupPath in -// every node. +// Conclusions: +// - the only thing that can possibly own the LookupPath is the leaf node +// because that's the only thing the caller actually has. +// - the internal nodes also need to be able to get the LookupPath so that they +// can impl their own Fetch() +// => The LookupPath should appear at the root, owned (indirectly) by each item +// in the chain, ending at the leaf. +// => Each item has to traverse the chain above it to get to the LookupPath +// +// NOTE: as I impl the first Fetch, I realize that I want Fetch to do an access +// check. But I can't do an access check when I'm only doing a fetch as a +// non-leaf node, for the same reason that foo_lookup_noauthz() cannot do the +// access check. I think this implies there need to be two different traits: +// - Fetch: the public trait that lets you fetch a complete record and the authz +// objects for the items in the hierarchy +// - Lookup: a trait private to this file that lets callers fetch the record and +// _does not_ do an access check +// If so, that may simplify things because: +// - I can have the leaf node store the LookupPath directly +// - it can pass the LookupPath (or whatever context is needed) to the lookup() +// function of the lookup() trait. use super::datastore::DataStore; +use super::identity::Resource; use super::model; -use crate::{authz, context::OpContext}; +use crate::{ + authz, + context::OpContext, + db, + db::error::{public_error_from_diesel_pool, ErrorHandler}, + db::model::Name, +}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; use futures::future::BoxFuture; -use omicron_common::api::external::{LookupResult, Name}; +use futures::FutureExt; +use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; use uuid::Uuid; pub trait Fetch { @@ -110,7 +139,77 @@ impl Fetch for Organization<'_> { type FetchType = (authz::Organization, model::Organization); fn fetch(&self) -> BoxFuture<'_, LookupResult> { - todo!() + // XXX-dap TODO This is a proof of concept. Each type in this path is + // going to need to get to the LookupPath through a sort of convoluted + // way. I wanted to implement a recursive function to get to the + // LookupPath, but then I'd need to create a trait for it that each type + // would have to impl, etc. (That might still be the way to go.) + // + // But see the note above -- maybe a different approach is better here! + let lookup = match &self.key { + Key::Name(Root { lookup }, _) => lookup, + Key::Id(lookup, _) => lookup, + }; + + let opctx = &lookup.opctx; + let datastore = lookup.datastore; + async { + let lookup_type = || match &self.key { + Key::Name(_, name) => { + LookupType::ByName(name.as_str().to_owned()) + } + Key::Id(_, id) => LookupType::ById(*id), + }; + + use db::schema::organization::dsl; + let conn = datastore.pool_authorized(opctx).await?; + // XXX-dap TODO This construction sucks. What I kind of want is a + // generic function that takes: + // - a table (e.g., dsl::organization) + // - a db model type to return + // - some information about the LookupType we're trying to do + // - a closure that can be used to apply filters + // (which would be either "name" or "id") + // and does the whole thing. + let db_org = match self.key { + Key::Name(_, name) => dsl::organization + .filter(dsl::time_deleted.is_null()) + .filter(dsl::name.eq(name.clone())) + .select(model::Organization::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::Organization, + lookup_type(), + ), + ) + }), + Key::Id(_, id) => dsl::organization + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(id)) + .select(model::Organization::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::Organization, + lookup_type(), + ), + ) + }), + }?; + + let authz_org = + authz::FLEET.organization(db_org.id(), lookup_type()); + opctx.authorize(authz::Action::Read, &authz_org).await?; + Ok((authz_org, db_org)) + } + .boxed() } } @@ -158,8 +257,8 @@ mod test { use super::Project; use super::Root; use crate::context::OpContext; + use crate::db::model::Name; use nexus_test_utils::db::test_setup_database; - use omicron_common::api::external::Name; use omicron_test_utils::dev; use std::sync::Arc; @@ -171,9 +270,9 @@ mod test { crate::db::datastore::datastore_test(&logctx, &db).await; let opctx = OpContext::for_tests(logctx.log.new(o!()), Arc::clone(&datastore)); - let org_name: Name = "my-org".parse().unwrap(); - let project_name: Name = "my-project".parse().unwrap(); - let instance_name: Name = "my-instance".parse().unwrap(); + let org_name: Name = Name("my-org".parse().unwrap()); + let project_name: Name = Name("my-project".parse().unwrap()); + let instance_name: Name = Name("my-instance".parse().unwrap()); let leaf = LookupPath::new(&opctx, &datastore) .organization_name(&org_name) From 037b78f165521cac71f41adabb61c7199dedcc51 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sun, 20 Mar 2022 18:32:18 -0700 Subject: [PATCH 07/59] get rid of Root --- nexus/src/db/lookup.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 6bba22f76cf..fb7a5898ffc 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -74,6 +74,14 @@ pub trait Fetch { fn fetch(&self) -> BoxFuture<'_, LookupResult>; } +trait Lookup { + type LookupType; + fn lookup( + &self, + lookup: &LookupPath, + ) -> BoxFuture<'_, LookupResult>; +} + enum Key<'a, P> { Name(P, &'a Name), Id(LookupPath<'a>, Uuid), @@ -84,10 +92,6 @@ pub struct LookupPath<'a> { datastore: &'a DataStore, } -struct Root<'a> { - lookup: LookupPath<'a>, -} - impl<'a> LookupPath<'a> { pub fn new<'b, 'c>( opctx: &'b OpContext, @@ -105,7 +109,7 @@ impl<'a> LookupPath<'a> { 'a: 'c, 'b: 'c, { - Organization { key: Key::Name(Root { lookup: self }, name) } + Organization { key: Key::Name(self, name) } } pub fn organization_id(self, id: Uuid) -> Organization<'a> { @@ -122,7 +126,7 @@ impl<'a> LookupPath<'a> { } pub struct Organization<'a> { - key: Key<'a, Root<'a>>, + key: Key<'a, LookupPath<'a>>, } impl<'a> Organization<'a> { @@ -147,7 +151,7 @@ impl Fetch for Organization<'_> { // // But see the note above -- maybe a different approach is better here! let lookup = match &self.key { - Key::Name(Root { lookup }, _) => lookup, + Key::Name(lookup, _) => lookup, Key::Id(lookup, _) => lookup, }; @@ -255,7 +259,6 @@ mod test { use super::LookupPath; use super::Organization; use super::Project; - use super::Root; use crate::context::OpContext; use crate::db::model::Name; use nexus_test_utils::db::test_setup_database; @@ -282,7 +285,7 @@ mod test { Instance { key: Key::Name(Project { key: Key::Name(Organization { - key: Key::Name(Root { .. }, o) + key: Key::Name(_, o) }, p) }, i) } From ebaefb11a2c3ae1d97bbe1236e3acd993ff8cdb2 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Sun, 20 Mar 2022 18:52:16 -0700 Subject: [PATCH 08/59] make LookupPath reachable through the whole path --- nexus/src/db/lookup.rs | 123 ++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 70 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index fb7a5898ffc..72f32fe3997 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -4,54 +4,6 @@ //! Facilities for looking up API resources from the database -// XXX-dap TODO status: -// - I've got the skeleton of an API here that looks pretty promising -// - Next step is probably to implement some of the `Fetch` interfaces. The -// challenge I'm facing here is that I need access to the `LookupPath` at the -// top of the tree in order to use the datastore's methods. -// - idea: pass the LookupPath down so it's only present at the leaf -// problem: doesn't that mean every node needs two versions: one as a leaf, -// and one as a node in the tree? (maybe every internal node can be the -// same class? but probably not because we're going to wind up using -// their `Fetch` impls recursively). Maybe instead of two versions, -// each internal node could be an enum with two variants, one as a leaf -// (which has the LookupPath) and one as an internal node (which has the -// rest)? But then how will its impl of Fetch work -- it will have _no_ -// way to get the datastore out. -// - idea: use Arc on the LookupPath -- this would probably work but feels -// cheesy -// - idea: put a reference to the LookupPath at each node -// problem: _somebody_ has to own it. Who will that be? Root? -// - idea: have every resource impl a trait that gets its own key out. Then -// we can impl `key.lookup()` in terms of the parent key. -// problem: lots of boilerplate -// - idea: just use Key instead of structs like Project, etc. Then we can -// impl `key.lookup()` more easily? -// problem: then we can't have different methods (like "instance_name") at -// each node along the way. -// -// Conclusions: -// - the only thing that can possibly own the LookupPath is the leaf node -// because that's the only thing the caller actually has. -// - the internal nodes also need to be able to get the LookupPath so that they -// can impl their own Fetch() -// => The LookupPath should appear at the root, owned (indirectly) by each item -// in the chain, ending at the leaf. -// => Each item has to traverse the chain above it to get to the LookupPath -// -// NOTE: as I impl the first Fetch, I realize that I want Fetch to do an access -// check. But I can't do an access check when I'm only doing a fetch as a -// non-leaf node, for the same reason that foo_lookup_noauthz() cannot do the -// access check. I think this implies there need to be two different traits: -// - Fetch: the public trait that lets you fetch a complete record and the authz -// objects for the items in the hierarchy -// - Lookup: a trait private to this file that lets callers fetch the record and -// _does not_ do an access check -// If so, that may simplify things because: -// - I can have the leaf node store the LookupPath directly -// - it can pass the LookupPath (or whatever context is needed) to the lookup() -// function of the lookup() trait. - use super::datastore::DataStore; use super::identity::Resource; use super::model; @@ -82,11 +34,36 @@ trait Lookup { ) -> BoxFuture<'_, LookupResult>; } +trait GetLookupRoot { + fn lookup_root(&self) -> &LookupPath<'_>; +} + enum Key<'a, P> { Name(P, &'a Name), Id(LookupPath<'a>, Uuid), } +impl<'a, T> GetLookupRoot for Key<'a, T> +where + T: GetLookupRoot, +{ + fn lookup_root(&self) -> &LookupPath<'_> { + match self { + Key::Name(parent, _) => parent.lookup_root(), + Key::Id(lookup, _) => lookup, + } + } +} + +impl<'a, P> Key<'a, P> { + fn lookup_type(&self) -> LookupType { + match self { + Key::Name(_, name) => LookupType::ByName(name.as_str().to_string()), + Key::Id(_, id) => LookupType::ById(*id), + } + } +} + pub struct LookupPath<'a> { opctx: &'a OpContext, datastore: &'a DataStore, @@ -125,6 +102,12 @@ impl<'a> LookupPath<'a> { } } +impl<'a> GetLookupRoot for LookupPath<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self + } +} + pub struct Organization<'a> { key: Key<'a, LookupPath<'a>>, } @@ -139,32 +122,20 @@ impl<'a> Organization<'a> { } } +impl<'a> GetLookupRoot for Organization<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self.key.lookup_root() + } +} + impl Fetch for Organization<'_> { type FetchType = (authz::Organization, model::Organization); fn fetch(&self) -> BoxFuture<'_, LookupResult> { - // XXX-dap TODO This is a proof of concept. Each type in this path is - // going to need to get to the LookupPath through a sort of convoluted - // way. I wanted to implement a recursive function to get to the - // LookupPath, but then I'd need to create a trait for it that each type - // would have to impl, etc. (That might still be the way to go.) - // - // But see the note above -- maybe a different approach is better here! - let lookup = match &self.key { - Key::Name(lookup, _) => lookup, - Key::Id(lookup, _) => lookup, - }; - + let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = lookup.datastore; async { - let lookup_type = || match &self.key { - Key::Name(_, name) => { - LookupType::ByName(name.as_str().to_owned()) - } - Key::Id(_, id) => LookupType::ById(*id), - }; - use db::schema::organization::dsl; let conn = datastore.pool_authorized(opctx).await?; // XXX-dap TODO This construction sucks. What I kind of want is a @@ -187,7 +158,7 @@ impl Fetch for Organization<'_> { e, ErrorHandler::NotFoundByLookup( ResourceType::Organization, - lookup_type(), + self.key.lookup_type(), ), ) }), @@ -202,14 +173,14 @@ impl Fetch for Organization<'_> { e, ErrorHandler::NotFoundByLookup( ResourceType::Organization, - lookup_type(), + self.key.lookup_type(), ), ) }), }?; let authz_org = - authz::FLEET.organization(db_org.id(), lookup_type()); + authz::FLEET.organization(db_org.id(), self.key.lookup_type()); opctx.authorize(authz::Action::Read, &authz_org).await?; Ok((authz_org, db_org)) } @@ -221,6 +192,12 @@ pub struct Project<'a> { key: Key<'a, Organization<'a>>, } +impl<'a> GetLookupRoot for Project<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self.key.lookup_root() + } +} + impl Fetch for Project<'_> { type FetchType = (authz::Organization, authz::Project, model::Project); @@ -252,6 +229,12 @@ pub struct Instance<'a> { key: Key<'a, Project<'a>>, } +impl<'a> GetLookupRoot for Instance<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self.key.lookup_root() + } +} + #[cfg(test)] mod test { use super::Instance; From ce4b36217d7eb5f71a5e96ace5974e6c64c961fc Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 09:09:02 -0700 Subject: [PATCH 09/59] some cleanup --- Cargo.lock | 1 + nexus/Cargo.toml | 1 + nexus/src/db/lookup.rs | 142 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3e92df11d9..465c768a444 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2009,6 +2009,7 @@ dependencies = [ "oximeter-instruments", "oximeter-producer", "parse-display", + "paste", "pq-sys", "rand", "ref-cast", diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 87ad1391026..2b26b3588f6 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -32,6 +32,7 @@ oso = "0.26" oximeter-client = { path = "../oximeter-client" } oximeter-db = { path = "../oximeter/db/" } parse-display = "0.5.4" +paste = "1.0" # See omicron-rpaths for more about the "pq-sys" dependency. pq-sys = "*" rand = "0.8.5" diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 72f32fe3997..e5aa8d9cd51 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -138,14 +138,6 @@ impl Fetch for Organization<'_> { async { use db::schema::organization::dsl; let conn = datastore.pool_authorized(opctx).await?; - // XXX-dap TODO This construction sucks. What I kind of want is a - // generic function that takes: - // - a table (e.g., dsl::organization) - // - a db model type to return - // - some information about the LookupType we're trying to do - // - a closure that can be used to apply filters - // (which would be either "name" or "id") - // and does the whole thing. let db_org = match self.key { Key::Name(_, name) => dsl::organization .filter(dsl::time_deleted.is_null()) @@ -235,6 +227,140 @@ impl<'a> GetLookupRoot for Instance<'a> { } } +macro_rules! define_lookup { + ($lc:ident, $tc:ident) => { + paste::paste! { + async fn [<$lc lookup_by_name>]( + opctx: &OpContext, + datastore: &DataStore, + name: &Name, + ) -> LookupResult { + use db::schema::$lc::dsl; + let conn = datastore.pool_authorized(opctx).await?; + dsl::$lc + .filter(dsl::time_deleted.is_null()) + .filter(dsl::name.eq(name.clone())) + .select(model::$tc::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::$tc, + LookupType::ByName(name.as_str().to_string()) + ) + ) + }) + } + } + }; +} + +macro_rules! define_lookup_with_parent { + ($lc:ident, $tc:ident, $authz_parent:ident, $parent_id:ident, $mkauthz:expr) => { + paste::paste! { + // XXX TODO-dap the lookup_by_id is not within the context of a + // particular parent. Thus, we can't return an authz struct for the + // new thing -- we have to look up the parent we find again in order + // to do that. + async fn [<$lc _lookup_by_id>]( + opctx: &OpContext, + datastore: &DataStore, + authz_parent: &authz::$authz_parent, + id: Uuid, + ) -> LookupResult<(authz::$tc, model::$tc)> { + use db::schema::$lc::dsl; + let conn = datastore.pool_authorized(opctx).await?; + dsl::$lc + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(id)) + .filter(dsl::$parent_id.eq(authz_parent.id())) + .select(model::$tc::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::$tc, + LookupType::ById(id) + ) + ) + }) + .map(|dbmodel| {( + $mkauthz( + authz_parent, + &dbmodel, + LookupType::ById(id) + ), + dbmodel + )}) + } + + async fn [<$lc _lookup_by_name>]( + opctx: &OpContext, + datastore: &DataStore, + authz_parent: &authz::$authz_parent, + name: &Name, + ) -> LookupResult<(authz::$tc, model::$tc)> { + use db::schema::$lc::dsl; + let conn = datastore.pool_authorized(opctx).await?; + dsl::$lc + .filter(dsl::time_deleted.is_null()) + .filter(dsl::name.eq(name.clone())) + .filter(dsl::$parent_id.eq(authz_parent.id())) + .select(model::$tc::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::$tc, + LookupType::ByName(name.as_str().to_string()) + ) + ) + }) + .map(|dbmodel| {( + $mkauthz( + authz_parent, + &dbmodel, + LookupType::ByName(name.as_str().to_string()) + ), + dbmodel + )}) + } + } + }; +} + +define_lookup!(organization, Organization); +define_lookup_with_parent!( + project, + Project, + Organization, + organization_id, + |authz_org: &authz::Organization, + project: &model::Project, + lookup: LookupType| { authz_org.project(project.id(), lookup) } +); +define_lookup_with_parent!( + instance, + Instance, + Project, + project_id, + |authz_project: &authz::Project, + instance: &model::Instance, + lookup: LookupType| { + authz_project.child_generic( + ResourceType::Instance, + instance.id(), + lookup, + ) + } +); + #[cfg(test)] mod test { use super::Instance; From aae2da4f6191d7e69a8934965448813f3f89c0e3 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 09:37:46 -0700 Subject: [PATCH 10/59] flesh out macros --- nexus/src/db/lookup.rs | 103 +++++++++++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index e5aa8d9cd51..23ea8c03ad8 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -228,37 +228,82 @@ impl<'a> GetLookupRoot for Instance<'a> { } macro_rules! define_lookup { - ($lc:ident, $tc:ident) => { + ($lc:ident, $pc:ident) => { paste::paste! { - async fn [<$lc lookup_by_name>]( + async fn [<$lc _lookup_by_id>]( + opctx: &OpContext, + datastore: &DataStore, + id: Uuid, + ) -> LookupResult<(authz::$pc, model::$pc)> { + use db::schema::$lc::dsl; + let conn = datastore.pool_authorized(opctx).await?; + dsl::$lc + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(id)) + .select(model::$pc::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::$pc, + LookupType::ById(id) + ) + ) + }) + .map(|o| {( + authz::FLEET.$lc(o.id(), LookupType::ById(id)), + o + )} + ) + } + + async fn [<$lc _lookup_by_name>]( opctx: &OpContext, datastore: &DataStore, name: &Name, - ) -> LookupResult { + ) -> LookupResult<(authz::$pc, model::$pc)> { use db::schema::$lc::dsl; let conn = datastore.pool_authorized(opctx).await?; dsl::$lc .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) - .select(model::$tc::as_select()) + .select(model::$pc::as_select()) .get_result_async(conn) .await .map_err(|e| { public_error_from_diesel_pool( e, ErrorHandler::NotFoundByLookup( - ResourceType::$tc, + ResourceType::$pc, LookupType::ByName(name.as_str().to_string()) ) ) }) + .map(|o| {( + authz::FLEET.$lc( + o.id(), + LookupType::ByName(name.as_str().to_string()) + ), + o + )} + ) } + } }; } macro_rules! define_lookup_with_parent { - ($lc:ident, $tc:ident, $authz_parent:ident, $parent_id:ident, $mkauthz:expr) => { + ( + $lc:ident, // Lowercase version of resource name + $pc:ident, // Pascal-case version of resource name + $parent_lc:ident, // Lowercase version of parent resource name + $parent_pc:ident, // Pascal-case version of parent resource name + $mkauthz:expr // Closure to generate resource's authz object + // from parent's + ) => { paste::paste! { // XXX TODO-dap the lookup_by_id is not within the context of a // particular parent. Thus, we can't return an authz struct for the @@ -267,63 +312,63 @@ macro_rules! define_lookup_with_parent { async fn [<$lc _lookup_by_id>]( opctx: &OpContext, datastore: &DataStore, - authz_parent: &authz::$authz_parent, id: Uuid, - ) -> LookupResult<(authz::$tc, model::$tc)> { + ) -> LookupResult<(authz::$pc, model::$pc)> { use db::schema::$lc::dsl; let conn = datastore.pool_authorized(opctx).await?; - dsl::$lc + let db_row = dsl::$lc .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(id)) - .filter(dsl::$parent_id.eq(authz_parent.id())) - .select(model::$tc::as_select()) + .select(model::$pc::as_select()) .get_result_async(conn) .await .map_err(|e| { public_error_from_diesel_pool( e, ErrorHandler::NotFoundByLookup( - ResourceType::$tc, + ResourceType::$pc, LookupType::ById(id) ) ) - }) - .map(|dbmodel| {( - $mkauthz( - authz_parent, - &dbmodel, - LookupType::ById(id) - ), - dbmodel - )}) + })?; + let (authz_parent, _) = + [< $parent_lc _lookup_by_id >]( + opctx, + datastore, + db_row.[<$parent_lc _id>] + ).await?; + let authz_child = ($mkauthz)( + &authz_parent, &db_row, LookupType::ById(id) + ); + Ok((authz_child, db_row)) } async fn [<$lc _lookup_by_name>]( opctx: &OpContext, datastore: &DataStore, - authz_parent: &authz::$authz_parent, + authz_parent: &authz::$parent_pc, name: &Name, - ) -> LookupResult<(authz::$tc, model::$tc)> { + ) -> LookupResult<(authz::$pc, model::$pc)> { use db::schema::$lc::dsl; let conn = datastore.pool_authorized(opctx).await?; dsl::$lc .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) - .filter(dsl::$parent_id.eq(authz_parent.id())) - .select(model::$tc::as_select()) + .filter(dsl::[<$parent_lc _id>].eq(authz_parent.id())) + .select(model::$pc::as_select()) .get_result_async(conn) .await .map_err(|e| { public_error_from_diesel_pool( e, ErrorHandler::NotFoundByLookup( - ResourceType::$tc, + ResourceType::$pc, LookupType::ByName(name.as_str().to_string()) ) ) }) .map(|dbmodel| {( - $mkauthz( + ($mkauthz)( authz_parent, &dbmodel, LookupType::ByName(name.as_str().to_string()) @@ -339,8 +384,8 @@ define_lookup!(organization, Organization); define_lookup_with_parent!( project, Project, + organization, Organization, - organization_id, |authz_org: &authz::Organization, project: &model::Project, lookup: LookupType| { authz_org.project(project.id(), lookup) } @@ -348,8 +393,8 @@ define_lookup_with_parent!( define_lookup_with_parent!( instance, Instance, + project, Project, - project_id, |authz_project: &authz::Project, instance: &model::Instance, lookup: LookupType| { From 73f0f4b0f924da3b0a75d2d61319d06c0254a308 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 09:56:46 -0700 Subject: [PATCH 11/59] more macro cleanup --- nexus/src/db/lookup.rs | 87 ++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 23ea8c03ad8..b9a668520c3 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -228,16 +228,18 @@ impl<'a> GetLookupRoot for Instance<'a> { } macro_rules! define_lookup { - ($lc:ident, $pc:ident) => { + ($pc:ident) => { paste::paste! { - async fn [<$lc _lookup_by_id>]( + // Do NOT make these functions public. They should instead be + // wrapped by functions that perform authz checks. + async fn [<$pc:lower _lookup_by_id_no_authz>]( opctx: &OpContext, datastore: &DataStore, id: Uuid, ) -> LookupResult<(authz::$pc, model::$pc)> { - use db::schema::$lc::dsl; + use db::schema::[<$pc:lower>]::dsl; let conn = datastore.pool_authorized(opctx).await?; - dsl::$lc + dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(id)) .select(model::$pc::as_select()) @@ -253,20 +255,22 @@ macro_rules! define_lookup { ) }) .map(|o| {( - authz::FLEET.$lc(o.id(), LookupType::ById(id)), + authz::FLEET.[<$pc:lower>](o.id(), LookupType::ById(id)), o )} ) } - async fn [<$lc _lookup_by_name>]( + // Do NOT make these functions public. They should instead be + // wrapped by functions that perform authz checks. + async fn [<$pc:lower _lookup_by_name_no_authz>]( opctx: &OpContext, datastore: &DataStore, name: &Name, ) -> LookupResult<(authz::$pc, model::$pc)> { - use db::schema::$lc::dsl; + use db::schema::[<$pc:lower>]::dsl; let conn = datastore.pool_authorized(opctx).await?; - dsl::$lc + dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) .select(model::$pc::as_select()) @@ -282,7 +286,7 @@ macro_rules! define_lookup { ) }) .map(|o| {( - authz::FLEET.$lc( + authz::FLEET.[<$pc:lower>]( o.id(), LookupType::ByName(name.as_str().to_string()) ), @@ -291,32 +295,42 @@ macro_rules! define_lookup { ) } + async fn [<$pc:lower _fetch_by_name>]( + opctx: &OpContext, + datastore: &DataStore, + name: &Name, + ) -> LookupResult<(authz::$pc, model::$pc)> { + let (authz_child, db_child) = + [<$pc:lower _lookup_by_name_no_authz>]( + opctx, + datastore, + name + ).await?; + opctx.authorize(authz::Action::Read, &authz_child).await?; + Ok((authz_child, db_child)) + } } }; } macro_rules! define_lookup_with_parent { ( - $lc:ident, // Lowercase version of resource name $pc:ident, // Pascal-case version of resource name - $parent_lc:ident, // Lowercase version of parent resource name $parent_pc:ident, // Pascal-case version of parent resource name $mkauthz:expr // Closure to generate resource's authz object // from parent's ) => { paste::paste! { - // XXX TODO-dap the lookup_by_id is not within the context of a - // particular parent. Thus, we can't return an authz struct for the - // new thing -- we have to look up the parent we find again in order - // to do that. - async fn [<$lc _lookup_by_id>]( + // Do NOT make these functions public. They should instead be + // wrapped by functions that perform authz checks. + async fn [<$pc:lower _lookup_by_id_no_authz>]( opctx: &OpContext, datastore: &DataStore, id: Uuid, ) -> LookupResult<(authz::$pc, model::$pc)> { - use db::schema::$lc::dsl; + use db::schema::[<$pc:lower>]::dsl; let conn = datastore.pool_authorized(opctx).await?; - let db_row = dsl::$lc + let db_row = dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(id)) .select(model::$pc::as_select()) @@ -332,10 +346,10 @@ macro_rules! define_lookup_with_parent { ) })?; let (authz_parent, _) = - [< $parent_lc _lookup_by_id >]( + [< $parent_pc:lower _lookup_by_id_no_authz >]( opctx, datastore, - db_row.[<$parent_lc _id>] + db_row.[<$parent_pc:lower _id>] ).await?; let authz_child = ($mkauthz)( &authz_parent, &db_row, LookupType::ById(id) @@ -343,18 +357,20 @@ macro_rules! define_lookup_with_parent { Ok((authz_child, db_row)) } - async fn [<$lc _lookup_by_name>]( + // Do NOT make these functions public. They should instead be + // wrapped by functions that perform authz checks. + async fn [<$pc:lower _lookup_by_name_no_authz>]( opctx: &OpContext, datastore: &DataStore, authz_parent: &authz::$parent_pc, name: &Name, ) -> LookupResult<(authz::$pc, model::$pc)> { - use db::schema::$lc::dsl; + use db::schema::[<$pc:lower>]::dsl; let conn = datastore.pool_authorized(opctx).await?; - dsl::$lc + dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) - .filter(dsl::[<$parent_lc _id>].eq(authz_parent.id())) + .filter(dsl::[<$parent_pc:lower _id>].eq(authz_parent.id())) .select(model::$pc::as_select()) .get_result_async(conn) .await @@ -376,24 +392,37 @@ macro_rules! define_lookup_with_parent { dbmodel )}) } + + async fn [<$pc:lower _fetch_by_name>]( + opctx: &OpContext, + datastore: &DataStore, + authz_parent: &authz::$parent_pc, + name: &Name, + ) -> LookupResult<(authz::$pc, model::$pc)> { + let (authz_child, db_child) = + [<$pc:lower _lookup_by_name_no_authz>]( + opctx, + datastore, + authz_parent, + name + ).await?; + opctx.authorize(authz::Action::Read, &authz_child).await?; + Ok((authz_child, db_child)) + } } }; } -define_lookup!(organization, Organization); +define_lookup!(Organization); define_lookup_with_parent!( - project, Project, - organization, Organization, |authz_org: &authz::Organization, project: &model::Project, lookup: LookupType| { authz_org.project(project.id(), lookup) } ); define_lookup_with_parent!( - instance, Instance, - project, Project, |authz_project: &authz::Project, instance: &model::Instance, From 68b51cf525ec30532c2b1b8b33858ac8a90af8d6 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 10:02:19 -0700 Subject: [PATCH 12/59] more cleanup --- nexus/src/db/lookup.rs | 61 ++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index b9a668520c3..ed02570f74d 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -136,45 +136,15 @@ impl Fetch for Organization<'_> { let opctx = &lookup.opctx; let datastore = lookup.datastore; async { - use db::schema::organization::dsl; let conn = datastore.pool_authorized(opctx).await?; - let db_org = match self.key { - Key::Name(_, name) => dsl::organization - .filter(dsl::time_deleted.is_null()) - .filter(dsl::name.eq(name.clone())) - .select(model::Organization::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::Organization, - self.key.lookup_type(), - ), - ) - }), - Key::Id(_, id) => dsl::organization - .filter(dsl::time_deleted.is_null()) - .filter(dsl::id.eq(id)) - .select(model::Organization::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::Organization, - self.key.lookup_type(), - ), - ) - }), - }?; - - let authz_org = - authz::FLEET.organization(db_org.id(), self.key.lookup_type()); - opctx.authorize(authz::Action::Read, &authz_org).await?; - Ok((authz_org, db_org)) + match self.key { + Key::Name(_, name) => { + organization_fetch_by_name(opctx, datastore, name).await + } + Key::Id(_, id) => { + organization_fetch_by_id(opctx, datastore, id).await + } + } } .boxed() } @@ -309,6 +279,21 @@ macro_rules! define_lookup { opctx.authorize(authz::Action::Read, &authz_child).await?; Ok((authz_child, db_child)) } + + async fn [<$pc:lower _fetch_by_id>]( + opctx: &OpContext, + datastore: &DataStore, + id: Uuid, + ) -> LookupResult<(authz::$pc, model::$pc)> { + let (authz_child, db_child) = + [<$pc:lower _lookup_by_id_no_authz>]( + opctx, + datastore, + id, + ).await?; + opctx.authorize(authz::Action::Read, &authz_child).await?; + Ok((authz_child, db_child)) + } } }; } From 6d265bb0ea75b0b512c9e68c48818b63d7593be8 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 10:25:44 -0700 Subject: [PATCH 13/59] more commonizing --- nexus/src/db/lookup.rs | 98 ++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index ed02570f74d..6e5c960ee54 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -136,7 +136,6 @@ impl Fetch for Organization<'_> { let opctx = &lookup.opctx; let datastore = lookup.datastore; async { - let conn = datastore.pool_authorized(opctx).await?; match self.key { Key::Name(_, name) => { organization_fetch_by_name(opctx, datastore, name).await @@ -154,20 +153,6 @@ pub struct Project<'a> { key: Key<'a, Organization<'a>>, } -impl<'a> GetLookupRoot for Project<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self.key.lookup_root() - } -} - -impl Fetch for Project<'_> { - type FetchType = (authz::Organization, authz::Project, model::Project); - - fn fetch(&self) -> BoxFuture<'_, LookupResult> { - todo!() - } -} - impl<'a> Project<'a> { fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> where @@ -178,25 +163,10 @@ impl<'a> Project<'a> { } } -impl Fetch for Instance<'_> { - type FetchType = - (authz::Organization, authz::Project, authz::Instance, model::Instance); - - fn fetch(&self) -> BoxFuture<'_, LookupResult> { - todo!() - } -} - pub struct Instance<'a> { key: Key<'a, Project<'a>>, } -impl<'a> GetLookupRoot for Instance<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self.key.lookup_root() - } -} - macro_rules! define_lookup { ($pc:ident) => { paste::paste! { @@ -265,31 +235,31 @@ macro_rules! define_lookup { ) } - async fn [<$pc:lower _fetch_by_name>]( + async fn [<$pc:lower _fetch_by_id>]( opctx: &OpContext, datastore: &DataStore, - name: &Name, + id: Uuid, ) -> LookupResult<(authz::$pc, model::$pc)> { let (authz_child, db_child) = - [<$pc:lower _lookup_by_name_no_authz>]( + [<$pc:lower _lookup_by_id_no_authz>]( opctx, datastore, - name + id, ).await?; opctx.authorize(authz::Action::Read, &authz_child).await?; Ok((authz_child, db_child)) } - async fn [<$pc:lower _fetch_by_id>]( + async fn [<$pc:lower _fetch_by_name>]( opctx: &OpContext, datastore: &DataStore, - id: Uuid, + name: &Name, ) -> LookupResult<(authz::$pc, model::$pc)> { let (authz_child, db_child) = - [<$pc:lower _lookup_by_id_no_authz>]( + [<$pc:lower _lookup_by_name_no_authz>]( opctx, datastore, - id, + name ).await?; opctx.authorize(authz::Action::Read, &authz_child).await?; Ok((authz_child, db_child)) @@ -378,6 +348,21 @@ macro_rules! define_lookup_with_parent { )}) } + async fn [<$pc:lower _fetch_by_id>]( + opctx: &OpContext, + datastore: &DataStore, + id: Uuid, + ) -> LookupResult<(authz::$pc, model::$pc)> { + let (authz_child, db_child) = + [<$pc:lower _lookup_by_id_no_authz>]( + opctx, + datastore, + id, + ).await?; + opctx.authorize(authz::Action::Read, &authz_child).await?; + Ok((authz_child, db_child)) + } + async fn [<$pc:lower _fetch_by_name>]( opctx: &OpContext, datastore: &DataStore, @@ -394,6 +379,43 @@ macro_rules! define_lookup_with_parent { opctx.authorize(authz::Action::Read, &authz_child).await?; Ok((authz_child, db_child)) } + + impl<'a> GetLookupRoot for $pc<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self.key.lookup_root() + } + } + + impl Fetch for $pc<'_> { + type FetchType = (authz::$pc, model::$pc); + + fn fetch(&self) -> BoxFuture<'_, LookupResult> { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = lookup.datastore; + async { + match &self.key { + Key::Name(parent, name) => { + let (parent_authz, _) = parent.fetch().await?; + [< $pc:lower _fetch_by_name >]( + opctx, + datastore, + &parent_authz, + *name + ).await + } + Key::Id(_, id) => { + [< $pc:lower _fetch_by_id >]( + opctx, + datastore, + *id + ).await + } + } + } + .boxed() + } + } } }; } From e4a58443c07c59706d75f6e4fcbee8eb969a4df5 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 10:42:47 -0700 Subject: [PATCH 14/59] cleanup --- nexus/src/db/lookup.rs | 65 ++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 6e5c960ee54..7f4ef94720b 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -122,33 +122,6 @@ impl<'a> Organization<'a> { } } -impl<'a> GetLookupRoot for Organization<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self.key.lookup_root() - } -} - -impl Fetch for Organization<'_> { - type FetchType = (authz::Organization, model::Organization); - - fn fetch(&self) -> BoxFuture<'_, LookupResult> { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = lookup.datastore; - async { - match self.key { - Key::Name(_, name) => { - organization_fetch_by_name(opctx, datastore, name).await - } - Key::Id(_, id) => { - organization_fetch_by_id(opctx, datastore, id).await - } - } - } - .boxed() - } -} - pub struct Project<'a> { key: Key<'a, Organization<'a>>, } @@ -264,6 +237,44 @@ macro_rules! define_lookup { opctx.authorize(authz::Action::Read, &authz_child).await?; Ok((authz_child, db_child)) } + + impl<'a> GetLookupRoot for $pc<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self.key.lookup_root() + } + } + + impl Fetch for $pc<'_> { + type FetchType = (authz::$pc, model::$pc); + + fn fetch(&self) -> BoxFuture<'_, LookupResult> { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = lookup.datastore; + async { + match self.key { + Key::Name(_, name) => { + [<$pc:lower _fetch_by_name>]( + opctx, + datastore, + name + ).await + } + Key::Id(_, id) => { + [<$pc:lower _fetch_by_id>]( + opctx, + datastore, + id + ).await + } + } + } + .boxed() + } + } + + + } }; } From 0e73f257bf456af6e09983d120b6c5a2131811c1 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 10:51:42 -0700 Subject: [PATCH 15/59] more commonizing --- nexus/src/db/lookup.rs | 47 ++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 7f4ef94720b..8be2e5285c5 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -108,10 +108,6 @@ impl<'a> GetLookupRoot for LookupPath<'a> { } } -pub struct Organization<'a> { - key: Key<'a, LookupPath<'a>>, -} - impl<'a> Organization<'a> { fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> where @@ -122,10 +118,6 @@ impl<'a> Organization<'a> { } } -pub struct Project<'a> { - key: Key<'a, Organization<'a>>, -} - impl<'a> Project<'a> { fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> where @@ -136,13 +128,19 @@ impl<'a> Project<'a> { } } -pub struct Instance<'a> { - key: Key<'a, Project<'a>>, -} - macro_rules! define_lookup { ($pc:ident) => { paste::paste! { + pub struct $pc<'a> { + key: Key<'a, LookupPath<'a>>, + } + + impl<'a> GetLookupRoot for $pc<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self.key.lookup_root() + } + } + // Do NOT make these functions public. They should instead be // wrapped by functions that perform authz checks. async fn [<$pc:lower _lookup_by_id_no_authz>]( @@ -238,12 +236,6 @@ macro_rules! define_lookup { Ok((authz_child, db_child)) } - impl<'a> GetLookupRoot for $pc<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self.key.lookup_root() - } - } - impl Fetch for $pc<'_> { type FetchType = (authz::$pc, model::$pc); @@ -272,9 +264,6 @@ macro_rules! define_lookup { .boxed() } } - - - } }; } @@ -287,6 +276,16 @@ macro_rules! define_lookup_with_parent { // from parent's ) => { paste::paste! { + pub struct $pc<'a> { + key: Key<'a, $parent_pc<'a>>, + } + + impl<'a> GetLookupRoot for $pc<'a> { + fn lookup_root(&self) -> &LookupPath<'_> { + self.key.lookup_root() + } + } + // Do NOT make these functions public. They should instead be // wrapped by functions that perform authz checks. async fn [<$pc:lower _lookup_by_id_no_authz>]( @@ -391,12 +390,6 @@ macro_rules! define_lookup_with_parent { Ok((authz_child, db_child)) } - impl<'a> GetLookupRoot for $pc<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self.key.lookup_root() - } - } - impl Fetch for $pc<'_> { type FetchType = (authz::$pc, model::$pc); From a414c1fc73aa7f33a84860803b5b14cca4fe5380 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 10:53:36 -0700 Subject: [PATCH 16/59] lookup done enough to start trying it out --- nexus/src/db/lookup.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 8be2e5285c5..0fbb85365b8 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -34,6 +34,9 @@ trait Lookup { ) -> BoxFuture<'_, LookupResult>; } +// TODO-dap XXX we could probably get rid of this trait and its impls because +// we're now calling `lookup_root()` from a macro that can essentially inline +// the impl of GetLookupRoot for Key. trait GetLookupRoot { fn lookup_root(&self) -> &LookupPath<'_>; } From c7941b3176777369c60e4992ae1462c53b084ca4 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 14:48:54 -0700 Subject: [PATCH 17/59] prototype a few call sites in Nexus --- nexus/src/db/lookup.rs | 149 ++++++++++++++++++++++++++++++++++++----- nexus/src/db/mod.rs | 2 +- nexus/src/nexus.rs | 42 +++++++----- 3 files changed, 157 insertions(+), 36 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 0fbb85365b8..cfcee4a3e7a 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -8,7 +8,7 @@ use super::datastore::DataStore; use super::identity::Resource; use super::model; use crate::{ - authz, + authz::{self, AuthorizedResource}, context::OpContext, db, db::error::{public_error_from_diesel_pool, ErrorHandler}, @@ -21,24 +21,68 @@ use futures::FutureExt; use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; use uuid::Uuid; +// TODO-dap XXX Neither "fetcH" nor "lookup" needs to be a trait now that the +// macro is defining the impls and the structs + pub trait Fetch { type FetchType; fn fetch(&self) -> BoxFuture<'_, LookupResult>; } -trait Lookup { +mod private { + use futures::future::BoxFuture; + use omicron_common::api::external::LookupResult; + + use crate::authz::AuthorizedResource; + + use super::LookupPath; + + // TODO-dap XXX we could probably get rid of this trait and its impls because + // we're now calling `lookup_root()` from a macro that can essentially inline + // the impl of GetLookupRoot for Key. + pub trait GetLookupRoot { + fn lookup_root(&self) -> &LookupPath<'_>; + } + + pub trait LookupNoauthz { + type LookupType: AuthorizedResource + Clone + std::fmt::Debug + Send; + fn lookup(&self) -> BoxFuture<'_, LookupResult>; + } +} + +// XXX-dap workaround compiler error with private mod +use private::GetLookupRoot; +use private::LookupNoauthz; + +pub trait LookupFor { type LookupType; - fn lookup( + fn lookup_for( &self, - lookup: &LookupPath, + action: authz::Action, ) -> BoxFuture<'_, LookupResult>; } -// TODO-dap XXX we could probably get rid of this trait and its impls because -// we're now calling `lookup_root()` from a macro that can essentially inline -// the impl of GetLookupRoot for Key. -trait GetLookupRoot { - fn lookup_root(&self) -> &LookupPath<'_>; +impl LookupFor for T +where + T: LookupNoauthz + GetLookupRoot + Send + Sync, // XXX-dap try removing Sync? + // XXX-dap + // ::LookupType: + // AuthorizedResource + Clone + std::fmt::Debug + Send, +{ + type LookupType = ::LookupType; + fn lookup_for( + &self, + action: authz::Action, + ) -> BoxFuture<'_, LookupResult> { + async move { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let rv = self.lookup().await?; + opctx.authorize(action, &rv).await?; + Ok(rv) + } + .boxed() + } } enum Key<'a, P> { @@ -112,7 +156,7 @@ impl<'a> GetLookupRoot for LookupPath<'a> { } impl<'a> Organization<'a> { - fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> + pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> where 'a: 'c, 'b: 'c, @@ -122,7 +166,7 @@ impl<'a> Organization<'a> { } impl<'a> Project<'a> { - fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> + pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> where 'a: 'c, 'b: 'c, @@ -239,14 +283,48 @@ macro_rules! define_lookup { Ok((authz_child, db_child)) } + impl LookupNoauthz for $pc<'_> { + type LookupType = authz::$pc; + + fn lookup( + &self, + ) -> BoxFuture<'_, LookupResult> { + async { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = lookup.datastore; + match self.key { + Key::Name(_, name) => { + let (rv, _) = + [<$pc:lower _lookup_by_name_no_authz>]( + opctx, + datastore, + name + ).await?; + Ok(rv) + } + Key::Id(_, id) => { + let (rv, _) = + [<$pc:lower _lookup_by_id_no_authz>]( + opctx, + datastore, + id + ).await?; + Ok(rv) + } + } + }.boxed() + } + } + impl Fetch for $pc<'_> { type FetchType = (authz::$pc, model::$pc); fn fetch(&self) -> BoxFuture<'_, LookupResult> { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = lookup.datastore; async { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = lookup.datastore; match self.key { Key::Name(_, name) => { [<$pc:lower _fetch_by_name>]( @@ -393,14 +471,51 @@ macro_rules! define_lookup_with_parent { Ok((authz_child, db_child)) } + impl LookupNoauthz for $pc<'_> { + type LookupType = authz::$pc; + + fn lookup( + &self, + ) -> BoxFuture<'_, LookupResult> { + async { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = lookup.datastore; + match &self.key { + Key::Name(parent, name) => { + let parent_authz = parent.lookup().await?; + let (rv, _) = + [< $pc:lower _lookup_by_name_no_authz >]( + opctx, + datastore, + &parent_authz, + *name + ).await?; + Ok(rv) + } + Key::Id(_, id) => { + let (rv, _) = + [< $pc:lower _lookup_by_id_no_authz >]( + opctx, + datastore, + *id + ).await?; + Ok(rv) + } + } + } + .boxed() + } + } + impl Fetch for $pc<'_> { type FetchType = (authz::$pc, model::$pc); fn fetch(&self) -> BoxFuture<'_, LookupResult> { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = lookup.datastore; async { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = lookup.datastore; match &self.key { Key::Name(parent, name) => { let (parent_authz, _) = parent.fetch().await?; diff --git a/nexus/src/db/mod.rs b/nexus/src/db/mod.rs index b6d503b360f..46d15f4ee2d 100644 --- a/nexus/src/db/mod.rs +++ b/nexus/src/db/mod.rs @@ -14,7 +14,7 @@ pub mod datastore; mod error; mod explain; pub mod fixed_data; -mod lookup; +pub mod lookup; mod pagination; mod pool; mod saga_recovery; diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 96131ff6e9e..c42a1a54aab 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -10,6 +10,9 @@ use crate::config; use crate::context::OpContext; use crate::db; use crate::db::identity::{Asset, Resource}; +use crate::db::lookup::Fetch; +use crate::db::lookup::LookupFor; +use crate::db::lookup::LookupPath; use crate::db::model::DatasetKind; use crate::db::model::Name; use crate::db::model::RouterRoute; @@ -533,15 +536,18 @@ impl Nexus { organization_name: &Name, new_project: ¶ms::ProjectCreate, ) -> CreateResult { - let org = self - .db_datastore - .organization_lookup_by_path(organization_name) + let authz_org = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .lookup_for(authz::Action::CreateChild) .await?; // Create a project. - let db_project = db::model::Project::new(org.id(), new_project.clone()); let db_project = - self.db_datastore.project_create(opctx, &org, db_project).await?; + db::model::Project::new(authz_org.id(), new_project.clone()); + let db_project = self + .db_datastore + .project_create(opctx, &authz_org, db_project) + .await?; // TODO: We probably want to have "project creation" and "default VPC // creation" co-located within a saga for atomicity. @@ -549,6 +555,9 @@ impl Nexus { // Until then, we just perform the operations sequentially. // Create a default VPC associated with the project. + // XXX-dap We need to be using the project_id we just created. + // project_create() should return authz::Project and we should use that + // here. let _ = self .project_create_vpc( opctx, @@ -577,13 +586,10 @@ impl Nexus { organization_name: &Name, project_name: &Name, ) -> LookupResult { - let authz_org = self - .db_datastore - .organization_lookup_by_path(organization_name) - .await?; - Ok(self - .db_datastore - .project_fetch(opctx, &authz_org, project_name) + Ok(LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .fetch() .await? .1) } @@ -594,9 +600,9 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let authz_org = self - .db_datastore - .organization_lookup_by_path(organization_name) + let authz_org = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .lookup_for(authz::Action::CreateChild) .await?; self.db_datastore .projects_list_by_name(opctx, &authz_org, pagparams) @@ -609,9 +615,9 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { - let authz_org = self - .db_datastore - .organization_lookup_by_path(organization_name) + let authz_org = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .lookup_for(authz::Action::CreateChild) .await?; self.db_datastore .projects_list_by_id(opctx, &authz_org, pagparams) From 5690db3dd91f778b97c2923c9c4bb1f2cfa2c15b Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 15:03:26 -0700 Subject: [PATCH 18/59] some basic tests are working by accident --- nexus/test-utils/src/http_testing.rs | 8 + nexus/tests/integration_tests/basic.rs | 156 +++++++++--------- nexus/tests/integration_tests/unauthorized.rs | 9 +- 3 files changed, 87 insertions(+), 86 deletions(-) diff --git a/nexus/test-utils/src/http_testing.rs b/nexus/test-utils/src/http_testing.rs index fca1073bf30..f0d6d3abcdb 100644 --- a/nexus/test-utils/src/http_testing.rs +++ b/nexus/test-utils/src/http_testing.rs @@ -448,6 +448,14 @@ impl<'a> NexusRequest<'a> { self.request_builder.execute().await } + /// Convenience function that executes the request, parses the body, and + /// unwraps any `Result`s along the way. + pub async fn execute_and_parse_unwrap( + self, + ) -> T { + self.execute().await.unwrap().parsed_body().unwrap() + } + /// Returns a new `NexusRequest` suitable for `POST $uri` with the given /// `body` pub fn objects_post( diff --git a/nexus/tests/integration_tests/basic.rs b/nexus/tests/integration_tests/basic.rs index a0fb62d396c..d11a98a5019 100644 --- a/nexus/tests/integration_tests/basic.rs +++ b/nexus/tests/integration_tests/basic.rs @@ -40,42 +40,36 @@ async fn test_basic_failures(cptestctx: &ControlPlaneTestContext) { create_organization(&client, &org_name).await; // Error case: GET /nonexistent (a path with no route at all) - let error = client - .make_request( - Method::GET, - "/nonexistent", - None as Option<()>, - StatusCode::NOT_FOUND, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::NOT_FOUND, + Method::GET, + "/nonexistent", + ) + .await; assert_eq!("Not Found", error.message); // Error case: GET /organizations/test-org/projects/nonexistent (a possible // value that does not exist inside a collection that does exist) - let error = client - .make_request( - Method::GET, - "/organizations/test-org/projects/nonexistent", - None as Option<()>, - StatusCode::NOT_FOUND, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::NOT_FOUND, + Method::GET, + "/organizations/test-org/projects/nonexistent", + ) + .await; assert_eq!("not found: project with name \"nonexistent\"", error.message); // Error case: GET /organizations/test-org/projects/-invalid-name // TODO-correctness is 400 the right error code here or is 404 more // appropriate? - let error = client - .make_request( - Method::GET, - "/organizations/test-org/projects/-invalid-name", - None as Option<()>, - StatusCode::BAD_REQUEST, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::BAD_REQUEST, + Method::GET, + "/organizations/test-org/projects/-invalid-name", + ) + .await; assert_eq!( "bad parameter in URL path: name must begin with an ASCII lowercase \ character", @@ -83,63 +77,53 @@ async fn test_basic_failures(cptestctx: &ControlPlaneTestContext) { ); // Error case: PUT /organizations/test-org/projects - let error = client - .make_request( - Method::PUT, - "/organizations/test-org/projects", - None as Option<()>, - StatusCode::METHOD_NOT_ALLOWED, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::METHOD_NOT_ALLOWED, + Method::PUT, + "/organizations/test-org/projects", + ) + .await; assert_eq!("Method Not Allowed", error.message); // Error case: DELETE /organizations/test-org/projects - let error = client - .make_request( - Method::DELETE, - "/organizations/test-org/projects", - None as Option<()>, - StatusCode::METHOD_NOT_ALLOWED, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::METHOD_NOT_ALLOWED, + Method::DELETE, + "/organizations/test-org/projects", + ) + .await; assert_eq!("Method Not Allowed", error.message); // Error case: list instances in a nonexistent project. - let error = client - .make_request_with_body( - Method::GET, - "/organizations/test-org/projects/nonexistent/instances", - "".into(), - StatusCode::NOT_FOUND, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::NOT_FOUND, + Method::GET, + "/organizations/test-org/projects/nonexistent/instances", + ) + .await; assert_eq!("not found: project with name \"nonexistent\"", error.message); // Error case: fetch an instance in a nonexistent project. - let error = client - .make_request_with_body( - Method::GET, - "/organizations/test-org/projects/nonexistent/instances/my-instance", - "".into(), - StatusCode::NOT_FOUND, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::NOT_FOUND, + Method::GET, + "/organizations/test-org/projects/nonexistent/instances/my-instance", + ) + .await; assert_eq!("not found: project with name \"nonexistent\"", error.message); // Error case: fetch an instance with an invalid name. - let error = client - .make_request_with_body( - Method::GET, - "/organizations/test-org/projects/nonexistent/instances/my_instance", - "".into(), - StatusCode::BAD_REQUEST, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::BAD_REQUEST, + Method::GET, + "/organizations/test-org/projects/nonexistent/instances/my_instance", + ) + .await; assert_eq!( "bad parameter in URL path: name contains invalid character: \"_\" \ (allowed characters are lowercase ASCII, digits, and \"-\")", @@ -147,15 +131,13 @@ async fn test_basic_failures(cptestctx: &ControlPlaneTestContext) { ); // Error case: delete an instance with an invalid name. - let error = client - .make_request_with_body( - Method::DELETE, - "/organizations/test-org/projects/nonexistent/instances/my_instance", - "".into(), - StatusCode::BAD_REQUEST, - ) - .await - .expect_err("expected error"); + let error = error_response( + client, + StatusCode::BAD_REQUEST, + Method::DELETE, + "/organizations/test-org/projects/nonexistent/instances/my_instance", + ) + .await; assert_eq!( "bad parameter in URL path: name contains invalid character: \"_\" \ (allowed characters are lowercase ASCII, digits, and \"-\")", @@ -163,6 +145,18 @@ async fn test_basic_failures(cptestctx: &ControlPlaneTestContext) { ); } +async fn error_response( + client: &ClientTestContext, + status_code: http::StatusCode, + method: http::Method, + url: &str, +) -> dropshot::HttpErrorResponseBody { + NexusRequest::expect_failure(client, status_code, method, url) + .authn_as(AuthnMode::PrivilegedUser) + .execute_and_parse_unwrap() + .await +} + #[nexus_test] async fn test_projects_basic(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 8f6f7745113..6495f21d151 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -42,13 +42,12 @@ use omicron_nexus::authn::external::spoof; // TODO-coverage: // * It would be good to add a built-in test user that can read everything in // the world and use that to exercise 404 vs. 401/403 behavior. -// * It'd be nice to verify that all the endpoints listed here are within the -// OpenAPI spec. -// * It'd be nice to produce a list of endpoints from the OpenAPI spec that are -// not checked here. We could put this into an expectorate file and make sure -// that we don't add new unchecked endpoints. // * When we finish authz, maybe the hardcoded information here can come instead // from the OpenAPI spec? +// * For each endpoint that hits a real resource, we should hit the same +// endpoint with a non-existent resource to ensure that we get the same result +// (so that we don't leak information about existence based on, say, 401 vs. +// 403). #[nexus_test] async fn test_unauthorized(cptestctx: &ControlPlaneTestContext) { DiskTest::new(cptestctx).await; From 295182be3fe7989457264568c2d1a3e15fb2bc08 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 15:16:17 -0700 Subject: [PATCH 19/59] cleanup --- nexus/src/db/lookup.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index cfcee4a3e7a..5d7969dfe69 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -30,22 +30,19 @@ pub trait Fetch { } mod private { + use super::LookupPath; use futures::future::BoxFuture; use omicron_common::api::external::LookupResult; - use crate::authz::AuthorizedResource; - - use super::LookupPath; - - // TODO-dap XXX we could probably get rid of this trait and its impls because - // we're now calling `lookup_root()` from a macro that can essentially inline - // the impl of GetLookupRoot for Key. + // TODO-dap XXX-dap we could probably get rid of this trait and its impls + // because we're now calling `lookup_root()` from a macro that can + // essentially inline the impl of GetLookupRoot for Key. pub trait GetLookupRoot { fn lookup_root(&self) -> &LookupPath<'_>; } pub trait LookupNoauthz { - type LookupType: AuthorizedResource + Clone + std::fmt::Debug + Send; + type LookupType; fn lookup(&self) -> BoxFuture<'_, LookupResult>; } } @@ -64,10 +61,9 @@ pub trait LookupFor { impl LookupFor for T where - T: LookupNoauthz + GetLookupRoot + Send + Sync, // XXX-dap try removing Sync? - // XXX-dap - // ::LookupType: - // AuthorizedResource + Clone + std::fmt::Debug + Send, + T: LookupNoauthz + GetLookupRoot + Send + Sync, + ::LookupType: + AuthorizedResource + Clone + std::fmt::Debug + Send, { type LookupType = ::LookupType; fn lookup_for( @@ -518,7 +514,7 @@ macro_rules! define_lookup_with_parent { let datastore = lookup.datastore; match &self.key { Key::Name(parent, name) => { - let (parent_authz, _) = parent.fetch().await?; + let parent_authz = parent.lookup().await?; [< $pc:lower _fetch_by_name >]( opctx, datastore, From bcf57a64565e23647bf0ca5166ba79967a70c4c9 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 15:40:04 -0700 Subject: [PATCH 20/59] fix up prototyping --- nexus/src/db/datastore.rs | 2 +- nexus/src/db/lookup.rs | 42 ++++++++++++++++++++++++++------ nexus/src/nexus.rs | 51 +++++++++++++++++++-------------------- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index fb39848db49..102e0941068 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -118,7 +118,7 @@ impl DataStore { // the database. Eventually, this function should only be used for doing // authentication in the first place (since we can't do an authz check in // that case). - fn pool(&self) -> &bb8::Pool> { + pub(super) fn pool(&self) -> &bb8::Pool> { self.pool.pool() } diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 5d7969dfe69..05ef7b6ca71 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -143,6 +143,10 @@ impl<'a> LookupPath<'a> { pub fn instance_id(self, id: Uuid) -> Instance<'a> { Instance { key: Key::Id(self, id) } } + + pub fn disk_id(self, id: Uuid) -> Disk<'a> { + Disk { key: Key::Id(self, id) } + } } impl<'a> GetLookupRoot for LookupPath<'a> { @@ -162,6 +166,14 @@ impl<'a> Organization<'a> { } impl<'a> Project<'a> { + pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> + where + 'a: 'c, + 'b: 'c, + { + Disk { key: Key::Name(self, name) } + } + pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> where 'a: 'c, @@ -187,12 +199,18 @@ macro_rules! define_lookup { // Do NOT make these functions public. They should instead be // wrapped by functions that perform authz checks. async fn [<$pc:lower _lookup_by_id_no_authz>]( - opctx: &OpContext, + _opctx: &OpContext, datastore: &DataStore, id: Uuid, ) -> LookupResult<(authz::$pc, model::$pc)> { use db::schema::[<$pc:lower>]::dsl; - let conn = datastore.pool_authorized(opctx).await?; + // TODO-security This could use pool_authorized() instead. + // However, it will change the response code for this case: + // unauthenticated users will get a 401 rather than a 404 + // because we'll kick them out sooner than we used to -- they + // won't even be able to make this database query. That's a + // good thing but this change can be deferred to a follow-up PR. + let conn = datastore.pool(); dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(id)) @@ -218,12 +236,13 @@ macro_rules! define_lookup { // Do NOT make these functions public. They should instead be // wrapped by functions that perform authz checks. async fn [<$pc:lower _lookup_by_name_no_authz>]( - opctx: &OpContext, + _opctx: &OpContext, datastore: &DataStore, name: &Name, ) -> LookupResult<(authz::$pc, model::$pc)> { use db::schema::[<$pc:lower>]::dsl; - let conn = datastore.pool_authorized(opctx).await?; + // TODO-security See the note about pool_authorized() above. + let conn = datastore.pool(); dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) @@ -371,7 +390,8 @@ macro_rules! define_lookup_with_parent { id: Uuid, ) -> LookupResult<(authz::$pc, model::$pc)> { use db::schema::[<$pc:lower>]::dsl; - let conn = datastore.pool_authorized(opctx).await?; + // TODO-security See the note about pool_authorized() above. + let conn = datastore.pool(); let db_row = dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(id)) @@ -402,13 +422,14 @@ macro_rules! define_lookup_with_parent { // Do NOT make these functions public. They should instead be // wrapped by functions that perform authz checks. async fn [<$pc:lower _lookup_by_name_no_authz>]( - opctx: &OpContext, + _opctx: &OpContext, datastore: &DataStore, authz_parent: &authz::$parent_pc, name: &Name, ) -> LookupResult<(authz::$pc, model::$pc)> { use db::schema::[<$pc:lower>]::dsl; - let conn = datastore.pool_authorized(opctx).await?; + // TODO-security See the note about pool_authorized() above. + let conn = datastore.pool(); dsl::[<$pc:lower>] .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) @@ -559,6 +580,13 @@ define_lookup_with_parent!( ) } ); +define_lookup_with_parent!( + Disk, + Project, + |authz_project: &authz::Project, disk: &model::Disk, lookup: LookupType| { + authz_project.child_generic(ResourceType::Disk, disk.id(), lookup) + } +); #[cfg(test)] mod test { diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index c42a1a54aab..75afc39036e 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -1298,20 +1298,20 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - // TODO: This shouldn't be looking up multiple database entries by name, - // it should resolve names to IDs first. - let authz_project = self - .db_datastore - .project_lookup_by_path(organization_name, project_name) - .await?; - let (authz_disk, db_disk) = self - .db_datastore - .disk_fetch(opctx, &authz_project, disk_name) - .await?; - let (authz_instance, db_instance) = self - .db_datastore - .instance_fetch(opctx, &authz_project, instance_name) + let (authz_disk, db_disk) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .disk_name(disk_name) + .fetch() .await?; + // TODO-dap XXX-dap can we have fetch() return this instead? + let authz_project = authz_disk.project(); + let (authz_instance, db_instance) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(authz_project.id()) + .instance_name(instance_name) + .fetch() + .await?; let instance_id = &authz_instance.id(); fn disk_attachment_error( @@ -1405,20 +1405,19 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - // TODO: This shouldn't be looking up multiple database entries by name, - // it should resolve names to IDs first. - let authz_project = self - .db_datastore - .project_lookup_by_path(organization_name, project_name) - .await?; - let (authz_disk, db_disk) = self - .db_datastore - .disk_fetch(opctx, &authz_project, disk_name) - .await?; - let (authz_instance, db_instance) = self - .db_datastore - .instance_fetch(opctx, &authz_project, instance_name) + let (authz_disk, db_disk) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .disk_name(disk_name) + .fetch() .await?; + let authz_project = authz_disk.project(); + let (authz_instance, db_instance) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(authz_project.id()) + .instance_name(instance_name) + .fetch() + .await?; let instance_id = &authz_instance.id(); match &db_disk.state().into() { From 43d01021365d7636c6552dc90ea49da6efad75d2 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 16:01:04 -0700 Subject: [PATCH 21/59] replace one XXX with a comment --- nexus/src/db/lookup.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 05ef7b6ca71..7fdabc1ba66 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -29,6 +29,9 @@ pub trait Fetch { fn fetch(&self) -> BoxFuture<'_, LookupResult>; } +// This private module exist solely to implement the "Sealed trait" pattern. +// This isn't about future-proofing ourselves. Rather, we don't want to expose +// an interface that accesses database objects without an authz check. mod private { use super::LookupPath; use futures::future::BoxFuture; @@ -47,7 +50,6 @@ mod private { } } -// XXX-dap workaround compiler error with private mod use private::GetLookupRoot; use private::LookupNoauthz; From 0a494aecb1e5702f7ee31f3011fdea8aa7b003a2 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 21 Mar 2022 19:13:25 -0700 Subject: [PATCH 22/59] in-progress update to return entire path -- cannot work with macro_rules! --- nexus/src/db/lookup.rs | 123 +++++++++++++++++++++++++++++++++-------- nexus/src/nexus.rs | 35 ++++++------ 2 files changed, 118 insertions(+), 40 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 7fdabc1ba66..cb7e2f8113c 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -368,10 +368,11 @@ macro_rules! define_lookup { macro_rules! define_lookup_with_parent { ( - $pc:ident, // Pascal-case version of resource name - $parent_pc:ident, // Pascal-case version of parent resource name - $mkauthz:expr // Closure to generate resource's authz object - // from parent's + $pc:ident, // Pascal-case version of resource name + $parent_pc:ident, // Pascal-case version of parent resource name + ($($ancestor:ident),*), // List of ancestors above parent + $mkauthz:expr // Closure to generate resource's authz object + // from parent's ) => { paste::paste! { pub struct $pc<'a> { @@ -390,7 +391,12 @@ macro_rules! define_lookup_with_parent { opctx: &OpContext, datastore: &DataStore, id: Uuid, - ) -> LookupResult<(authz::$pc, model::$pc)> { + ) -> LookupResult<( + $(authz::[<$ancestor>],)* + authz::$parent_pc, + authz::$pc, + model::$pc + )> { use db::schema::[<$pc:lower>]::dsl; // TODO-security See the note about pool_authorized() above. let conn = datastore.pool(); @@ -409,7 +415,7 @@ macro_rules! define_lookup_with_parent { ) ) })?; - let (authz_parent, _) = + let ($([],)* authz_parent, _) = [< $parent_pc:lower _lookup_by_id_no_authz >]( opctx, datastore, @@ -418,7 +424,12 @@ macro_rules! define_lookup_with_parent { let authz_child = ($mkauthz)( &authz_parent, &db_row, LookupType::ById(id) ); - Ok((authz_child, db_row)) + Ok(( + $([],)* + authz_parent, + authz_child, + db_row + )) } // Do NOT make these functions public. They should instead be @@ -428,7 +439,12 @@ macro_rules! define_lookup_with_parent { datastore: &DataStore, authz_parent: &authz::$parent_pc, name: &Name, - ) -> LookupResult<(authz::$pc, model::$pc)> { + ) -> LookupResult<( + $(authz::[<$ancestor>],)* + authz::$parent_pc, + authz::$pc, + model::$pc + )> { use db::schema::[<$pc:lower>]::dsl; // TODO-security See the note about pool_authorized() above. let conn = datastore.pool(); @@ -449,6 +465,7 @@ macro_rules! define_lookup_with_parent { ) }) .map(|dbmodel| {( + // XXX-dap XXX-dap ($mkauthz)( authz_parent, &dbmodel, @@ -462,15 +479,30 @@ macro_rules! define_lookup_with_parent { opctx: &OpContext, datastore: &DataStore, id: Uuid, - ) -> LookupResult<(authz::$pc, model::$pc)> { - let (authz_child, db_child) = + ) -> LookupResult<( + $(authz::[<$ancestor>],)* + authz::$parent_pc, + authz::$pc, + model::$pc + )> { + let ( + $([],)* + authz_parent, + authz_child, + db_child + ) = [<$pc:lower _lookup_by_id_no_authz>]( opctx, datastore, id, ).await?; opctx.authorize(authz::Action::Read, &authz_child).await?; - Ok((authz_child, db_child)) + Ok(( + $([],)* + authz_parent, + authz_child, + db_child + )) } async fn [<$pc:lower _fetch_by_name>]( @@ -478,8 +510,18 @@ macro_rules! define_lookup_with_parent { datastore: &DataStore, authz_parent: &authz::$parent_pc, name: &Name, - ) -> LookupResult<(authz::$pc, model::$pc)> { - let (authz_child, db_child) = + ) -> LookupResult<( + $(authz::[<$ancestor>],)* + authz::$parent_pc, + authz::$pc, + model::$pc + )> { + let ( + $([],)* + authz_parent, + authz_child, + db_child + ) = [<$pc:lower _lookup_by_name_no_authz>]( opctx, datastore, @@ -487,11 +529,20 @@ macro_rules! define_lookup_with_parent { name ).await?; opctx.authorize(authz::Action::Read, &authz_child).await?; - Ok((authz_child, db_child)) + Ok(( + $([],)* + authz_parent, + authz_child, + db_child + )) } impl LookupNoauthz for $pc<'_> { - type LookupType = authz::$pc; + type LookupType = ( + $(authz::[<$ancestor>],)* + authz::[<$parent_pc>], + authz::$pc, + ); fn lookup( &self, @@ -503,23 +554,37 @@ macro_rules! define_lookup_with_parent { match &self.key { Key::Name(parent, name) => { let parent_authz = parent.lookup().await?; - let (rv, _) = + let ( + $([],)* + authz_parent, + authz_child, _) = [< $pc:lower _lookup_by_name_no_authz >]( opctx, datastore, &parent_authz, *name ).await?; - Ok(rv) + Ok(( + $([],)* + authz_parent, + authz_child + )) } Key::Id(_, id) => { - let (rv, _) = + let ( + $([],)* + authz_parent, + authz_child, _) = [< $pc:lower _lookup_by_id_no_authz >]( opctx, datastore, *id ).await?; - Ok(rv) + Ok(( + $([],)* + authz_parent, + authz_child + )) } } } @@ -528,7 +593,12 @@ macro_rules! define_lookup_with_parent { } impl Fetch for $pc<'_> { - type FetchType = (authz::$pc, model::$pc); + type FetchType = ( + $(authz::[<$ancestor>],)* + authz::[<$parent_pc>], + authz::$pc, + model::$pc + ); fn fetch(&self) -> BoxFuture<'_, LookupResult> { async { @@ -537,11 +607,14 @@ macro_rules! define_lookup_with_parent { let datastore = lookup.datastore; match &self.key { Key::Name(parent, name) => { - let parent_authz = parent.lookup().await?; + let ( + $([<_authz_ $ancestor:lower>],)* + authz_parent, + ) = parent.lookup().await?; [< $pc:lower _fetch_by_name >]( opctx, datastore, - &parent_authz, + &authz_parent, *name ).await } @@ -562,16 +635,20 @@ macro_rules! define_lookup_with_parent { } define_lookup!(Organization); + define_lookup_with_parent!( Project, Organization, + (), |authz_org: &authz::Organization, project: &model::Project, lookup: LookupType| { authz_org.project(project.id(), lookup) } ); + define_lookup_with_parent!( Instance, Project, + (Organization), |authz_project: &authz::Project, instance: &model::Instance, lookup: LookupType| { @@ -582,9 +659,11 @@ define_lookup_with_parent!( ) } ); + define_lookup_with_parent!( Disk, Project, + (Organization), |authz_project: &authz::Project, disk: &model::Disk, lookup: LookupType| { authz_project.child_generic(ResourceType::Disk, disk.id(), lookup) } diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 75afc39036e..62dff92114b 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -591,7 +591,7 @@ impl Nexus { .project_name(project_name) .fetch() .await? - .1) + .2) } pub async fn projects_list_by_name( @@ -1298,15 +1298,14 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - let (authz_disk, db_disk) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) - .fetch() - .await?; - // TODO-dap XXX-dap can we have fetch() return this instead? - let authz_project = authz_disk.project(); - let (authz_instance, db_instance) = + let (_, authz_project, authz_disk, db_disk) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .disk_name(disk_name) + .fetch() + .await?; + let (_, _, authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) .project_id(authz_project.id()) .instance_name(instance_name) @@ -1405,14 +1404,14 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - let (authz_disk, db_disk) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) - .fetch() - .await?; - let authz_project = authz_disk.project(); - let (authz_instance, db_instance) = + let (_, authz_project, authz_disk, db_disk) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .disk_name(disk_name) + .fetch() + .await?; + let (_, _, authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) .project_id(authz_project.id()) .instance_name(instance_name) From 013e9814a469941e4b8db9a964354f5e70f557be Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 22 Mar 2022 11:29:23 -0700 Subject: [PATCH 23/59] it works with macro_rules! --- nexus/src/authz/api_resources.rs | 4 + nexus/src/db/lookup.rs | 139 +++++++++++++++++++------------ nexus/src/nexus.rs | 6 +- 3 files changed, 92 insertions(+), 57 deletions(-) diff --git a/nexus/src/authz/api_resources.rs b/nexus/src/authz/api_resources.rs index 36ff91aabb5..e6c9f8438b5 100644 --- a/nexus/src/authz/api_resources.rs +++ b/nexus/src/authz/api_resources.rs @@ -369,6 +369,10 @@ impl Project { lookup_type, } } + + pub fn organization(&self) -> &Organization { + &self.parent + } } impl Eq for Project {} diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index cb7e2f8113c..8fdfd4f989e 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -61,28 +61,6 @@ pub trait LookupFor { ) -> BoxFuture<'_, LookupResult>; } -impl LookupFor for T -where - T: LookupNoauthz + GetLookupRoot + Send + Sync, - ::LookupType: - AuthorizedResource + Clone + std::fmt::Debug + Send, -{ - type LookupType = ::LookupType; - fn lookup_for( - &self, - action: authz::Action, - ) -> BoxFuture<'_, LookupResult> { - async move { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let rv = self.lookup().await?; - opctx.authorize(action, &rv).await?; - Ok(rv) - } - .boxed() - } -} - enum Key<'a, P> { Name(P, &'a Name), Id(LookupPath<'a>, Uuid), @@ -301,7 +279,7 @@ macro_rules! define_lookup { } impl LookupNoauthz for $pc<'_> { - type LookupType = authz::$pc; + type LookupType = (authz::$pc,); fn lookup( &self, @@ -318,7 +296,7 @@ macro_rules! define_lookup { datastore, name ).await?; - Ok(rv) + Ok((rv,)) } Key::Id(_, id) => { let (rv, _) = @@ -327,7 +305,7 @@ macro_rules! define_lookup { datastore, id ).await?; - Ok(rv) + Ok((rv,)) } } }.boxed() @@ -362,6 +340,24 @@ macro_rules! define_lookup { .boxed() } } + + impl LookupFor for $pc<'_> { + type LookupType = ::LookupType; + + fn lookup_for( + &self, + action: authz::Action, + ) -> BoxFuture<'_, LookupResult> { + async move { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let (authz_child,) = self.lookup().await?; + opctx.authorize(action, &authz_child).await?; + Ok((authz_child,)) + } + .boxed() + } + } } }; } @@ -371,6 +367,7 @@ macro_rules! define_lookup_with_parent { $pc:ident, // Pascal-case version of resource name $parent_pc:ident, // Pascal-case version of parent resource name ($($ancestor:ident),*), // List of ancestors above parent + // XXX-dap update comment $mkauthz:expr // Closure to generate resource's authz object // from parent's ) => { @@ -415,21 +412,16 @@ macro_rules! define_lookup_with_parent { ) ) })?; - let ($([],)* authz_parent, _) = + let ($([<_authz_ $ancestor:lower>],)* authz_parent, _) = [< $parent_pc:lower _lookup_by_id_no_authz >]( opctx, datastore, db_row.[<$parent_pc:lower _id>] ).await?; - let authz_child = ($mkauthz)( - &authz_parent, &db_row, LookupType::ById(id) + let authz_list = ($mkauthz)( + authz_parent, db_row, LookupType::ById(id) ); - Ok(( - $([],)* - authz_parent, - authz_child, - db_row - )) + Ok(authz_list) } // Do NOT make these functions public. They should instead be @@ -464,15 +456,13 @@ macro_rules! define_lookup_with_parent { ) ) }) - .map(|dbmodel| {( - // XXX-dap XXX-dap + .map(|dbmodel| { ($mkauthz)( - authz_parent, - &dbmodel, + authz_parent.clone(), + dbmodel, LookupType::ByName(name.as_str().to_string()) - ), - dbmodel - )}) + ) + }) } async fn [<$pc:lower _fetch_by_id>]( @@ -553,15 +543,18 @@ macro_rules! define_lookup_with_parent { let datastore = lookup.datastore; match &self.key { Key::Name(parent, name) => { - let parent_authz = parent.lookup().await?; let ( $([],)* authz_parent, + ) = parent.lookup().await?; + let ( + $([<_authz_ $ancestor:lower>],)* + _authz_parent, authz_child, _) = [< $pc:lower _lookup_by_name_no_authz >]( opctx, datastore, - &parent_authz, + &authz_parent, *name ).await?; Ok(( @@ -630,6 +623,32 @@ macro_rules! define_lookup_with_parent { .boxed() } } + + impl LookupFor for $pc<'_> { + type LookupType = ::LookupType; + + fn lookup_for( + &self, + action: authz::Action, + ) -> BoxFuture<'_, LookupResult> { + async move { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let ( + $([],)* + authz_parent, + authz_child + ) = self.lookup().await?; + opctx.authorize(action, &authz_child).await?; + Ok(( + $([],)* + authz_parent, + authz_child + )) + } + .boxed() + } + } } }; } @@ -640,22 +659,29 @@ define_lookup_with_parent!( Project, Organization, (), - |authz_org: &authz::Organization, - project: &model::Project, - lookup: LookupType| { authz_org.project(project.id(), lookup) } + |authz_org: authz::Organization, + project: model::Project, + lookup: LookupType| { + (authz_org.clone(), authz_org.project(project.id(), lookup), project) + } ); define_lookup_with_parent!( Instance, Project, (Organization), - |authz_project: &authz::Project, - instance: &model::Instance, + |authz_project: authz::Project, + instance: model::Instance, lookup: LookupType| { - authz_project.child_generic( - ResourceType::Instance, - instance.id(), - lookup, + ( + authz_project.organization().clone(), + authz_project.clone(), + authz_project.child_generic( + ResourceType::Instance, + instance.id(), + lookup, + ), + instance, ) } ); @@ -664,8 +690,13 @@ define_lookup_with_parent!( Disk, Project, (Organization), - |authz_project: &authz::Project, disk: &model::Disk, lookup: LookupType| { - authz_project.child_generic(ResourceType::Disk, disk.id(), lookup) + |authz_project: authz::Project, disk: model::Disk, lookup: LookupType| { + ( + authz_project.organization().clone(), + authz_project.clone(), + authz_project.child_generic(ResourceType::Disk, disk.id(), lookup), + disk, + ) } ); diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 62dff92114b..a52956975cc 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -536,7 +536,7 @@ impl Nexus { organization_name: &Name, new_project: ¶ms::ProjectCreate, ) -> CreateResult { - let authz_org = LookupPath::new(opctx, &self.db_datastore) + let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) .organization_name(organization_name) .lookup_for(authz::Action::CreateChild) .await?; @@ -600,7 +600,7 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let authz_org = LookupPath::new(opctx, &self.db_datastore) + let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) .organization_name(organization_name) .lookup_for(authz::Action::CreateChild) .await?; @@ -615,7 +615,7 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { - let authz_org = LookupPath::new(opctx, &self.db_datastore) + let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) .organization_name(organization_name) .lookup_for(authz::Action::CreateChild) .await?; From 785eda2d851f3a9b2fde075db1c310f3fff5bf25 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 25 Mar 2022 13:08:51 -0700 Subject: [PATCH 24/59] half-finished effort at proc macro approah --- Cargo.lock | 2 ++ nexus/src/db/db-macros/Cargo.toml | 2 ++ nexus/src/db/db-macros/src/lib.rs | 16 ++++++++++ nexus/src/db/db-macros/src/lookup.rs | 44 ++++++++++++++++++++++++++++ nexus/src/db/lookup.rs | 6 ++++ 5 files changed, 70 insertions(+) create mode 100644 nexus/src/db/db-macros/src/lookup.rs diff --git a/Cargo.lock b/Cargo.lock index 465c768a444..1c17e271337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -646,6 +646,8 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", + "serde", + "serde_tokenstream", "syn", ] diff --git a/nexus/src/db/db-macros/Cargo.toml b/nexus/src/db/db-macros/Cargo.toml index 2d0cce7eaf5..62911535ec3 100644 --- a/nexus/src/db/db-macros/Cargo.toml +++ b/nexus/src/db/db-macros/Cargo.toml @@ -11,4 +11,6 @@ proc-macro = true [dependencies] proc-macro2 = { version = "1.0" } quote = { version = "1.0" } +serde = { version = "1.0", features = [ "derive" ] } +serde_tokenstream = "0.1" syn = { version = "1.0", features = [ "full", "derive", "extra-traits" ] } diff --git a/nexus/src/db/db-macros/src/lib.rs b/nexus/src/db/db-macros/src/lib.rs index 3b29099c5dc..3ecda3a73b8 100644 --- a/nexus/src/db/db-macros/src/lib.rs +++ b/nexus/src/db/db-macros/src/lib.rs @@ -18,6 +18,8 @@ use quote::{format_ident, quote}; use syn::spanned::Spanned; use syn::{Data, DataStruct, DeriveInput, Error, Fields, Ident, Lit, Meta}; +mod lookup; + /// Looks for a Meta-style attribute with a particular identifier. /// /// As an example, for an attribute like `#[foo = "bar"]`, we can find this @@ -330,6 +332,20 @@ fn build_asset_impl( } } +/// Identical to [`macro@Resource`], but generates fewer fields. +/// +/// Contains: +/// - ID +/// - Time Created +/// - Time Modified +#[proc_macro_attribute] +pub fn lookup_resource( + attr: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + lookup::lookup_resource(attr, input) +} + #[cfg(test)] mod tests { use super::*; diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs new file mode 100644 index 00000000000..0069fd6ab5d --- /dev/null +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Procedure macro for generating lookup structures and related functions +//! +//! See nexus/src/db/lookup.rs. + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::spanned::Spanned; +use syn::{ + Data, DataStruct, DeriveInput, Error, Fields, Ident, ItemStruct, Lit, Meta, +}; + +#[derive(serde::Deserialize)] +struct Config { + ancestors: Vec, +} + +pub fn lookup_resource( + attr: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let config = match serde_tokenstream::from_tokenstream::( + &TokenStream::from(attr), + ) { + Ok(c) => c, + Err(err) => return err.to_compile_error().into(), + }; + + let item = syn::parse_macro_input!(input as ItemStruct); + let name = &item.ident; + let generics = &item.generics; + let fields = &item.fields; + let parent = config.ancestors.first().unwrap_or("LookupPath"); + println!("{}", fields.to_token_stream()); + + quote! { + struct #name #generics { + // #fields + } + }.into() +} diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 8fdfd4f989e..1d7c4c6b73a 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -15,12 +15,18 @@ use crate::{ db::model::Name, }; use async_bb8_diesel::AsyncRunQueryDsl; +use db_macros::lookup_resource; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; use futures::future::BoxFuture; use futures::FutureExt; use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; use uuid::Uuid; +#[lookup_resource { + ancestors = [ "Project", "Organization" ] +}] +struct Foo {} + // TODO-dap XXX Neither "fetcH" nor "lookup" needs to be a trait now that the // macro is defining the impls and the structs From a48722a018db35774424e03004b313af1c44ae09 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 25 Mar 2022 16:32:03 -0700 Subject: [PATCH 25/59] more progress --- Cargo.lock | 1 + nexus/src/db/db-macros/Cargo.toml | 1 + nexus/src/db/db-macros/src/lookup.rs | 208 +++- nexus/src/db/lookup.rs | 1434 +++++++++++++------------- nexus/src/nexus.rs | 98 +- rust-toolchain.toml | 3 +- 6 files changed, 961 insertions(+), 784 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c17e271337..11efe522cab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -644,6 +644,7 @@ dependencies = [ name = "db-macros" version = "0.1.0" dependencies = [ + "heck 0.4.0", "proc-macro2", "quote", "serde", diff --git a/nexus/src/db/db-macros/Cargo.toml b/nexus/src/db/db-macros/Cargo.toml index 62911535ec3..0031ff2f465 100644 --- a/nexus/src/db/db-macros/Cargo.toml +++ b/nexus/src/db/db-macros/Cargo.toml @@ -9,6 +9,7 @@ license = "MPL-2.0" proc-macro = true [dependencies] +heck = "0.4" proc-macro2 = { version = "1.0" } quote = { version = "1.0" } serde = { version = "1.0", features = [ "derive" ] } diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 0069fd6ab5d..b68a4062624 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -7,11 +7,8 @@ //! See nexus/src/db/lookup.rs. use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; -use syn::spanned::Spanned; -use syn::{ - Data, DataStruct, DeriveInput, Error, Fields, Ident, ItemStruct, Lit, Meta, -}; +use quote::{format_ident, quote}; +use syn::ItemStruct; #[derive(serde::Deserialize)] struct Config { @@ -22,23 +19,194 @@ pub fn lookup_resource( attr: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let config = match serde_tokenstream::from_tokenstream::( - &TokenStream::from(attr), - ) { - Ok(c) => c, - Err(err) => return err.to_compile_error().into(), + match do_lookup_resource(attr.into(), input.into()) { + Ok(output) => output.into(), + Err(error) => error.to_compile_error().into(), + } +} + +fn do_lookup_resource( + attr: TokenStream, + input: TokenStream, +) -> Result { + let config = serde_tokenstream::from_tokenstream::(&attr)?; + + // TODO + // - validate no generics and no fields? + let raw_input = input.clone(); + let item: ItemStruct = syn::parse2(input)?; + let resource_name = &item.ident; + let resource_as_snake = format_ident!( + "{}", + heck::AsSnakeCase(resource_name.to_string()).to_string() + ); + let authz_resource = quote! { authz::#resource_name }; + let model_resource = quote! { model::#resource_name }; + + // It's important that even if there's only one item in this list, it should + // still have a trailing comma. + let authz_ancestors_types_vec: Vec<_> = config + .ancestors + .iter() + .map(|a| { + let name = format_ident!("{}", a); + quote! { authz::#name, } + }) + .collect(); + let authz_ancestors_values_vec: Vec<_> = config + .ancestors + .iter() + .map(|a| { + let v = format_ident!( + "authz_{}", + heck::AsSnakeCase(a.to_string()).to_string() + ); + quote! { #v , } + }) + .collect(); + + let mut authz_path_types_vec = authz_ancestors_types_vec.clone(); + authz_path_types_vec.push(authz_resource.clone()); + + let mut authz_path_values_vec = authz_ancestors_values_vec.clone(); + authz_path_values_vec.push(quote! { authz_self, }); + + let authz_ancestors_types = quote! { + #(#authz_ancestors_types_vec)* + }; + let authz_ancestors_values = quote! { + #(#authz_ancestors_values_vec)* + }; + let authz_path_types = quote! { + #(#authz_path_types_vec)* + }; + let authz_path_values = quote! { + #(#authz_path_values_vec)* }; + let root_name = format_ident!("Root"); - let item = syn::parse_macro_input!(input as ItemStruct); - let name = &item.ident; - let generics = &item.generics; - let fields = &item.fields; - let parent = config.ancestors.first().unwrap_or("LookupPath"); - println!("{}", fields.to_token_stream()); + let ( + parent_resource_name, + parent_lookup_arg, + parent_filter, + authz_ancestors_values_assign, + parent_authz, + ) = match config.ancestors.first() { + Some(parent_resource_name) => { + let parent_snake_str = + heck::AsSnakeCase(parent_resource_name).to_string(); + let parent_id = format_ident!("{}_id", parent_snake_str,); + let parent_resource_name = + format_ident!("{}", parent_resource_name); + let parent_lookup_arg = + quote! { authz_parent: &authz::#parent_resource_name }; + let parent_filter = + quote! { .filter(dsl::#parent_id.eq(authz_parent.id())) }; + let authz_ancestors_values_assign = quote! { + let (#authz_ancestors_values _) = + #parent_resource_name::lookup_by_id_no_authz( + _opctx, datastore, db_row.#parent_id + ).await?; + }; + let parent_authz = &authz_ancestors_values_vec[0]; + ( + parent_resource_name, + parent_lookup_arg, + parent_filter, + authz_ancestors_values_assign, + quote! { #parent_authz }, + ) + } + None => ( + format_ident!("Root"), + quote! {}, + quote! {}, + quote! {}, + quote! { authz::FLEET }, + ), + }; + + Ok(quote! { + pub struct #resource_name<'a> { + key: Key<'a, #parent_resource_name> + } + + impl #resource_name<'a> { + // Do NOT make this function public. It should instead be wrapped + // by functions that perform authz checks. + async fn lookup_by_id_no_authz( + _opctx: &OpContext, + datastore: &DataStore, + id: Uuid, + ) -> LookupResult<(#authz_path_types, #model_resource)> { + use db::schema::#resource_as_snake::dsl; - quote! { - struct #name #generics { - // #fields + // TODO-security This could use pool_authorized() instead. + // However, it will change the response code for this case: + // unauthenticated users will get a 401 rather than a 404 + // because we'll kick them out sooner than we used to -- they + // won't even be able to make this database query. That's a + // good thing but this change can be deferred to a follow-up PR. + let conn = datastore.pool(); + let db_row = dsl::#resource_as_snake + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(id)) + .select(model::#resource_name::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::#resource_name, + LookupType::ById(id) + ) + ) + })?; + #authz_ancestors_values_assign + let authz_self = self.make_authz( + &#parent_authz + &db_row, + LookupType::ById(id) + ); + Ok((#authz_path_values db_row)) + } } - }.into() + }) +} + +mod test { + use super::do_lookup_resource; + use quote::quote; + + #[test] + fn test_lookup_resource() { + // XXX-dap this should actually do something + eprintln!( + "{}", + do_lookup_resource( + quote! { ancestors = [] }, + quote! { struct Organization; }, + ) + .unwrap(), + ); + + eprintln!( + "{}", + do_lookup_resource( + quote! { ancestors = [ "Organization" ] }, + quote! { struct Project; }, + ) + .unwrap(), + ); + + eprintln!( + "{}", + do_lookup_resource( + quote! { ancestors = [ "Organization", "Project" ] }, + quote! { struct Instance; }, + ) + .unwrap(), + ); + } } diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 1d7c4c6b73a..1461749c1eb 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! Facilities for looking up API resources from the database +//! Look up API resources from the database use super::datastore::DataStore; use super::identity::Resource; @@ -22,77 +22,87 @@ use futures::FutureExt; use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; use uuid::Uuid; -#[lookup_resource { - ancestors = [ "Project", "Organization" ] -}] -struct Foo {} - -// TODO-dap XXX Neither "fetcH" nor "lookup" needs to be a trait now that the -// macro is defining the impls and the structs - -pub trait Fetch { - type FetchType; - fn fetch(&self) -> BoxFuture<'_, LookupResult>; -} - -// This private module exist solely to implement the "Sealed trait" pattern. -// This isn't about future-proofing ourselves. Rather, we don't want to expose -// an interface that accesses database objects without an authz check. -mod private { - use super::LookupPath; - use futures::future::BoxFuture; - use omicron_common::api::external::LookupResult; - - // TODO-dap XXX-dap we could probably get rid of this trait and its impls - // because we're now calling `lookup_root()` from a macro that can - // essentially inline the impl of GetLookupRoot for Key. - pub trait GetLookupRoot { - fn lookup_root(&self) -> &LookupPath<'_>; - } - - pub trait LookupNoauthz { - type LookupType; - fn lookup(&self) -> BoxFuture<'_, LookupResult>; - } -} - -use private::GetLookupRoot; -use private::LookupNoauthz; - -pub trait LookupFor { - type LookupType; - fn lookup_for( - &self, - action: authz::Action, - ) -> BoxFuture<'_, LookupResult>; -} - enum Key<'a, P> { Name(P, &'a Name), Id(LookupPath<'a>, Uuid), } -impl<'a, T> GetLookupRoot for Key<'a, T> -where - T: GetLookupRoot, -{ - fn lookup_root(&self) -> &LookupPath<'_> { - match self { - Key::Name(parent, _) => parent.lookup_root(), - Key::Id(lookup, _) => lookup, - } - } -} +#[lookup_resource { + ancestors = [] +}] +struct Organization; -impl<'a, P> Key<'a, P> { - fn lookup_type(&self) -> LookupType { - match self { - Key::Name(_, name) => LookupType::ByName(name.as_str().to_string()), - Key::Id(_, id) => LookupType::ById(*id), - } - } -} +// #[lookup_resource { +// ancestors = [ "Project", "Organization" ] +// }] +// struct Instance; +// TODO-dap XXX Neither "fetcH" nor "lookup" needs to be a trait now that the +// macro is defining the impls and the structs + +//pub trait Fetch { +// type FetchType; +// fn fetch(&self) -> BoxFuture<'_, LookupResult>; +//} +// +//// This private module exist solely to implement the "Sealed trait" pattern. +//// This isn't about future-proofing ourselves. Rather, we don't want to expose +//// an interface that accesses database objects without an authz check. +//mod private { +// use super::LookupPath; +// use futures::future::BoxFuture; +// use omicron_common::api::external::LookupResult; +// +// // TODO-dap XXX-dap we could probably get rid of this trait and its impls +// // because we're now calling `lookup_root()` from a macro that can +// // essentially inline the impl of GetLookupRoot for Key. +// pub trait GetLookupRoot { +// fn lookup_root(&self) -> &LookupPath<'_>; +// } +// +// pub trait LookupNoauthz { +// type LookupType; +// fn lookup(&self) -> BoxFuture<'_, LookupResult>; +// } +//} +// +//use private::GetLookupRoot; +//use private::LookupNoauthz; +// +//pub trait LookupFor { +// type LookupType; +// fn lookup_for( +// &self, +// action: authz::Action, +// ) -> BoxFuture<'_, LookupResult>; +//} +// +//enum Key<'a, P> { +// Name(P, &'a Name), +// Id(LookupPath<'a>, Uuid), +//} +// +//impl<'a, T> GetLookupRoot for Key<'a, T> +//where +// T: GetLookupRoot, +//{ +// fn lookup_root(&self) -> &LookupPath<'_> { +// match self { +// Key::Name(parent, _) => parent.lookup_root(), +// Key::Id(lookup, _) => lookup, +// } +// } +//} +// +//impl<'a, P> Key<'a, P> { +// fn lookup_type(&self) -> LookupType { +// match self { +// Key::Name(_, name) => LookupType::ByName(name.as_str().to_string()), +// Key::Id(_, id) => LookupType::ById(*id), +// } +// } +//} +// pub struct LookupPath<'a> { opctx: &'a OpContext, datastore: &'a DataStore, @@ -110,651 +120,651 @@ impl<'a> LookupPath<'a> { LookupPath { opctx, datastore } } - pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> - where - 'a: 'c, - 'b: 'c, - { - Organization { key: Key::Name(self, name) } - } - - pub fn organization_id(self, id: Uuid) -> Organization<'a> { - Organization { key: Key::Id(self, id) } - } - - pub fn project_id(self, id: Uuid) -> Project<'a> { - Project { key: Key::Id(self, id) } - } - - pub fn instance_id(self, id: Uuid) -> Instance<'a> { - Instance { key: Key::Id(self, id) } - } - - pub fn disk_id(self, id: Uuid) -> Disk<'a> { - Disk { key: Key::Id(self, id) } - } -} - -impl<'a> GetLookupRoot for LookupPath<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self - } -} - -impl<'a> Organization<'a> { - pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> - where - 'a: 'c, - 'b: 'c, - { - Project { key: Key::Name(self, name) } - } -} - -impl<'a> Project<'a> { - pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> - where - 'a: 'c, - 'b: 'c, - { - Disk { key: Key::Name(self, name) } - } - - pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> - where - 'a: 'c, - 'b: 'c, - { - Instance { key: Key::Name(self, name) } - } -} - -macro_rules! define_lookup { - ($pc:ident) => { - paste::paste! { - pub struct $pc<'a> { - key: Key<'a, LookupPath<'a>>, - } - - impl<'a> GetLookupRoot for $pc<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self.key.lookup_root() - } - } - - // Do NOT make these functions public. They should instead be - // wrapped by functions that perform authz checks. - async fn [<$pc:lower _lookup_by_id_no_authz>]( - _opctx: &OpContext, - datastore: &DataStore, - id: Uuid, - ) -> LookupResult<(authz::$pc, model::$pc)> { - use db::schema::[<$pc:lower>]::dsl; - // TODO-security This could use pool_authorized() instead. - // However, it will change the response code for this case: - // unauthenticated users will get a 401 rather than a 404 - // because we'll kick them out sooner than we used to -- they - // won't even be able to make this database query. That's a - // good thing but this change can be deferred to a follow-up PR. - let conn = datastore.pool(); - dsl::[<$pc:lower>] - .filter(dsl::time_deleted.is_null()) - .filter(dsl::id.eq(id)) - .select(model::$pc::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::$pc, - LookupType::ById(id) - ) - ) - }) - .map(|o| {( - authz::FLEET.[<$pc:lower>](o.id(), LookupType::ById(id)), - o - )} - ) - } - - // Do NOT make these functions public. They should instead be - // wrapped by functions that perform authz checks. - async fn [<$pc:lower _lookup_by_name_no_authz>]( - _opctx: &OpContext, - datastore: &DataStore, - name: &Name, - ) -> LookupResult<(authz::$pc, model::$pc)> { - use db::schema::[<$pc:lower>]::dsl; - // TODO-security See the note about pool_authorized() above. - let conn = datastore.pool(); - dsl::[<$pc:lower>] - .filter(dsl::time_deleted.is_null()) - .filter(dsl::name.eq(name.clone())) - .select(model::$pc::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::$pc, - LookupType::ByName(name.as_str().to_string()) - ) - ) - }) - .map(|o| {( - authz::FLEET.[<$pc:lower>]( - o.id(), - LookupType::ByName(name.as_str().to_string()) - ), - o - )} - ) - } - - async fn [<$pc:lower _fetch_by_id>]( - opctx: &OpContext, - datastore: &DataStore, - id: Uuid, - ) -> LookupResult<(authz::$pc, model::$pc)> { - let (authz_child, db_child) = - [<$pc:lower _lookup_by_id_no_authz>]( - opctx, - datastore, - id, - ).await?; - opctx.authorize(authz::Action::Read, &authz_child).await?; - Ok((authz_child, db_child)) - } - - async fn [<$pc:lower _fetch_by_name>]( - opctx: &OpContext, - datastore: &DataStore, - name: &Name, - ) -> LookupResult<(authz::$pc, model::$pc)> { - let (authz_child, db_child) = - [<$pc:lower _lookup_by_name_no_authz>]( - opctx, - datastore, - name - ).await?; - opctx.authorize(authz::Action::Read, &authz_child).await?; - Ok((authz_child, db_child)) - } - - impl LookupNoauthz for $pc<'_> { - type LookupType = (authz::$pc,); - - fn lookup( - &self, - ) -> BoxFuture<'_, LookupResult> { - async { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = lookup.datastore; - match self.key { - Key::Name(_, name) => { - let (rv, _) = - [<$pc:lower _lookup_by_name_no_authz>]( - opctx, - datastore, - name - ).await?; - Ok((rv,)) - } - Key::Id(_, id) => { - let (rv, _) = - [<$pc:lower _lookup_by_id_no_authz>]( - opctx, - datastore, - id - ).await?; - Ok((rv,)) - } - } - }.boxed() - } - } - - impl Fetch for $pc<'_> { - type FetchType = (authz::$pc, model::$pc); - - fn fetch(&self) -> BoxFuture<'_, LookupResult> { - async { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = lookup.datastore; - match self.key { - Key::Name(_, name) => { - [<$pc:lower _fetch_by_name>]( - opctx, - datastore, - name - ).await - } - Key::Id(_, id) => { - [<$pc:lower _fetch_by_id>]( - opctx, - datastore, - id - ).await - } - } - } - .boxed() - } - } - - impl LookupFor for $pc<'_> { - type LookupType = ::LookupType; - - fn lookup_for( - &self, - action: authz::Action, - ) -> BoxFuture<'_, LookupResult> { - async move { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let (authz_child,) = self.lookup().await?; - opctx.authorize(action, &authz_child).await?; - Ok((authz_child,)) - } - .boxed() - } - } - } - }; -} - -macro_rules! define_lookup_with_parent { - ( - $pc:ident, // Pascal-case version of resource name - $parent_pc:ident, // Pascal-case version of parent resource name - ($($ancestor:ident),*), // List of ancestors above parent - // XXX-dap update comment - $mkauthz:expr // Closure to generate resource's authz object - // from parent's - ) => { - paste::paste! { - pub struct $pc<'a> { - key: Key<'a, $parent_pc<'a>>, - } - - impl<'a> GetLookupRoot for $pc<'a> { - fn lookup_root(&self) -> &LookupPath<'_> { - self.key.lookup_root() - } - } - - // Do NOT make these functions public. They should instead be - // wrapped by functions that perform authz checks. - async fn [<$pc:lower _lookup_by_id_no_authz>]( - opctx: &OpContext, - datastore: &DataStore, - id: Uuid, - ) -> LookupResult<( - $(authz::[<$ancestor>],)* - authz::$parent_pc, - authz::$pc, - model::$pc - )> { - use db::schema::[<$pc:lower>]::dsl; - // TODO-security See the note about pool_authorized() above. - let conn = datastore.pool(); - let db_row = dsl::[<$pc:lower>] - .filter(dsl::time_deleted.is_null()) - .filter(dsl::id.eq(id)) - .select(model::$pc::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::$pc, - LookupType::ById(id) - ) - ) - })?; - let ($([<_authz_ $ancestor:lower>],)* authz_parent, _) = - [< $parent_pc:lower _lookup_by_id_no_authz >]( - opctx, - datastore, - db_row.[<$parent_pc:lower _id>] - ).await?; - let authz_list = ($mkauthz)( - authz_parent, db_row, LookupType::ById(id) - ); - Ok(authz_list) - } - - // Do NOT make these functions public. They should instead be - // wrapped by functions that perform authz checks. - async fn [<$pc:lower _lookup_by_name_no_authz>]( - _opctx: &OpContext, - datastore: &DataStore, - authz_parent: &authz::$parent_pc, - name: &Name, - ) -> LookupResult<( - $(authz::[<$ancestor>],)* - authz::$parent_pc, - authz::$pc, - model::$pc - )> { - use db::schema::[<$pc:lower>]::dsl; - // TODO-security See the note about pool_authorized() above. - let conn = datastore.pool(); - dsl::[<$pc:lower>] - .filter(dsl::time_deleted.is_null()) - .filter(dsl::name.eq(name.clone())) - .filter(dsl::[<$parent_pc:lower _id>].eq(authz_parent.id())) - .select(model::$pc::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::$pc, - LookupType::ByName(name.as_str().to_string()) - ) - ) - }) - .map(|dbmodel| { - ($mkauthz)( - authz_parent.clone(), - dbmodel, - LookupType::ByName(name.as_str().to_string()) - ) - }) - } - - async fn [<$pc:lower _fetch_by_id>]( - opctx: &OpContext, - datastore: &DataStore, - id: Uuid, - ) -> LookupResult<( - $(authz::[<$ancestor>],)* - authz::$parent_pc, - authz::$pc, - model::$pc - )> { - let ( - $([],)* - authz_parent, - authz_child, - db_child - ) = - [<$pc:lower _lookup_by_id_no_authz>]( - opctx, - datastore, - id, - ).await?; - opctx.authorize(authz::Action::Read, &authz_child).await?; - Ok(( - $([],)* - authz_parent, - authz_child, - db_child - )) - } - - async fn [<$pc:lower _fetch_by_name>]( - opctx: &OpContext, - datastore: &DataStore, - authz_parent: &authz::$parent_pc, - name: &Name, - ) -> LookupResult<( - $(authz::[<$ancestor>],)* - authz::$parent_pc, - authz::$pc, - model::$pc - )> { - let ( - $([],)* - authz_parent, - authz_child, - db_child - ) = - [<$pc:lower _lookup_by_name_no_authz>]( - opctx, - datastore, - authz_parent, - name - ).await?; - opctx.authorize(authz::Action::Read, &authz_child).await?; - Ok(( - $([],)* - authz_parent, - authz_child, - db_child - )) - } - - impl LookupNoauthz for $pc<'_> { - type LookupType = ( - $(authz::[<$ancestor>],)* - authz::[<$parent_pc>], - authz::$pc, - ); - - fn lookup( - &self, - ) -> BoxFuture<'_, LookupResult> { - async { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = lookup.datastore; - match &self.key { - Key::Name(parent, name) => { - let ( - $([],)* - authz_parent, - ) = parent.lookup().await?; - let ( - $([<_authz_ $ancestor:lower>],)* - _authz_parent, - authz_child, _) = - [< $pc:lower _lookup_by_name_no_authz >]( - opctx, - datastore, - &authz_parent, - *name - ).await?; - Ok(( - $([],)* - authz_parent, - authz_child - )) - } - Key::Id(_, id) => { - let ( - $([],)* - authz_parent, - authz_child, _) = - [< $pc:lower _lookup_by_id_no_authz >]( - opctx, - datastore, - *id - ).await?; - Ok(( - $([],)* - authz_parent, - authz_child - )) - } - } - } - .boxed() - } - } - - impl Fetch for $pc<'_> { - type FetchType = ( - $(authz::[<$ancestor>],)* - authz::[<$parent_pc>], - authz::$pc, - model::$pc - ); - - fn fetch(&self) -> BoxFuture<'_, LookupResult> { - async { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = lookup.datastore; - match &self.key { - Key::Name(parent, name) => { - let ( - $([<_authz_ $ancestor:lower>],)* - authz_parent, - ) = parent.lookup().await?; - [< $pc:lower _fetch_by_name >]( - opctx, - datastore, - &authz_parent, - *name - ).await - } - Key::Id(_, id) => { - [< $pc:lower _fetch_by_id >]( - opctx, - datastore, - *id - ).await - } - } - } - .boxed() - } - } - - impl LookupFor for $pc<'_> { - type LookupType = ::LookupType; - - fn lookup_for( - &self, - action: authz::Action, - ) -> BoxFuture<'_, LookupResult> { - async move { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let ( - $([],)* - authz_parent, - authz_child - ) = self.lookup().await?; - opctx.authorize(action, &authz_child).await?; - Ok(( - $([],)* - authz_parent, - authz_child - )) - } - .boxed() - } - } - } - }; -} - -define_lookup!(Organization); - -define_lookup_with_parent!( - Project, - Organization, - (), - |authz_org: authz::Organization, - project: model::Project, - lookup: LookupType| { - (authz_org.clone(), authz_org.project(project.id(), lookup), project) - } -); - -define_lookup_with_parent!( - Instance, - Project, - (Organization), - |authz_project: authz::Project, - instance: model::Instance, - lookup: LookupType| { - ( - authz_project.organization().clone(), - authz_project.clone(), - authz_project.child_generic( - ResourceType::Instance, - instance.id(), - lookup, - ), - instance, - ) - } -); - -define_lookup_with_parent!( - Disk, - Project, - (Organization), - |authz_project: authz::Project, disk: model::Disk, lookup: LookupType| { - ( - authz_project.organization().clone(), - authz_project.clone(), - authz_project.child_generic(ResourceType::Disk, disk.id(), lookup), - disk, - ) - } -); - -#[cfg(test)] -mod test { - use super::Instance; - use super::Key; - use super::LookupPath; - use super::Organization; - use super::Project; - use crate::context::OpContext; - use crate::db::model::Name; - use nexus_test_utils::db::test_setup_database; - use omicron_test_utils::dev; - use std::sync::Arc; - - #[tokio::test] - async fn test_lookup() { - let logctx = dev::test_setup_log("test_lookup"); - let mut db = test_setup_database(&logctx.log).await; - let (_, datastore) = - crate::db::datastore::datastore_test(&logctx, &db).await; - let opctx = - OpContext::for_tests(logctx.log.new(o!()), Arc::clone(&datastore)); - let org_name: Name = Name("my-org".parse().unwrap()); - let project_name: Name = Name("my-project".parse().unwrap()); - let instance_name: Name = Name("my-instance".parse().unwrap()); - - let leaf = LookupPath::new(&opctx, &datastore) - .organization_name(&org_name) - .project_name(&project_name) - .instance_name(&instance_name); - assert!(matches!(&leaf, - Instance { - key: Key::Name(Project { - key: Key::Name(Organization { - key: Key::Name(_, o) - }, p) - }, i) - } - if **o == org_name && **p == project_name && **i == instance_name)); - - let org_id = "006f29d9-0ff0-e2d2-a022-87e152440122".parse().unwrap(); - let leaf = LookupPath::new(&opctx, &datastore) - .organization_id(org_id) - .project_name(&project_name); - assert!(matches!(&leaf, Project { - key: Key::Name(Organization { - key: Key::Id(LookupPath { .. }, o) - }, p) - } if *o == org_id && **p == project_name)); - - db.cleanup().await.unwrap(); - } + // pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> + // where + // 'a: 'c, + // 'b: 'c, + // { + // Organization { key: Key::Name(self, name) } + // } + // + // pub fn organization_id(self, id: Uuid) -> Organization<'a> { + // Organization { key: Key::Id(self, id) } + // } + // + // pub fn project_id(self, id: Uuid) -> Project<'a> { + // Project { key: Key::Id(self, id) } + // } + // + // pub fn instance_id(self, id: Uuid) -> Instance<'a> { + // Instance { key: Key::Id(self, id) } + // } + // + // pub fn disk_id(self, id: Uuid) -> Disk<'a> { + // Disk { key: Key::Id(self, id) } + // } } +// +//impl<'a> GetLookupRoot for LookupPath<'a> { +// fn lookup_root(&self) -> &LookupPath<'_> { +// self +// } +//} +// +//impl<'a> Organization<'a> { +// pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Project { key: Key::Name(self, name) } +// } +//} +// +//impl<'a> Project<'a> { +// pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Disk { key: Key::Name(self, name) } +// } +// +// pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Instance { key: Key::Name(self, name) } +// } +//} +// +//macro_rules! define_lookup { +// ($pc:ident) => { +// paste::paste! { +// pub struct $pc<'a> { +// key: Key<'a, LookupPath<'a>>, +// } +// +// impl<'a> GetLookupRoot for $pc<'a> { +// fn lookup_root(&self) -> &LookupPath<'_> { +// self.key.lookup_root() +// } +// } +// +// // Do NOT make these functions public. They should instead be +// // wrapped by functions that perform authz checks. +// async fn [<$pc:lower _lookup_by_id_no_authz>]( +// _opctx: &OpContext, +// datastore: &DataStore, +// id: Uuid, +// ) -> LookupResult<(authz::$pc, model::$pc)> { +// use db::schema::[<$pc:lower>]::dsl; +// // TODO-security This could use pool_authorized() instead. +// // However, it will change the response code for this case: +// // unauthenticated users will get a 401 rather than a 404 +// // because we'll kick them out sooner than we used to -- they +// // won't even be able to make this database query. That's a +// // good thing but this change can be deferred to a follow-up PR. +// let conn = datastore.pool(); +// dsl::[<$pc:lower>] +// .filter(dsl::time_deleted.is_null()) +// .filter(dsl::id.eq(id)) +// .select(model::$pc::as_select()) +// .get_result_async(conn) +// .await +// .map_err(|e| { +// public_error_from_diesel_pool( +// e, +// ErrorHandler::NotFoundByLookup( +// ResourceType::$pc, +// LookupType::ById(id) +// ) +// ) +// }) +// .map(|o| {( +// authz::FLEET.[<$pc:lower>](o.id(), LookupType::ById(id)), +// o +// )} +// ) +// } +// +// // Do NOT make these functions public. They should instead be +// // wrapped by functions that perform authz checks. +// async fn [<$pc:lower _lookup_by_name_no_authz>]( +// _opctx: &OpContext, +// datastore: &DataStore, +// name: &Name, +// ) -> LookupResult<(authz::$pc, model::$pc)> { +// use db::schema::[<$pc:lower>]::dsl; +// // TODO-security See the note about pool_authorized() above. +// let conn = datastore.pool(); +// dsl::[<$pc:lower>] +// .filter(dsl::time_deleted.is_null()) +// .filter(dsl::name.eq(name.clone())) +// .select(model::$pc::as_select()) +// .get_result_async(conn) +// .await +// .map_err(|e| { +// public_error_from_diesel_pool( +// e, +// ErrorHandler::NotFoundByLookup( +// ResourceType::$pc, +// LookupType::ByName(name.as_str().to_string()) +// ) +// ) +// }) +// .map(|o| {( +// authz::FLEET.[<$pc:lower>]( +// o.id(), +// LookupType::ByName(name.as_str().to_string()) +// ), +// o +// )} +// ) +// } +// +// async fn [<$pc:lower _fetch_by_id>]( +// opctx: &OpContext, +// datastore: &DataStore, +// id: Uuid, +// ) -> LookupResult<(authz::$pc, model::$pc)> { +// let (authz_child, db_child) = +// [<$pc:lower _lookup_by_id_no_authz>]( +// opctx, +// datastore, +// id, +// ).await?; +// opctx.authorize(authz::Action::Read, &authz_child).await?; +// Ok((authz_child, db_child)) +// } +// +// async fn [<$pc:lower _fetch_by_name>]( +// opctx: &OpContext, +// datastore: &DataStore, +// name: &Name, +// ) -> LookupResult<(authz::$pc, model::$pc)> { +// let (authz_child, db_child) = +// [<$pc:lower _lookup_by_name_no_authz>]( +// opctx, +// datastore, +// name +// ).await?; +// opctx.authorize(authz::Action::Read, &authz_child).await?; +// Ok((authz_child, db_child)) +// } +// +// impl LookupNoauthz for $pc<'_> { +// type LookupType = (authz::$pc,); +// +// fn lookup( +// &self, +// ) -> BoxFuture<'_, LookupResult> { +// async { +// let lookup = self.lookup_root(); +// let opctx = &lookup.opctx; +// let datastore = lookup.datastore; +// match self.key { +// Key::Name(_, name) => { +// let (rv, _) = +// [<$pc:lower _lookup_by_name_no_authz>]( +// opctx, +// datastore, +// name +// ).await?; +// Ok((rv,)) +// } +// Key::Id(_, id) => { +// let (rv, _) = +// [<$pc:lower _lookup_by_id_no_authz>]( +// opctx, +// datastore, +// id +// ).await?; +// Ok((rv,)) +// } +// } +// }.boxed() +// } +// } +// +// impl Fetch for $pc<'_> { +// type FetchType = (authz::$pc, model::$pc); +// +// fn fetch(&self) -> BoxFuture<'_, LookupResult> { +// async { +// let lookup = self.lookup_root(); +// let opctx = &lookup.opctx; +// let datastore = lookup.datastore; +// match self.key { +// Key::Name(_, name) => { +// [<$pc:lower _fetch_by_name>]( +// opctx, +// datastore, +// name +// ).await +// } +// Key::Id(_, id) => { +// [<$pc:lower _fetch_by_id>]( +// opctx, +// datastore, +// id +// ).await +// } +// } +// } +// .boxed() +// } +// } +// +// impl LookupFor for $pc<'_> { +// type LookupType = ::LookupType; +// +// fn lookup_for( +// &self, +// action: authz::Action, +// ) -> BoxFuture<'_, LookupResult> { +// async move { +// let lookup = self.lookup_root(); +// let opctx = &lookup.opctx; +// let (authz_child,) = self.lookup().await?; +// opctx.authorize(action, &authz_child).await?; +// Ok((authz_child,)) +// } +// .boxed() +// } +// } +// } +// }; +//} +// +//macro_rules! define_lookup_with_parent { +// ( +// $pc:ident, // Pascal-case version of resource name +// $parent_pc:ident, // Pascal-case version of parent resource name +// ($($ancestor:ident),*), // List of ancestors above parent +// // XXX-dap update comment +// $mkauthz:expr // Closure to generate resource's authz object +// // from parent's +// ) => { +// paste::paste! { +// pub struct $pc<'a> { +// key: Key<'a, $parent_pc<'a>>, +// } +// +// impl<'a> GetLookupRoot for $pc<'a> { +// fn lookup_root(&self) -> &LookupPath<'_> { +// self.key.lookup_root() +// } +// } +// +// // Do NOT make these functions public. They should instead be +// // wrapped by functions that perform authz checks. +// async fn [<$pc:lower _lookup_by_id_no_authz>]( +// opctx: &OpContext, +// datastore: &DataStore, +// id: Uuid, +// ) -> LookupResult<( +// $(authz::[<$ancestor>],)* +// authz::$parent_pc, +// authz::$pc, +// model::$pc +// )> { +// use db::schema::[<$pc:lower>]::dsl; +// // TODO-security See the note about pool_authorized() above. +// let conn = datastore.pool(); +// let db_row = dsl::[<$pc:lower>] +// .filter(dsl::time_deleted.is_null()) +// .filter(dsl::id.eq(id)) +// .select(model::$pc::as_select()) +// .get_result_async(conn) +// .await +// .map_err(|e| { +// public_error_from_diesel_pool( +// e, +// ErrorHandler::NotFoundByLookup( +// ResourceType::$pc, +// LookupType::ById(id) +// ) +// ) +// })?; +// let ($([<_authz_ $ancestor:lower>],)* authz_parent, _) = +// [< $parent_pc:lower _lookup_by_id_no_authz >]( +// opctx, +// datastore, +// db_row.[<$parent_pc:lower _id>] +// ).await?; +// let authz_list = ($mkauthz)( +// authz_parent, db_row, LookupType::ById(id) +// ); +// Ok(authz_list) +// } +// +// // Do NOT make these functions public. They should instead be +// // wrapped by functions that perform authz checks. +// async fn [<$pc:lower _lookup_by_name_no_authz>]( +// _opctx: &OpContext, +// datastore: &DataStore, +// authz_parent: &authz::$parent_pc, +// name: &Name, +// ) -> LookupResult<( +// $(authz::[<$ancestor>],)* +// authz::$parent_pc, +// authz::$pc, +// model::$pc +// )> { +// use db::schema::[<$pc:lower>]::dsl; +// // TODO-security See the note about pool_authorized() above. +// let conn = datastore.pool(); +// dsl::[<$pc:lower>] +// .filter(dsl::time_deleted.is_null()) +// .filter(dsl::name.eq(name.clone())) +// .filter(dsl::[<$parent_pc:lower _id>].eq(authz_parent.id())) +// .select(model::$pc::as_select()) +// .get_result_async(conn) +// .await +// .map_err(|e| { +// public_error_from_diesel_pool( +// e, +// ErrorHandler::NotFoundByLookup( +// ResourceType::$pc, +// LookupType::ByName(name.as_str().to_string()) +// ) +// ) +// }) +// .map(|dbmodel| { +// ($mkauthz)( +// authz_parent.clone(), +// dbmodel, +// LookupType::ByName(name.as_str().to_string()) +// ) +// }) +// } +// +// async fn [<$pc:lower _fetch_by_id>]( +// opctx: &OpContext, +// datastore: &DataStore, +// id: Uuid, +// ) -> LookupResult<( +// $(authz::[<$ancestor>],)* +// authz::$parent_pc, +// authz::$pc, +// model::$pc +// )> { +// let ( +// $([],)* +// authz_parent, +// authz_child, +// db_child +// ) = +// [<$pc:lower _lookup_by_id_no_authz>]( +// opctx, +// datastore, +// id, +// ).await?; +// opctx.authorize(authz::Action::Read, &authz_child).await?; +// Ok(( +// $([],)* +// authz_parent, +// authz_child, +// db_child +// )) +// } +// +// async fn [<$pc:lower _fetch_by_name>]( +// opctx: &OpContext, +// datastore: &DataStore, +// authz_parent: &authz::$parent_pc, +// name: &Name, +// ) -> LookupResult<( +// $(authz::[<$ancestor>],)* +// authz::$parent_pc, +// authz::$pc, +// model::$pc +// )> { +// let ( +// $([],)* +// authz_parent, +// authz_child, +// db_child +// ) = +// [<$pc:lower _lookup_by_name_no_authz>]( +// opctx, +// datastore, +// authz_parent, +// name +// ).await?; +// opctx.authorize(authz::Action::Read, &authz_child).await?; +// Ok(( +// $([],)* +// authz_parent, +// authz_child, +// db_child +// )) +// } +// +// impl LookupNoauthz for $pc<'_> { +// type LookupType = ( +// $(authz::[<$ancestor>],)* +// authz::[<$parent_pc>], +// authz::$pc, +// ); +// +// fn lookup( +// &self, +// ) -> BoxFuture<'_, LookupResult> { +// async { +// let lookup = self.lookup_root(); +// let opctx = &lookup.opctx; +// let datastore = lookup.datastore; +// match &self.key { +// Key::Name(parent, name) => { +// let ( +// $([],)* +// authz_parent, +// ) = parent.lookup().await?; +// let ( +// $([<_authz_ $ancestor:lower>],)* +// _authz_parent, +// authz_child, _) = +// [< $pc:lower _lookup_by_name_no_authz >]( +// opctx, +// datastore, +// &authz_parent, +// *name +// ).await?; +// Ok(( +// $([],)* +// authz_parent, +// authz_child +// )) +// } +// Key::Id(_, id) => { +// let ( +// $([],)* +// authz_parent, +// authz_child, _) = +// [< $pc:lower _lookup_by_id_no_authz >]( +// opctx, +// datastore, +// *id +// ).await?; +// Ok(( +// $([],)* +// authz_parent, +// authz_child +// )) +// } +// } +// } +// .boxed() +// } +// } +// +// impl Fetch for $pc<'_> { +// type FetchType = ( +// $(authz::[<$ancestor>],)* +// authz::[<$parent_pc>], +// authz::$pc, +// model::$pc +// ); +// +// fn fetch(&self) -> BoxFuture<'_, LookupResult> { +// async { +// let lookup = self.lookup_root(); +// let opctx = &lookup.opctx; +// let datastore = lookup.datastore; +// match &self.key { +// Key::Name(parent, name) => { +// let ( +// $([<_authz_ $ancestor:lower>],)* +// authz_parent, +// ) = parent.lookup().await?; +// [< $pc:lower _fetch_by_name >]( +// opctx, +// datastore, +// &authz_parent, +// *name +// ).await +// } +// Key::Id(_, id) => { +// [< $pc:lower _fetch_by_id >]( +// opctx, +// datastore, +// *id +// ).await +// } +// } +// } +// .boxed() +// } +// } +// +// impl LookupFor for $pc<'_> { +// type LookupType = ::LookupType; +// +// fn lookup_for( +// &self, +// action: authz::Action, +// ) -> BoxFuture<'_, LookupResult> { +// async move { +// let lookup = self.lookup_root(); +// let opctx = &lookup.opctx; +// let ( +// $([],)* +// authz_parent, +// authz_child +// ) = self.lookup().await?; +// opctx.authorize(action, &authz_child).await?; +// Ok(( +// $([],)* +// authz_parent, +// authz_child +// )) +// } +// .boxed() +// } +// } +// } +// }; +//} +// +//define_lookup!(Organization); +// +//define_lookup_with_parent!( +// Project, +// Organization, +// (), +// |authz_org: authz::Organization, +// project: model::Project, +// lookup: LookupType| { +// (authz_org.clone(), authz_org.project(project.id(), lookup), project) +// } +//); +// +//define_lookup_with_parent!( +// Instance, +// Project, +// (Organization), +// |authz_project: authz::Project, +// instance: model::Instance, +// lookup: LookupType| { +// ( +// authz_project.organization().clone(), +// authz_project.clone(), +// authz_project.child_generic( +// ResourceType::Instance, +// instance.id(), +// lookup, +// ), +// instance, +// ) +// } +//); +// +//define_lookup_with_parent!( +// Disk, +// Project, +// (Organization), +// |authz_project: authz::Project, disk: model::Disk, lookup: LookupType| { +// ( +// authz_project.organization().clone(), +// authz_project.clone(), +// authz_project.child_generic(ResourceType::Disk, disk.id(), lookup), +// disk, +// ) +// } +//); +// +//#[cfg(test)] +//mod test { +// use super::Instance; +// use super::Key; +// use super::LookupPath; +// use super::Organization; +// use super::Project; +// use crate::context::OpContext; +// use crate::db::model::Name; +// use nexus_test_utils::db::test_setup_database; +// use omicron_test_utils::dev; +// use std::sync::Arc; +// +// #[tokio::test] +// async fn test_lookup() { +// let logctx = dev::test_setup_log("test_lookup"); +// let mut db = test_setup_database(&logctx.log).await; +// let (_, datastore) = +// crate::db::datastore::datastore_test(&logctx, &db).await; +// let opctx = +// OpContext::for_tests(logctx.log.new(o!()), Arc::clone(&datastore)); +// let org_name: Name = Name("my-org".parse().unwrap()); +// let project_name: Name = Name("my-project".parse().unwrap()); +// let instance_name: Name = Name("my-instance".parse().unwrap()); +// +// let leaf = LookupPath::new(&opctx, &datastore) +// .organization_name(&org_name) +// .project_name(&project_name) +// .instance_name(&instance_name); +// assert!(matches!(&leaf, +// Instance { +// key: Key::Name(Project { +// key: Key::Name(Organization { +// key: Key::Name(_, o) +// }, p) +// }, i) +// } +// if **o == org_name && **p == project_name && **i == instance_name)); +// +// let org_id = "006f29d9-0ff0-e2d2-a022-87e152440122".parse().unwrap(); +// let leaf = LookupPath::new(&opctx, &datastore) +// .organization_id(org_id) +// .project_name(&project_name); +// assert!(matches!(&leaf, Project { +// key: Key::Name(Organization { +// key: Key::Id(LookupPath { .. }, o) +// }, p) +// } if *o == org_id && **p == project_name)); +// +// db.cleanup().await.unwrap(); +// } +//} diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index a52956975cc..96131ff6e9e 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -10,9 +10,6 @@ use crate::config; use crate::context::OpContext; use crate::db; use crate::db::identity::{Asset, Resource}; -use crate::db::lookup::Fetch; -use crate::db::lookup::LookupFor; -use crate::db::lookup::LookupPath; use crate::db::model::DatasetKind; use crate::db::model::Name; use crate::db::model::RouterRoute; @@ -536,18 +533,15 @@ impl Nexus { organization_name: &Name, new_project: ¶ms::ProjectCreate, ) -> CreateResult { - let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::CreateChild) + let org = self + .db_datastore + .organization_lookup_by_path(organization_name) .await?; // Create a project. + let db_project = db::model::Project::new(org.id(), new_project.clone()); let db_project = - db::model::Project::new(authz_org.id(), new_project.clone()); - let db_project = self - .db_datastore - .project_create(opctx, &authz_org, db_project) - .await?; + self.db_datastore.project_create(opctx, &org, db_project).await?; // TODO: We probably want to have "project creation" and "default VPC // creation" co-located within a saga for atomicity. @@ -555,9 +549,6 @@ impl Nexus { // Until then, we just perform the operations sequentially. // Create a default VPC associated with the project. - // XXX-dap We need to be using the project_id we just created. - // project_create() should return authz::Project and we should use that - // here. let _ = self .project_create_vpc( opctx, @@ -586,12 +577,15 @@ impl Nexus { organization_name: &Name, project_name: &Name, ) -> LookupResult { - Ok(LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .fetch() + let authz_org = self + .db_datastore + .organization_lookup_by_path(organization_name) + .await?; + Ok(self + .db_datastore + .project_fetch(opctx, &authz_org, project_name) .await? - .2) + .1) } pub async fn projects_list_by_name( @@ -600,9 +594,9 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::CreateChild) + let authz_org = self + .db_datastore + .organization_lookup_by_path(organization_name) .await?; self.db_datastore .projects_list_by_name(opctx, &authz_org, pagparams) @@ -615,9 +609,9 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { - let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::CreateChild) + let authz_org = self + .db_datastore + .organization_lookup_by_path(organization_name) .await?; self.db_datastore .projects_list_by_id(opctx, &authz_org, pagparams) @@ -1298,19 +1292,20 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - let (_, authz_project, authz_disk, db_disk) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) - .fetch() - .await?; - let (_, _, authz_instance, db_instance) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(authz_project.id()) - .instance_name(instance_name) - .fetch() - .await?; + // TODO: This shouldn't be looking up multiple database entries by name, + // it should resolve names to IDs first. + let authz_project = self + .db_datastore + .project_lookup_by_path(organization_name, project_name) + .await?; + let (authz_disk, db_disk) = self + .db_datastore + .disk_fetch(opctx, &authz_project, disk_name) + .await?; + let (authz_instance, db_instance) = self + .db_datastore + .instance_fetch(opctx, &authz_project, instance_name) + .await?; let instance_id = &authz_instance.id(); fn disk_attachment_error( @@ -1404,19 +1399,20 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - let (_, authz_project, authz_disk, db_disk) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) - .fetch() - .await?; - let (_, _, authz_instance, db_instance) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(authz_project.id()) - .instance_name(instance_name) - .fetch() - .await?; + // TODO: This shouldn't be looking up multiple database entries by name, + // it should resolve names to IDs first. + let authz_project = self + .db_datastore + .project_lookup_by_path(organization_name, project_name) + .await?; + let (authz_disk, db_disk) = self + .db_datastore + .disk_fetch(opctx, &authz_project, disk_name) + .await?; + let (authz_instance, db_instance) = self + .db_datastore + .instance_fetch(opctx, &authz_project, instance_name) + .await?; let instance_id = &authz_instance.id(); match &db_disk.state().into() { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a33f38d0c95..b866e4568b7 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -10,5 +10,6 @@ [toolchain] # NOTE: This toolchain is also specified within .github/buildomat/jobs/build-and-test.sh. # If you update it here, update that file too. -channel = "nightly-2021-11-24" +#channel = "nightly-2021-11-24" +channel = "stable" profile = "default" From dd5ce400e82a53847da2c108baff93acb0aa282e Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 25 Mar 2022 20:59:48 -0700 Subject: [PATCH 26/59] more work --- nexus/src/db/db-macros/src/lookup.rs | 202 +++++++++++++++++++++++++-- nexus/src/db/lookup.rs | 4 + 2 files changed, 197 insertions(+), 9 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index b68a4062624..80d7d184759 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -33,7 +33,6 @@ fn do_lookup_resource( // TODO // - validate no generics and no fields? - let raw_input = input.clone(); let item: ItemStruct = syn::parse2(input)?; let resource_name = &item.ident; let resource_as_snake = format_ident!( @@ -71,9 +70,6 @@ fn do_lookup_resource( let mut authz_path_values_vec = authz_ancestors_values_vec.clone(); authz_path_values_vec.push(quote! { authz_self, }); - let authz_ancestors_types = quote! { - #(#authz_ancestors_types_vec)* - }; let authz_ancestors_values = quote! { #(#authz_ancestors_values_vec)* }; @@ -83,23 +79,26 @@ fn do_lookup_resource( let authz_path_values = quote! { #(#authz_path_values_vec)* }; - let root_name = format_ident!("Root"); let ( parent_resource_name, parent_lookup_arg, + parent_lookup_arg_value, parent_filter, authz_ancestors_values_assign, parent_authz, - ) = match config.ancestors.first() { + authz_ancestors_values_assign_lookup, + ) = match config.ancestors.last() { Some(parent_resource_name) => { let parent_snake_str = heck::AsSnakeCase(parent_resource_name).to_string(); let parent_id = format_ident!("{}_id", parent_snake_str,); let parent_resource_name = format_ident!("{}", parent_resource_name); + let parent_authz_type = quote! { &authz::#parent_resource_name }; let parent_lookup_arg = - quote! { authz_parent: &authz::#parent_resource_name }; + quote! { authz_parent: #parent_authz_type, }; + let parent_lookup_arg_value = quote! { authz_parent, }; let parent_filter = quote! { .filter(dsl::#parent_id.eq(authz_parent.id())) }; let authz_ancestors_values_assign = quote! { @@ -108,13 +107,19 @@ fn do_lookup_resource( _opctx, datastore, db_row.#parent_id ).await?; }; - let parent_authz = &authz_ancestors_values_vec[0]; + let authz_ancestors_values_assign_lookup = quote! { + let (#authz_ancestors_values) = parent.lookup().await?; + }; + let parent_authz = &authz_ancestors_values_vec + [authz_ancestors_values_vec.len() - 1]; ( parent_resource_name, parent_lookup_arg, + parent_lookup_arg_value, parent_filter, authz_ancestors_values_assign, quote! { #parent_authz }, + authz_ancestors_values_assign_lookup, ) } None => ( @@ -122,7 +127,9 @@ fn do_lookup_resource( quote! {}, quote! {}, quote! {}, - quote! { authz::FLEET }, + quote! {}, + quote! { authz::FLEET, }, + quote! {}, ), }; @@ -132,6 +139,180 @@ fn do_lookup_resource( } impl #resource_name<'a> { + fn lookup_root(&self) -> LookupPath<'a> { + match self { + Key::Name(parent, _) => parent.lookup_root(), + Key::Id(root, _) => root.lookup_root(), + } + } + + pub async fn fetch( + &self, + ) -> LookupResult<(#authz_path_types, #model_resource)> { + self.fetch_for(authz::Action::Read) + } + + pub async fn fetch_for( + &self, + action: authz::Action, + ) -> LookupResult<(#authz_path_types, #model_resource)> { + match &self.key { + Key::Name(parent, name) => { + #authz_ancestors_values_assign_lookup + let (authz_self, db_row) = Self::fetch_by_name_for( + opctx, + datastore, + &authz_parent, + *name, + action, + ).await; + Ok((#authz_path_values db_row)) + } + Key::Id(_, id) => { + Self::fetch_by_id_for( + opctx, + datastore, + *id, + action, + ).await + } + + } + } + + pub async fn lookup_for( + &self, + action: authz::Action, + ) -> LookupResult<(#authz_path_types)> { + let (#authz_path_types) = self.lookup(); + opctx.authorize(action, &authz_self).await?; + Ok((#authz_path_values)) + } + + // Do NOT make this function public. It's a helper for fetch() and + // lookup_for(). It's exposed in a safer way via lookup_for(). + async fn lookup( + &self, + ) -> LookupResult<(#authz_path_types)> { + let lookup = parent.lookup_root(); + let opctx = &lookup.opctx; + let datastore = &lookup.datastore; + + match &self.key { + Key::Name(parent, name) => { + // When doing a by-name lookup, we have to look up the + // parent first. Since this is recursive, we wind up + // hitting the database once for each item in the path, + // in order descending from the root of the tree. (So + // we'll look up Organization, then Project, then + // Instance, etc.) + // TODO-performance Instead of doing database queries at + // each level of recursion, we could be building up one + // big "join" query and hit the database just once. + #authz_ancestors_values_assign_lookup + let (authz_self, _) = Self::lookup_by_name_no_authz( + opctx, + datastore, + #parent_lookup_arg_value + *name + ).await?; + Ok((#authz_path_values)) + } + Key::Id(_, id) => { + // When doing a by-id lookup, we start directly with the + // resource we're looking up. But we still want to + // return a full path of authz objects. So we look up + // the parent by id, then its parent, etc. Like the + // by-name case, we wind up hitting the database once + // for each item in the path, but in the reverse order. + // So we'll look up the Instance, then the Project, then + // the Organization. + // TODO-performance Instead of doing database queries at + // each level of recursion, we could be building up one + // big "join" query and hit the database just once. + let (#authz_path_values _) = + Self::lookup_by_id_no_authz( + opctx, + datastore, + id + ).await?; + Ok((#authz_path_values)) + } + } + } + + // Do NOT make this function public. It's exposed via fetch_for(). + async fn fetch_by_name_for( + opctx: &OpContext, + datastore: &DataStore, + #parent_lookup_arg + name: &Name, + action: authz::Action, + ) -> LookupResult<(#authz_resource, #model_resource)> { + let (authz_self, db_row) = Self::lookup_by_name_no_authz( + opctx, + datastore, + #parent_lookup_arg_value + name + ).await?; + opctx.authorize(action, &authz_self).await?; + Ok((authz_self, db_row)) + } + + // Do NOT make these functions public. They should instead be + // wrapped by functions that perform authz checks. + async fn lookup_by_name_no_authz( + _opctx: &OpContext, + #parent_lookup_arg + datastore: &DataStore, + name: &Name, + ) -> LookupResult<(#authz_resource, #model_resource)> { + use db::schema::#resource_as_snake::dsl; + + // TODO-security See the note about pool_authorized() above. + let conn = datastore.pool(); + dsl::#resource_as_snake + .filter(dsl::time_deleted.is_null()) + .filter(dsl::name.eq(name.clone())) + #parent_filter + .select(model::#resource_name::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::#resource_name, + LookupType::ByName(name.as_str().to_string()) + ) + ) + }) + .map(|db_row| {( + self.make_authz( + &#parent_authz + &db_row, + LookupType::ByName(name.as_str().to_string()) + ), + db_row + )}) + } + + // Do NOT make this function public. It's exposed via fetch_for(). + async fn fetch_by_id_for( + opctx: &OpContext, + datastore: &DataStore, + id: Uuid, + action: authz::Action, + ) -> LookupResult<(#authz_path_types, #model_resource)> { + let (#authz_path_values db_row) = Self::lookup_by_id_no_authz( + opctx, + datastore, + id + ).await?; + opctx.authorize(action, &authz_self).await?; + Ok((#authz_path_values db_row)) + } + // Do NOT make this function public. It should instead be wrapped // by functions that perform authz checks. async fn lookup_by_id_no_authz( @@ -171,10 +352,13 @@ fn do_lookup_resource( ); Ok((#authz_path_values db_row)) } + + // XXX-dap doc these functions } }) } +#[cfg(test)] mod test { use super::do_lookup_resource; use quote::quote; diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 1461749c1eb..2ba5af6232d 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -27,6 +27,10 @@ enum Key<'a, P> { Id(LookupPath<'a>, Uuid), } +struct Root<'a> { + lookup_root: LookupPath<'a>, +} + #[lookup_resource { ancestors = [] }] From 1f1615ce09874549ac10d119045c29cd82089460 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 25 Mar 2022 21:38:35 -0700 Subject: [PATCH 27/59] closer to compiling --- nexus/src/db/db-macros/src/lookup.rs | 29 +-- nexus/src/db/lookup.rs | 281 +++++++++++---------------- rust-toolchain.toml | 4 +- 3 files changed, 130 insertions(+), 184 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 80d7d184759..052eab57d26 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -96,11 +96,12 @@ fn do_lookup_resource( let parent_resource_name = format_ident!("{}", parent_resource_name); let parent_authz_type = quote! { &authz::#parent_resource_name }; + let authz_parent = format_ident!("authz_{}", parent_snake_str); let parent_lookup_arg = - quote! { authz_parent: #parent_authz_type, }; - let parent_lookup_arg_value = quote! { authz_parent, }; + quote! { #authz_parent: #parent_authz_type, }; + let parent_lookup_arg_value = quote! { #authz_parent , }; let parent_filter = - quote! { .filter(dsl::#parent_id.eq(authz_parent.id())) }; + quote! { .filter(dsl::#parent_id.eq( #authz_parent.id())) }; let authz_ancestors_values_assign = quote! { let (#authz_ancestors_values _) = #parent_resource_name::lookup_by_id_no_authz( @@ -138,8 +139,8 @@ fn do_lookup_resource( key: Key<'a, #parent_resource_name> } - impl #resource_name<'a> { - fn lookup_root(&self) -> LookupPath<'a> { + impl<'a> #resource_name<'a> { + fn lookup_root(&self) -> &LookupPath<'a> { match self { Key::Name(parent, _) => parent.lookup_root(), Key::Id(root, _) => root.lookup_root(), @@ -156,13 +157,17 @@ fn do_lookup_resource( &self, action: authz::Action, ) -> LookupResult<(#authz_path_types, #model_resource)> { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = &lookup.datastore; + match &self.key { Key::Name(parent, name) => { #authz_ancestors_values_assign_lookup let (authz_self, db_row) = Self::fetch_by_name_for( opctx, datastore, - &authz_parent, + #parent_lookup_arg_value *name, action, ).await; @@ -184,7 +189,9 @@ fn do_lookup_resource( &self, action: authz::Action, ) -> LookupResult<(#authz_path_types)> { - let (#authz_path_types) = self.lookup(); + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let (#authz_path_values) = self.lookup(); opctx.authorize(action, &authz_self).await?; Ok((#authz_path_values)) } @@ -194,7 +201,7 @@ fn do_lookup_resource( async fn lookup( &self, ) -> LookupResult<(#authz_path_types)> { - let lookup = parent.lookup_root(); + let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = &lookup.datastore; @@ -288,7 +295,7 @@ fn do_lookup_resource( ) }) .map(|db_row| {( - self.make_authz( + Self::make_authz( &#parent_authz &db_row, LookupType::ByName(name.as_str().to_string()) @@ -345,7 +352,7 @@ fn do_lookup_resource( ) })?; #authz_ancestors_values_assign - let authz_self = self.make_authz( + let authz_self = Self::make_authz( &#parent_authz &db_row, LookupType::ById(id) @@ -365,7 +372,7 @@ mod test { #[test] fn test_lookup_resource() { - // XXX-dap this should actually do something + // XXX-dap this should actually test something eprintln!( "{}", do_lookup_resource( diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 2ba5af6232d..ee1edcf432b 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -8,7 +8,7 @@ use super::datastore::DataStore; use super::identity::Resource; use super::model; use crate::{ - authz::{self, AuthorizedResource}, + authz::{self}, context::OpContext, db, db::error::{public_error_from_diesel_pool, ErrorHandler}, @@ -17,7 +17,6 @@ use crate::{ use async_bb8_diesel::AsyncRunQueryDsl; use db_macros::lookup_resource; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; -use futures::future::BoxFuture; use futures::FutureExt; use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; use uuid::Uuid; @@ -31,82 +30,27 @@ struct Root<'a> { lookup_root: LookupPath<'a>, } +impl<'a> Root<'a> { + fn lookup_root(&self) -> &LookupPath<'a> { + &self.lookup_root + } +} + #[lookup_resource { ancestors = [] }] struct Organization; +#[lookup_resource { + ancestors = [ "Organization" ] +}] +struct Project; + // #[lookup_resource { -// ancestors = [ "Project", "Organization" ] +// ancestors = [ "Organization", "Project" ] // }] // struct Instance; -// TODO-dap XXX Neither "fetcH" nor "lookup" needs to be a trait now that the -// macro is defining the impls and the structs - -//pub trait Fetch { -// type FetchType; -// fn fetch(&self) -> BoxFuture<'_, LookupResult>; -//} -// -//// This private module exist solely to implement the "Sealed trait" pattern. -//// This isn't about future-proofing ourselves. Rather, we don't want to expose -//// an interface that accesses database objects without an authz check. -//mod private { -// use super::LookupPath; -// use futures::future::BoxFuture; -// use omicron_common::api::external::LookupResult; -// -// // TODO-dap XXX-dap we could probably get rid of this trait and its impls -// // because we're now calling `lookup_root()` from a macro that can -// // essentially inline the impl of GetLookupRoot for Key. -// pub trait GetLookupRoot { -// fn lookup_root(&self) -> &LookupPath<'_>; -// } -// -// pub trait LookupNoauthz { -// type LookupType; -// fn lookup(&self) -> BoxFuture<'_, LookupResult>; -// } -//} -// -//use private::GetLookupRoot; -//use private::LookupNoauthz; -// -//pub trait LookupFor { -// type LookupType; -// fn lookup_for( -// &self, -// action: authz::Action, -// ) -> BoxFuture<'_, LookupResult>; -//} -// -//enum Key<'a, P> { -// Name(P, &'a Name), -// Id(LookupPath<'a>, Uuid), -//} -// -//impl<'a, T> GetLookupRoot for Key<'a, T> -//where -// T: GetLookupRoot, -//{ -// fn lookup_root(&self) -> &LookupPath<'_> { -// match self { -// Key::Name(parent, _) => parent.lookup_root(), -// Key::Id(lookup, _) => lookup, -// } -// } -//} -// -//impl<'a, P> Key<'a, P> { -// fn lookup_type(&self) -> LookupType { -// match self { -// Key::Name(_, name) => LookupType::ByName(name.as_str().to_string()), -// Key::Id(_, id) => LookupType::ById(*id), -// } -// } -//} -// pub struct LookupPath<'a> { opctx: &'a OpContext, datastore: &'a DataStore, @@ -124,21 +68,21 @@ impl<'a> LookupPath<'a> { LookupPath { opctx, datastore } } - // pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> - // where - // 'a: 'c, - // 'b: 'c, - // { - // Organization { key: Key::Name(self, name) } - // } - // - // pub fn organization_id(self, id: Uuid) -> Organization<'a> { - // Organization { key: Key::Id(self, id) } - // } - // - // pub fn project_id(self, id: Uuid) -> Project<'a> { - // Project { key: Key::Id(self, id) } - // } + pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> + where + 'a: 'c, + 'b: 'c, + { + Organization { key: Key::Name(self, name) } + } + + pub fn organization_id(self, id: Uuid) -> Organization<'a> { + Organization { key: Key::Id(self, id) } + } + + pub fn project_id(self, id: Uuid) -> Project<'a> { + Project { key: Key::Id(self, id) } + } // // pub fn instance_id(self, id: Uuid) -> Instance<'a> { // Instance { key: Key::Id(self, id) } @@ -148,41 +92,35 @@ impl<'a> LookupPath<'a> { // Disk { key: Key::Id(self, id) } // } } -// -//impl<'a> GetLookupRoot for LookupPath<'a> { -// fn lookup_root(&self) -> &LookupPath<'_> { -// self -// } -//} -// -//impl<'a> Organization<'a> { -// pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Project { key: Key::Name(self, name) } -// } -//} -// -//impl<'a> Project<'a> { -// pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Disk { key: Key::Name(self, name) } -// } -// -// pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Instance { key: Key::Name(self, name) } -// } -//} -// + +impl<'a> Organization<'a> { + pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> + where + 'a: 'c, + 'b: 'c, + { + Project { key: Key::Name(self, name) } + } +} + +// impl<'a> Project<'a> { +// pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Disk { key: Key::Name(self, name) } +// } +// +// pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Instance { key: Key::Name(self, name) } +// } +// } + //macro_rules! define_lookup { // ($pc:ident) => { // paste::paste! { @@ -720,55 +658,56 @@ impl<'a> LookupPath<'a> { // } //); // -//#[cfg(test)] -//mod test { -// use super::Instance; -// use super::Key; -// use super::LookupPath; -// use super::Organization; -// use super::Project; -// use crate::context::OpContext; -// use crate::db::model::Name; -// use nexus_test_utils::db::test_setup_database; -// use omicron_test_utils::dev; -// use std::sync::Arc; -// -// #[tokio::test] -// async fn test_lookup() { -// let logctx = dev::test_setup_log("test_lookup"); -// let mut db = test_setup_database(&logctx.log).await; -// let (_, datastore) = -// crate::db::datastore::datastore_test(&logctx, &db).await; -// let opctx = -// OpContext::for_tests(logctx.log.new(o!()), Arc::clone(&datastore)); -// let org_name: Name = Name("my-org".parse().unwrap()); -// let project_name: Name = Name("my-project".parse().unwrap()); -// let instance_name: Name = Name("my-instance".parse().unwrap()); -// -// let leaf = LookupPath::new(&opctx, &datastore) -// .organization_name(&org_name) -// .project_name(&project_name) -// .instance_name(&instance_name); -// assert!(matches!(&leaf, -// Instance { -// key: Key::Name(Project { -// key: Key::Name(Organization { -// key: Key::Name(_, o) -// }, p) -// }, i) -// } -// if **o == org_name && **p == project_name && **i == instance_name)); -// -// let org_id = "006f29d9-0ff0-e2d2-a022-87e152440122".parse().unwrap(); -// let leaf = LookupPath::new(&opctx, &datastore) -// .organization_id(org_id) -// .project_name(&project_name); -// assert!(matches!(&leaf, Project { -// key: Key::Name(Organization { -// key: Key::Id(LookupPath { .. }, o) -// }, p) -// } if *o == org_id && **p == project_name)); -// -// db.cleanup().await.unwrap(); -// } -//} + +#[cfg(test)] +mod test { + use super::Instance; + use super::Key; + use super::LookupPath; + use super::Organization; + use super::Project; + use crate::context::OpContext; + use crate::db::model::Name; + use nexus_test_utils::db::test_setup_database; + use omicron_test_utils::dev; + use std::sync::Arc; + + #[tokio::test] + async fn test_lookup() { + let logctx = dev::test_setup_log("test_lookup"); + let mut db = test_setup_database(&logctx.log).await; + let (_, datastore) = + crate::db::datastore::datastore_test(&logctx, &db).await; + let opctx = + OpContext::for_tests(logctx.log.new(o!()), Arc::clone(&datastore)); + let org_name: Name = Name("my-org".parse().unwrap()); + let project_name: Name = Name("my-project".parse().unwrap()); + let instance_name: Name = Name("my-instance".parse().unwrap()); + + let leaf = LookupPath::new(&opctx, &datastore) + .organization_name(&org_name) + .project_name(&project_name) + .instance_name(&instance_name); + assert!(matches!(&leaf, + Instance { + key: Key::Name(Project { + key: Key::Name(Organization { + key: Key::Name(_, o) + }, p) + }, i) + } + if **o == org_name && **p == project_name && **i == instance_name)); + + let org_id = "006f29d9-0ff0-e2d2-a022-87e152440122".parse().unwrap(); + let leaf = LookupPath::new(&opctx, &datastore) + .organization_id(org_id) + .project_name(&project_name); + assert!(matches!(&leaf, Project { + key: Key::Name(Organization { + key: Key::Id(LookupPath { .. }, o) + }, p) + } if *o == org_id && **p == project_name)); + + db.cleanup().await.unwrap(); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b866e4568b7..41613dcca8a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -10,6 +10,6 @@ [toolchain] # NOTE: This toolchain is also specified within .github/buildomat/jobs/build-and-test.sh. # If you update it here, update that file too. -#channel = "nightly-2021-11-24" -channel = "stable" +channel = "nightly-2021-11-24" +#channel = "stable" profile = "default" From 4fede01b96c5963fbc7a67fee59184617c50dd21 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 25 Mar 2022 21:41:53 -0700 Subject: [PATCH 28/59] closer to compiling --- nexus/src/db/db-macros/src/lookup.rs | 2 +- nexus/src/db/lookup.rs | 68 +++++++++++++++------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 052eab57d26..e5f05139b3b 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -136,7 +136,7 @@ fn do_lookup_resource( Ok(quote! { pub struct #resource_name<'a> { - key: Key<'a, #parent_resource_name> + key: Key<'a, #parent_resource_name<'a>> } impl<'a> #resource_name<'a> { diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index ee1edcf432b..25101c0067d 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -17,7 +17,6 @@ use crate::{ use async_bb8_diesel::AsyncRunQueryDsl; use db_macros::lookup_resource; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; -use futures::FutureExt; use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; use uuid::Uuid; @@ -46,10 +45,15 @@ struct Organization; }] struct Project; -// #[lookup_resource { -// ancestors = [ "Organization", "Project" ] -// }] -// struct Instance; +#[lookup_resource { + ancestors = [ "Organization", "Project" ] +}] +struct Instance; + +#[lookup_resource { + ancestors = [ "Organization", "Project" ] +}] +struct Disk; pub struct LookupPath<'a> { opctx: &'a OpContext, @@ -73,7 +77,7 @@ impl<'a> LookupPath<'a> { 'a: 'c, 'b: 'c, { - Organization { key: Key::Name(self, name) } + Organization { key: Key::Name(Root { lookup_path: self }, name) } } pub fn organization_id(self, id: Uuid) -> Organization<'a> { @@ -83,14 +87,14 @@ impl<'a> LookupPath<'a> { pub fn project_id(self, id: Uuid) -> Project<'a> { Project { key: Key::Id(self, id) } } - // - // pub fn instance_id(self, id: Uuid) -> Instance<'a> { - // Instance { key: Key::Id(self, id) } - // } - // - // pub fn disk_id(self, id: Uuid) -> Disk<'a> { - // Disk { key: Key::Id(self, id) } - // } + + pub fn instance_id(self, id: Uuid) -> Instance<'a> { + Instance { key: Key::Id(self, id) } + } + + pub fn disk_id(self, id: Uuid) -> Disk<'a> { + Disk { key: Key::Id(self, id) } + } } impl<'a> Organization<'a> { @@ -103,23 +107,23 @@ impl<'a> Organization<'a> { } } -// impl<'a> Project<'a> { -// pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Disk { key: Key::Name(self, name) } -// } -// -// pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Instance { key: Key::Name(self, name) } -// } -// } +impl<'a> Project<'a> { + pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> + where + 'a: 'c, + 'b: 'c, + { + Disk { key: Key::Name(self, name) } + } + + pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> + where + 'a: 'c, + 'b: 'c, + { + Instance { key: Key::Name(self, name) } + } +} //macro_rules! define_lookup { // ($pc:ident) => { @@ -657,7 +661,7 @@ impl<'a> Organization<'a> { // ) // } //); -// + #[cfg(test)] mod test { From 30ecdc85f449c42e8df54c33a78aeeaaf1d42972 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 28 Mar 2022 14:41:17 -0700 Subject: [PATCH 29/59] nearly fixed --- nexus/src/db/db-macros/src/lookup.rs | 34 +++++++------ nexus/src/db/lookup.rs | 74 +++++++++++++++------------- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index e5f05139b3b..4334d0035fc 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -39,7 +39,7 @@ fn do_lookup_resource( "{}", heck::AsSnakeCase(resource_name.to_string()).to_string() ); - let authz_resource = quote! { authz::#resource_name }; + let authz_resource = quote! { authz::#resource_name, }; let model_resource = quote! { model::#resource_name }; // It's important that even if there's only one item in this list, it should @@ -84,6 +84,7 @@ fn do_lookup_resource( parent_resource_name, parent_lookup_arg, parent_lookup_arg_value, + parent_lookup_arg_value_deref, parent_filter, authz_ancestors_values_assign, parent_authz, @@ -99,7 +100,8 @@ fn do_lookup_resource( let authz_parent = format_ident!("authz_{}", parent_snake_str); let parent_lookup_arg = quote! { #authz_parent: #parent_authz_type, }; - let parent_lookup_arg_value = quote! { #authz_parent , }; + let parent_lookup_arg_value = quote! { &#authz_parent , }; + let parent_lookup_arg_value_deref = quote! { #authz_parent , }; let parent_filter = quote! { .filter(dsl::#parent_id.eq( #authz_parent.id())) }; let authz_ancestors_values_assign = quote! { @@ -117,6 +119,7 @@ fn do_lookup_resource( parent_resource_name, parent_lookup_arg, parent_lookup_arg_value, + parent_lookup_arg_value_deref, parent_filter, authz_ancestors_values_assign, quote! { #parent_authz }, @@ -129,6 +132,7 @@ fn do_lookup_resource( quote! {}, quote! {}, quote! {}, + quote! {}, quote! { authz::FLEET, }, quote! {}, ), @@ -141,7 +145,7 @@ fn do_lookup_resource( impl<'a> #resource_name<'a> { fn lookup_root(&self) -> &LookupPath<'a> { - match self { + match &self.key { Key::Name(parent, _) => parent.lookup_root(), Key::Id(root, _) => root.lookup_root(), } @@ -149,14 +153,14 @@ fn do_lookup_resource( pub async fn fetch( &self, - ) -> LookupResult<(#authz_path_types, #model_resource)> { - self.fetch_for(authz::Action::Read) + ) -> LookupResult<(#authz_path_types #model_resource)> { + self.fetch_for(authz::Action::Read).await } pub async fn fetch_for( &self, action: authz::Action, - ) -> LookupResult<(#authz_path_types, #model_resource)> { + ) -> LookupResult<(#authz_path_types #model_resource)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = &lookup.datastore; @@ -170,7 +174,7 @@ fn do_lookup_resource( #parent_lookup_arg_value *name, action, - ).await; + ).await?; Ok((#authz_path_values db_row)) } Key::Id(_, id) => { @@ -191,7 +195,7 @@ fn do_lookup_resource( ) -> LookupResult<(#authz_path_types)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; - let (#authz_path_values) = self.lookup(); + let (#authz_path_values) = self.lookup().await?; opctx.authorize(action, &authz_self).await?; Ok((#authz_path_values)) } @@ -241,7 +245,7 @@ fn do_lookup_resource( Self::lookup_by_id_no_authz( opctx, datastore, - id + *id ).await?; Ok((#authz_path_values)) } @@ -255,11 +259,11 @@ fn do_lookup_resource( #parent_lookup_arg name: &Name, action: authz::Action, - ) -> LookupResult<(#authz_resource, #model_resource)> { + ) -> LookupResult<(#authz_resource #model_resource)> { let (authz_self, db_row) = Self::lookup_by_name_no_authz( opctx, datastore, - #parent_lookup_arg_value + #parent_lookup_arg_value_deref name ).await?; opctx.authorize(action, &authz_self).await?; @@ -270,10 +274,10 @@ fn do_lookup_resource( // wrapped by functions that perform authz checks. async fn lookup_by_name_no_authz( _opctx: &OpContext, - #parent_lookup_arg datastore: &DataStore, + #parent_lookup_arg name: &Name, - ) -> LookupResult<(#authz_resource, #model_resource)> { + ) -> LookupResult<(#authz_resource #model_resource)> { use db::schema::#resource_as_snake::dsl; // TODO-security See the note about pool_authorized() above. @@ -310,7 +314,7 @@ fn do_lookup_resource( datastore: &DataStore, id: Uuid, action: authz::Action, - ) -> LookupResult<(#authz_path_types, #model_resource)> { + ) -> LookupResult<(#authz_path_types #model_resource)> { let (#authz_path_values db_row) = Self::lookup_by_id_no_authz( opctx, datastore, @@ -326,7 +330,7 @@ fn do_lookup_resource( _opctx: &OpContext, datastore: &DataStore, id: Uuid, - ) -> LookupResult<(#authz_path_types, #model_resource)> { + ) -> LookupResult<(#authz_path_types #model_resource)> { use db::schema::#resource_as_snake::dsl; // TODO-security This could use pool_authorized() instead. diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 25101c0067d..bd154a1309a 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -22,7 +22,7 @@ use uuid::Uuid; enum Key<'a, P> { Name(P, &'a Name), - Id(LookupPath<'a>, Uuid), + Id(Root<'a>, Uuid), } struct Root<'a> { @@ -35,25 +35,32 @@ impl<'a> Root<'a> { } } -#[lookup_resource { - ancestors = [] -}] -struct Organization; - -#[lookup_resource { - ancestors = [ "Organization" ] -}] -struct Project; - -#[lookup_resource { - ancestors = [ "Organization", "Project" ] -}] -struct Instance; +// #[lookup_resource { +// ancestors = [], +// authz_kind = "named" +// }] +// struct Organization; +// +// #[lookup_resource { +// ancestors = [ "Organization" ], +// authz_kind = "named" +// }] +// struct Project; +// +// #[lookup_resource { +// ancestors = [ "Organization", "Project" ] +// authz_kind = "generic" +// }] +// struct Instance; +// +// #[lookup_resource { +// ancestors = [ "Organization", "Project" ] +// authz_kind = "generic" +// }] +// struct Disk; -#[lookup_resource { - ancestors = [ "Organization", "Project" ] -}] -struct Disk; +// TODO XXX-dap remove me -- expanded +// TODO XXX-dap end remove-me -- expanded pub struct LookupPath<'a> { opctx: &'a OpContext, @@ -77,24 +84,24 @@ impl<'a> LookupPath<'a> { 'a: 'c, 'b: 'c, { - Organization { key: Key::Name(Root { lookup_path: self }, name) } + Organization { key: Key::Name(Root { lookup_root: self }, name) } } pub fn organization_id(self, id: Uuid) -> Organization<'a> { - Organization { key: Key::Id(self, id) } + Organization { key: Key::Id(Root { lookup_root: self }, id) } } pub fn project_id(self, id: Uuid) -> Project<'a> { - Project { key: Key::Id(self, id) } + Project { key: Key::Id(Root { lookup_root: self }, id) } } pub fn instance_id(self, id: Uuid) -> Instance<'a> { - Instance { key: Key::Id(self, id) } + Instance { key: Key::Id(Root { lookup_root: self }, id) } } - pub fn disk_id(self, id: Uuid) -> Disk<'a> { - Disk { key: Key::Id(self, id) } - } + // pub fn disk_id(self, id: Uuid) -> Disk<'a> { + // Disk { key: Key::Id(Root { lookup_root: self }, id) } + // } } impl<'a> Organization<'a> { @@ -108,13 +115,13 @@ impl<'a> Organization<'a> { } impl<'a> Project<'a> { - pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> - where - 'a: 'c, - 'b: 'c, - { - Disk { key: Key::Name(self, name) } - } + // pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> + // where + // 'a: 'c, + // 'b: 'c, + // { + // Disk { key: Key::Name(self, name) } + // } pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> where @@ -662,7 +669,6 @@ impl<'a> Project<'a> { // } //); - #[cfg(test)] mod test { use super::Instance; From 9f557dbf56c66fc0ba4754f58f7991c3c953982d Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 28 Mar 2022 15:38:06 -0700 Subject: [PATCH 30/59] compiles! --- nexus/src/db/db-macros/src/lookup.rs | 36 ++++++++++++++++++++-- nexus/src/db/lookup.rs | 46 ++++++++++++++-------------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 4334d0035fc..8748c0a9d2f 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -13,6 +13,13 @@ use syn::ItemStruct; #[derive(serde::Deserialize)] struct Config { ancestors: Vec, + authz_kind: AuthzKind, +} + +#[derive(serde::Deserialize)] +enum AuthzKind { + Generic, + Typed, } pub fn lookup_resource( @@ -88,6 +95,7 @@ fn do_lookup_resource( parent_filter, authz_ancestors_values_assign, parent_authz, + parent_authz_type, authz_ancestors_values_assign_lookup, ) = match config.ancestors.last() { Some(parent_resource_name) => { @@ -123,6 +131,7 @@ fn do_lookup_resource( parent_filter, authz_ancestors_values_assign, quote! { #parent_authz }, + parent_authz_type, authz_ancestors_values_assign_lookup, ) } @@ -134,10 +143,19 @@ fn do_lookup_resource( quote! {}, quote! {}, quote! { authz::FLEET, }, + quote! { &authz::Fleet }, quote! {}, ), }; + let (mkauthz_func, mkauthz_arg) = match &config.authz_kind { + AuthzKind::Generic => ( + format_ident!("child_generic"), + quote! { ResourceType::#resource_name, }, + ), + AuthzKind::Typed => (resource_as_snake.clone(), quote! {}), + }; + Ok(quote! { pub struct #resource_name<'a> { key: Key<'a, #parent_resource_name<'a>> @@ -151,6 +169,18 @@ fn do_lookup_resource( } } + fn make_authz( + authz_parent: #parent_authz_type, + db_row: &#model_resource, + lookup_type: LookupType, + ) -> authz::#resource_name { + authz_parent.#mkauthz_func( + #mkauthz_arg + db_row.id(), + lookup_type + ) + } + pub async fn fetch( &self, ) -> LookupResult<(#authz_path_types #model_resource)> { @@ -380,7 +410,7 @@ mod test { eprintln!( "{}", do_lookup_resource( - quote! { ancestors = [] }, + quote! { ancestors = [], authz_kind = Typed }, quote! { struct Organization; }, ) .unwrap(), @@ -389,7 +419,7 @@ mod test { eprintln!( "{}", do_lookup_resource( - quote! { ancestors = [ "Organization" ] }, + quote! { ancestors = [ "Organization" ], authz_kind = Typed }, quote! { struct Project; }, ) .unwrap(), @@ -398,7 +428,7 @@ mod test { eprintln!( "{}", do_lookup_resource( - quote! { ancestors = [ "Organization", "Project" ] }, + quote! { ancestors = [ "Organization", "Project", authz_kind = Generic ] }, quote! { struct Instance; }, ) .unwrap(), diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index bd154a1309a..29f91f2b2a4 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -35,29 +35,29 @@ impl<'a> Root<'a> { } } -// #[lookup_resource { -// ancestors = [], -// authz_kind = "named" -// }] -// struct Organization; -// -// #[lookup_resource { -// ancestors = [ "Organization" ], -// authz_kind = "named" -// }] -// struct Project; -// -// #[lookup_resource { -// ancestors = [ "Organization", "Project" ] -// authz_kind = "generic" -// }] -// struct Instance; -// -// #[lookup_resource { -// ancestors = [ "Organization", "Project" ] -// authz_kind = "generic" -// }] -// struct Disk; +#[lookup_resource { + ancestors = [], + authz_kind = Typed +}] +struct Organization; + +#[lookup_resource { + ancestors = [ "Organization" ], + authz_kind = Typed +}] +struct Project; + +#[lookup_resource { + ancestors = [ "Organization", "Project" ], + authz_kind = Generic +}] +struct Instance; + +#[lookup_resource { + ancestors = [ "Organization", "Project" ], + authz_kind = Generic +}] +struct Disk; // TODO XXX-dap remove me -- expanded // TODO XXX-dap end remove-me -- expanded From 064e64da60c297198c0fb87cd4b42600d2f8bf5c Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 28 Mar 2022 15:53:22 -0700 Subject: [PATCH 31/59] generate child lookup functions --- nexus/src/db/db-macros/src/lookup.rs | 22 ++++++++++ nexus/src/db/lookup.rs | 62 +++++++++++++++------------- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 8748c0a9d2f..5744ab912cd 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -13,6 +13,7 @@ use syn::ItemStruct; #[derive(serde::Deserialize)] struct Config { ancestors: Vec, + children: Vec, authz_kind: AuthzKind, } @@ -156,6 +157,14 @@ fn do_lookup_resource( AuthzKind::Typed => (resource_as_snake.clone(), quote! {}), }; + let child_names: Vec<_> = + config.children.iter().map(|c| format_ident!("{}", c)).collect(); + let child_by_names: Vec<_> = config + .children + .iter() + .map(|c| format_ident!("{}_name", heck::AsSnakeCase(c).to_string())) + .collect(); + Ok(quote! { pub struct #resource_name<'a> { key: Key<'a, #parent_resource_name<'a>> @@ -394,6 +403,19 @@ fn do_lookup_resource( Ok((#authz_path_values db_row)) } + #( + pub fn #child_by_names<'b, 'c>(self, name: &'b Name) + -> #child_names<'c> + where + 'a: 'c, + 'b: 'c, + { + #child_names { + key: Key::Name(self, name) + } + } + )* + // XXX-dap doc these functions } }) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 29f91f2b2a4..68cd3e4f955 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -37,24 +37,28 @@ impl<'a> Root<'a> { #[lookup_resource { ancestors = [], + children = [ "Project" ], authz_kind = Typed }] struct Organization; #[lookup_resource { ancestors = [ "Organization" ], + children = [ "Disk", "Instance" ], authz_kind = Typed }] struct Project; #[lookup_resource { ancestors = [ "Organization", "Project" ], + children = [], authz_kind = Generic }] struct Instance; #[lookup_resource { ancestors = [ "Organization", "Project" ], + children = [], authz_kind = Generic }] struct Disk; @@ -99,38 +103,38 @@ impl<'a> LookupPath<'a> { Instance { key: Key::Id(Root { lookup_root: self }, id) } } - // pub fn disk_id(self, id: Uuid) -> Disk<'a> { - // Disk { key: Key::Id(Root { lookup_root: self }, id) } - // } -} - -impl<'a> Organization<'a> { - pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> - where - 'a: 'c, - 'b: 'c, - { - Project { key: Key::Name(self, name) } + pub fn disk_id(self, id: Uuid) -> Disk<'a> { + Disk { key: Key::Id(Root { lookup_root: self }, id) } } } -impl<'a> Project<'a> { - // pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> - // where - // 'a: 'c, - // 'b: 'c, - // { - // Disk { key: Key::Name(self, name) } - // } - - pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> - where - 'a: 'c, - 'b: 'c, - { - Instance { key: Key::Name(self, name) } - } -} +// impl<'a> Organization<'a> { +// pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Project { key: Key::Name(self, name) } +// } +// } +// +// impl<'a> Project<'a> { +// pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Disk { key: Key::Name(self, name) } +// } +// +// pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> +// where +// 'a: 'c, +// 'b: 'c, +// { +// Instance { key: Key::Name(self, name) } +// } +// } //macro_rules! define_lookup { // ($pc:ident) => { From f2d57709c639d110804f719d31cf7a1d187394ca Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Mon, 28 Mar 2022 16:00:30 -0700 Subject: [PATCH 32/59] rip out dead code, fix test --- nexus/src/db/lookup.rs | 567 +---------------------------------------- 1 file changed, 1 insertion(+), 566 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 68cd3e4f955..d40fdbebd25 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -108,571 +108,6 @@ impl<'a> LookupPath<'a> { } } -// impl<'a> Organization<'a> { -// pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Project { key: Key::Name(self, name) } -// } -// } -// -// impl<'a> Project<'a> { -// pub fn disk_name<'b, 'c>(self, name: &'b Name) -> Disk<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Disk { key: Key::Name(self, name) } -// } -// -// pub fn instance_name<'b, 'c>(self, name: &'b Name) -> Instance<'c> -// where -// 'a: 'c, -// 'b: 'c, -// { -// Instance { key: Key::Name(self, name) } -// } -// } - -//macro_rules! define_lookup { -// ($pc:ident) => { -// paste::paste! { -// pub struct $pc<'a> { -// key: Key<'a, LookupPath<'a>>, -// } -// -// impl<'a> GetLookupRoot for $pc<'a> { -// fn lookup_root(&self) -> &LookupPath<'_> { -// self.key.lookup_root() -// } -// } -// -// // Do NOT make these functions public. They should instead be -// // wrapped by functions that perform authz checks. -// async fn [<$pc:lower _lookup_by_id_no_authz>]( -// _opctx: &OpContext, -// datastore: &DataStore, -// id: Uuid, -// ) -> LookupResult<(authz::$pc, model::$pc)> { -// use db::schema::[<$pc:lower>]::dsl; -// // TODO-security This could use pool_authorized() instead. -// // However, it will change the response code for this case: -// // unauthenticated users will get a 401 rather than a 404 -// // because we'll kick them out sooner than we used to -- they -// // won't even be able to make this database query. That's a -// // good thing but this change can be deferred to a follow-up PR. -// let conn = datastore.pool(); -// dsl::[<$pc:lower>] -// .filter(dsl::time_deleted.is_null()) -// .filter(dsl::id.eq(id)) -// .select(model::$pc::as_select()) -// .get_result_async(conn) -// .await -// .map_err(|e| { -// public_error_from_diesel_pool( -// e, -// ErrorHandler::NotFoundByLookup( -// ResourceType::$pc, -// LookupType::ById(id) -// ) -// ) -// }) -// .map(|o| {( -// authz::FLEET.[<$pc:lower>](o.id(), LookupType::ById(id)), -// o -// )} -// ) -// } -// -// // Do NOT make these functions public. They should instead be -// // wrapped by functions that perform authz checks. -// async fn [<$pc:lower _lookup_by_name_no_authz>]( -// _opctx: &OpContext, -// datastore: &DataStore, -// name: &Name, -// ) -> LookupResult<(authz::$pc, model::$pc)> { -// use db::schema::[<$pc:lower>]::dsl; -// // TODO-security See the note about pool_authorized() above. -// let conn = datastore.pool(); -// dsl::[<$pc:lower>] -// .filter(dsl::time_deleted.is_null()) -// .filter(dsl::name.eq(name.clone())) -// .select(model::$pc::as_select()) -// .get_result_async(conn) -// .await -// .map_err(|e| { -// public_error_from_diesel_pool( -// e, -// ErrorHandler::NotFoundByLookup( -// ResourceType::$pc, -// LookupType::ByName(name.as_str().to_string()) -// ) -// ) -// }) -// .map(|o| {( -// authz::FLEET.[<$pc:lower>]( -// o.id(), -// LookupType::ByName(name.as_str().to_string()) -// ), -// o -// )} -// ) -// } -// -// async fn [<$pc:lower _fetch_by_id>]( -// opctx: &OpContext, -// datastore: &DataStore, -// id: Uuid, -// ) -> LookupResult<(authz::$pc, model::$pc)> { -// let (authz_child, db_child) = -// [<$pc:lower _lookup_by_id_no_authz>]( -// opctx, -// datastore, -// id, -// ).await?; -// opctx.authorize(authz::Action::Read, &authz_child).await?; -// Ok((authz_child, db_child)) -// } -// -// async fn [<$pc:lower _fetch_by_name>]( -// opctx: &OpContext, -// datastore: &DataStore, -// name: &Name, -// ) -> LookupResult<(authz::$pc, model::$pc)> { -// let (authz_child, db_child) = -// [<$pc:lower _lookup_by_name_no_authz>]( -// opctx, -// datastore, -// name -// ).await?; -// opctx.authorize(authz::Action::Read, &authz_child).await?; -// Ok((authz_child, db_child)) -// } -// -// impl LookupNoauthz for $pc<'_> { -// type LookupType = (authz::$pc,); -// -// fn lookup( -// &self, -// ) -> BoxFuture<'_, LookupResult> { -// async { -// let lookup = self.lookup_root(); -// let opctx = &lookup.opctx; -// let datastore = lookup.datastore; -// match self.key { -// Key::Name(_, name) => { -// let (rv, _) = -// [<$pc:lower _lookup_by_name_no_authz>]( -// opctx, -// datastore, -// name -// ).await?; -// Ok((rv,)) -// } -// Key::Id(_, id) => { -// let (rv, _) = -// [<$pc:lower _lookup_by_id_no_authz>]( -// opctx, -// datastore, -// id -// ).await?; -// Ok((rv,)) -// } -// } -// }.boxed() -// } -// } -// -// impl Fetch for $pc<'_> { -// type FetchType = (authz::$pc, model::$pc); -// -// fn fetch(&self) -> BoxFuture<'_, LookupResult> { -// async { -// let lookup = self.lookup_root(); -// let opctx = &lookup.opctx; -// let datastore = lookup.datastore; -// match self.key { -// Key::Name(_, name) => { -// [<$pc:lower _fetch_by_name>]( -// opctx, -// datastore, -// name -// ).await -// } -// Key::Id(_, id) => { -// [<$pc:lower _fetch_by_id>]( -// opctx, -// datastore, -// id -// ).await -// } -// } -// } -// .boxed() -// } -// } -// -// impl LookupFor for $pc<'_> { -// type LookupType = ::LookupType; -// -// fn lookup_for( -// &self, -// action: authz::Action, -// ) -> BoxFuture<'_, LookupResult> { -// async move { -// let lookup = self.lookup_root(); -// let opctx = &lookup.opctx; -// let (authz_child,) = self.lookup().await?; -// opctx.authorize(action, &authz_child).await?; -// Ok((authz_child,)) -// } -// .boxed() -// } -// } -// } -// }; -//} -// -//macro_rules! define_lookup_with_parent { -// ( -// $pc:ident, // Pascal-case version of resource name -// $parent_pc:ident, // Pascal-case version of parent resource name -// ($($ancestor:ident),*), // List of ancestors above parent -// // XXX-dap update comment -// $mkauthz:expr // Closure to generate resource's authz object -// // from parent's -// ) => { -// paste::paste! { -// pub struct $pc<'a> { -// key: Key<'a, $parent_pc<'a>>, -// } -// -// impl<'a> GetLookupRoot for $pc<'a> { -// fn lookup_root(&self) -> &LookupPath<'_> { -// self.key.lookup_root() -// } -// } -// -// // Do NOT make these functions public. They should instead be -// // wrapped by functions that perform authz checks. -// async fn [<$pc:lower _lookup_by_id_no_authz>]( -// opctx: &OpContext, -// datastore: &DataStore, -// id: Uuid, -// ) -> LookupResult<( -// $(authz::[<$ancestor>],)* -// authz::$parent_pc, -// authz::$pc, -// model::$pc -// )> { -// use db::schema::[<$pc:lower>]::dsl; -// // TODO-security See the note about pool_authorized() above. -// let conn = datastore.pool(); -// let db_row = dsl::[<$pc:lower>] -// .filter(dsl::time_deleted.is_null()) -// .filter(dsl::id.eq(id)) -// .select(model::$pc::as_select()) -// .get_result_async(conn) -// .await -// .map_err(|e| { -// public_error_from_diesel_pool( -// e, -// ErrorHandler::NotFoundByLookup( -// ResourceType::$pc, -// LookupType::ById(id) -// ) -// ) -// })?; -// let ($([<_authz_ $ancestor:lower>],)* authz_parent, _) = -// [< $parent_pc:lower _lookup_by_id_no_authz >]( -// opctx, -// datastore, -// db_row.[<$parent_pc:lower _id>] -// ).await?; -// let authz_list = ($mkauthz)( -// authz_parent, db_row, LookupType::ById(id) -// ); -// Ok(authz_list) -// } -// -// // Do NOT make these functions public. They should instead be -// // wrapped by functions that perform authz checks. -// async fn [<$pc:lower _lookup_by_name_no_authz>]( -// _opctx: &OpContext, -// datastore: &DataStore, -// authz_parent: &authz::$parent_pc, -// name: &Name, -// ) -> LookupResult<( -// $(authz::[<$ancestor>],)* -// authz::$parent_pc, -// authz::$pc, -// model::$pc -// )> { -// use db::schema::[<$pc:lower>]::dsl; -// // TODO-security See the note about pool_authorized() above. -// let conn = datastore.pool(); -// dsl::[<$pc:lower>] -// .filter(dsl::time_deleted.is_null()) -// .filter(dsl::name.eq(name.clone())) -// .filter(dsl::[<$parent_pc:lower _id>].eq(authz_parent.id())) -// .select(model::$pc::as_select()) -// .get_result_async(conn) -// .await -// .map_err(|e| { -// public_error_from_diesel_pool( -// e, -// ErrorHandler::NotFoundByLookup( -// ResourceType::$pc, -// LookupType::ByName(name.as_str().to_string()) -// ) -// ) -// }) -// .map(|dbmodel| { -// ($mkauthz)( -// authz_parent.clone(), -// dbmodel, -// LookupType::ByName(name.as_str().to_string()) -// ) -// }) -// } -// -// async fn [<$pc:lower _fetch_by_id>]( -// opctx: &OpContext, -// datastore: &DataStore, -// id: Uuid, -// ) -> LookupResult<( -// $(authz::[<$ancestor>],)* -// authz::$parent_pc, -// authz::$pc, -// model::$pc -// )> { -// let ( -// $([],)* -// authz_parent, -// authz_child, -// db_child -// ) = -// [<$pc:lower _lookup_by_id_no_authz>]( -// opctx, -// datastore, -// id, -// ).await?; -// opctx.authorize(authz::Action::Read, &authz_child).await?; -// Ok(( -// $([],)* -// authz_parent, -// authz_child, -// db_child -// )) -// } -// -// async fn [<$pc:lower _fetch_by_name>]( -// opctx: &OpContext, -// datastore: &DataStore, -// authz_parent: &authz::$parent_pc, -// name: &Name, -// ) -> LookupResult<( -// $(authz::[<$ancestor>],)* -// authz::$parent_pc, -// authz::$pc, -// model::$pc -// )> { -// let ( -// $([],)* -// authz_parent, -// authz_child, -// db_child -// ) = -// [<$pc:lower _lookup_by_name_no_authz>]( -// opctx, -// datastore, -// authz_parent, -// name -// ).await?; -// opctx.authorize(authz::Action::Read, &authz_child).await?; -// Ok(( -// $([],)* -// authz_parent, -// authz_child, -// db_child -// )) -// } -// -// impl LookupNoauthz for $pc<'_> { -// type LookupType = ( -// $(authz::[<$ancestor>],)* -// authz::[<$parent_pc>], -// authz::$pc, -// ); -// -// fn lookup( -// &self, -// ) -> BoxFuture<'_, LookupResult> { -// async { -// let lookup = self.lookup_root(); -// let opctx = &lookup.opctx; -// let datastore = lookup.datastore; -// match &self.key { -// Key::Name(parent, name) => { -// let ( -// $([],)* -// authz_parent, -// ) = parent.lookup().await?; -// let ( -// $([<_authz_ $ancestor:lower>],)* -// _authz_parent, -// authz_child, _) = -// [< $pc:lower _lookup_by_name_no_authz >]( -// opctx, -// datastore, -// &authz_parent, -// *name -// ).await?; -// Ok(( -// $([],)* -// authz_parent, -// authz_child -// )) -// } -// Key::Id(_, id) => { -// let ( -// $([],)* -// authz_parent, -// authz_child, _) = -// [< $pc:lower _lookup_by_id_no_authz >]( -// opctx, -// datastore, -// *id -// ).await?; -// Ok(( -// $([],)* -// authz_parent, -// authz_child -// )) -// } -// } -// } -// .boxed() -// } -// } -// -// impl Fetch for $pc<'_> { -// type FetchType = ( -// $(authz::[<$ancestor>],)* -// authz::[<$parent_pc>], -// authz::$pc, -// model::$pc -// ); -// -// fn fetch(&self) -> BoxFuture<'_, LookupResult> { -// async { -// let lookup = self.lookup_root(); -// let opctx = &lookup.opctx; -// let datastore = lookup.datastore; -// match &self.key { -// Key::Name(parent, name) => { -// let ( -// $([<_authz_ $ancestor:lower>],)* -// authz_parent, -// ) = parent.lookup().await?; -// [< $pc:lower _fetch_by_name >]( -// opctx, -// datastore, -// &authz_parent, -// *name -// ).await -// } -// Key::Id(_, id) => { -// [< $pc:lower _fetch_by_id >]( -// opctx, -// datastore, -// *id -// ).await -// } -// } -// } -// .boxed() -// } -// } -// -// impl LookupFor for $pc<'_> { -// type LookupType = ::LookupType; -// -// fn lookup_for( -// &self, -// action: authz::Action, -// ) -> BoxFuture<'_, LookupResult> { -// async move { -// let lookup = self.lookup_root(); -// let opctx = &lookup.opctx; -// let ( -// $([],)* -// authz_parent, -// authz_child -// ) = self.lookup().await?; -// opctx.authorize(action, &authz_child).await?; -// Ok(( -// $([],)* -// authz_parent, -// authz_child -// )) -// } -// .boxed() -// } -// } -// } -// }; -//} -// -//define_lookup!(Organization); -// -//define_lookup_with_parent!( -// Project, -// Organization, -// (), -// |authz_org: authz::Organization, -// project: model::Project, -// lookup: LookupType| { -// (authz_org.clone(), authz_org.project(project.id(), lookup), project) -// } -//); -// -//define_lookup_with_parent!( -// Instance, -// Project, -// (Organization), -// |authz_project: authz::Project, -// instance: model::Instance, -// lookup: LookupType| { -// ( -// authz_project.organization().clone(), -// authz_project.clone(), -// authz_project.child_generic( -// ResourceType::Instance, -// instance.id(), -// lookup, -// ), -// instance, -// ) -// } -//); -// -//define_lookup_with_parent!( -// Disk, -// Project, -// (Organization), -// |authz_project: authz::Project, disk: model::Disk, lookup: LookupType| { -// ( -// authz_project.organization().clone(), -// authz_project.clone(), -// authz_project.child_generic(ResourceType::Disk, disk.id(), lookup), -// disk, -// ) -// } -//); - #[cfg(test)] mod test { use super::Instance; @@ -718,7 +153,7 @@ mod test { .project_name(&project_name); assert!(matches!(&leaf, Project { key: Key::Name(Organization { - key: Key::Id(LookupPath { .. }, o) + key: Key::Id(_, o) }, p) } if *o == org_id && **p == project_name)); From c222c7ba4ac9b205f2395fe9ebec83bf115deeec Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 15:03:10 -0700 Subject: [PATCH 33/59] clippy --- nexus/src/db/db-macros/src/lookup.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 5744ab912cd..5818cc849e7 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -52,7 +52,7 @@ fn do_lookup_resource( // It's important that even if there's only one item in this list, it should // still have a trailing comma. - let authz_ancestors_types_vec: Vec<_> = config + let mut authz_path_types_vec: Vec<_> = config .ancestors .iter() .map(|a| { @@ -60,6 +60,8 @@ fn do_lookup_resource( quote! { authz::#name, } }) .collect(); + authz_path_types_vec.push(authz_resource.clone()); + let authz_ancestors_values_vec: Vec<_> = config .ancestors .iter() @@ -71,10 +73,6 @@ fn do_lookup_resource( quote! { #v , } }) .collect(); - - let mut authz_path_types_vec = authz_ancestors_types_vec.clone(); - authz_path_types_vec.push(authz_resource.clone()); - let mut authz_path_values_vec = authz_ancestors_values_vec.clone(); authz_path_values_vec.push(quote! { authz_self, }); From 019d15f6f76097cbb70a86ae1b5ee0dde9d95f74 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 15:16:47 -0700 Subject: [PATCH 34/59] make it a function-like macro --- nexus/src/db/db-macros/src/lib.rs | 15 ++-- nexus/src/db/db-macros/src/lookup.rs | 119 ++++++++++++++------------- nexus/src/db/lookup.rs | 24 +++--- 3 files changed, 80 insertions(+), 78 deletions(-) diff --git a/nexus/src/db/db-macros/src/lib.rs b/nexus/src/db/db-macros/src/lib.rs index 3ecda3a73b8..d11bfa80742 100644 --- a/nexus/src/db/db-macros/src/lib.rs +++ b/nexus/src/db/db-macros/src/lib.rs @@ -332,18 +332,15 @@ fn build_asset_impl( } } -/// Identical to [`macro@Resource`], but generates fewer fields. -/// -/// Contains: -/// - ID -/// - Time Created -/// - Time Modified -#[proc_macro_attribute] +// XXX-dap doc +#[proc_macro] pub fn lookup_resource( - attr: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - lookup::lookup_resource(attr, input) + match lookup::lookup_resource(input.into()) { + Ok(output) => output.into(), + Err(error) => error.to_compile_error().into(), + } } #[cfg(test)] diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 5818cc849e7..f63b20206f1 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -8,41 +8,46 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::ItemStruct; +/// Arguments for [`lookup_resource!`] #[derive(serde::Deserialize)] struct Config { + /// Name of the resource (PascalCase) + name: String, + /// ordered list of resources that are ancestors of this resource, starting + /// with the top of the hierarchy + /// (e.g., for an Instance, this would be `[ "Organization", "Project" ]` ancestors: Vec, + /// unordered list of resources that are direct children of this resource + /// (e.g., for a Project, these would include "Instance" and "Disk") children: Vec, + /// describes how the authz object for a resource is constructed from its + /// parent's authz object authz_kind: AuthzKind, } +/// Describes how the authz object for a resource is constructed from its +/// parent's authz object +/// +/// By "authz object", we mean the objects in [`nexus::authz::api_resources`]. +/// +/// This ought to be made more uniform with more typed authz objects, but that's +/// not the way they work today. #[derive(serde::Deserialize)] enum AuthzKind { + /// The authz object is constructed using + /// `authz_parent.child_generic(ResourceType, Uuid, LookupType)` Generic, - Typed, -} -pub fn lookup_resource( - attr: proc_macro::TokenStream, - input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - match do_lookup_resource(attr.into(), input.into()) { - Ok(output) => output.into(), - Err(error) => error.to_compile_error().into(), - } + /// The authz object is constructed using + /// `authz_parent.$resource_type(Uuid, LookupType)`. + Typed, } -fn do_lookup_resource( - attr: TokenStream, - input: TokenStream, -) -> Result { - let config = serde_tokenstream::from_tokenstream::(&attr)?; - - // TODO - // - validate no generics and no fields? - let item: ItemStruct = syn::parse2(input)?; - let resource_name = &item.ident; +/// Implementation of [`lookup_resource!]'. +pub fn lookup_resource(input: TokenStream) -> Result { + let config = serde_tokenstream::from_tokenstream::(&input)?; + let resource_name = format_ident!("{}", config.name); let resource_as_snake = format_ident!( "{}", heck::AsSnakeCase(resource_name.to_string()).to_string() @@ -419,39 +424,39 @@ fn do_lookup_resource( }) } -#[cfg(test)] -mod test { - use super::do_lookup_resource; - use quote::quote; - - #[test] - fn test_lookup_resource() { - // XXX-dap this should actually test something - eprintln!( - "{}", - do_lookup_resource( - quote! { ancestors = [], authz_kind = Typed }, - quote! { struct Organization; }, - ) - .unwrap(), - ); - - eprintln!( - "{}", - do_lookup_resource( - quote! { ancestors = [ "Organization" ], authz_kind = Typed }, - quote! { struct Project; }, - ) - .unwrap(), - ); - - eprintln!( - "{}", - do_lookup_resource( - quote! { ancestors = [ "Organization", "Project", authz_kind = Generic ] }, - quote! { struct Instance; }, - ) - .unwrap(), - ); - } -} +// #[cfg(test)] +// mod test { +// use super::do_lookup_resource; +// use quote::quote; +// +// #[test] +// fn test_lookup_resource() { +// // XXX-dap this should actually test something +// eprintln!( +// "{}", +// do_lookup_resource( +// quote! { ancestors = [], authz_kind = Typed }, +// quote! { struct Organization; }, +// ) +// .unwrap(), +// ); +// +// eprintln!( +// "{}", +// do_lookup_resource( +// quote! { ancestors = [ "Organization" ], authz_kind = Typed }, +// quote! { struct Project; }, +// ) +// .unwrap(), +// ); +// +// eprintln!( +// "{}", +// do_lookup_resource( +// quote! { ancestors = [ "Organization", "Project", authz_kind = Generic ] }, +// quote! { struct Instance; }, +// ) +// .unwrap(), +// ); +// } +// } diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index d40fdbebd25..fbe7cb9fa45 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -35,33 +35,33 @@ impl<'a> Root<'a> { } } -#[lookup_resource { +lookup_resource! { + name = "Organization", ancestors = [], children = [ "Project" ], authz_kind = Typed -}] -struct Organization; +} -#[lookup_resource { +lookup_resource! { + name = "Project", ancestors = [ "Organization" ], children = [ "Disk", "Instance" ], authz_kind = Typed -}] -struct Project; +} -#[lookup_resource { +lookup_resource! { + name = "Instance", ancestors = [ "Organization", "Project" ], children = [], authz_kind = Generic -}] -struct Instance; +} -#[lookup_resource { +lookup_resource! { + name = "Disk", ancestors = [ "Organization", "Project" ], children = [], authz_kind = Generic -}] -struct Disk; +} // TODO XXX-dap remove me -- expanded // TODO XXX-dap end remove-me -- expanded From de35510f2e9d65591140ae804bb26f197299ba13 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 15:17:10 -0700 Subject: [PATCH 35/59] remove dead code --- nexus/src/db/db-macros/src/lookup.rs | 37 ---------------------------- 1 file changed, 37 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index f63b20206f1..ad0f2af4e25 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -423,40 +423,3 @@ pub fn lookup_resource(input: TokenStream) -> Result { } }) } - -// #[cfg(test)] -// mod test { -// use super::do_lookup_resource; -// use quote::quote; -// -// #[test] -// fn test_lookup_resource() { -// // XXX-dap this should actually test something -// eprintln!( -// "{}", -// do_lookup_resource( -// quote! { ancestors = [], authz_kind = Typed }, -// quote! { struct Organization; }, -// ) -// .unwrap(), -// ); -// -// eprintln!( -// "{}", -// do_lookup_resource( -// quote! { ancestors = [ "Organization" ], authz_kind = Typed }, -// quote! { struct Project; }, -// ) -// .unwrap(), -// ); -// -// eprintln!( -// "{}", -// do_lookup_resource( -// quote! { ancestors = [ "Organization", "Project", authz_kind = Generic ] }, -// quote! { struct Instance; }, -// ) -// .unwrap(), -// ); -// } -// } From 872ea9a2829df03c3f40a786364a2ef6c457ef5d Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 15:18:39 -0700 Subject: [PATCH 36/59] re-add Nexus changes --- nexus/src/nexus.rs | 96 +++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 96131ff6e9e..a6c9fae5c51 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -10,6 +10,7 @@ use crate::config; use crate::context::OpContext; use crate::db; use crate::db::identity::{Asset, Resource}; +use crate::db::lookup::LookupPath; use crate::db::model::DatasetKind; use crate::db::model::Name; use crate::db::model::RouterRoute; @@ -533,15 +534,18 @@ impl Nexus { organization_name: &Name, new_project: ¶ms::ProjectCreate, ) -> CreateResult { - let org = self - .db_datastore - .organization_lookup_by_path(organization_name) + let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .lookup_for(authz::Action::CreateChild) .await?; // Create a project. - let db_project = db::model::Project::new(org.id(), new_project.clone()); let db_project = - self.db_datastore.project_create(opctx, &org, db_project).await?; + db::model::Project::new(authz_org.id(), new_project.clone()); + let db_project = self + .db_datastore + .project_create(opctx, &authz_org, db_project) + .await?; // TODO: We probably want to have "project creation" and "default VPC // creation" co-located within a saga for atomicity. @@ -549,6 +553,9 @@ impl Nexus { // Until then, we just perform the operations sequentially. // Create a default VPC associated with the project. + // XXX-dap We need to be using the project_id we just created. + // project_create() should return authz::Project and we should use that + // here. let _ = self .project_create_vpc( opctx, @@ -577,15 +584,12 @@ impl Nexus { organization_name: &Name, project_name: &Name, ) -> LookupResult { - let authz_org = self - .db_datastore - .organization_lookup_by_path(organization_name) - .await?; - Ok(self - .db_datastore - .project_fetch(opctx, &authz_org, project_name) + Ok(LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .fetch() .await? - .1) + .2) } pub async fn projects_list_by_name( @@ -594,9 +598,9 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let authz_org = self - .db_datastore - .organization_lookup_by_path(organization_name) + let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .lookup_for(authz::Action::CreateChild) .await?; self.db_datastore .projects_list_by_name(opctx, &authz_org, pagparams) @@ -609,9 +613,9 @@ impl Nexus { organization_name: &Name, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { - let authz_org = self - .db_datastore - .organization_lookup_by_path(organization_name) + let (authz_org,) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .lookup_for(authz::Action::CreateChild) .await?; self.db_datastore .projects_list_by_id(opctx, &authz_org, pagparams) @@ -1292,20 +1296,19 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - // TODO: This shouldn't be looking up multiple database entries by name, - // it should resolve names to IDs first. - let authz_project = self - .db_datastore - .project_lookup_by_path(organization_name, project_name) - .await?; - let (authz_disk, db_disk) = self - .db_datastore - .disk_fetch(opctx, &authz_project, disk_name) - .await?; - let (authz_instance, db_instance) = self - .db_datastore - .instance_fetch(opctx, &authz_project, instance_name) - .await?; + let (_, authz_project, authz_disk, db_disk) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .disk_name(disk_name) + .fetch() + .await?; + let (_, _, authz_instance, db_instance) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(authz_project.id()) + .instance_name(instance_name) + .fetch() + .await?; let instance_id = &authz_instance.id(); fn disk_attachment_error( @@ -1399,20 +1402,19 @@ impl Nexus { instance_name: &Name, disk_name: &Name, ) -> UpdateResult { - // TODO: This shouldn't be looking up multiple database entries by name, - // it should resolve names to IDs first. - let authz_project = self - .db_datastore - .project_lookup_by_path(organization_name, project_name) - .await?; - let (authz_disk, db_disk) = self - .db_datastore - .disk_fetch(opctx, &authz_project, disk_name) - .await?; - let (authz_instance, db_instance) = self - .db_datastore - .instance_fetch(opctx, &authz_project, instance_name) - .await?; + let (_, authz_project, authz_disk, db_disk) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .disk_name(disk_name) + .fetch() + .await?; + let (_, _, authz_instance, db_instance) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(authz_project.id()) + .instance_name(instance_name) + .fetch() + .await?; let instance_id = &authz_instance.id(); match &db_disk.state().into() { From 44acb43bc23117947de63624ec456629fe6c2d97 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 16:02:23 -0700 Subject: [PATCH 37/59] add some docs --- nexus/src/db/db-macros/src/lib.rs | 65 +++++++++++++++---- nexus/src/db/db-macros/src/lookup.rs | 93 ++++++++++++++++++++++++---- nexus/src/db/lookup.rs | 3 - 3 files changed, 136 insertions(+), 25 deletions(-) diff --git a/nexus/src/db/db-macros/src/lib.rs b/nexus/src/db/db-macros/src/lib.rs index d11bfa80742..f3b8478c9ce 100644 --- a/nexus/src/db/db-macros/src/lib.rs +++ b/nexus/src/db/db-macros/src/lib.rs @@ -20,6 +20,60 @@ use syn::{Data, DataStruct, DeriveInput, Error, Fields, Ident, Lit, Meta}; mod lookup; +/// Defines a structure and helper functions for looking up resources +/// +/// # Examples +/// +/// ``` +/// lookup_resource! { +/// name = "Organization", +/// ancestors = [], +/// children = [ "Project" ], +/// authz_kind = Typed +/// } +/// ``` +/// +/// See [`lookup::Config`] for documentation on the named arguments. +/// +/// This defines a struct `Organization<'a>` with functions `fetch()`, +/// `fetch_for(authz::Action)`, and `lookup_for(authz::Action)` for looking up +/// an Organization in the database. These functions are all protected by +/// access controls. +/// +/// Building on that, we have: +/// +/// ``` +/// lookup_resource! { +/// name = "Organization", +/// ancestors = [], +/// children = [ "Project" ], +/// authz_kind = Typed +/// } +/// +/// lookup_resource! { +/// name = "Instance", +/// ancestors = [ "Organization", "Project" ], +/// children = [], +/// authz_kind = Generic +/// } +/// ``` +/// +/// These define `Project<'a>` and `Instance<'a>`. For more on these structs +/// and how they're used, see nexus/src/db/lookup.rs. +// Allow private intra-doc links. This is useful because the `Config` struct +// cannot be exported (since we're a proc macro crate, and we can't expose +// a struct), but its documentation is very useful. +#[allow(rustdoc::private_intra_doc_links)] +#[proc_macro] +pub fn lookup_resource( + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + match lookup::lookup_resource(input.into()) { + Ok(output) => output.into(), + Err(error) => error.to_compile_error().into(), + } +} + /// Looks for a Meta-style attribute with a particular identifier. /// /// As an example, for an attribute like `#[foo = "bar"]`, we can find this @@ -332,17 +386,6 @@ fn build_asset_impl( } } -// XXX-dap doc -#[proc_macro] -pub fn lookup_resource( - input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - match lookup::lookup_resource(input.into()) { - Ok(output) => output.into(), - Err(error) => error.to_compile_error().into(), - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index ad0f2af4e25..1123c0bf091 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -10,8 +10,9 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; /// Arguments for [`lookup_resource!`] +// NOTE: this is only "pub" for the `cargo doc` link on [`lookup_resource!`]. #[derive(serde::Deserialize)] -struct Config { +pub struct Config { /// Name of the resource (PascalCase) name: String, /// ordered list of resources that are ancestors of this resource, starting @@ -29,7 +30,7 @@ struct Config { /// Describes how the authz object for a resource is constructed from its /// parent's authz object /// -/// By "authz object", we mean the objects in [`nexus::authz::api_resources`]. +/// By "authz object", we mean the objects in nexus/src/authz/api_resources.rs. /// /// This ought to be made more uniform with more typed authz objects, but that's /// not the way they work today. @@ -167,13 +168,35 @@ pub fn lookup_resource(input: TokenStream) -> Result { .iter() .map(|c| format_ident!("{}_name", heck::AsSnakeCase(c).to_string())) .collect(); + let doc_struct = format!( + "Selects a resource of type {} (or any of its children, using the \ + functions on this struct) for lookup or fetch", + &config.name, + ); + let child_docs: Vec<_> = config + .children + .iter() + .map(|child| { + format!( + "Select a resource of type {} within this {}, \ + identified by its name", + child, &config.name + ) + }) + .collect(); Ok(quote! { + #[doc = #doc_struct] pub struct #resource_name<'a> { key: Key<'a, #parent_resource_name<'a>> } impl<'a> #resource_name<'a> { + /// Getting the LookupPath for this lookup + /// + /// This is used when we actually query the database. At that + /// point, we need the `OpContext` and `DataStore` that are being + /// used for this lookup. fn lookup_root(&self) -> &LookupPath<'a> { match &self.key { Key::Name(parent, _) => parent.lookup_root(), @@ -181,6 +204,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { } } + /// Build the `authz` object for this resource fn make_authz( authz_parent: #parent_authz_type, db_row: &#model_resource, @@ -193,12 +217,24 @@ pub fn lookup_resource(input: TokenStream) -> Result { ) } + /// Fetch the record corresponding to the selected resource + /// + /// This is equivalent to `fetch_for(authz::Action::Read)`. pub async fn fetch( &self, ) -> LookupResult<(#authz_path_types #model_resource)> { self.fetch_for(authz::Action::Read).await } + /// Fetch the record corresponding to the selected resource and + /// check whether the caller is allowed to do the specified `action` + /// + /// The return value is a tuple that also includes the `authz` + /// objects for all resources along the path to this one (i.e., all + /// parent resources) and the authz object for this resource itself. + /// These objects are useful for identifying those resources by + /// id, for doing other authz checks, or for looking up related + /// objects. pub async fn fetch_for( &self, action: authz::Action, @@ -231,6 +267,15 @@ pub fn lookup_resource(input: TokenStream) -> Result { } } + /// Fetch an `authz` object for the selected resource and check + /// whether the caller is allowed to do the specified `action` + /// + /// The return value is a tuple that also includes the `authz` + /// objects for all resources along the path to this one (i.e., all + /// parent resources) and the authz object for this resource itself. + /// These objects are useful for identifying those resources by + /// id, for doing other authz checks, or for looking up related + /// objects. pub async fn lookup_for( &self, action: authz::Action, @@ -242,6 +287,12 @@ pub fn lookup_resource(input: TokenStream) -> Result { Ok((#authz_path_values)) } + /// Fetch the "authz" objects for the selected resource and all its + /// parents + /// + /// This function does not check whether the caller has permission + /// to read this information. That's why it's not `pub`. Outside + /// this module, you want `lookup_for(authz::Action)`. // Do NOT make this function public. It's a helper for fetch() and // lookup_for(). It's exposed in a safer way via lookup_for(). async fn lookup( @@ -294,7 +345,13 @@ pub fn lookup_resource(input: TokenStream) -> Result { } } - // Do NOT make this function public. It's exposed via fetch_for(). + /// Fetch the database row for a resource by doing a lookup by name, + /// possibly within a collection + /// + /// This function checks whether the caller has permissions to read + /// the requested data. However, it's not intended to be used + /// outside this module. See `fetch_for(authz::Action)`. + // Do NOT make this function public. async fn fetch_by_name_for( opctx: &OpContext, datastore: &DataStore, @@ -312,8 +369,13 @@ pub fn lookup_resource(input: TokenStream) -> Result { Ok((authz_self, db_row)) } - // Do NOT make these functions public. They should instead be - // wrapped by functions that perform authz checks. + /// Lowest-level function for looking up a resource in the database + /// by name, possibly within a collection + /// + /// This function does not check whether the caller has permission + /// to read this information. That's why it's not `pub`. Outside + /// this module, you want `fetch()` or `lookup_for(authz::Action)`. + // Do NOT make this function public. async fn lookup_by_name_no_authz( _opctx: &OpContext, datastore: &DataStore, @@ -322,7 +384,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { ) -> LookupResult<(#authz_resource #model_resource)> { use db::schema::#resource_as_snake::dsl; - // TODO-security See the note about pool_authorized() above. + // TODO-security See the note about pool_authorized() below. let conn = datastore.pool(); dsl::#resource_as_snake .filter(dsl::time_deleted.is_null()) @@ -350,7 +412,12 @@ pub fn lookup_resource(input: TokenStream) -> Result { )}) } - // Do NOT make this function public. It's exposed via fetch_for(). + /// Fetch the database row for a resource by doing a lookup by id + /// + /// This function checks whether the caller has permissions to read + /// the requested data. However, it's not intended to be used + /// outside this module. See `fetch_for(authz::Action)`. + // Do NOT make this function public. async fn fetch_by_id_for( opctx: &OpContext, datastore: &DataStore, @@ -366,8 +433,13 @@ pub fn lookup_resource(input: TokenStream) -> Result { Ok((#authz_path_values db_row)) } - // Do NOT make this function public. It should instead be wrapped - // by functions that perform authz checks. + /// Lowest-level function for looking up a resource in the database + /// by id + /// + /// This function does not check whether the caller has permission + /// to read this information. That's why it's not `pub`. Outside + /// this module, you want `fetch()` or `lookup_for(authz::Action)`. + // Do NOT make this function public. async fn lookup_by_id_no_authz( _opctx: &OpContext, datastore: &DataStore, @@ -407,6 +479,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { } #( + #[doc = #child_docs] pub fn #child_by_names<'b, 'c>(self, name: &'b Name) -> #child_names<'c> where @@ -418,8 +491,6 @@ pub fn lookup_resource(input: TokenStream) -> Result { } } )* - - // XXX-dap doc these functions } }) } diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index fbe7cb9fa45..dc39d524b1d 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -63,9 +63,6 @@ lookup_resource! { authz_kind = Generic } -// TODO XXX-dap remove me -- expanded -// TODO XXX-dap end remove-me -- expanded - pub struct LookupPath<'a> { opctx: &'a OpContext, datastore: &'a DataStore, From 51b0af859e2778f5c40cd25794a7d6ab1808fa9e Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 17:07:08 -0700 Subject: [PATCH 38/59] documentation for `LookupPath` --- nexus/src/db/lookup.rs | 140 ++++++++++++++++++++++++++++------------- nexus/src/lib.rs | 12 ++-- 2 files changed, 104 insertions(+), 48 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index dc39d524b1d..8bd74acdd7c 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -20,6 +20,104 @@ use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; use uuid::Uuid; +/// Look up an API resource in the database +/// +/// `LookupPath` provides a builder-like interface for identifying a resource by +/// id or a path of names. Once you've selected a resource, you can use one of +/// a few different functions to get information about it from the database: +/// +/// * `fetch()`: fetches the database record and `authz` objects for all parents +/// in the path to this object. This function checks that the caller has +/// permission to `authz::Action::Read` the resoure. +/// * `fetch_for(authz::Action)`: like `fetch()`, but allows you to specify some +/// other action that will be checked rather than `authz::Action::Read`. +/// * `lookup_for(authz::Action)`: fetch just the `authz` objects for a resource +/// and its parents. This function checks that the caller has permissions to +/// perform the specified action. +/// +/// # Examples +/// +/// ``` +/// # use omicron_nexus::authz; +/// # use omicron_nexus::context::OpContext; +/// # use omicron_nexus::db; +/// # use omicron_nexus::db::DataStore; +/// # use omicron_nexus::db::lookup::LookupPath; +/// # use uuid::Uuid; +/// # async fn foo(opctx: &OpContext, datastore: &DataStore) +/// # -> Result<(), omicron_common::api::external::Error> { +/// +/// // Fetch an organization by name +/// let organization_name = db::model::Name("engineering".parse().unwrap()); +/// let (authz_org, db_org): (authz::Organization, db::model::Organization) = +/// LookupPath::new(opctx, datastore) +/// .organization_name(&organization_name) +/// .fetch() +/// .await?; +/// +/// // Fetch an organization by id +/// let id: Uuid = todo!(); +/// let (authz_org, db_org): (authz::Organization, db::model::Organization) = +/// LookupPath::new(opctx, datastore) +/// .organization_id(id) +/// .fetch() +/// .await?; +/// +/// // Fetch an Instance by a path of names (Organization name, Project name, +/// // Instance name) +/// let project_name = db::model::Name("omicron".parse().unwrap()); +/// let instance_name = db::model::Name("test-server".parse().unwrap()); +/// let (authz_org, authz_project, authz_instance, db_instance) = +/// LookupPath::new(opctx, datastore) +/// .organization_name(&organization_name) +/// .project_name(&project_name) +/// .instance_name(&instance_name) +/// .fetch() +/// .await?; +/// # } +/// ``` +pub struct LookupPath<'a> { + opctx: &'a OpContext, + datastore: &'a DataStore, +} + +impl<'a> LookupPath<'a> { + pub fn new<'b, 'c>( + opctx: &'b OpContext, + datastore: &'c DataStore, + ) -> LookupPath<'a> + where + 'b: 'a, + 'c: 'a, + { + LookupPath { opctx, datastore } + } + + pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> + where + 'a: 'c, + 'b: 'c, + { + Organization { key: Key::Name(Root { lookup_root: self }, name) } + } + + pub fn organization_id(self, id: Uuid) -> Organization<'a> { + Organization { key: Key::Id(Root { lookup_root: self }, id) } + } + + pub fn project_id(self, id: Uuid) -> Project<'a> { + Project { key: Key::Id(Root { lookup_root: self }, id) } + } + + pub fn instance_id(self, id: Uuid) -> Instance<'a> { + Instance { key: Key::Id(Root { lookup_root: self }, id) } + } + + pub fn disk_id(self, id: Uuid) -> Disk<'a> { + Disk { key: Key::Id(Root { lookup_root: self }, id) } + } +} + enum Key<'a, P> { Name(P, &'a Name), Id(Root<'a>, Uuid), @@ -63,48 +161,6 @@ lookup_resource! { authz_kind = Generic } -pub struct LookupPath<'a> { - opctx: &'a OpContext, - datastore: &'a DataStore, -} - -impl<'a> LookupPath<'a> { - pub fn new<'b, 'c>( - opctx: &'b OpContext, - datastore: &'c DataStore, - ) -> LookupPath<'a> - where - 'b: 'a, - 'c: 'a, - { - LookupPath { opctx, datastore } - } - - pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> - where - 'a: 'c, - 'b: 'c, - { - Organization { key: Key::Name(Root { lookup_root: self }, name) } - } - - pub fn organization_id(self, id: Uuid) -> Organization<'a> { - Organization { key: Key::Id(Root { lookup_root: self }, id) } - } - - pub fn project_id(self, id: Uuid) -> Project<'a> { - Project { key: Key::Id(Root { lookup_root: self }, id) } - } - - pub fn instance_id(self, id: Uuid) -> Instance<'a> { - Instance { key: Key::Id(Root { lookup_root: self }, id) } - } - - pub fn disk_id(self, id: Uuid) -> Disk<'a> { - Disk { key: Key::Id(Root { lookup_root: self }, id) } - } -} - #[cfg(test)] mod test { use super::Instance; diff --git a/nexus/src/lib.rs b/nexus/src/lib.rs index bf24258464f..fc50e0a32ff 100644 --- a/nexus/src/lib.rs +++ b/nexus/src/lib.rs @@ -13,13 +13,13 @@ #![allow(clippy::style)] pub mod authn; // Public only for testing -pub mod authz; -pub mod config; // public for testing -mod context; -pub mod db; // Public only for some documentation examples +pub mod authz; // Public for documentation examples +pub mod config; // Public for testing +pub mod context; // Public for documentation examples +pub mod db; // Public for documentation examples mod defaults; -pub mod external_api; // public for testing -pub mod internal_api; // public for testing +pub mod external_api; // Public for testing +pub mod internal_api; // Public for testing mod nexus; mod populate; mod saga_interface; From 9897ca2e1501ab93cf8fd96c2cee1a1890c69cb9 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 17:16:11 -0700 Subject: [PATCH 39/59] more docs --- nexus/src/db/lookup.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 8bd74acdd7c..65e63f9b936 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -63,9 +63,19 @@ use uuid::Uuid; /// .fetch() /// .await?; /// +/// // Lookup a Project with the intent of creating an Instance inside it. For +/// // this purpose, we don't need the database row for the Project, so we use +/// // `lookup_for()`. +/// let project_name = db::model::Name("omicron".parse().unwrap()); +/// let (authz_org, authz_project) = +/// LookupPath::new(opctx, datastore) +/// .organization_name(&organization_name) +/// .project_name(&project_name) +/// .lookup_for(authz::Action::CreateChild) +/// .await?; +/// /// // Fetch an Instance by a path of names (Organization name, Project name, /// // Instance name) -/// let project_name = db::model::Name("omicron".parse().unwrap()); /// let instance_name = db::model::Name("test-server".parse().unwrap()); /// let (authz_org, authz_project, authz_instance, db_instance) = /// LookupPath::new(opctx, datastore) @@ -74,6 +84,16 @@ use uuid::Uuid; /// .instance_name(&instance_name) /// .fetch() /// .await?; +/// +/// // Having looked up the Instance, you have the `authz::Project`. Use this +/// // to look up a Disk that you expect is in the same Project. +/// let disk_name = db::model::Name("my-disk".parse().unwrap()); +/// let (_, _, authz_disk, db_disk) = +/// LookupPath::new(opctx, datastore) +/// .project_id(authz_project.id()) +/// .disk_name(&disk_name) +/// .fetch() +/// .await?; /// # } /// ``` pub struct LookupPath<'a> { @@ -82,6 +102,9 @@ pub struct LookupPath<'a> { } impl<'a> LookupPath<'a> { + /// Begin selecting a resource for lookup + /// + /// Authorization checks will be applied to the caller in `opctx`. pub fn new<'b, 'c>( opctx: &'b OpContext, datastore: &'c DataStore, @@ -93,6 +116,7 @@ impl<'a> LookupPath<'a> { LookupPath { opctx, datastore } } + /// Select a resource of type Organization, identified by its name pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> where 'a: 'c, @@ -101,18 +125,22 @@ impl<'a> LookupPath<'a> { Organization { key: Key::Name(Root { lookup_root: self }, name) } } + /// Select a resource of type Organization, identified by its id pub fn organization_id(self, id: Uuid) -> Organization<'a> { Organization { key: Key::Id(Root { lookup_root: self }, id) } } + /// Select a resource of type Project, identified by its id pub fn project_id(self, id: Uuid) -> Project<'a> { Project { key: Key::Id(Root { lookup_root: self }, id) } } + /// Select a resource of type Instance, identified by its id pub fn instance_id(self, id: Uuid) -> Instance<'a> { Instance { key: Key::Id(Root { lookup_root: self }, id) } } + /// Select a resource of type Disk, identified by its id pub fn disk_id(self, id: Uuid) -> Disk<'a> { Disk { key: Key::Id(Root { lookup_root: self }, id) } } From af6fca3e5535e754ff64441a4bc0ca1aac63e978 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 29 Mar 2022 17:17:52 -0700 Subject: [PATCH 40/59] more docs --- nexus/src/db/lookup.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 65e63f9b936..84d1a69c051 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -151,6 +151,8 @@ enum Key<'a, P> { Id(Root<'a>, Uuid), } +/// `Root` represents the root of whatever path the caller is using to select a +/// resource (whether it's by-id, by-name, or a combination) struct Root<'a> { lookup_root: LookupPath<'a>, } From 977b9c01594729a088d6f8b2837a0a91a4d482f4 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 09:05:20 -0700 Subject: [PATCH 41/59] more documentation --- nexus/src/db/lookup.rs | 76 ++++++++++++++++++++++++++++++++++++++++-- nexus/src/nexus.rs | 2 +- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 84d1a69c051..379401d0166 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -96,6 +96,63 @@ use uuid::Uuid; /// .await?; /// # } /// ``` +// Implementation notes +// +// We say that a caller using `LookupPath` is building a _selection path_ for a +// resource. They use this builder interface to _select_ a specific resource. +// Example selection paths: +// +// - From the root, select Organization with name "org1", then Project with name +// "proj1", then Instance with name "instance1". +// +// - From the root, select Project with id 123, then Instance "instance1". +// +// A selection path always starts at the root, then _may_ contain a lookup-by-id +// node, and then _may_ contain any number of lookup-by-name nodes. It must +// include at least one lookup-by-id or lookup-by-name node. +// +// Once constructed, it looks like this: +// +// Instance +// key: Key::Name(p, "instance1") +// | +// +----------+ +// | +// v +// Project +// key: Key::Name(o, "proj") +// | +// +----------+ +// | +// v +// Organization +// key: Key::Name(r, "org1") +// | +// +----------+ +// | +// v +// Root +// lookup_root: LookupPath (references OpContext and +// DataStore) +// +// This is essentially a singly-linked list, except that each node _owns_ +// (rather than references) the previous node. This is important: the caller's +// going to do something like this: +// +// let (authz_org, authz_project, authz_instance, db_instance) = +// LookupPath::new(opctx, datastore) // returns LookupPath +// .organization_name("org1") // consumes LookupPath, +// // returns Organization +// .project_name("proj1") // consumes Organization, +// returns Project +// .instance_name("instance1") // consumes Project, +// returns Instance +// .fetch().await?; +// +// As you can see, at each step, a selection function (like "organization_name") +// consumes the current tail of the list and returns a new tail. We don't want +// the caller to have to keep track of multiple objects, so that implies that +// the tail must own all the state that we're building up as we go. pub struct LookupPath<'a> { opctx: &'a OpContext, datastore: &'a DataStore, @@ -116,6 +173,9 @@ impl<'a> LookupPath<'a> { LookupPath { opctx, datastore } } + // The top-level selection functions are implemented by hand because the + // macro is not in a great position to do this. + /// Select a resource of type Organization, identified by its name pub fn organization_name<'b, 'c>(self, name: &'b Name) -> Organization<'c> where @@ -146,13 +206,19 @@ impl<'a> LookupPath<'a> { } } +/// Describes a node along the selection path of a resource enum Key<'a, P> { + /// We're looking for a resource with the given name within the given parent + /// collection Name(P, &'a Name), + + /// We're looking for a resource with the given id + /// + /// This has no parent container -- a by-id lookup is always global. Id(Root<'a>, Uuid), } -/// `Root` represents the root of whatever path the caller is using to select a -/// resource (whether it's by-id, by-name, or a combination) +/// Represents the head of the selection path for a resource struct Root<'a> { lookup_root: LookupPath<'a>, } @@ -163,6 +229,11 @@ impl<'a> Root<'a> { } } +// Define the specific builder types for each resource. The `lookup_resource` +// macro defines a struct for the resource, helper functions for selecting child +// resources, and the publicly-exposed fetch functions (fetch(), fetch_for(), +// and lookup_for()). + lookup_resource! { name = "Organization", ancestors = [], @@ -204,6 +275,7 @@ mod test { use omicron_test_utils::dev; use std::sync::Arc; + /* This is a smoke test that things basically appear to work. */ #[tokio::test] async fn test_lookup() { let logctx = dev::test_setup_log("test_lookup"); diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index a6c9fae5c51..f5f6522810c 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -553,7 +553,7 @@ impl Nexus { // Until then, we just perform the operations sequentially. // Create a default VPC associated with the project. - // XXX-dap We need to be using the project_id we just created. + // TODO-correctness We need to be using the project_id we just created. // project_create() should return authz::Project and we should use that // here. let _ = self From 58b03edfe5282d5bc948c19d859cd9b4457503a3 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 09:23:34 -0700 Subject: [PATCH 42/59] more cleanup --- Cargo.lock | 1 - nexus/Cargo.toml | 1 - nexus/src/db/db-macros/src/lib.rs | 4 ++-- rust-toolchain.toml | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11efe522cab..60f58fc384f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2012,7 +2012,6 @@ dependencies = [ "oximeter-instruments", "oximeter-producer", "parse-display", - "paste", "pq-sys", "rand", "ref-cast", diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 2b26b3588f6..87ad1391026 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -32,7 +32,6 @@ oso = "0.26" oximeter-client = { path = "../oximeter-client" } oximeter-db = { path = "../oximeter/db/" } parse-display = "0.5.4" -paste = "1.0" # See omicron-rpaths for more about the "pq-sys" dependency. pq-sys = "*" rand = "0.8.5" diff --git a/nexus/src/db/db-macros/src/lib.rs b/nexus/src/db/db-macros/src/lib.rs index f3b8478c9ce..e8ad949c908 100644 --- a/nexus/src/db/db-macros/src/lib.rs +++ b/nexus/src/db/db-macros/src/lib.rs @@ -24,7 +24,7 @@ mod lookup; /// /// # Examples /// -/// ``` +/// ```ignore /// lookup_resource! { /// name = "Organization", /// ancestors = [], @@ -42,7 +42,7 @@ mod lookup; /// /// Building on that, we have: /// -/// ``` +/// ```ignore /// lookup_resource! { /// name = "Organization", /// ancestors = [], diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 41613dcca8a..a33f38d0c95 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -11,5 +11,4 @@ # NOTE: This toolchain is also specified within .github/buildomat/jobs/build-and-test.sh. # If you update it here, update that file too. channel = "nightly-2021-11-24" -#channel = "stable" profile = "default" From d68e1c5f80c6dd5fce04edf269686bff7de88969 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 13:01:51 -0700 Subject: [PATCH 43/59] macro impl cleanup: part 1 (naming) --- nexus/src/db/db-macros/src/lib.rs | 4 +- nexus/src/db/db-macros/src/lookup.rs | 329 ++++++++++++++++++--------- 2 files changed, 222 insertions(+), 111 deletions(-) diff --git a/nexus/src/db/db-macros/src/lib.rs b/nexus/src/db/db-macros/src/lib.rs index e8ad949c908..534aa0ddbb6 100644 --- a/nexus/src/db/db-macros/src/lib.rs +++ b/nexus/src/db/db-macros/src/lib.rs @@ -33,7 +33,7 @@ mod lookup; /// } /// ``` /// -/// See [`lookup::Config`] for documentation on the named arguments. +/// See [`lookup::Input`] for documentation on the named arguments. /// /// This defines a struct `Organization<'a>` with functions `fetch()`, /// `fetch_for(authz::Action)`, and `lookup_for(authz::Action)` for looking up @@ -60,7 +60,7 @@ mod lookup; /// /// These define `Project<'a>` and `Instance<'a>`. For more on these structs /// and how they're used, see nexus/src/db/lookup.rs. -// Allow private intra-doc links. This is useful because the `Config` struct +// Allow private intra-doc links. This is useful because the `Input` struct // cannot be exported (since we're a proc macro crate, and we can't expose // a struct), but its documentation is very useful. #[allow(rustdoc::private_intra_doc_links)] diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 1123c0bf091..6f83e5a1a3c 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -12,7 +12,7 @@ use quote::{format_ident, quote}; /// Arguments for [`lookup_resource!`] // NOTE: this is only "pub" for the `cargo doc` link on [`lookup_resource!`]. #[derive(serde::Deserialize)] -pub struct Config { +pub struct Input { /// Name of the resource (PascalCase) name: String, /// ordered list of resources that are ancestors of this resource, starting @@ -45,52 +45,79 @@ enum AuthzKind { Typed, } -/// Implementation of [`lookup_resource!]'. -pub fn lookup_resource(input: TokenStream) -> Result { - let config = serde_tokenstream::from_tokenstream::(&input)?; - let resource_name = format_ident!("{}", config.name); - let resource_as_snake = format_ident!( - "{}", - heck::AsSnakeCase(resource_name.to_string()).to_string() - ); - let authz_resource = quote! { authz::#resource_name, }; - let model_resource = quote! { model::#resource_name }; +/// Configuration for [`lookup_resource`] and its helper functions +/// +/// This is all computable from [`Input`]. The purpose is to put the various +/// output strings that need to appear in various chunks of output into one +/// place with uniform names and documentation. +pub struct Config { + // The resource itself + /// PascalCase (raw input) name of the resource we're generating + /// (e.g., `Project`") + resource_name: syn::Ident, - // It's important that even if there's only one item in this list, it should - // still have a trailing comma. - let mut authz_path_types_vec: Vec<_> = config - .ancestors - .iter() - .map(|a| { - let name = format_ident!("{}", a); - quote! { authz::#name, } - }) - .collect(); - authz_path_types_vec.push(authz_resource.clone()); + /// Snake case name of the resource we're generating + /// (e.g., `project`) + resource_as_snake: syn::Ident, - let authz_ancestors_values_vec: Vec<_> = config - .ancestors - .iter() - .map(|a| { - let v = format_ident!( - "authz_{}", - heck::AsSnakeCase(a.to_string()).to_string() - ); - quote! { #v , } - }) - .collect(); - let mut authz_path_values_vec = authz_ancestors_values_vec.clone(); - authz_path_values_vec.push(quote! { authz_self, }); + /// name of the `authz` type for this resource (e.g., `authz::Project`) + resource_authz: TokenStream, - let authz_ancestors_values = quote! { - #(#authz_ancestors_values_vec)* - }; - let authz_path_types = quote! { - #(#authz_path_types_vec)* - }; - let authz_path_values = quote! { - #(#authz_path_values_vec)* - }; + /// name of the `model` type for this resource (e.g., `db::model::Project`) + resource_model: TokenStream, + + /// See [`AuthzKind`] + authz_kind: AuthzKind, + + // The path to the resource + /// list of `authz` types for this resource and its parents + /// (e.g., [`authz::Organization`, `authz::Project`]) + authz_path_types: Vec, + + /// list of identifiers used for the authz objects for this resource and its + /// parents, in the same order as `authz_path_types` + /// (e.g., [`authz_organization`, `authz_project`]) + authz_path_values: Vec, + + // Child resources + /// list of names of child resources (PascalCase, raw input to the macro) + /// (e.g., [`Instance`, `Disk`]) + child_resources: Vec, + + // Parent resource, if any + /// Information about the parent resource, if any + parent: Option, +} + +// XXX-dap TODO-doc +struct Parent { + resource_name: syn::Ident, + lookup_arg: TokenStream, + lookup_arg_value: TokenStream, + lookup_arg_value_deref: TokenStream, + lookup_filter: TokenStream, + resource_authz_type: TokenStream, + resource_authz_name: syn::Ident, + ancestors_values_assign: TokenStream, + ancestors_values_assign_lookup: TokenStream, +} + +/// Implementation of [`lookup_resource!]'. +pub fn lookup_resource( + raw_input: TokenStream, +) -> Result { + let input = serde_tokenstream::from_tokenstream::(&raw_input)?; + let config = configure(input); + + // XXX-dap TODO take another edit pass. For now, I'm going to try to assign + // variables to the elements of "config" and produce the same (working) + // output. + let resource_name = config.resource_name; + let resource_as_snake = config.resource_as_snake; + let authz_resource = config.resource_authz; + let model_resource = config.resource_model; + let authz_path_types = config.authz_path_types; + let authz_path_values = config.authz_path_values; let ( parent_resource_name, @@ -102,42 +129,19 @@ pub fn lookup_resource(input: TokenStream) -> Result { parent_authz, parent_authz_type, authz_ancestors_values_assign_lookup, - ) = match config.ancestors.last() { - Some(parent_resource_name) => { - let parent_snake_str = - heck::AsSnakeCase(parent_resource_name).to_string(); - let parent_id = format_ident!("{}_id", parent_snake_str,); - let parent_resource_name = - format_ident!("{}", parent_resource_name); - let parent_authz_type = quote! { &authz::#parent_resource_name }; - let authz_parent = format_ident!("authz_{}", parent_snake_str); - let parent_lookup_arg = - quote! { #authz_parent: #parent_authz_type, }; - let parent_lookup_arg_value = quote! { &#authz_parent , }; - let parent_lookup_arg_value_deref = quote! { #authz_parent , }; - let parent_filter = - quote! { .filter(dsl::#parent_id.eq( #authz_parent.id())) }; - let authz_ancestors_values_assign = quote! { - let (#authz_ancestors_values _) = - #parent_resource_name::lookup_by_id_no_authz( - _opctx, datastore, db_row.#parent_id - ).await?; - }; - let authz_ancestors_values_assign_lookup = quote! { - let (#authz_ancestors_values) = parent.lookup().await?; - }; - let parent_authz = &authz_ancestors_values_vec - [authz_ancestors_values_vec.len() - 1]; + ) = match config.parent { + Some(parent) => { + let parent_authz_arg = parent.resource_authz_name; ( - parent_resource_name, - parent_lookup_arg, - parent_lookup_arg_value, - parent_lookup_arg_value_deref, - parent_filter, - authz_ancestors_values_assign, - quote! { #parent_authz }, - parent_authz_type, - authz_ancestors_values_assign_lookup, + parent.resource_name, + parent.lookup_arg, + parent.lookup_arg_value, + parent.lookup_arg_value_deref, + parent.lookup_filter, + parent.ancestors_values_assign, + quote! { #parent_authz_arg, }, + parent.resource_authz_type, + parent.ancestors_values_assign_lookup, ) } None => ( @@ -147,8 +151,8 @@ pub fn lookup_resource(input: TokenStream) -> Result { quote! {}, quote! {}, quote! {}, - quote! { authz::FLEET, }, - quote! { &authz::Fleet }, + quote! { &authz::FLEET, }, + quote! { authz::Fleet }, quote! {}, ), }; @@ -162,25 +166,25 @@ pub fn lookup_resource(input: TokenStream) -> Result { }; let child_names: Vec<_> = - config.children.iter().map(|c| format_ident!("{}", c)).collect(); + config.child_resources.iter().map(|c| format_ident!("{}", c)).collect(); let child_by_names: Vec<_> = config - .children + .child_resources .iter() .map(|c| format_ident!("{}_name", heck::AsSnakeCase(c).to_string())) .collect(); let doc_struct = format!( "Selects a resource of type {} (or any of its children, using the \ functions on this struct) for lookup or fetch", - &config.name, + resource_name, ); let child_docs: Vec<_> = config - .children + .child_resources .iter() .map(|child| { format!( "Select a resource of type {} within this {}, \ identified by its name", - child, &config.name + child, resource_name, ) }) .collect(); @@ -206,7 +210,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { /// Build the `authz` object for this resource fn make_authz( - authz_parent: #parent_authz_type, + authz_parent: &#parent_authz_type, db_row: &#model_resource, lookup_type: LookupType, ) -> authz::#resource_name { @@ -222,7 +226,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { /// This is equivalent to `fetch_for(authz::Action::Read)`. pub async fn fetch( &self, - ) -> LookupResult<(#authz_path_types #model_resource)> { + ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { self.fetch_for(authz::Action::Read).await } @@ -238,7 +242,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { pub async fn fetch_for( &self, action: authz::Action, - ) -> LookupResult<(#authz_path_types #model_resource)> { + ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = &lookup.datastore; @@ -253,7 +257,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { *name, action, ).await?; - Ok((#authz_path_values db_row)) + Ok((#(#authz_path_values,)* db_row)) } Key::Id(_, id) => { Self::fetch_by_id_for( @@ -279,12 +283,12 @@ pub fn lookup_resource(input: TokenStream) -> Result { pub async fn lookup_for( &self, action: authz::Action, - ) -> LookupResult<(#authz_path_types)> { + ) -> LookupResult<(#(#authz_path_types,)*)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; - let (#authz_path_values) = self.lookup().await?; + let (#(#authz_path_values,)*) = self.lookup().await?; opctx.authorize(action, &authz_self).await?; - Ok((#authz_path_values)) + Ok((#(#authz_path_values,)*)) } /// Fetch the "authz" objects for the selected resource and all its @@ -297,7 +301,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { // lookup_for(). It's exposed in a safer way via lookup_for(). async fn lookup( &self, - ) -> LookupResult<(#authz_path_types)> { + ) -> LookupResult<(#(#authz_path_types,)*)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = &lookup.datastore; @@ -320,7 +324,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { #parent_lookup_arg_value *name ).await?; - Ok((#authz_path_values)) + Ok((#(#authz_path_values,)*)) } Key::Id(_, id) => { // When doing a by-id lookup, we start directly with the @@ -334,13 +338,13 @@ pub fn lookup_resource(input: TokenStream) -> Result { // TODO-performance Instead of doing database queries at // each level of recursion, we could be building up one // big "join" query and hit the database just once. - let (#authz_path_values _) = + let (#(#authz_path_values,)* _) = Self::lookup_by_id_no_authz( opctx, datastore, *id ).await?; - Ok((#authz_path_values)) + Ok((#(#authz_path_values,)*)) } } } @@ -358,7 +362,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { #parent_lookup_arg name: &Name, action: authz::Action, - ) -> LookupResult<(#authz_resource #model_resource)> { + ) -> LookupResult<(#authz_resource, #model_resource)> { let (authz_self, db_row) = Self::lookup_by_name_no_authz( opctx, datastore, @@ -381,7 +385,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { datastore: &DataStore, #parent_lookup_arg name: &Name, - ) -> LookupResult<(#authz_resource #model_resource)> { + ) -> LookupResult<(#authz_resource, #model_resource)> { use db::schema::#resource_as_snake::dsl; // TODO-security See the note about pool_authorized() below. @@ -404,7 +408,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { }) .map(|db_row| {( Self::make_authz( - &#parent_authz + #parent_authz &db_row, LookupType::ByName(name.as_str().to_string()) ), @@ -423,14 +427,15 @@ pub fn lookup_resource(input: TokenStream) -> Result { datastore: &DataStore, id: Uuid, action: authz::Action, - ) -> LookupResult<(#authz_path_types #model_resource)> { - let (#authz_path_values db_row) = Self::lookup_by_id_no_authz( - opctx, - datastore, - id - ).await?; + ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { + let (#(#authz_path_values,)* db_row) = + Self::lookup_by_id_no_authz( + opctx, + datastore, + id + ).await?; opctx.authorize(action, &authz_self).await?; - Ok((#authz_path_values db_row)) + Ok((#(#authz_path_values,)* db_row)) } /// Lowest-level function for looking up a resource in the database @@ -444,7 +449,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { _opctx: &OpContext, datastore: &DataStore, id: Uuid, - ) -> LookupResult<(#authz_path_types #model_resource)> { + ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { use db::schema::#resource_as_snake::dsl; // TODO-security This could use pool_authorized() instead. @@ -475,7 +480,7 @@ pub fn lookup_resource(input: TokenStream) -> Result { &db_row, LookupType::ById(id) ); - Ok((#authz_path_values db_row)) + Ok((#(#authz_path_values,)* db_row)) } #( @@ -494,3 +499,109 @@ pub fn lookup_resource(input: TokenStream) -> Result { } }) } + +fn configure(input: Input) -> Config { + let resource_name = format_ident!("{}", input.name); + // XXX-dap TODO I removed a comma from resource_authz + let resource_authz = quote! { authz::#resource_name }; + let resource_model = quote! { model::#resource_name }; + + // XXX-dap TODO I removed a comma from these elements + // XXX-dap TODO Can we just make this an array of the PascalCase + // identifiers? + let mut authz_path_types: Vec<_> = input + .ancestors + .iter() + .map(|a| { + let name = format_ident!("{}", a); + quote! { authz::#name } + }) + .collect(); + authz_path_types.push(resource_authz.clone()); + + // XXX-dap TODO I removed a comma from these elements + // (authz_ancestors_values and authz_path_values) + let authz_ancestors_values: Vec<_> = input + .ancestors + .iter() + .map(|a| format_ident!("authz_{}", heck::AsSnakeCase(&a).to_string())) + .collect(); + let mut authz_path_values = authz_ancestors_values.clone(); + // XXX-dap TODO replace authz_self with resource_authz? + authz_path_values.push(format_ident!("authz_self")); + + let parent = input.ancestors.last().map(|parent_resource_name| { + // XXX working here + let resource_name = format_ident!("{}", parent_resource_name); + let resource_snake_name = + heck::AsSnakeCase(parent_resource_name).to_string(); + let parent_id = format_ident!("{}_id", resource_snake_name); + let resource_authz_type = quote! { authz::#resource_name }; + let resource_authz_name = + format_ident!("authz_{}", resource_snake_name); + let lookup_filter = + quote! { .filter(dsl::#parent_id.eq(#resource_authz_name.id())) }; + let ancestors_values_assign = quote! { + let (#(#authz_ancestors_values,)* _) = + #resource_name::lookup_by_id_no_authz( + _opctx, datastore, db_row.#parent_id + ).await?; + }; + Parent { + resource_name, + lookup_arg: quote! { #resource_authz_name: &#resource_authz_type, }, + lookup_arg_value: quote! { &#resource_authz_name, }, + lookup_arg_value_deref: quote! { #resource_authz_name, }, + lookup_filter, + resource_authz_type, + resource_authz_name, + ancestors_values_assign, + ancestors_values_assign_lookup: quote! { + let (#(#authz_ancestors_values,)*) = parent.lookup().await?; + }, + } + }); + + Config { + resource_name, + resource_as_snake: format_ident!( + "{}", + heck::AsSnakeCase(&input.name).to_string() + ), + resource_authz, + resource_model, + authz_kind: input.authz_kind, + authz_path_types: authz_path_types, + authz_path_values, + child_resources: input.children, + parent, + } +} + +#[cfg(test)] +#[test] +fn test_lookup() { + // let output = lookup_resource( + // quote! { + // name = "Organization", + // ancestors = [], + // children = [ "Project" ], + // authz_kind = Typed + // } + // .into(), + // ) + // .unwrap(); + // println!("{}", output); + + let output = lookup_resource( + quote! { + name = "Project", + ancestors = ["Organization"], + children = [ "Disk", "Instance" ], + authz_kind = Typed + } + .into(), + ) + .unwrap(); + println!("{}", output); +} From cf6591eead77fd34d29c24cdf6dbbc6c5321d635 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 13:29:19 -0700 Subject: [PATCH 44/59] more extraction --- nexus/src/db/db-macros/src/lookup.rs | 222 ++++++++++++++++----------- 1 file changed, 135 insertions(+), 87 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 6f83e5a1a3c..557a23a142f 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -34,7 +34,7 @@ pub struct Input { /// /// This ought to be made more uniform with more typed authz objects, but that's /// not the way they work today. -#[derive(serde::Deserialize)] +#[derive(Clone, serde::Deserialize)] // XXX-dap enum AuthzKind { /// The authz object is constructed using /// `authz_parent.child_generic(ResourceType, Uuid, LookupType)` @@ -50,6 +50,7 @@ enum AuthzKind { /// This is all computable from [`Input`]. The purpose is to put the various /// output strings that need to appear in various chunks of output into one /// place with uniform names and documentation. +#[derive(Clone)] // XXX-dap pub struct Config { // The resource itself /// PascalCase (raw input) name of the resource we're generating @@ -61,10 +62,10 @@ pub struct Config { resource_as_snake: syn::Ident, /// name of the `authz` type for this resource (e.g., `authz::Project`) - resource_authz: TokenStream, + resource_authz_type: TokenStream, /// name of the `model` type for this resource (e.g., `db::model::Project`) - resource_model: TokenStream, + resource_model_type: TokenStream, /// See [`AuthzKind`] authz_kind: AuthzKind, @@ -72,12 +73,12 @@ pub struct Config { // The path to the resource /// list of `authz` types for this resource and its parents /// (e.g., [`authz::Organization`, `authz::Project`]) - authz_path_types: Vec, + path_authz_types: Vec, /// list of identifiers used for the authz objects for this resource and its /// parents, in the same order as `authz_path_types` /// (e.g., [`authz_organization`, `authz_project`]) - authz_path_values: Vec, + path_authz_types: Vec, // Child resources /// list of names of child resources (PascalCase, raw input to the macro) @@ -90,6 +91,7 @@ pub struct Config { } // XXX-dap TODO-doc +#[derive(Clone)] // XXX-dap struct Parent { resource_name: syn::Ident, lookup_arg: TokenStream, @@ -108,95 +110,66 @@ pub fn lookup_resource( ) -> Result { let input = serde_tokenstream::from_tokenstream::(&raw_input)?; let config = configure(input); + Ok(generate(&config)) +} +fn generate(config: &Config) -> TokenStream { // XXX-dap TODO take another edit pass. For now, I'm going to try to assign // variables to the elements of "config" and produce the same (working) // output. - let resource_name = config.resource_name; - let resource_as_snake = config.resource_as_snake; - let authz_resource = config.resource_authz; - let model_resource = config.resource_model; - let authz_path_types = config.authz_path_types; - let authz_path_values = config.authz_path_values; + let config2 = config.clone(); + let resource_name = config2.resource_name; + let resource_as_snake = config2.resource_as_snake; + let authz_resource = config2.resource_authz_type; + let model_resource = config2.resource_model_type; + let authz_path_types = config2.path_authz_types; + let authz_path_values = config2.path_authz_types; let ( - parent_resource_name, parent_lookup_arg, parent_lookup_arg_value, parent_lookup_arg_value_deref, parent_filter, authz_ancestors_values_assign, parent_authz, - parent_authz_type, authz_ancestors_values_assign_lookup, - ) = match config.parent { + ) = match config2.parent { Some(parent) => { let parent_authz_arg = parent.resource_authz_name; ( - parent.resource_name, parent.lookup_arg, parent.lookup_arg_value, parent.lookup_arg_value_deref, parent.lookup_filter, parent.ancestors_values_assign, quote! { #parent_authz_arg, }, - parent.resource_authz_type, parent.ancestors_values_assign_lookup, ) } None => ( - format_ident!("Root"), quote! {}, quote! {}, quote! {}, quote! {}, quote! {}, quote! { &authz::FLEET, }, - quote! { authz::Fleet }, quote! {}, ), }; - let (mkauthz_func, mkauthz_arg) = match &config.authz_kind { - AuthzKind::Generic => ( - format_ident!("child_generic"), - quote! { ResourceType::#resource_name, }, - ), - AuthzKind::Typed => (resource_as_snake.clone(), quote! {}), - }; + let the_struct = generate_struct(&config); + let helper_authz = generate_authz_helper(&config); + let child_selectors = generate_child_selectors(&config); - let child_names: Vec<_> = - config.child_resources.iter().map(|c| format_ident!("{}", c)).collect(); - let child_by_names: Vec<_> = config - .child_resources - .iter() - .map(|c| format_ident!("{}_name", heck::AsSnakeCase(c).to_string())) - .collect(); - let doc_struct = format!( - "Selects a resource of type {} (or any of its children, using the \ - functions on this struct) for lookup or fetch", - resource_name, - ); - let child_docs: Vec<_> = config - .child_resources - .iter() - .map(|child| { - format!( - "Select a resource of type {} within this {}, \ - identified by its name", - child, resource_name, - ) - }) - .collect(); - - Ok(quote! { - #[doc = #doc_struct] - pub struct #resource_name<'a> { - key: Key<'a, #parent_resource_name<'a>> - } + quote! { + #the_struct impl<'a> #resource_name<'a> { - /// Getting the LookupPath for this lookup + #child_selectors + + #helper_authz + + /// Getting the [`LookupPath`] for this lookup /// /// This is used when we actually query the database. At that /// point, we need the `OpContext` and `DataStore` that are being @@ -208,19 +181,6 @@ pub fn lookup_resource( } } - /// Build the `authz` object for this resource - fn make_authz( - authz_parent: &#parent_authz_type, - db_row: &#model_resource, - lookup_type: LookupType, - ) -> authz::#resource_name { - authz_parent.#mkauthz_func( - #mkauthz_arg - db_row.id(), - lookup_type - ) - } - /// Fetch the record corresponding to the selected resource /// /// This is equivalent to `fetch_for(authz::Action::Read)`. @@ -482,22 +442,8 @@ pub fn lookup_resource( ); Ok((#(#authz_path_values,)* db_row)) } - - #( - #[doc = #child_docs] - pub fn #child_by_names<'b, 'c>(self, name: &'b Name) - -> #child_names<'c> - where - 'a: 'c, - 'b: 'c, - { - #child_names { - key: Key::Name(self, name) - } - } - )* } - }) + } } fn configure(input: Input) -> Config { @@ -568,16 +514,118 @@ fn configure(input: Input) -> Config { "{}", heck::AsSnakeCase(&input.name).to_string() ), - resource_authz, - resource_model, + resource_authz_type: resource_authz, + resource_model_type: resource_model, authz_kind: input.authz_kind, - authz_path_types: authz_path_types, - authz_path_values, + path_authz_types: authz_path_types, + path_authz_types: authz_path_values, child_resources: input.children, parent, } } +/// Generates the struct definition for this resource +fn generate_struct(config: &Config) -> TokenStream { + let root_sym = format_ident!("Root"); + let resource_name = &config.resource_name; + let parent_resource_name = config + .parent + .as_ref() + .map(|p| &p.resource_name) + .unwrap_or_else(|| &root_sym); + let doc_struct = format!( + "Selects a resource of type {} (or any of its children, using the \ + functions on this struct) for lookup or fetch", + config.resource_name.to_string(), + ); + + quote! { + #[doc = #doc_struct] + pub struct #resource_name<'a> { + key: Key<'a, #parent_resource_name<'a>> + } + } +} + +/// Generates the child selectors for this resource +/// +/// For example, for the "Project" resource with child resources "Instance" and +/// "Disk", this will generate the `Project::instance_name()` and +/// `Project::disk_name()` functions. +fn generate_child_selectors(config: &Config) -> TokenStream { + let child_resource_types: Vec<_> = + config.child_resources.iter().map(|c| format_ident!("{}", c)).collect(); + let child_selector_fn_names: Vec<_> = config + .child_resources + .iter() + .map(|c| format_ident!("{}_name", heck::AsSnakeCase(c).to_string())) + .collect(); + let child_selector_fn_docs: Vec<_> = config + .child_resources + .iter() + .map(|child| { + format!( + "Select a resource of type {} within this {}, \ + identified by its name", + child, config.resource_name, + ) + }) + .collect(); + + quote! { + #( + #[doc = #child_selector_fn_docs] + pub fn #child_selector_fn_names<'b, 'c>( + self, + name: &'b Name + ) -> #child_resource_types<'c> + where + 'a: 'c, + 'b: 'c, + { + #child_resource_types { + key: Key::Name(self, name), + } + } + )* + } +} + +/// Generates the `make_authz()` helper function for this resource +fn generate_authz_helper(config: &Config) -> TokenStream { + let fleet = quote! { authz::Fleet }; + let resource_name = &config.resource_name; + let resource_authz_type = &config.resource_authz_type; + let resource_model = &config.resource_model_type; + let parent_authz_type = config + .parent + .as_ref() + .map(|p| &p.resource_authz_type) + .unwrap_or(&fleet); + let (mkauthz_func, mkauthz_arg) = match &config.authz_kind { + AuthzKind::Generic => ( + format_ident!("child_generic"), + quote! { ResourceType::#resource_name, }, + ), + AuthzKind::Typed => (config.resource_as_snake.clone(), quote! {}), + }; + + quote! { + /// Build the `authz` object for this resource + fn make_authz( + authz_parent: &#parent_authz_type, + db_row: &#resource_model, + lookup_type: LookupType, + ) -> #resource_authz_type { + authz_parent.#mkauthz_func( + #mkauthz_arg + db_row.id(), + lookup_type + ) + } + } +} + #[cfg(test)] #[test] fn test_lookup() { From 718b0a000b4a90c3f2375815ca31fa0ed3cb3a7a Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 13:38:47 -0700 Subject: [PATCH 45/59] more cleanup --- nexus/src/db/db-macros/src/lookup.rs | 73 ++++++++++++++++------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 557a23a142f..d540115815e 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -78,7 +78,7 @@ pub struct Config { /// list of identifiers used for the authz objects for this resource and its /// parents, in the same order as `authz_path_types` /// (e.g., [`authz_organization`, `authz_project`]) - path_authz_types: Vec, + path_authz_names: Vec, // Child resources /// list of names of child resources (PascalCase, raw input to the macro) @@ -123,7 +123,7 @@ fn generate(config: &Config) -> TokenStream { let authz_resource = config2.resource_authz_type; let model_resource = config2.resource_model_type; let authz_path_types = config2.path_authz_types; - let authz_path_values = config2.path_authz_types; + let authz_path_values = config2.path_authz_names; let ( parent_lookup_arg, @@ -158,7 +158,7 @@ fn generate(config: &Config) -> TokenStream { }; let the_struct = generate_struct(&config); - let helper_authz = generate_authz_helper(&config); + let misc_helpers = generate_misc_helpers(&config); let child_selectors = generate_child_selectors(&config); quote! { @@ -167,19 +167,7 @@ fn generate(config: &Config) -> TokenStream { impl<'a> #resource_name<'a> { #child_selectors - #helper_authz - - /// Getting the [`LookupPath`] for this lookup - /// - /// This is used when we actually query the database. At that - /// point, we need the `OpContext` and `DataStore` that are being - /// used for this lookup. - fn lookup_root(&self) -> &LookupPath<'a> { - match &self.key { - Key::Name(parent, _) => parent.lookup_root(), - Key::Id(root, _) => root.lookup_root(), - } - } + #misc_helpers /// Fetch the record corresponding to the selected resource /// @@ -449,13 +437,13 @@ fn generate(config: &Config) -> TokenStream { fn configure(input: Input) -> Config { let resource_name = format_ident!("{}", input.name); // XXX-dap TODO I removed a comma from resource_authz - let resource_authz = quote! { authz::#resource_name }; - let resource_model = quote! { model::#resource_name }; + let resource_authz_type = quote! { authz::#resource_name }; + let resource_model_type = quote! { model::#resource_name }; // XXX-dap TODO I removed a comma from these elements // XXX-dap TODO Can we just make this an array of the PascalCase // identifiers? - let mut authz_path_types: Vec<_> = input + let mut path_authz_types: Vec<_> = input .ancestors .iter() .map(|a| { @@ -463,7 +451,7 @@ fn configure(input: Input) -> Config { quote! { authz::#name } }) .collect(); - authz_path_types.push(resource_authz.clone()); + path_authz_types.push(resource_authz_type.clone()); // XXX-dap TODO I removed a comma from these elements // (authz_ancestors_values and authz_path_values) @@ -472,9 +460,9 @@ fn configure(input: Input) -> Config { .iter() .map(|a| format_ident!("authz_{}", heck::AsSnakeCase(&a).to_string())) .collect(); - let mut authz_path_values = authz_ancestors_values.clone(); + let mut path_authz_names = authz_ancestors_values.clone(); // XXX-dap TODO replace authz_self with resource_authz? - authz_path_values.push(format_ident!("authz_self")); + path_authz_names.push(format_ident!("authz_self")); let parent = input.ancestors.last().map(|parent_resource_name| { // XXX working here @@ -514,11 +502,11 @@ fn configure(input: Input) -> Config { "{}", heck::AsSnakeCase(&input.name).to_string() ), - resource_authz_type: resource_authz, - resource_model_type: resource_model, + resource_authz_type, + resource_model_type, authz_kind: input.authz_kind, - path_authz_types: authz_path_types, - path_authz_types: authz_path_values, + path_authz_types, + path_authz_names, child_resources: input.children, parent, } @@ -591,17 +579,26 @@ fn generate_child_selectors(config: &Config) -> TokenStream { } } -/// Generates the `make_authz()` helper function for this resource -fn generate_authz_helper(config: &Config) -> TokenStream { - let fleet = quote! { authz::Fleet }; +/// Generates the simple helper functions for this resource +fn generate_misc_helpers(config: &Config) -> TokenStream { + let fleet_type = quote! { authz::Fleet }; let resource_name = &config.resource_name; let resource_authz_type = &config.resource_authz_type; - let resource_model = &config.resource_model_type; + let resource_model_type = &config.resource_model_type; let parent_authz_type = config .parent .as_ref() .map(|p| &p.resource_authz_type) - .unwrap_or(&fleet); + .unwrap_or(&fleet_type); + + // Given a parent authz type, when we want to construct an authz object for + // a child resource, there are two different patterns. We need to pick the + // right one. For "typed" resources (`AuthzKind::Typed`), the parent + // resource has a method with the snake case name of the child resource. + // For example: `authz_organization.project()`. For "generic" resources + // (`AuthzKind::Generic`), the parent has a function called `child_generic` + // that's used to construct all child resources, and there's an extra + // `ResourceType` argument to say what resource it is. let (mkauthz_func, mkauthz_arg) = match &config.authz_kind { AuthzKind::Generic => ( format_ident!("child_generic"), @@ -614,7 +611,7 @@ fn generate_authz_helper(config: &Config) -> TokenStream { /// Build the `authz` object for this resource fn make_authz( authz_parent: &#parent_authz_type, - db_row: &#resource_model, + db_row: &#resource_model_type, lookup_type: LookupType, ) -> #resource_authz_type { authz_parent.#mkauthz_func( @@ -623,6 +620,18 @@ fn generate_authz_helper(config: &Config) -> TokenStream { lookup_type ) } + + /// Getting the [`LookupPath`] for this lookup + /// + /// This is used when we actually query the database. At that + /// point, we need the `OpContext` and `DataStore` that are being + /// used for this lookup. + fn lookup_root(&self) -> &LookupPath<'a> { + match &self.key { + Key::Name(parent, _) => parent.lookup_root(), + Key::Id(root, _) => root.lookup_root(), + } + } } } From ce6066ddb1c536a2c0d4b59736f2cf650d5d9ce3 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 14:35:17 -0700 Subject: [PATCH 46/59] more cleanup --- nexus/src/db/db-macros/src/lookup.rs | 702 ++++++++++++++------------- 1 file changed, 352 insertions(+), 350 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index d540115815e..a0287e91e60 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -34,7 +34,7 @@ pub struct Input { /// /// This ought to be made more uniform with more typed authz objects, but that's /// not the way they work today. -#[derive(Clone, serde::Deserialize)] // XXX-dap +#[derive(serde::Deserialize)] enum AuthzKind { /// The authz object is constructed using /// `authz_parent.child_generic(ResourceType, Uuid, LookupType)` @@ -50,7 +50,6 @@ enum AuthzKind { /// This is all computable from [`Input`]. The purpose is to put the various /// output strings that need to appear in various chunks of output into one /// place with uniform names and documentation. -#[derive(Clone)] // XXX-dap pub struct Config { // The resource itself /// PascalCase (raw input) name of the resource we're generating @@ -91,17 +90,11 @@ pub struct Config { } // XXX-dap TODO-doc -#[derive(Clone)] // XXX-dap struct Parent { resource_name: syn::Ident, - lookup_arg: TokenStream, - lookup_arg_value: TokenStream, - lookup_arg_value_deref: TokenStream, - lookup_filter: TokenStream, + resource_as_snake: String, resource_authz_type: TokenStream, resource_authz_name: syn::Ident, - ancestors_values_assign: TokenStream, - ancestors_values_assign_lookup: TokenStream, } /// Implementation of [`lookup_resource!]'. @@ -114,52 +107,12 @@ pub fn lookup_resource( } fn generate(config: &Config) -> TokenStream { - // XXX-dap TODO take another edit pass. For now, I'm going to try to assign - // variables to the elements of "config" and produce the same (working) - // output. - let config2 = config.clone(); - let resource_name = config2.resource_name; - let resource_as_snake = config2.resource_as_snake; - let authz_resource = config2.resource_authz_type; - let model_resource = config2.resource_model_type; - let authz_path_types = config2.path_authz_types; - let authz_path_values = config2.path_authz_names; - - let ( - parent_lookup_arg, - parent_lookup_arg_value, - parent_lookup_arg_value_deref, - parent_filter, - authz_ancestors_values_assign, - parent_authz, - authz_ancestors_values_assign_lookup, - ) = match config2.parent { - Some(parent) => { - let parent_authz_arg = parent.resource_authz_name; - ( - parent.lookup_arg, - parent.lookup_arg_value, - parent.lookup_arg_value_deref, - parent.lookup_filter, - parent.ancestors_values_assign, - quote! { #parent_authz_arg, }, - parent.ancestors_values_assign_lookup, - ) - } - None => ( - quote! {}, - quote! {}, - quote! {}, - quote! {}, - quote! {}, - quote! { &authz::FLEET, }, - quote! {}, - ), - }; - + let resource_name = &config.resource_name; let the_struct = generate_struct(&config); let misc_helpers = generate_misc_helpers(&config); let child_selectors = generate_child_selectors(&config); + let lookup_methods = generate_lookup_methods(&config); + let database_functions = generate_database_functions(&config); quote! { #the_struct @@ -167,280 +120,20 @@ fn generate(config: &Config) -> TokenStream { impl<'a> #resource_name<'a> { #child_selectors - #misc_helpers - - /// Fetch the record corresponding to the selected resource - /// - /// This is equivalent to `fetch_for(authz::Action::Read)`. - pub async fn fetch( - &self, - ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { - self.fetch_for(authz::Action::Read).await - } + #lookup_methods - /// Fetch the record corresponding to the selected resource and - /// check whether the caller is allowed to do the specified `action` - /// - /// The return value is a tuple that also includes the `authz` - /// objects for all resources along the path to this one (i.e., all - /// parent resources) and the authz object for this resource itself. - /// These objects are useful for identifying those resources by - /// id, for doing other authz checks, or for looking up related - /// objects. - pub async fn fetch_for( - &self, - action: authz::Action, - ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = &lookup.datastore; - - match &self.key { - Key::Name(parent, name) => { - #authz_ancestors_values_assign_lookup - let (authz_self, db_row) = Self::fetch_by_name_for( - opctx, - datastore, - #parent_lookup_arg_value - *name, - action, - ).await?; - Ok((#(#authz_path_values,)* db_row)) - } - Key::Id(_, id) => { - Self::fetch_by_id_for( - opctx, - datastore, - *id, - action, - ).await - } - - } - } - - /// Fetch an `authz` object for the selected resource and check - /// whether the caller is allowed to do the specified `action` - /// - /// The return value is a tuple that also includes the `authz` - /// objects for all resources along the path to this one (i.e., all - /// parent resources) and the authz object for this resource itself. - /// These objects are useful for identifying those resources by - /// id, for doing other authz checks, or for looking up related - /// objects. - pub async fn lookup_for( - &self, - action: authz::Action, - ) -> LookupResult<(#(#authz_path_types,)*)> { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let (#(#authz_path_values,)*) = self.lookup().await?; - opctx.authorize(action, &authz_self).await?; - Ok((#(#authz_path_values,)*)) - } - - /// Fetch the "authz" objects for the selected resource and all its - /// parents - /// - /// This function does not check whether the caller has permission - /// to read this information. That's why it's not `pub`. Outside - /// this module, you want `lookup_for(authz::Action)`. - // Do NOT make this function public. It's a helper for fetch() and - // lookup_for(). It's exposed in a safer way via lookup_for(). - async fn lookup( - &self, - ) -> LookupResult<(#(#authz_path_types,)*)> { - let lookup = self.lookup_root(); - let opctx = &lookup.opctx; - let datastore = &lookup.datastore; - - match &self.key { - Key::Name(parent, name) => { - // When doing a by-name lookup, we have to look up the - // parent first. Since this is recursive, we wind up - // hitting the database once for each item in the path, - // in order descending from the root of the tree. (So - // we'll look up Organization, then Project, then - // Instance, etc.) - // TODO-performance Instead of doing database queries at - // each level of recursion, we could be building up one - // big "join" query and hit the database just once. - #authz_ancestors_values_assign_lookup - let (authz_self, _) = Self::lookup_by_name_no_authz( - opctx, - datastore, - #parent_lookup_arg_value - *name - ).await?; - Ok((#(#authz_path_values,)*)) - } - Key::Id(_, id) => { - // When doing a by-id lookup, we start directly with the - // resource we're looking up. But we still want to - // return a full path of authz objects. So we look up - // the parent by id, then its parent, etc. Like the - // by-name case, we wind up hitting the database once - // for each item in the path, but in the reverse order. - // So we'll look up the Instance, then the Project, then - // the Organization. - // TODO-performance Instead of doing database queries at - // each level of recursion, we could be building up one - // big "join" query and hit the database just once. - let (#(#authz_path_values,)* _) = - Self::lookup_by_id_no_authz( - opctx, - datastore, - *id - ).await?; - Ok((#(#authz_path_values,)*)) - } - } - } - - /// Fetch the database row for a resource by doing a lookup by name, - /// possibly within a collection - /// - /// This function checks whether the caller has permissions to read - /// the requested data. However, it's not intended to be used - /// outside this module. See `fetch_for(authz::Action)`. - // Do NOT make this function public. - async fn fetch_by_name_for( - opctx: &OpContext, - datastore: &DataStore, - #parent_lookup_arg - name: &Name, - action: authz::Action, - ) -> LookupResult<(#authz_resource, #model_resource)> { - let (authz_self, db_row) = Self::lookup_by_name_no_authz( - opctx, - datastore, - #parent_lookup_arg_value_deref - name - ).await?; - opctx.authorize(action, &authz_self).await?; - Ok((authz_self, db_row)) - } - - /// Lowest-level function for looking up a resource in the database - /// by name, possibly within a collection - /// - /// This function does not check whether the caller has permission - /// to read this information. That's why it's not `pub`. Outside - /// this module, you want `fetch()` or `lookup_for(authz::Action)`. - // Do NOT make this function public. - async fn lookup_by_name_no_authz( - _opctx: &OpContext, - datastore: &DataStore, - #parent_lookup_arg - name: &Name, - ) -> LookupResult<(#authz_resource, #model_resource)> { - use db::schema::#resource_as_snake::dsl; - - // TODO-security See the note about pool_authorized() below. - let conn = datastore.pool(); - dsl::#resource_as_snake - .filter(dsl::time_deleted.is_null()) - .filter(dsl::name.eq(name.clone())) - #parent_filter - .select(model::#resource_name::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::#resource_name, - LookupType::ByName(name.as_str().to_string()) - ) - ) - }) - .map(|db_row| {( - Self::make_authz( - #parent_authz - &db_row, - LookupType::ByName(name.as_str().to_string()) - ), - db_row - )}) - } - - /// Fetch the database row for a resource by doing a lookup by id - /// - /// This function checks whether the caller has permissions to read - /// the requested data. However, it's not intended to be used - /// outside this module. See `fetch_for(authz::Action)`. - // Do NOT make this function public. - async fn fetch_by_id_for( - opctx: &OpContext, - datastore: &DataStore, - id: Uuid, - action: authz::Action, - ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { - let (#(#authz_path_values,)* db_row) = - Self::lookup_by_id_no_authz( - opctx, - datastore, - id - ).await?; - opctx.authorize(action, &authz_self).await?; - Ok((#(#authz_path_values,)* db_row)) - } + #misc_helpers - /// Lowest-level function for looking up a resource in the database - /// by id - /// - /// This function does not check whether the caller has permission - /// to read this information. That's why it's not `pub`. Outside - /// this module, you want `fetch()` or `lookup_for(authz::Action)`. - // Do NOT make this function public. - async fn lookup_by_id_no_authz( - _opctx: &OpContext, - datastore: &DataStore, - id: Uuid, - ) -> LookupResult<(#(#authz_path_types,)* #model_resource)> { - use db::schema::#resource_as_snake::dsl; - - // TODO-security This could use pool_authorized() instead. - // However, it will change the response code for this case: - // unauthenticated users will get a 401 rather than a 404 - // because we'll kick them out sooner than we used to -- they - // won't even be able to make this database query. That's a - // good thing but this change can be deferred to a follow-up PR. - let conn = datastore.pool(); - let db_row = dsl::#resource_as_snake - .filter(dsl::time_deleted.is_null()) - .filter(dsl::id.eq(id)) - .select(model::#resource_name::as_select()) - .get_result_async(conn) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::#resource_name, - LookupType::ById(id) - ) - ) - })?; - #authz_ancestors_values_assign - let authz_self = Self::make_authz( - &#parent_authz - &db_row, - LookupType::ById(id) - ); - Ok((#(#authz_path_values,)* db_row)) - } + #database_functions } } } fn configure(input: Input) -> Config { let resource_name = format_ident!("{}", input.name); - // XXX-dap TODO I removed a comma from resource_authz let resource_authz_type = quote! { authz::#resource_name }; let resource_model_type = quote! { model::#resource_name }; - // XXX-dap TODO I removed a comma from these elements // XXX-dap TODO Can we just make this an array of the PascalCase // identifiers? let mut path_authz_types: Vec<_> = input @@ -453,8 +146,6 @@ fn configure(input: Input) -> Config { .collect(); path_authz_types.push(resource_authz_type.clone()); - // XXX-dap TODO I removed a comma from these elements - // (authz_ancestors_values and authz_path_values) let authz_ancestors_values: Vec<_> = input .ancestors .iter() @@ -465,34 +156,16 @@ fn configure(input: Input) -> Config { path_authz_names.push(format_ident!("authz_self")); let parent = input.ancestors.last().map(|parent_resource_name| { - // XXX working here let resource_name = format_ident!("{}", parent_resource_name); - let resource_snake_name = + let resource_as_snake = heck::AsSnakeCase(parent_resource_name).to_string(); - let parent_id = format_ident!("{}_id", resource_snake_name); let resource_authz_type = quote! { authz::#resource_name }; - let resource_authz_name = - format_ident!("authz_{}", resource_snake_name); - let lookup_filter = - quote! { .filter(dsl::#parent_id.eq(#resource_authz_name.id())) }; - let ancestors_values_assign = quote! { - let (#(#authz_ancestors_values,)* _) = - #resource_name::lookup_by_id_no_authz( - _opctx, datastore, db_row.#parent_id - ).await?; - }; + let resource_authz_name = format_ident!("authz_{}", resource_as_snake); Parent { resource_name, - lookup_arg: quote! { #resource_authz_name: &#resource_authz_type, }, - lookup_arg_value: quote! { &#resource_authz_name, }, - lookup_arg_value_deref: quote! { #resource_authz_name, }, - lookup_filter, + resource_as_snake, resource_authz_type, resource_authz_name, - ancestors_values_assign, - ancestors_values_assign_lookup: quote! { - let (#(#authz_ancestors_values,)*) = parent.lookup().await?; - }, } }); @@ -635,20 +308,349 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { } } +/// Generates the lookup-related methods, including the public ones (`fetch()`, +/// `fetch_for()`, and `lookup_for()`) and the private helper (`lookup()`). +fn generate_lookup_methods(config: &Config) -> TokenStream { + let path_authz_names = &config.path_authz_names; + let path_authz_types = &config.path_authz_types; + let resource_model_type = &config.resource_model_type; + let (ancestors_authz_names_assign, parent_lookup_arg_actual) = + if let Some(p) = &config.parent { + let nancestors = config.path_authz_names.len() - 1; + let ancestors_authz_names = &config.path_authz_names[0..nancestors]; + let parent_authz_name = &p.resource_authz_name; + ( + quote! { + let (#(#ancestors_authz_names,)*) = parent.lookup().await?; + }, + quote! { &#parent_authz_name, }, + ) + } else { + (quote! {}, quote! {}) + }; + + quote! { + /// Fetch the record corresponding to the selected resource + /// + /// This is equivalent to `fetch_for(authz::Action::Read)`. + pub async fn fetch( + &self, + ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + self.fetch_for(authz::Action::Read).await + } + + /// Fetch the record corresponding to the selected resource and + /// check whether the caller is allowed to do the specified `action` + /// + /// The return value is a tuple that also includes the `authz` + /// objects for all resources along the path to this one (i.e., all + /// parent resources) and the authz object for this resource itself. + /// These objects are useful for identifying those resources by + /// id, for doing other authz checks, or for looking up related + /// objects. + pub async fn fetch_for( + &self, + action: authz::Action, + ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = &lookup.datastore; + + match &self.key { + Key::Name(parent, name) => { + #ancestors_authz_names_assign + let (authz_self, db_row) = Self::fetch_by_name_for( + opctx, + datastore, + #parent_lookup_arg_actual + *name, + action, + ).await?; + Ok((#(#path_authz_names,)* db_row)) + } + Key::Id(_, id) => { + Self::fetch_by_id_for( + opctx, + datastore, + *id, + action, + ).await + } + } + } + + /// Fetch an `authz` object for the selected resource and check + /// whether the caller is allowed to do the specified `action` + /// + /// The return value is a tuple that also includes the `authz` + /// objects for all resources along the path to this one (i.e., all + /// parent resources) and the authz object for this resource itself. + /// These objects are useful for identifying those resources by + /// id, for doing other authz checks, or for looking up related + /// objects. + pub async fn lookup_for( + &self, + action: authz::Action, + ) -> LookupResult<(#(#path_authz_types,)*)> { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let (#(#path_authz_names,)*) = self.lookup().await?; + opctx.authorize(action, &authz_self).await?; + Ok((#(#path_authz_names,)*)) + } + + /// Fetch the "authz" objects for the selected resource and all its + /// parents + /// + /// This function does not check whether the caller has permission + /// to read this information. That's why it's not `pub`. Outside + /// this module, you want `lookup_for(authz::Action)`. + // Do NOT make this function public. It's a helper for fetch() and + // lookup_for(). It's exposed in a safer way via lookup_for(). + async fn lookup( + &self, + ) -> LookupResult<(#(#path_authz_types,)*)> { + let lookup = self.lookup_root(); + let opctx = &lookup.opctx; + let datastore = &lookup.datastore; + + match &self.key { + Key::Name(parent, name) => { + // When doing a by-name lookup, we have to look up the + // parent first. Since this is recursive, we wind up + // hitting the database once for each item in the path, + // in order descending from the root of the tree. (So + // we'll look up Organization, then Project, then + // Instance, etc.) + // TODO-performance Instead of doing database queries at + // each level of recursion, we could be building up one + // big "join" query and hit the database just once. + #ancestors_authz_names_assign + let (authz_self, _) = Self::lookup_by_name_no_authz( + opctx, + datastore, + #parent_lookup_arg_actual + *name + ).await?; + Ok((#(#path_authz_names,)*)) + } + Key::Id(_, id) => { + // When doing a by-id lookup, we start directly with the + // resource we're looking up. But we still want to + // return a full path of authz objects. So we look up + // the parent by id, then its parent, etc. Like the + // by-name case, we wind up hitting the database once + // for each item in the path, but in the reverse order. + // So we'll look up the Instance, then the Project, then + // the Organization. + // TODO-performance Instead of doing database queries at + // each level of recursion, we could be building up one + // big "join" query and hit the database just once. + let (#(#path_authz_names,)* _) = + Self::lookup_by_id_no_authz( + opctx, + datastore, + *id + ).await?; + Ok((#(#path_authz_names,)*)) + } + } + } + } +} + +/// Generates low-level functions to fetch database records for this resource +/// +/// These are standalone functions, not methods. They operate on more primitive +/// objects than do the methods: authz objects (representing parent ids), names, +/// and ids. They also take the `opctx` and `datastore` directly as arguments. +fn generate_database_functions(config: &Config) -> TokenStream { + let resource_name = &config.resource_name; + let resource_authz_type = &config.resource_authz_type; + let resource_model_type = &config.resource_model_type; + let resource_as_snake = &config.resource_as_snake; + let path_authz_names = &config.path_authz_names; + let path_authz_types = &config.path_authz_types; + let ( + parent_lookup_arg_formal, + parent_lookup_arg_actual, + ancestors_authz_names_assign, + lookup_filter, + parent_authz_value, + ) = if let Some(p) = &config.parent { + let nancestors = config.path_authz_names.len() - 1; + let ancestors_authz_names = &config.path_authz_names[0..nancestors]; + let parent_resource_name = &p.resource_name; + let parent_authz_name = &p.resource_authz_name; + let parent_authz_type = &p.resource_authz_type; + let parent_id = format_ident!("{}_id", &p.resource_as_snake); + ( + quote! { #parent_authz_name: &#parent_authz_type, }, + quote! { #parent_authz_name, }, + quote! { + let (#(#ancestors_authz_names,)* _) = + #parent_resource_name::lookup_by_id_no_authz( + _opctx, datastore, db_row.#parent_id + ).await?; + }, + quote! { .filter(dsl::#parent_id.eq(#parent_authz_name.id())) }, + quote! { #parent_authz_name }, + ) + } else { + (quote! {}, quote! {}, quote! {}, quote! {}, quote! { &authz::FLEET }) + }; + + quote! { + /// Fetch the database row for a resource by doing a lookup by name, + /// possibly within a collection + /// + /// This function checks whether the caller has permissions to read + /// the requested data. However, it's not intended to be used + /// outside this module. See `fetch_for(authz::Action)`. + // Do NOT make this function public. + async fn fetch_by_name_for( + opctx: &OpContext, + datastore: &DataStore, + #parent_lookup_arg_formal + name: &Name, + action: authz::Action, + ) -> LookupResult<(#resource_authz_type, #resource_model_type)> { + let (authz_self, db_row) = Self::lookup_by_name_no_authz( + opctx, + datastore, + #parent_lookup_arg_actual + name + ).await?; + opctx.authorize(action, &authz_self).await?; + Ok((authz_self, db_row)) + } + + /// Lowest-level function for looking up a resource in the database + /// by name, possibly within a collection + /// + /// This function does not check whether the caller has permission + /// to read this information. That's why it's not `pub`. Outside + /// this module, you want `fetch()` or `lookup_for(authz::Action)`. + // Do NOT make this function public. + async fn lookup_by_name_no_authz( + _opctx: &OpContext, + datastore: &DataStore, + #parent_lookup_arg_formal + name: &Name, + ) -> LookupResult<(#resource_authz_type, #resource_model_type)> { + use db::schema::#resource_as_snake::dsl; + + // TODO-security See the note about pool_authorized() below. + let conn = datastore.pool(); + dsl::#resource_as_snake + .filter(dsl::time_deleted.is_null()) + .filter(dsl::name.eq(name.clone())) + #lookup_filter + .select(#resource_model_type::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::#resource_name, + LookupType::ByName(name.as_str().to_string()) + ) + ) + }) + .map(|db_row| {( + Self::make_authz( + #parent_authz_value, + &db_row, + LookupType::ByName(name.as_str().to_string()) + ), + db_row + )}) + } + + /// Fetch the database row for a resource by doing a lookup by id + /// + /// This function checks whether the caller has permissions to read + /// the requested data. However, it's not intended to be used + /// outside this module. See `fetch_for(authz::Action)`. + // Do NOT make this function public. + async fn fetch_by_id_for( + opctx: &OpContext, + datastore: &DataStore, + id: Uuid, + action: authz::Action, + ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + let (#(#path_authz_names,)* db_row) = + Self::lookup_by_id_no_authz( + opctx, + datastore, + id + ).await?; + opctx.authorize(action, &authz_self).await?; + Ok((#(#path_authz_names,)* db_row)) + } + + /// Lowest-level function for looking up a resource in the database + /// by id + /// + /// This function does not check whether the caller has permission + /// to read this information. That's why it's not `pub`. Outside + /// this module, you want `fetch()` or `lookup_for(authz::Action)`. + // Do NOT make this function public. + async fn lookup_by_id_no_authz( + _opctx: &OpContext, + datastore: &DataStore, + id: Uuid, + ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + use db::schema::#resource_as_snake::dsl; + + // TODO-security This could use pool_authorized() instead. + // However, it will change the response code for this case: + // unauthenticated users will get a 401 rather than a 404 + // because we'll kick them out sooner than we used to -- they + // won't even be able to make this database query. That's a + // good thing but this change can be deferred to a follow-up PR. + let conn = datastore.pool(); + let db_row = dsl::#resource_as_snake + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(id)) + .select(#resource_model_type::as_select()) + .get_result_async(conn) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::#resource_name, + LookupType::ById(id) + ) + ) + })?; + #ancestors_authz_names_assign + let authz_self = Self::make_authz( + &#parent_authz_value, + &db_row, + LookupType::ById(id) + ); + Ok((#(#path_authz_names,)* db_row)) + } + } +} + #[cfg(test)] #[test] -fn test_lookup() { - // let output = lookup_resource( - // quote! { - // name = "Organization", - // ancestors = [], - // children = [ "Project" ], - // authz_kind = Typed - // } - // .into(), - // ) - // .unwrap(); - // println!("{}", output); +fn test_lookup_dump() { + let output = lookup_resource( + quote! { + name = "Organization", + ancestors = [], + children = [ "Project" ], + authz_kind = Typed + } + .into(), + ) + .unwrap(); + println!("{}", output); let output = lookup_resource( quote! { From 2d5353d80a71be86c24a220d3052e6d05a14f20c Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:10:01 -0700 Subject: [PATCH 47/59] macro impl: commonize Resource --- nexus/src/db/db-macros/src/lookup.rs | 178 +++++++++++++-------------- 1 file changed, 83 insertions(+), 95 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index a0287e91e60..d424a2b49df 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -9,6 +9,10 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; +// +// INPUT (arguments to the macro) +// + /// Arguments for [`lookup_resource!`] // NOTE: this is only "pub" for the `cargo doc` link on [`lookup_resource!`]. #[derive(serde::Deserialize)] @@ -45,26 +49,19 @@ enum AuthzKind { Typed, } +// +// MACRO STATE +// + /// Configuration for [`lookup_resource`] and its helper functions /// /// This is all computable from [`Input`]. The purpose is to put the various /// output strings that need to appear in various chunks of output into one /// place with uniform names and documentation. pub struct Config { - // The resource itself - /// PascalCase (raw input) name of the resource we're generating - /// (e.g., `Project`") - resource_name: syn::Ident, - - /// Snake case name of the resource we're generating - /// (e.g., `project`) - resource_as_snake: syn::Ident, - - /// name of the `authz` type for this resource (e.g., `authz::Project`) - resource_authz_type: TokenStream, - - /// name of the `model` type for this resource (e.g., `db::model::Project`) - resource_model_type: TokenStream, + // The resource itself that we're generating + /// Basic information about the resource we're generating + resource: Resource, /// See [`AuthzKind`] authz_kind: AuthzKind, @@ -86,15 +83,33 @@ pub struct Config { // Parent resource, if any /// Information about the parent resource, if any - parent: Option, + parent: Option, } -// XXX-dap TODO-doc -struct Parent { - resource_name: syn::Ident, - resource_as_snake: String, - resource_authz_type: TokenStream, - resource_authz_name: syn::Ident, +/// Information about a resource (either the one we're generating or an +/// ancestor in the path) +struct Resource { + /// PascalCase resource name itself (e.g., `Project`) + name: syn::Ident, + /// snake_case resource name (e.g., `project`) + name_as_snake: String, + /// name of the `authz` type for this resource (e.g., `authz::Project`) + authz_type: TokenStream, + /// identifier for an authz object for this resource (e.g., `authz_project`) + authz_name: syn::Ident, + /// name of the `model` type for this resource (e.g., `db::model::Project`) + model_type: TokenStream, +} + +impl Resource { + fn for_name(name: &str) -> Resource { + let name_as_snake = heck::AsSnakeCase(&name).to_string(); + let name = format_ident!("{}", name); + let authz_name = format_ident!("authz_{}", name_as_snake); + let authz_type = quote! { authz::#name }; + let model_type = quote! { model::#name }; + Resource { name, authz_name, authz_type, name_as_snake, model_type } + } } /// Implementation of [`lookup_resource!]'. @@ -103,18 +118,15 @@ pub fn lookup_resource( ) -> Result { let input = serde_tokenstream::from_tokenstream::(&raw_input)?; let config = configure(input); - Ok(generate(&config)) -} -fn generate(config: &Config) -> TokenStream { - let resource_name = &config.resource_name; + let resource_name = &config.resource.name; let the_struct = generate_struct(&config); let misc_helpers = generate_misc_helpers(&config); let child_selectors = generate_child_selectors(&config); let lookup_methods = generate_lookup_methods(&config); let database_functions = generate_database_functions(&config); - quote! { + Ok(quote! { #the_struct impl<'a> #resource_name<'a> { @@ -126,13 +138,11 @@ fn generate(config: &Config) -> TokenStream { #database_functions } - } + }) } fn configure(input: Input) -> Config { - let resource_name = format_ident!("{}", input.name); - let resource_authz_type = quote! { authz::#resource_name }; - let resource_model_type = quote! { model::#resource_name }; + let resource = Resource::for_name(&input.name); // XXX-dap TODO Can we just make this an array of the PascalCase // identifiers? @@ -144,7 +154,7 @@ fn configure(input: Input) -> Config { quote! { authz::#name } }) .collect(); - path_authz_types.push(resource_authz_type.clone()); + path_authz_types.push(resource.authz_type.clone()); let authz_ancestors_values: Vec<_> = input .ancestors @@ -152,52 +162,28 @@ fn configure(input: Input) -> Config { .map(|a| format_ident!("authz_{}", heck::AsSnakeCase(&a).to_string())) .collect(); let mut path_authz_names = authz_ancestors_values.clone(); - // XXX-dap TODO replace authz_self with resource_authz? - path_authz_names.push(format_ident!("authz_self")); - - let parent = input.ancestors.last().map(|parent_resource_name| { - let resource_name = format_ident!("{}", parent_resource_name); - let resource_as_snake = - heck::AsSnakeCase(parent_resource_name).to_string(); - let resource_authz_type = quote! { authz::#resource_name }; - let resource_authz_name = format_ident!("authz_{}", resource_as_snake); - Parent { - resource_name, - resource_as_snake, - resource_authz_type, - resource_authz_name, - } - }); + path_authz_names.push(resource.authz_name.clone()); Config { - resource_name, - resource_as_snake: format_ident!( - "{}", - heck::AsSnakeCase(&input.name).to_string() - ), - resource_authz_type, - resource_model_type, + resource, authz_kind: input.authz_kind, path_authz_types, path_authz_names, + parent: input.ancestors.last().map(|s| Resource::for_name(&s)), child_resources: input.children, - parent, } } /// Generates the struct definition for this resource fn generate_struct(config: &Config) -> TokenStream { let root_sym = format_ident!("Root"); - let resource_name = &config.resource_name; - let parent_resource_name = config - .parent - .as_ref() - .map(|p| &p.resource_name) - .unwrap_or_else(|| &root_sym); + let resource_name = &config.resource.name; + let parent_resource_name = + config.parent.as_ref().map(|p| &p.name).unwrap_or_else(|| &root_sym); let doc_struct = format!( "Selects a resource of type {} (or any of its children, using the \ functions on this struct) for lookup or fetch", - config.resource_name.to_string(), + config.resource.name.to_string(), ); quote! { @@ -228,7 +214,7 @@ fn generate_child_selectors(config: &Config) -> TokenStream { format!( "Select a resource of type {} within this {}, \ identified by its name", - child, config.resource_name, + child, config.resource.name, ) }) .collect(); @@ -255,14 +241,11 @@ fn generate_child_selectors(config: &Config) -> TokenStream { /// Generates the simple helper functions for this resource fn generate_misc_helpers(config: &Config) -> TokenStream { let fleet_type = quote! { authz::Fleet }; - let resource_name = &config.resource_name; - let resource_authz_type = &config.resource_authz_type; - let resource_model_type = &config.resource_model_type; - let parent_authz_type = config - .parent - .as_ref() - .map(|p| &p.resource_authz_type) - .unwrap_or(&fleet_type); + let resource_name = &config.resource.name; + let resource_authz_type = &config.resource.authz_type; + let resource_model_type = &config.resource.model_type; + let parent_authz_type = + config.parent.as_ref().map(|p| &p.authz_type).unwrap_or(&fleet_type); // Given a parent authz type, when we want to construct an authz object for // a child resource, there are two different patterns. We need to pick the @@ -277,7 +260,9 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { format_ident!("child_generic"), quote! { ResourceType::#resource_name, }, ), - AuthzKind::Typed => (config.resource_as_snake.clone(), quote! {}), + AuthzKind::Typed => { + (format_ident!("{}", config.resource.name_as_snake), quote! {}) + } }; quote! { @@ -313,12 +298,13 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { fn generate_lookup_methods(config: &Config) -> TokenStream { let path_authz_names = &config.path_authz_names; let path_authz_types = &config.path_authz_types; - let resource_model_type = &config.resource_model_type; + let resource_authz_name = &config.resource.authz_name; + let resource_model_type = &config.resource.model_type; let (ancestors_authz_names_assign, parent_lookup_arg_actual) = if let Some(p) = &config.parent { let nancestors = config.path_authz_names.len() - 1; let ancestors_authz_names = &config.path_authz_names[0..nancestors]; - let parent_authz_name = &p.resource_authz_name; + let parent_authz_name = &p.authz_name; ( quote! { let (#(#ancestors_authz_names,)*) = parent.lookup().await?; @@ -359,7 +345,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { match &self.key { Key::Name(parent, name) => { #ancestors_authz_names_assign - let (authz_self, db_row) = Self::fetch_by_name_for( + let (#resource_authz_name, db_row) = Self::fetch_by_name_for( opctx, datastore, #parent_lookup_arg_actual @@ -395,7 +381,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let (#(#path_authz_names,)*) = self.lookup().await?; - opctx.authorize(action, &authz_self).await?; + opctx.authorize(action, &#resource_authz_name).await?; Ok((#(#path_authz_names,)*)) } @@ -426,12 +412,13 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { // each level of recursion, we could be building up one // big "join" query and hit the database just once. #ancestors_authz_names_assign - let (authz_self, _) = Self::lookup_by_name_no_authz( - opctx, - datastore, - #parent_lookup_arg_actual - *name - ).await?; + let (#resource_authz_name, _) = + Self::lookup_by_name_no_authz( + opctx, + datastore, + #parent_lookup_arg_actual + *name + ).await?; Ok((#(#path_authz_names,)*)) } Key::Id(_, id) => { @@ -465,10 +452,11 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { /// objects than do the methods: authz objects (representing parent ids), names, /// and ids. They also take the `opctx` and `datastore` directly as arguments. fn generate_database_functions(config: &Config) -> TokenStream { - let resource_name = &config.resource_name; - let resource_authz_type = &config.resource_authz_type; - let resource_model_type = &config.resource_model_type; - let resource_as_snake = &config.resource_as_snake; + let resource_name = &config.resource.name; + let resource_authz_type = &config.resource.authz_type; + let resource_authz_name = &config.resource.authz_name; + let resource_model_type = &config.resource.model_type; + let resource_as_snake = format_ident!("{}", &config.resource.name_as_snake); let path_authz_names = &config.path_authz_names; let path_authz_types = &config.path_authz_types; let ( @@ -480,10 +468,10 @@ fn generate_database_functions(config: &Config) -> TokenStream { ) = if let Some(p) = &config.parent { let nancestors = config.path_authz_names.len() - 1; let ancestors_authz_names = &config.path_authz_names[0..nancestors]; - let parent_resource_name = &p.resource_name; - let parent_authz_name = &p.resource_authz_name; - let parent_authz_type = &p.resource_authz_type; - let parent_id = format_ident!("{}_id", &p.resource_as_snake); + let parent_resource_name = &p.name; + let parent_authz_name = &p.authz_name; + let parent_authz_type = &p.authz_type; + let parent_id = format_ident!("{}_id", &p.name_as_snake); ( quote! { #parent_authz_name: &#parent_authz_type, }, quote! { #parent_authz_name, }, @@ -515,14 +503,14 @@ fn generate_database_functions(config: &Config) -> TokenStream { name: &Name, action: authz::Action, ) -> LookupResult<(#resource_authz_type, #resource_model_type)> { - let (authz_self, db_row) = Self::lookup_by_name_no_authz( + let (#resource_authz_name, db_row) = Self::lookup_by_name_no_authz( opctx, datastore, #parent_lookup_arg_actual name ).await?; - opctx.authorize(action, &authz_self).await?; - Ok((authz_self, db_row)) + opctx.authorize(action, &#resource_authz_name).await?; + Ok((#resource_authz_name, db_row)) } /// Lowest-level function for looking up a resource in the database @@ -586,7 +574,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { datastore, id ).await?; - opctx.authorize(action, &authz_self).await?; + opctx.authorize(action, &#resource_authz_name).await?; Ok((#(#path_authz_names,)* db_row)) } @@ -627,7 +615,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { ) })?; #ancestors_authz_names_assign - let authz_self = Self::make_authz( + let #resource_authz_name = Self::make_authz( &#parent_authz_value, &db_row, LookupType::ById(id) From a71e1a13cd355b6d785102a75292d8cc3092565b Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:21:04 -0700 Subject: [PATCH 48/59] macro impl: no need to special-case authz names --- nexus/src/db/db-macros/src/lookup.rs | 41 +++++++++++----------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index d424a2b49df..98b8d4332b5 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -67,9 +67,9 @@ pub struct Config { authz_kind: AuthzKind, // The path to the resource - /// list of `authz` types for this resource and its parents - /// (e.g., [`authz::Organization`, `authz::Project`]) - path_authz_types: Vec, + /// list of type names for this resource and its parents + /// (e.g., [`Organization`, `Project`]) + path_types: Vec, /// list of identifiers used for the authz objects for this resource and its /// parents, in the same order as `authz_path_types` @@ -144,30 +144,21 @@ pub fn lookup_resource( fn configure(input: Input) -> Config { let resource = Resource::for_name(&input.name); - // XXX-dap TODO Can we just make this an array of the PascalCase - // identifiers? - let mut path_authz_types: Vec<_> = input - .ancestors - .iter() - .map(|a| { - let name = format_ident!("{}", a); - quote! { authz::#name } - }) - .collect(); - path_authz_types.push(resource.authz_type.clone()); + let mut path_types: Vec<_> = + input.ancestors.iter().map(|a| format_ident!("{}", a)).collect(); + path_types.push(resource.name.clone()); - let authz_ancestors_values: Vec<_> = input + let mut path_authz_names: Vec<_> = input .ancestors .iter() .map(|a| format_ident!("authz_{}", heck::AsSnakeCase(&a).to_string())) .collect(); - let mut path_authz_names = authz_ancestors_values.clone(); path_authz_names.push(resource.authz_name.clone()); Config { resource, authz_kind: input.authz_kind, - path_authz_types, + path_types, path_authz_names, parent: input.ancestors.last().map(|s| Resource::for_name(&s)), child_resources: input.children, @@ -296,8 +287,8 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { /// Generates the lookup-related methods, including the public ones (`fetch()`, /// `fetch_for()`, and `lookup_for()`) and the private helper (`lookup()`). fn generate_lookup_methods(config: &Config) -> TokenStream { + let path_types = &config.path_types; let path_authz_names = &config.path_authz_names; - let path_authz_types = &config.path_authz_types; let resource_authz_name = &config.resource.authz_name; let resource_model_type = &config.resource.model_type; let (ancestors_authz_names_assign, parent_lookup_arg_actual) = @@ -321,7 +312,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { /// This is equivalent to `fetch_for(authz::Action::Read)`. pub async fn fetch( &self, - ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { self.fetch_for(authz::Action::Read).await } @@ -337,7 +328,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { pub async fn fetch_for( &self, action: authz::Action, - ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = &lookup.datastore; @@ -377,7 +368,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { pub async fn lookup_for( &self, action: authz::Action, - ) -> LookupResult<(#(#path_authz_types,)*)> { + ) -> LookupResult<(#(authz::#path_types,)*)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let (#(#path_authz_names,)*) = self.lookup().await?; @@ -395,7 +386,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { // lookup_for(). It's exposed in a safer way via lookup_for(). async fn lookup( &self, - ) -> LookupResult<(#(#path_authz_types,)*)> { + ) -> LookupResult<(#(authz::#path_types,)*)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = &lookup.datastore; @@ -457,8 +448,8 @@ fn generate_database_functions(config: &Config) -> TokenStream { let resource_authz_name = &config.resource.authz_name; let resource_model_type = &config.resource.model_type; let resource_as_snake = format_ident!("{}", &config.resource.name_as_snake); + let path_types = &config.path_types; let path_authz_names = &config.path_authz_names; - let path_authz_types = &config.path_authz_types; let ( parent_lookup_arg_formal, parent_lookup_arg_actual, @@ -567,7 +558,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { datastore: &DataStore, id: Uuid, action: authz::Action, - ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { let (#(#path_authz_names,)* db_row) = Self::lookup_by_id_no_authz( opctx, @@ -589,7 +580,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { _opctx: &OpContext, datastore: &DataStore, id: Uuid, - ) -> LookupResult<(#(#path_authz_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { use db::schema::#resource_as_snake::dsl; // TODO-security This could use pool_authorized() instead. From e25e9aa36320012af37d84822d23d35c21a1a195 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:30:03 -0700 Subject: [PATCH 49/59] macro impl: minor cleanup --- nexus/src/db/db-macros/src/lookup.rs | 68 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 98b8d4332b5..ea68150fa83 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -55,9 +55,9 @@ enum AuthzKind { /// Configuration for [`lookup_resource`] and its helper functions /// -/// This is all computable from [`Input`]. The purpose is to put the various -/// output strings that need to appear in various chunks of output into one -/// place with uniform names and documentation. +/// This is all computable from [`Input`]. This precomputes a bunch of useful +/// identifiers and token streams, which makes the generator functions a lot +/// easier to grok. pub struct Config { // The resource itself that we're generating /// Basic information about the resource we're generating @@ -86,8 +86,36 @@ pub struct Config { parent: Option, } +impl Config { + fn for_input(input: Input) -> Config { + let resource = Resource::for_name(&input.name); + + let mut path_types: Vec<_> = + input.ancestors.iter().map(|a| format_ident!("{}", a)).collect(); + path_types.push(resource.name.clone()); + + let mut path_authz_names: Vec<_> = input + .ancestors + .iter() + .map(|a| { + format_ident!("authz_{}", heck::AsSnakeCase(&a).to_string()) + }) + .collect(); + path_authz_names.push(resource.authz_name.clone()); + + Config { + resource, + authz_kind: input.authz_kind, + path_types, + path_authz_names, + parent: input.ancestors.last().map(|s| Resource::for_name(&s)), + child_resources: input.children, + } + } +} + /// Information about a resource (either the one we're generating or an -/// ancestor in the path) +/// ancestor in its path) struct Resource { /// PascalCase resource name itself (e.g., `Project`) name: syn::Ident, @@ -112,12 +140,16 @@ impl Resource { } } +// +// MACRO IMPLEMENTATION +// + /// Implementation of [`lookup_resource!]'. pub fn lookup_resource( raw_input: TokenStream, ) -> Result { let input = serde_tokenstream::from_tokenstream::(&raw_input)?; - let config = configure(input); + let config = Config::for_input(input); let resource_name = &config.resource.name; let the_struct = generate_struct(&config); @@ -141,30 +173,6 @@ pub fn lookup_resource( }) } -fn configure(input: Input) -> Config { - let resource = Resource::for_name(&input.name); - - let mut path_types: Vec<_> = - input.ancestors.iter().map(|a| format_ident!("{}", a)).collect(); - path_types.push(resource.name.clone()); - - let mut path_authz_names: Vec<_> = input - .ancestors - .iter() - .map(|a| format_ident!("authz_{}", heck::AsSnakeCase(&a).to_string())) - .collect(); - path_authz_names.push(resource.authz_name.clone()); - - Config { - resource, - authz_kind: input.authz_kind, - path_types, - path_authz_names, - parent: input.ancestors.last().map(|s| Resource::for_name(&s)), - child_resources: input.children, - } -} - /// Generates the struct definition for this resource fn generate_struct(config: &Config) -> TokenStream { let root_sym = format_ident!("Root"); @@ -174,7 +182,7 @@ fn generate_struct(config: &Config) -> TokenStream { let doc_struct = format!( "Selects a resource of type {} (or any of its children, using the \ functions on this struct) for lookup or fetch", - config.resource.name.to_string(), + resource_name.to_string(), ); quote! { From 723087134a33ff8faf6143127ff6a7f41f55644a Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:30:17 -0700 Subject: [PATCH 50/59] macro impl: no need to first-class model type --- nexus/src/db/db-macros/src/lookup.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index ea68150fa83..0e6c2a3d684 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -125,8 +125,6 @@ struct Resource { authz_type: TokenStream, /// identifier for an authz object for this resource (e.g., `authz_project`) authz_name: syn::Ident, - /// name of the `model` type for this resource (e.g., `db::model::Project`) - model_type: TokenStream, } impl Resource { @@ -135,8 +133,7 @@ impl Resource { let name = format_ident!("{}", name); let authz_name = format_ident!("authz_{}", name_as_snake); let authz_type = quote! { authz::#name }; - let model_type = quote! { model::#name }; - Resource { name, authz_name, authz_type, name_as_snake, model_type } + Resource { name, authz_name, authz_type, name_as_snake } } } @@ -242,7 +239,6 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { let fleet_type = quote! { authz::Fleet }; let resource_name = &config.resource.name; let resource_authz_type = &config.resource.authz_type; - let resource_model_type = &config.resource.model_type; let parent_authz_type = config.parent.as_ref().map(|p| &p.authz_type).unwrap_or(&fleet_type); @@ -268,7 +264,7 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { /// Build the `authz` object for this resource fn make_authz( authz_parent: &#parent_authz_type, - db_row: &#resource_model_type, + db_row: &model::#resource_name, lookup_type: LookupType, ) -> #resource_authz_type { authz_parent.#mkauthz_func( @@ -297,8 +293,8 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { fn generate_lookup_methods(config: &Config) -> TokenStream { let path_types = &config.path_types; let path_authz_names = &config.path_authz_names; + let resource_name = &config.resource.name; let resource_authz_name = &config.resource.authz_name; - let resource_model_type = &config.resource.model_type; let (ancestors_authz_names_assign, parent_lookup_arg_actual) = if let Some(p) = &config.parent { let nancestors = config.path_authz_names.len() - 1; @@ -320,7 +316,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { /// This is equivalent to `fetch_for(authz::Action::Read)`. pub async fn fetch( &self, - ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* model::#resource_name)> { self.fetch_for(authz::Action::Read).await } @@ -336,7 +332,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { pub async fn fetch_for( &self, action: authz::Action, - ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* model::#resource_name)> { let lookup = self.lookup_root(); let opctx = &lookup.opctx; let datastore = &lookup.datastore; @@ -454,7 +450,6 @@ fn generate_database_functions(config: &Config) -> TokenStream { let resource_name = &config.resource.name; let resource_authz_type = &config.resource.authz_type; let resource_authz_name = &config.resource.authz_name; - let resource_model_type = &config.resource.model_type; let resource_as_snake = format_ident!("{}", &config.resource.name_as_snake); let path_types = &config.path_types; let path_authz_names = &config.path_authz_names; @@ -501,7 +496,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { #parent_lookup_arg_formal name: &Name, action: authz::Action, - ) -> LookupResult<(#resource_authz_type, #resource_model_type)> { + ) -> LookupResult<(#resource_authz_type, model::#resource_name)> { let (#resource_authz_name, db_row) = Self::lookup_by_name_no_authz( opctx, datastore, @@ -524,7 +519,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { datastore: &DataStore, #parent_lookup_arg_formal name: &Name, - ) -> LookupResult<(#resource_authz_type, #resource_model_type)> { + ) -> LookupResult<(#resource_authz_type, model::#resource_name)> { use db::schema::#resource_as_snake::dsl; // TODO-security See the note about pool_authorized() below. @@ -533,7 +528,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) #lookup_filter - .select(#resource_model_type::as_select()) + .select(model::#resource_name::as_select()) .get_result_async(conn) .await .map_err(|e| { @@ -566,7 +561,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { datastore: &DataStore, id: Uuid, action: authz::Action, - ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* model::#resource_name)> { let (#(#path_authz_names,)* db_row) = Self::lookup_by_id_no_authz( opctx, @@ -588,7 +583,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { _opctx: &OpContext, datastore: &DataStore, id: Uuid, - ) -> LookupResult<(#(authz::#path_types,)* #resource_model_type)> { + ) -> LookupResult<(#(authz::#path_types,)* model::#resource_name)> { use db::schema::#resource_as_snake::dsl; // TODO-security This could use pool_authorized() instead. @@ -601,7 +596,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { let db_row = dsl::#resource_as_snake .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(id)) - .select(#resource_model_type::as_select()) + .select(model::#resource_name::as_select()) .get_result_async(conn) .await .map_err(|e| { From 07eaab82156e6e86a82a1ceb396260b1b491b4a9 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:35:06 -0700 Subject: [PATCH 51/59] macro impl: authz type need not be first-classed --- nexus/src/db/db-macros/src/lookup.rs | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 0e6c2a3d684..5a4f2aa3a2b 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -121,8 +121,6 @@ struct Resource { name: syn::Ident, /// snake_case resource name (e.g., `project`) name_as_snake: String, - /// name of the `authz` type for this resource (e.g., `authz::Project`) - authz_type: TokenStream, /// identifier for an authz object for this resource (e.g., `authz_project`) authz_name: syn::Ident, } @@ -132,8 +130,7 @@ impl Resource { let name_as_snake = heck::AsSnakeCase(&name).to_string(); let name = format_ident!("{}", name); let authz_name = format_ident!("authz_{}", name_as_snake); - let authz_type = quote! { authz::#name }; - Resource { name, authz_name, authz_type, name_as_snake } + Resource { name, authz_name, name_as_snake } } } @@ -236,11 +233,10 @@ fn generate_child_selectors(config: &Config) -> TokenStream { /// Generates the simple helper functions for this resource fn generate_misc_helpers(config: &Config) -> TokenStream { - let fleet_type = quote! { authz::Fleet }; + let fleet_name = format_ident!("Fleet"); let resource_name = &config.resource.name; - let resource_authz_type = &config.resource.authz_type; - let parent_authz_type = - config.parent.as_ref().map(|p| &p.authz_type).unwrap_or(&fleet_type); + let parent_resource_name = + config.parent.as_ref().map(|p| &p.name).unwrap_or(&fleet_name); // Given a parent authz type, when we want to construct an authz object for // a child resource, there are two different patterns. We need to pick the @@ -263,10 +259,10 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { quote! { /// Build the `authz` object for this resource fn make_authz( - authz_parent: &#parent_authz_type, + authz_parent: &authz::#parent_resource_name, db_row: &model::#resource_name, lookup_type: LookupType, - ) -> #resource_authz_type { + ) -> authz::#resource_name { authz_parent.#mkauthz_func( #mkauthz_arg db_row.id(), @@ -448,7 +444,6 @@ fn generate_lookup_methods(config: &Config) -> TokenStream { /// and ids. They also take the `opctx` and `datastore` directly as arguments. fn generate_database_functions(config: &Config) -> TokenStream { let resource_name = &config.resource.name; - let resource_authz_type = &config.resource.authz_type; let resource_authz_name = &config.resource.authz_name; let resource_as_snake = format_ident!("{}", &config.resource.name_as_snake); let path_types = &config.path_types; @@ -464,10 +459,9 @@ fn generate_database_functions(config: &Config) -> TokenStream { let ancestors_authz_names = &config.path_authz_names[0..nancestors]; let parent_resource_name = &p.name; let parent_authz_name = &p.authz_name; - let parent_authz_type = &p.authz_type; let parent_id = format_ident!("{}_id", &p.name_as_snake); ( - quote! { #parent_authz_name: &#parent_authz_type, }, + quote! { #parent_authz_name: &authz::#parent_resource_name, }, quote! { #parent_authz_name, }, quote! { let (#(#ancestors_authz_names,)* _) = @@ -496,7 +490,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { #parent_lookup_arg_formal name: &Name, action: authz::Action, - ) -> LookupResult<(#resource_authz_type, model::#resource_name)> { + ) -> LookupResult<(authz::#resource_name, model::#resource_name)> { let (#resource_authz_name, db_row) = Self::lookup_by_name_no_authz( opctx, datastore, @@ -519,7 +513,7 @@ fn generate_database_functions(config: &Config) -> TokenStream { datastore: &DataStore, #parent_lookup_arg_formal name: &Name, - ) -> LookupResult<(#resource_authz_type, model::#resource_name)> { + ) -> LookupResult<(authz::#resource_name, model::#resource_name)> { use db::schema::#resource_as_snake::dsl; // TODO-security See the note about pool_authorized() below. From ea67303af70e2f5b5ff083a4249909ff3dd88417 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:35:34 -0700 Subject: [PATCH 52/59] clippy --- nexus/src/db/db-macros/src/lookup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 5a4f2aa3a2b..427fe98acb1 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -176,7 +176,7 @@ fn generate_struct(config: &Config) -> TokenStream { let doc_struct = format!( "Selects a resource of type {} (or any of its children, using the \ functions on this struct) for lookup or fetch", - resource_name.to_string(), + resource_name ); quote! { From 62e5eeade1e88779174c372a0e948059ba7f766b Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:41:11 -0700 Subject: [PATCH 53/59] macro impl: add comment --- nexus/src/db/db-macros/src/lookup.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/src/db/db-macros/src/lookup.rs index 427fe98acb1..90c6b8041d3 100644 --- a/nexus/src/db/db-macros/src/lookup.rs +++ b/nexus/src/db/db-macros/src/lookup.rs @@ -613,6 +613,13 @@ fn generate_database_functions(config: &Config) -> TokenStream { } } +// This isn't so much a test (although it does make sure we don't panic on some +// basic cases). This is a way to dump the output of the macro for some common +// inputs. This is invaluable for debugging. If there's a bug where the macro +// generates syntactically invalid Rust, `cargo expand` will often not print the +// macro's output. Instead, you can paste the output of this test into +// lookup.rs, replacing the call to the macro, then reformat the file, and then +// build it in order to see the compiler error in context. #[cfg(test)] #[test] fn test_lookup_dump() { From fa0f1d63ddb9adfa112e22cf0f38a975adce2826 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 15:59:46 -0700 Subject: [PATCH 54/59] fix mismerge --- nexus/tests/integration_tests/basic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/tests/integration_tests/basic.rs b/nexus/tests/integration_tests/basic.rs index 73b18901849..633f4d1a9a2 100644 --- a/nexus/tests/integration_tests/basic.rs +++ b/nexus/tests/integration_tests/basic.rs @@ -133,7 +133,7 @@ async fn test_basic_failures(cptestctx: &ControlPlaneTestContext) { ]; for test_case in test_cases { - let error = if let Some(body) = test_case.body { + let error: HttpErrorResponseBody = if let Some(body) = test_case.body { NexusRequest::expect_failure_with_body( client, test_case.expected_code, From 6f9411c20c067fdf9c0f9a2303d4cd17d2f21b07 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 19:48:03 -0700 Subject: [PATCH 55/59] convert route endpoints to new lookup API --- common/src/api/external/mod.rs | 2 +- common/src/sql/dbinit.sql | 4 +- nexus/src/db/datastore.rs | 8 ++-- nexus/src/db/lookup.rs | 38 ++++++++++++++++++- nexus/src/db/model.rs | 10 ++--- nexus/src/db/schema.rs | 2 +- nexus/src/nexus.rs | 69 ++++++++++++++-------------------- 7 files changed, 79 insertions(+), 54 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index a7cb8cb3344..527327450aa 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -1256,7 +1256,7 @@ pub struct RouterRoute { pub identity: IdentityMetadata, /// The VPC Router to which the route belongs. - pub router_id: Uuid, + pub vpc_router_id: Uuid, /// Describes the kind of router. Set at creation. `read-only` pub kind: RouterRouteKind, diff --git a/common/src/sql/dbinit.sql b/common/src/sql/dbinit.sql index 5a72569ee99..a936946bf95 100644 --- a/common/src/sql/dbinit.sql +++ b/common/src/sql/dbinit.sql @@ -687,14 +687,14 @@ CREATE TABLE omicron.public.router_route ( /* Indicates that the object has been deleted */ time_deleted TIMESTAMPTZ, - router_id UUID NOT NULL, + vpc_router_id UUID NOT NULL, kind omicron.public.router_route_kind NOT NULL, target STRING(128) NOT NULL, destination STRING(128) NOT NULL ); CREATE UNIQUE INDEX ON omicron.public.router_route ( - router_id, + vpc_router_id, name ) WHERE time_deleted IS NULL; diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 641c2f181ee..e06b35b48a5 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -2854,7 +2854,7 @@ impl DataStore { use db::schema::router_route::dsl; paginated(dsl::router_route, dsl::name, pagparams) .filter(dsl::time_deleted.is_null()) - .filter(dsl::router_id.eq(authz_router.id())) + .filter(dsl::vpc_router_id.eq(authz_router.id())) .select(RouterRoute::as_select()) .load_async::( self.pool_authorized(opctx).await?, @@ -2877,7 +2877,7 @@ impl DataStore { use db::schema::router_route::dsl; dsl::router_route .filter(dsl::time_deleted.is_null()) - .filter(dsl::router_id.eq(authz_vpc_router.id())) + .filter(dsl::vpc_router_id.eq(authz_vpc_router.id())) .filter(dsl::name.eq(route_name.clone())) .select(RouterRoute::as_select()) .get_result_async(self.pool()) @@ -2950,11 +2950,11 @@ impl DataStore { authz_router: &authz::VpcRouter, route: RouterRoute, ) -> CreateResult { - assert_eq!(authz_router.id(), route.router_id); + assert_eq!(authz_router.id(), route.vpc_router_id); opctx.authorize(authz::Action::CreateChild, authz_router).await?; use db::schema::router_route::dsl; - let router_id = route.router_id; + let router_id = route.vpc_router_id; let name = route.name().clone(); VpcRouter::insert_resource( diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 379401d0166..fa3cca6a676 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -204,6 +204,21 @@ impl<'a> LookupPath<'a> { pub fn disk_id(self, id: Uuid) -> Disk<'a> { Disk { key: Key::Id(Root { lookup_root: self }, id) } } + + /// Select a resource of type Vpc, identified by its id + pub fn vpc_id(self, id: Uuid) -> Vpc<'a> { + Vpc { key: Key::Id(Root { lookup_root: self }, id) } + } + + /// Select a resource of type VpcRouter, identified by its id + pub fn vpc_router_id(self, id: Uuid) -> VpcRouter<'a> { + VpcRouter { key: Key::Id(Root { lookup_root: self }, id) } + } + + /// Select a resource of type RouterRoute, identified by its id + pub fn router_router_id(self, id: Uuid) -> RouterRoute<'a> { + RouterRoute { key: Key::Id(Root { lookup_root: self }, id) } + } } /// Describes a node along the selection path of a resource @@ -244,7 +259,7 @@ lookup_resource! { lookup_resource! { name = "Project", ancestors = [ "Organization" ], - children = [ "Disk", "Instance" ], + children = [ "Disk", "Instance", "Vpc" ], authz_kind = Typed } @@ -262,6 +277,27 @@ lookup_resource! { authz_kind = Generic } +lookup_resource! { + name = "Vpc", + ancestors = [ "Organization", "Project" ], + children = [ "VpcRouter" ], + authz_kind = Generic +} + +lookup_resource! { + name = "VpcRouter", + ancestors = [ "Organization", "Project", "Vpc" ], + children = [ "RouterRoute" ], + authz_kind = Generic +} + +lookup_resource! { + name = "RouterRoute", + ancestors = [ "Organization", "Project", "Vpc", "VpcRouter" ], + children = [], + authz_kind = Generic +} + #[cfg(test)] mod test { use super::Instance; diff --git a/nexus/src/db/model.rs b/nexus/src/db/model.rs index 5f51ec218b1..084de900961 100644 --- a/nexus/src/db/model.rs +++ b/nexus/src/db/model.rs @@ -1810,7 +1810,7 @@ impl DatastoreCollection for VpcRouter { type CollectionId = Uuid; type GenerationNumberColumn = vpc_router::dsl::rcgen; type CollectionTimeDeletedColumn = vpc_router::dsl::time_deleted; - type CollectionIdColumn = router_route::dsl::router_id; + type CollectionIdColumn = router_route::dsl::vpc_router_id; } #[derive(AsChangeset)] @@ -1922,7 +1922,7 @@ pub struct RouterRoute { identity: RouterRouteIdentity, pub kind: RouterRouteKind, - pub router_id: Uuid, + pub vpc_router_id: Uuid, pub target: RouteTarget, pub destination: RouteDestination, } @@ -1930,14 +1930,14 @@ pub struct RouterRoute { impl RouterRoute { pub fn new( route_id: Uuid, - router_id: Uuid, + vpc_router_id: Uuid, kind: external::RouterRouteKind, params: external::RouterRouteCreateParams, ) -> Self { let identity = RouterRouteIdentity::new(route_id, params.identity); Self { identity, - router_id, + vpc_router_id, kind: RouterRouteKind(kind), target: RouteTarget(params.target), destination: RouteDestination::new(params.destination), @@ -1949,7 +1949,7 @@ impl Into for RouterRoute { fn into(self) -> external::RouterRoute { external::RouterRoute { identity: self.identity(), - router_id: self.router_id, + vpc_router_id: self.vpc_router_id, kind: self.kind.0, target: self.target.0.clone(), destination: self.destination.state().clone(), diff --git a/nexus/src/db/schema.rs b/nexus/src/db/schema.rs index 4353f5e375f..f37e3ea7c63 100644 --- a/nexus/src/db/schema.rs +++ b/nexus/src/db/schema.rs @@ -324,7 +324,7 @@ table! { time_modified -> Timestamptz, time_deleted -> Nullable, kind -> crate::db::model::RouterRouteKindEnum, - router_id -> Uuid, + vpc_router_id -> Uuid, target -> Text, destination -> Text, } diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 3ad7de6efe7..7c43f7f9030 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -2535,20 +2535,16 @@ impl Nexus { router_name: &Name, route_name: &Name, ) -> LookupResult { - let authz_router = self - .db_datastore - .vpc_router_lookup_by_path( - organization_name, - project_name, - vpc_name, - router_name, - ) - .await?; - Ok(self - .db_datastore - .route_fetch(&opctx, &authz_router, route_name) - .await? - .1) + let (.., authz_route, db_route) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .route_name(route_name) + .fetch() + .await?; + Ok(db_route) } #[allow(clippy::too_many_arguments)] @@ -2594,19 +2590,16 @@ impl Nexus { router_name: &Name, route_name: &Name, ) -> DeleteResult { - let authz_router = self - .db_datastore - .vpc_router_lookup_by_path( - organization_name, - project_name, - vpc_name, - router_name, - ) - .await?; - let (authz_route, db_route) = self - .db_datastore - .route_fetch(opctx, &authz_router, route_name) - .await?; + let (.., authz_route, db_route) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .route_name(route_name) + .fetch() + .await?; + // Only custom routes can be deleted // TODO Shouldn't this constraint be checked by the database query? if db_route.kind.0 != RouterRouteKind::Custom { @@ -2629,19 +2622,15 @@ impl Nexus { route_name: &Name, params: &RouterRouteUpdateParams, ) -> UpdateResult { - let authz_router = self - .db_datastore - .vpc_router_lookup_by_path( - organization_name, - project_name, - vpc_name, - router_name, - ) - .await?; - let (authz_route, db_route) = self - .db_datastore - .route_fetch(opctx, &authz_router, route_name) - .await?; + let (.., authz_route, db_route) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .route_name(route_name) + .fetch() + .await?; // TODO: Write a test for this once there's a way to test it (i.e. // subnets automatically register to the system router table) match db_route.kind.0 { From d3c9f1d3759def82ff8fd835e20585d9614d346b Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 19:56:35 -0700 Subject: [PATCH 56/59] fixes --- nexus/src/db/lookup.rs | 2 +- nexus/src/nexus.rs | 8 ++++---- openapi/nexus.json | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index fa3cca6a676..bf9d232eb38 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -216,7 +216,7 @@ impl<'a> LookupPath<'a> { } /// Select a resource of type RouterRoute, identified by its id - pub fn router_router_id(self, id: Uuid) -> RouterRoute<'a> { + pub fn router_route_id(self, id: Uuid) -> RouterRoute<'a> { RouterRoute { key: Key::Id(Root { lookup_root: self }, id) } } } diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 7c43f7f9030..465f71f7f1e 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -2535,13 +2535,13 @@ impl Nexus { router_name: &Name, route_name: &Name, ) -> LookupResult { - let (.., authz_route, db_route) = + let (.., db_route) = LookupPath::new(opctx, &self.db_datastore) .organization_name(organization_name) .project_name(project_name) .vpc_name(vpc_name) .vpc_router_name(router_name) - .route_name(route_name) + .router_route_name(route_name) .fetch() .await?; Ok(db_route) @@ -2596,7 +2596,7 @@ impl Nexus { .project_name(project_name) .vpc_name(vpc_name) .vpc_router_name(router_name) - .route_name(route_name) + .router_route_name(route_name) .fetch() .await?; @@ -2628,7 +2628,7 @@ impl Nexus { .project_name(project_name) .vpc_name(vpc_name) .vpc_router_name(router_name) - .route_name(route_name) + .router_route_name(route_name) .fetch() .await?; // TODO: Write a test for this once there's a way to test it (i.e. diff --git a/openapi/nexus.json b/openapi/nexus.json index 6d337a41cee..6a823a04b48 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -5707,11 +5707,6 @@ } ] }, - "router_id": { - "description": "The VPC Router to which the route belongs.", - "type": "string", - "format": "uuid" - }, "target": { "$ref": "#/components/schemas/RouteTarget" }, @@ -5724,6 +5719,11 @@ "description": "timestamp when this resource was last modified", "type": "string", "format": "date-time" + }, + "vpc_router_id": { + "description": "The VPC Router to which the route belongs.", + "type": "string", + "format": "uuid" } }, "required": [ @@ -5732,10 +5732,10 @@ "id", "kind", "name", - "router_id", "target", "time_created", - "time_modified" + "time_modified", + "vpc_router_id" ] }, "RouterRouteCreateParams": { From 1cb923559d84e83fbf3990b82767aceb6e3841c1 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 19:58:32 -0700 Subject: [PATCH 57/59] fix style --- nexus/src/nexus.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 465f71f7f1e..31f4c2614cf 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -2535,15 +2535,14 @@ impl Nexus { router_name: &Name, route_name: &Name, ) -> LookupResult { - let (.., db_route) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .router_route_name(route_name) - .fetch() - .await?; + let (.., db_route) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .router_route_name(route_name) + .fetch() + .await?; Ok(db_route) } From 386549a38969594d4b5372cbaf9725a0f96df18e Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 20:18:22 -0700 Subject: [PATCH 58/59] oh right, we can remove the hand-written database functions now --- nexus/src/db/datastore.rs | 81 --------------------------------------- 1 file changed, 81 deletions(-) diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index e06b35b48a5..31875701639 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -2863,87 +2863,6 @@ impl DataStore { .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } - /// Fetches a RouterRoute from the database and returns both the database - /// row and an [`authz::RouterRoute`] for doing authz checks - /// - /// See [`DataStore::organization_lookup_noauthz()`] for intended use cases - /// and caveats. - // TODO-security See the note on organization_lookup_noauthz(). - async fn route_lookup_noauthz( - &self, - authz_vpc_router: &authz::VpcRouter, - route_name: &Name, - ) -> LookupResult<(authz::RouterRoute, RouterRoute)> { - use db::schema::router_route::dsl; - dsl::router_route - .filter(dsl::time_deleted.is_null()) - .filter(dsl::vpc_router_id.eq(authz_vpc_router.id())) - .filter(dsl::name.eq(route_name.clone())) - .select(RouterRoute::as_select()) - .get_result_async(self.pool()) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::RouterRoute, - LookupType::ByName(route_name.as_str().to_owned()), - ), - ) - }) - .map(|r| { - ( - authz_vpc_router.child_generic( - ResourceType::RouterRoute, - r.id(), - LookupType::ByName(route_name.to_string()), - ), - r, - ) - }) - } - - /// Lookup a RouterRoute by name and return the full database record, along - /// with an [`authz::RouterRoute`] for subsequent authorization checks - pub async fn route_fetch( - &self, - opctx: &OpContext, - authz_vpc_router: &authz::VpcRouter, - name: &Name, - ) -> LookupResult<(authz::RouterRoute, RouterRoute)> { - let (authz_route, db_route) = - self.route_lookup_noauthz(authz_vpc_router, name).await?; - opctx.authorize(authz::Action::Read, &authz_route).await?; - Ok((authz_route, db_route)) - } - - /// Look up the id for a RouterRoute based on its name - /// - /// Returns an [`authz::RouterRoute`] (which makes the id available). - /// - /// Like the other "lookup_by_path()" functions, this function does no authz - /// checks. - pub async fn route_lookup_by_path( - &self, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, - route_name: &Name, - ) -> LookupResult { - let authz_vpc_router = self - .vpc_router_lookup_by_path( - organization_name, - project_name, - vpc_name, - router_name, - ) - .await?; - self.vpc_router_lookup_noauthz(&authz_vpc_router, route_name) - .await - .map(|(v, _)| v) - } - pub async fn router_create_route( &self, opctx: &OpContext, From 342771c00227cf9dc1503edfdcfd6d9057825880 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 30 Mar 2022 20:14:05 -0700 Subject: [PATCH 59/59] convert routers to new lookup API --- nexus/src/db/datastore.rs | 75 ------------------------------------- nexus/src/nexus.rs | 79 +++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 119 deletions(-) diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 31875701639..ccfc5d0e6e5 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -2683,81 +2683,6 @@ impl DataStore { .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } - /// Fetches a VpcRouter from the database and returns both the database row - /// and an [`authz::VpcRouter`] for doing authz checks - /// - /// See [`DataStore::organization_lookup_noauthz()`] for intended use cases - /// and caveats. - // TODO-security See the note on organization_lookup_noauthz(). - async fn vpc_router_lookup_noauthz( - &self, - authz_vpc: &authz::Vpc, - router_name: &Name, - ) -> LookupResult<(authz::VpcRouter, VpcRouter)> { - use db::schema::vpc_router::dsl; - dsl::vpc_router - .filter(dsl::time_deleted.is_null()) - .filter(dsl::vpc_id.eq(authz_vpc.id())) - .filter(dsl::name.eq(router_name.clone())) - .select(VpcRouter::as_select()) - .get_result_async(self.pool()) - .await - .map_err(|e| { - public_error_from_diesel_pool( - e, - ErrorHandler::NotFoundByLookup( - ResourceType::VpcRouter, - LookupType::ByName(router_name.as_str().to_owned()), - ), - ) - }) - .map(|r| { - ( - authz_vpc.child_generic( - ResourceType::VpcRouter, - r.id(), - LookupType::ByName(router_name.to_string()), - ), - r, - ) - }) - } - - /// Lookup a VpcRouter by name and return the full database record, along - /// with an [`authz::VpcRouter`] for subsequent authorization checks - pub async fn vpc_router_fetch( - &self, - opctx: &OpContext, - authz_vpc: &authz::Vpc, - name: &Name, - ) -> LookupResult<(authz::VpcRouter, VpcRouter)> { - let (authz_vpc_router, db_vpc_router) = - self.vpc_router_lookup_noauthz(authz_vpc, name).await?; - opctx.authorize(authz::Action::Read, &authz_vpc_router).await?; - Ok((authz_vpc_router, db_vpc_router)) - } - - /// Look up the id for a VpcRouter based on its name - /// - /// Returns an [`authz::VpcRouter`] (which makes the id available). - /// - /// Like the other "lookup_by_path()" functions, this function does no authz - /// checks. - pub async fn vpc_router_lookup_by_path( - &self, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, - ) -> LookupResult { - let authz_vpc = self - .vpc_lookup_by_path(organization_name, project_name, vpc_name) - .await?; - self.vpc_router_lookup_noauthz(&authz_vpc, router_name) - .await - .map(|(v, _)| v) - } - pub async fn vpc_create_router( &self, opctx: &OpContext, diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 31f4c2614cf..96dc03cfd44 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -2407,15 +2407,14 @@ impl Nexus { vpc_name: &Name, router_name: &Name, ) -> LookupResult { - let authz_vpc = self - .db_datastore - .vpc_lookup_by_path(organization_name, project_name, vpc_name) + let (.., db_router) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .fetch() .await?; - Ok(self - .db_datastore - .vpc_router_fetch(&opctx, &authz_vpc, router_name) - .await? - .1) + Ok(db_router) } pub async fn vpc_create_router( @@ -2456,14 +2455,14 @@ impl Nexus { vpc_name: &Name, router_name: &Name, ) -> DeleteResult { - let authz_vpc = self - .db_datastore - .vpc_lookup_by_path(organization_name, project_name, vpc_name) - .await?; - let (authz_router, db_router) = self - .db_datastore - .vpc_router_fetch(opctx, &authz_vpc, router_name) - .await?; + let (.., authz_router, db_router) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .fetch() + .await?; // TODO-performance shouldn't this check be part of the "update" // database query? This shouldn't affect correctness, assuming that a // router kind cannot be changed, but it might be able to save us a @@ -2485,14 +2484,12 @@ impl Nexus { router_name: &Name, params: ¶ms::VpcRouterUpdate, ) -> UpdateResult { - let authz_router = self - .db_datastore - .vpc_router_lookup_by_path( - organization_name, - project_name, - vpc_name, - router_name, - ) + let (.., authz_router) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .lookup_for(authz::Action::Modify) .await?; self.db_datastore .vpc_update_router(opctx, &authz_router, params.clone().into()) @@ -2510,20 +2507,16 @@ impl Nexus { router_name: &Name, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let authz_router = self - .db_datastore - .vpc_router_lookup_by_path( - organization_name, - project_name, - vpc_name, - router_name, - ) + let (.., authz_router) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .lookup_for(authz::Action::ListChildren) .await?; - let routes = self - .db_datastore + self.db_datastore .router_list_routes(opctx, &authz_router, pagparams) - .await?; - Ok(routes) + .await } pub async fn route_fetch( @@ -2557,14 +2550,12 @@ impl Nexus { kind: &RouterRouteKind, params: &RouterRouteCreateParams, ) -> CreateResult { - let authz_router = self - .db_datastore - .vpc_router_lookup_by_path( - organization_name, - project_name, - vpc_name, - router_name, - ) + let (.., authz_router) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .vpc_name(vpc_name) + .vpc_router_name(router_name) + .lookup_for(authz::Action::CreateChild) .await?; let id = Uuid::new_v4(); let route = db::model::RouterRoute::new(