-
Notifications
You must be signed in to change notification settings - Fork 45
add authz checks for top-level Organization endpoints #592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
33b1624
0b0bb87
b7f0b85
70d6dd0
760718e
b9845ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -263,17 +263,45 @@ impl DataStore { | |
}) | ||
} | ||
|
||
/// Lookup a organization by name. | ||
pub async fn organization_fetch( | ||
/// Fetches an Organization from the database and returns both the database | ||
/// row and an authz::Organization for doing authz checks | ||
/// | ||
/// There are a few different ways this can be used: | ||
/// | ||
/// * If a code path wants to use anything in the database row _aside_ from | ||
/// the id, it should do an authz check for `authz::Action::Read` on the | ||
/// returned [`authz::Organization`]. `organization_fetch()` does this, | ||
/// for example. | ||
/// * If a code path is only doing this lookup to get the id so that it can | ||
/// look up something else inside the Organization, then the database | ||
/// record is not record -- and neither is an authz check on the | ||
/// Organization. Callers usually use `organization_lookup_id()` for | ||
/// this. That function does not expose the database row to the caller. | ||
/// | ||
/// Callers in this bucket should still do _some_ authz check. Clients | ||
/// must not be able to discover whether an Organization exists with a | ||
/// particular name. It's just that we don't know at this point whether | ||
/// they're allowed to see the Organization. It depends on whether, as we | ||
/// look up things inside the Organization, we find something that they | ||
/// _are_ able to see. | ||
/// | ||
/// **This is an internal-only function.** This function cannot know what | ||
/// authz checks are required. As a result, it should not be made | ||
/// accessible outside the DataStore. It should always be wrapped by | ||
/// something that does the appropriate authz check. | ||
// TODO-security We should refactor things so that it's harder to | ||
// accidentally mark this "pub" or otherwise expose database data without | ||
// doing an authz check. | ||
async fn organization_lookup_noauthz( | ||
&self, | ||
name: &Name, | ||
) -> LookupResult<Organization> { | ||
) -> LookupResult<(authz::Organization, Organization)> { | ||
use db::schema::organization::dsl; | ||
dsl::organization | ||
.filter(dsl::time_deleted.is_null()) | ||
.filter(dsl::name.eq(name.clone())) | ||
.select(Organization::as_select()) | ||
.first_async::<Organization>(self.pool()) | ||
.get_result_async::<Organization>(self.pool()) | ||
.await | ||
.map_err(|e| { | ||
public_error_from_diesel_pool( | ||
|
@@ -282,10 +310,42 @@ impl DataStore { | |
LookupType::ByName(name.as_str().to_owned()), | ||
) | ||
}) | ||
.map(|o| (authz::FLEET.organization(o.id()), o)) | ||
} | ||
|
||
/// Look up the id for an organization based on its name | ||
/// | ||
/// Returns an [`authz::Organization`] (which makes the id available). | ||
/// | ||
/// This function does no authz checks because it is not possible to know | ||
/// just by looking up an Organization's id what privileges are required. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I understand this point. Couldn't you check for the read permission? Are you accounting for the possibility that someone could access a sub-resource without having read permission on the organization? I wonder if that warrants another permission that's like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I don't think we want to give you full read permissions on the Organization just because you have some permission on something inside the Organization. We could create a I think not creating a permission for this more closely resembles the user experience, too: users get access to, say, a Project. That doesn't mean they can do anything with the Organization aside from seeing that it exists because it happens to be part of the Project's path. (On the other hand, maybe every user implicitly has some read access to their own parent Organization, and that lets them see some basic stuff. But if someone were to share a specific Project in another Organization with you, I think you should still be able to look it up without somebody giving you some extra other permission on the Organization.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's all fine, seems like the optimal middle ground. We'll see how we can make it harder to accidentally make the noauth thing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. My (vague, hand-wavy) thinking here is to put the noauth functions into a submodule that itself is not exposed. Another idea I thought of is to have the fields in the db models all private, with an authz-protected interface for getting a similar struct with the same fields "pub" or a trait that provides access. Either way, we'd do an authz check before giving an object with that data. This seems tricky and annoying (yet another type called |
||
pub async fn organization_lookup_id( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the same as the old |
||
&self, | ||
name: &Name, | ||
) -> LookupResult<authz::Organization> { | ||
self.organization_lookup_noauthz(name).await.map(|(o, _)| o) | ||
} | ||
|
||
/// Lookup an organization by name. | ||
pub async fn organization_fetch( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes from the old
|
||
&self, | ||
opctx: &OpContext, | ||
name: &Name, | ||
) -> LookupResult<(authz::Organization, Organization)> { | ||
let (authz_org, db_org) = | ||
self.organization_lookup_noauthz(name).await?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm totally on-board with the "noauthz" prefix, but is it necessary to use both "fetch" and "lookup" as different verbs to access the organization? We have:
Couldn't these be:
? I know this is kinda a nit, but given the large surface area of the DB, it seems like an as-simple-as-possible naming convention will be worthwhile investing into. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha, I think maybe what you find clearer is what I find more confusing. If it's okay, I'll think about this some more as I iterate on the other lookup functions. I expect these to evolve, potentially a lot: at the very least, we're going to be adding lookup-by-id, for example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Narrowing my question, when do you view "fetch" as an appropriate verb vs "lookup"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll explain my intuition below, with the caveat that I don't think my intuition here is necessarily based on a good reason. It's probably a product of the systems I've worked on before. I can see how a reader might not have the same intuition and I'm on board for changing the names. I'm just not sure how to do it yet and I don't think it makes sense to spend a lot of time on it right now because I think a bunch of upcoming changes are likely to invalidate whatever convention we try to establish now based only on these three functions. "fetch" to me suggests:
"lookup" to me is much more general. It could be an internal or external function. I wouldn't have expectations about whether authorization is done. It might accept any kind of criteria (not even necessarily an identifier) and return any number of objects or even some field from some object. I don't like the specific thing you suggested because the function names look so similar that I think I'd find it hard to keep track of which ones do authz. (After all, you said the "authz" is implied in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the record, the first time I read through your explanation, I found it strange. Now it makes a lot of sense to me. Not sure what that means, but I find the distinction you draw—between external-facing auth-gated functions that return a whole resource and internal non-auth-gated functions that return IDs and/or auth metadata about a resource—to be a coherent one, so I think assigning the two sides names make sense. We may have to get over some prior intuitions about what these words mean but that's ok. I have a feeling |
||
// TODO-security See the note in authz::authorize(). This needs to | ||
// return a 404, not a 403. | ||
opctx.authorize(authz::Action::Read, authz_org).await?; | ||
Ok((authz_org, db_org)) | ||
} | ||
|
||
/// Delete a organization | ||
pub async fn organization_delete(&self, name: &Name) -> DeleteResult { | ||
pub async fn organization_delete( | ||
&self, | ||
opctx: &OpContext, | ||
name: &Name, | ||
) -> DeleteResult { | ||
use db::schema::organization::dsl; | ||
use db::schema::project; | ||
|
||
|
@@ -303,6 +363,11 @@ impl DataStore { | |
) | ||
})?; | ||
|
||
// TODO-cleanup TODO-security This should use a more common lookup | ||
// function. | ||
let authz_org = authz::FLEET.organization(id); | ||
opctx.authorize(authz::Action::Delete, authz_org).await?; | ||
|
||
// Make sure there are no projects present within this organization. | ||
let project_found = diesel_pool_result_optional( | ||
project::dsl::project | ||
|
@@ -352,39 +417,6 @@ impl DataStore { | |
Ok(()) | ||
} | ||
|
||
/// Look up an organization by name | ||
pub async fn organization_lookup( | ||
&self, | ||
name: &Name, | ||
) -> Result<authz::Organization, Error> { | ||
use db::schema::organization::dsl; | ||
dsl::organization | ||
.filter(dsl::time_deleted.is_null()) | ||
.filter(dsl::name.eq(name.clone())) | ||
.select(dsl::id) | ||
.get_result_async::<Uuid>(self.pool()) | ||
.await | ||
.map_err(|e| { | ||
public_error_from_diesel_pool( | ||
e, | ||
ResourceType::Organization, | ||
LookupType::ByName(name.as_str().to_owned()), | ||
) | ||
}) | ||
.map(|o| authz::FLEET.organization(o)) | ||
} | ||
|
||
/// Look up the id for a organization based on its name | ||
/// | ||
/// As endpoints move to doing authorization, they should move to | ||
/// [`organization_lookup()`] instead of this function. | ||
pub async fn organization_lookup_id_by_name( | ||
&self, | ||
name: &Name, | ||
) -> Result<Uuid, Error> { | ||
self.organization_lookup(name).await.map(|o| o.id()) | ||
} | ||
|
||
pub async fn organizations_list_by_id( | ||
&self, | ||
opctx: &OpContext, | ||
|
@@ -430,17 +462,21 @@ impl DataStore { | |
/// Updates a organization by name (clobbering update -- no etag) | ||
pub async fn organization_update( | ||
&self, | ||
opctx: &OpContext, | ||
name: &Name, | ||
updates: OrganizationUpdate, | ||
) -> UpdateResult<Organization> { | ||
use db::schema::organization::dsl; | ||
|
||
let (authz_org, _) = self.organization_lookup_noauthz(name).await?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use I know it's functionally the same, but it seems like a good pattern to minimize the number of calls to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes -- good call! And nice catch. Fixed in #616. |
||
opctx.authorize(authz::Action::Modify, authz_org).await?; | ||
|
||
diesel::update(dsl::organization) | ||
.filter(dsl::time_deleted.is_null()) | ||
.filter(dsl::name.eq(name.clone())) | ||
.filter(dsl::id.eq(authz_org.id())) | ||
.set(updates) | ||
.returning(Organization::as_returning()) | ||
.get_result_async(self.pool()) | ||
.get_result_async(self.pool_authorized(opctx).await?) | ||
.await | ||
.map_err(|e| { | ||
public_error_from_diesel_pool( | ||
|
@@ -2383,8 +2419,10 @@ mod test { | |
); | ||
let org = authz::FLEET.organization(organization.id()); | ||
datastore.project_create(&opctx, &org, project).await.unwrap(); | ||
let organization_after_project_create = | ||
datastore.organization_fetch(organization.name()).await.unwrap(); | ||
let (_, organization_after_project_create) = datastore | ||
.organization_fetch(&opctx, organization.name()) | ||
.await | ||
.unwrap(); | ||
assert!(organization_after_project_create.rcgen > organization.rcgen); | ||
|
||
db.cleanup().await.unwrap(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"record is not record"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch. Fixed in #616.