return 404 rather than 401/403 when user can't even see the object#600
Conversation
|
Great, I was just wondering about this while reviewing #592 |
| actor: AnyActor, | ||
| action: Action, | ||
| ) -> Result<(), Error> { | ||
| if action == Action::Read { |
There was a problem hiding this comment.
This function is the crux of the change. It's invoked by the main authorize() function if authorization fails. Here we're saying if we were already checking for "read", and that failed, then return 404. Otherwise, do a "read" check and return 404 (if that fails) or whatever error authorize() had already picked (which is either 401 or 403 based on whether the user was authenticated).
| pub fn organization( | ||
| &self, | ||
| organization_id: Uuid, | ||
| lookup_type: LookupType, |
There was a problem hiding this comment.
In order to produce the type-specific 404 errors (Error::ObjectNotFound), the authz:: resource structs now have a LookupType that describes how they were looked up in the first place.
For several types, this meant we could no longer derive Eq and PartialEq. Those traits are only used for types for which we do an equality check in the Polar file:
omicron/nexus/src/authz/omicron.polar
Lines 204 to 212 in 2e5c819
For some types, I removed these impls because we didn't need them. For others, I impl'd them by hand in a way that ignores the new LookupType field.
| _: AnyActor, | ||
| _: Action, | ||
| ) -> Result<(), Error> { | ||
| Err(error) |
There was a problem hiding this comment.
What does passing through the error mean in practice? I gather that it means preserve the existing behavior for Fleet (and for Database), but what is the plain-language explanation of what that behavior is? Something like "directly return whatever error is returned by the auth check" ? In other words, why don't fleet and database need the special logic the resources get? Is it because they're never being fetched directly — only as a means to getting something else?
There was a problem hiding this comment.
Yes, "directly return whatever error is returned by the auth check". In practice, that will either be a 401 (if credentials weren't provided) or a 403 (if user is authenticated and not authorized). By contrast, for API resources, this does an additional check for "read" to decide whether it should be a 404 instead.
| * But we don't really know if you should be able to see the Organization. | ||
| * Right now, the only real way to tell that is if you have permissions on | ||
| * anything _inside_ the Organization, which is incredibly expensive to | ||
| * determine in general. |
There was a problem hiding this comment.
another point for the READ_THRU optimization (just kidding)
| 'd: 'f, | ||
| 'e: 'f; | ||
|
|
||
| /// Invoked on authz failure to determine the final authz result |
There was a problem hiding this comment.
Why is this method a part of this trait? From what I can tell, we implement Authorize for objects that already implement AuthzApiResource + oso::PolarClass, and only actually use methods related to those objects in the implementation I saw.
Isn't this more related to the resource? Could this be a part of the AuthzApiResource trait instead?
There was a problem hiding this comment.
AuthorizedResource (formerly Authorize) is also implemented for Database and Fleet. Those types do not impl ApiResource.
There was a problem hiding this comment.
Gotcha - I see that dependency exists more on the callsite of on_unauthorized than the implementation.
| /// corresponding ResourceType and is stored in the database | ||
| /// | ||
| /// This is a helper trait used to impl [`AuthzResource`]. | ||
| pub trait AuthzApiResource: Clone + Send + Sync + 'static { |
There was a problem hiding this comment.
Are there any objects that implement AuthzApiResource, but don't implement oso::PolarClass?
Would it simplify the traits if AuthzApiResource required the object to implement oso::PolarClass? It kinda seems like that's an expectation.
(FWIW, this currently seems possible, by adding trait AuthzApiResource: oso::PolarClass + ...)
|
Thanks for the reviews, @david-crespo and @smklein! I believe I've addressed the feedback and this is ready for another look. |
| 'd: 'f, | ||
| 'e: 'f; | ||
|
|
||
| /// Invoked on authz failure to determine the final authz result |
There was a problem hiding this comment.
Gotcha - I see that dependency exists more on the callsite of on_unauthorized than the implementation.
When a user attempts to perform some action on an object, and they shouldn't even see that the object exists, they should get a 404 rather than a 401 or 403. (If we return a 403, then we're leaking information to a potential attacker: they can find out whether an Organization exists based on whether they get a 404 or 403 when they try to create a Project inside it.)
I think this is widely understood, but for a reference, see the Enforcement chapter of Oso's Authorization Academy, which says:
That's essentially what this change does. It took some refactoring to make this work well, especially because our 404 errors have type information about what you're looking up.