-
Notifications
You must be signed in to change notification settings - Fork 45
return 404 rather than 401/403 when user can't even see the object #600
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
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
# Define relationships | |
has_relation(fleet: Fleet, "parent_fleet", organization: Organization) | |
if organization.fleet = fleet; | |
has_relation(organization: Organization, "parent_organization", project: Project) | |
if project.organization = organization; | |
has_relation(project: Project, "parent_project", project_child: ProjectChild) | |
if project_child.project = project; | |
has_relation(fleet: Fleet, "parent_fleet", fleet_child: FleetChild) | |
if fleet_child.fleet = fleet; |
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.
nexus/src/authz/api_resources.rs
Outdated
_: AnyActor, | ||
_: Action, | ||
) -> Result<(), Error> { | ||
Err(error) |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AuthorizedResource
(formerly Authorize
) is also implemented for Database
and Fleet
. Those types do not impl ApiResource
.
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.
Gotcha - I see that dependency exists more on the callsite of on_unauthorized
than the implementation.
nexus/src/authz/api_resources.rs
Outdated
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 + ...
)
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.
Fixed in 484ae6b.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.