-
Notifications
You must be signed in to change notification settings - Fork 40
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
Authenticated user need privileges on global images and themselves #1341
Authenticated user need privileges on global images and themselves #1341
Conversation
Add polar statements so that any authenticated user can list and read global images. In order to test this, the set of integration tests that check endpoints for their authz visibility had to change: `GET /images/` was now accessible by any authenticated user, even if they had no permissions at all, meaning new type of visibility was required. But `POST /images` was protected by authz, meaning separate visibility settings were required for different HTTP methods. This commit also adds per-method authz visibility and updates each VerifyEndpoint struct accordingly. A few of the endpoints in uncovered-authz-endpoints.txt were able to be added in because of the new visibility variant, but not all. In order to test those for authz visibility, more work is required to support endpoints that redirect (for example) or endpoints with query parameters.
To anyone reviewing: there's a few |
Are their separate permissions for viewing global images and uploading (or replacing) global images? |
There are :) This PR grants "list_children" and "read" only, not "modify". |
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 looks great to me, but I'd feel better if @davepacheco gave his 👍 too.
allowed_methods: vec![ | ||
AllowedMethod { | ||
method: MethodAndBody::Get, | ||
visibility: Visibility::Protected, // XXX is this right? |
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, I believe this (and the ones below) is correct. There's an argument to be made that SSH public keys should be truly public (as they are on GitHub, for example), but we decided not to go that route for now. In particular, there's no was currently for one user to even address another user's keys, since we go through /session/me
.
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 I mean is it was a little surprising that GET /session/me/sshkeys
is expected to return a 403 - there is no endpoint to address other user's keys, this is for the current user, and I would have expected 200 OK with a body of []
or something like that 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.
Oh, I misunderstood what the test was saying. You are correct: for the user that added the demo key, they should get a 200 OK
with the key that was added; for a user that has no keys, they should get 200 OK
with an empty list; and for an unauthenticated user, they should get 403 Forbidden
.
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.
Shouldn't an unauthenticated user get a 401?
session_me (get "/session/me") | ||
sshkeys_get (get "/session/me/sshkeys") | ||
sshkeys_get_key (get "/session/me/sshkeys/{ssh_key_name}") |
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.
Hooray 🎉
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.
Thanks for doing this. The policy change unblocks the demo flow and it's great to get a bunch of endpoints out of the ignore list. For what it's worth I sometimes make separate PRs for Nexus vs. this test so that it's easier to be sure we didn't break behavior (and not notice because we also broke the test). I'm not sure if it's worth doing that here.
pub visibility: Visibility, | ||
|
||
/// Specifies what HTTP methods are supported for this HTTP resource | ||
/// Specifies what HTTP methods are supported for this HTTP resource, along |
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 feels a little off to me and I'm trying to figure out why.
Before this change, there was an idea of "visibility" that applied at the HTTP-resource-level. e.g., "/organizations" is visible to everybody, but "/organizations/some-org" is not). That's not just "can you GET it", but also whether you get a 403 or 404 when you can't get it. This also enforced some level of API coherence: you could not specify that an unprivileged user is allowed to delete a resource but not see it. I think this was useful because in practice I expect us to conform to a small number of patterns: "everyone can see and modify this" (like your own ssh keys), "everyone can see but not modify this" (like /images), and "only privileged users can do anything with this" (most other things). This also enforced some useful invariants like "if you can delete this thing, then you can also see it".
With this change, the idea of "visibility" of an HTTP resource goes away and we're really talking about access for a specific method+URL combination. This allows allows you to specify some nonsensical configurations now (e.g., you can say that an unauthenticated user can delete a resource but you have to be authenticated to see it). There's nothing to enforce that the behavior matches one of these patterns.
An alternative might be to have another field here at the endpoint level called "unprivileged_access" or something that specifies what authenticated, unprivileged users are expected to be able to do here. We could have variants for the patterns that we expect to have: maybe UnprivilegedAccess::Full
, UnprivilegedAccess::ReadOnly
, or UnprivilegedAccess::None
corresponding to the cases I mentioned above. This would enforce only a handful of possible configurations here, which I think is a good thing because you couldn't (accidentally or otherwise) create the nonsensical configurations I mentioned above. It'd also be a lot less boilerplate for each endpoint and this change would be much smaller. What do you think?
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.
I agree wholeheartedly with this suggestion, and rolled back all the original changes. There's nothing to enforce that the behavior matches one of these patterns.
was a very clarifying statement about the intent of the visibility part of the test and see now how the original changes allow for nonsense combinations like you said. Sorry for all the churn :)
} | ||
).unwrap() | ||
), | ||
AllowedMethod { |
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 terms of reviewing these changes, since there are a lot of them and I want to make sure I got it right, I think the rule is: for any endpoint that was already in this file, whatever visibility it had before should now be applied identically and individually to each of its methods, modulo that Public becomes PublicPermissionRequired. Is that right?
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.
With the latest changes, it should be more straight forward to review. Almost everything gets UnprivilegedAccess::None
, except for a few endpoints.
visibility: Visibility::Protected, // XXX is this right? | ||
}, | ||
AllowedMethod { | ||
method: MethodAndBody::Delete, |
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.
How does this not cause us to actually delete the ssh key when we run this test?
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.
It would! Any combination of UnprivilegedAccess::Full
and a DELETE call would. But I'm deferring the SSH keys changes because I couldn't get the authz policy to work and didn't want to hold the global image changes up.
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.
I think this is ok from the context of this test - we should be testing that DELETE works to test what permissions UnprivilegedAccess::Full
should give
allowed_methods: vec![ | ||
AllowedMethod { | ||
method: MethodAndBody::Get, | ||
visibility: Visibility::Protected, // XXX is this right? |
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.
Shouldn't an unauthenticated user get a 401?
Can't get SSH keys tests to work, deferring for another commit.
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! I like this a lot.
nexus/src/authz/omicron.polar
Outdated
@@ -240,15 +244,28 @@ resource SiloUser { | |||
"create_child", | |||
]; | |||
|
|||
roles = ["admin", "viewer"]; |
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 isn't ironclad or anything but we don't bother specifying Oso roles for things where we don't support actually assigning those roles. That is, Oso roles are 1:1 with roles that might be stored in the database.
You could remove these, plus the rules at L255-L260, and just define some has_permission
rules.
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.
It's funny because that was the original way we implemented it :)
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.
done in d0ced98
fmt, clippy, and uncovered-authz-endpoints.txt update
I have some thoughts on some cleanup here but I think we should land this now and deal with that later. |
Sorry for the terseness in my previous comment. I was trying to avoid holding up today's demo. One cleanup we could make is that we don't need four After talking with @jmpesp offline I realized we (myself included) might be confusing the purpose of this test. Today, the purpose of this test ("unauthorized.rs") is to verify uniform behavior of the API when users are not authorized to do things. If we stick with that, then I think the expected behavior here is:
Of course, something should probably test that unprivileged users can read things that have There's a related problem here, which is the output format of the test assumes that unprivileged users will never get 200-level responses. If we punt this testing to a separate test as I suggested, we don't have to deal with this. It seems like this PR now has three changes that could be separated:
As I understand it, the first two are straightforward and unrelated to each other. The third thing is where we're still working through details. Is that right? |
I filed #1374 with some thoughts on how to better test that particular roles can access certain resources. |
Changing the unauthorized.rs test to support unprivileged users retrieving results for
I agree that given the original purpose of the unauthorized tests, there shouldn't be paths testing for when users do have access - that belongs somewhere else. Right now I'm backing out basically all changes to the unauthorized.rs test(s), and writing separate tests for an unprivileged user's access. |
Yeah, that path makes sense. I still think the changes with the |
Roll back changes to unauthorized.rs (mostly) and add specific authz tests for policy that allows unprivileged access.
This is ready for re-review now :) |
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.
I have some suggestions here but they're mostly minor. Given the urgency of this I think we should land it as-is and follow up separately on any of these other things.
// Some authz policy states that authenticated users get implicit | ||
// privileges for some resources. Do not test for those here, they | ||
// should be covered in other resource specific tests. | ||
if endpoint.unprivileged_access != UnprivilegedAccess::None { |
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.
I think we still want to test POST/PUT/DELETE on "readonly" endpoints here.
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.
Addressed in #1399.
@@ -249,6 +251,10 @@ resource SiloUser { | |||
has_relation(silo: Silo, "parent_silo", user: SiloUser) | |||
if user.silo = silo; | |||
|
|||
# authenticated actors have all permissions on themselves | |||
has_permission(actor: AuthenticatedActor, _perm: String, silo_user: SiloUser) | |||
if actor.equals_silo_user(silo_user); |
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.
Is there any reason not to just do something like actor.id = silo_user.id
?
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.
Yeah, we can't make UUIDs into Polar classes, since we don't own the crate.
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.
I think they're already supported:
https://docs.osohq.com/rust/reference/polar/classes.html#uuids-via-the-uuid-crate
I'll take a swing at this in a follow-up PR.
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.
The other problem here is that there's no id
attribute on silo_user
. We could add one, but this type is generated by the authz_resource!
macro so either we'd have to make that configurable or everything would get a new id
attribute. I decided this isn't worth spending more time on.
.parsed_body() | ||
.unwrap(); | ||
|
||
// - cannot create a global image - should get 403 |
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.
I think this should be covered by the unauthorized test (commented there that we should still test ReadOnly endpoints)
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.
Addressed in #1399.
.await | ||
.expect("POST request should have failed"); | ||
|
||
// - cannot delete a global image - also should get a 404 because the |
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.
Same
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.
Addressed in #1399.
|
||
use uuid::Uuid; | ||
|
||
// Test that an authenticated, unprivileged user has full CRUD access to their SSH keys |
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.
Is this not tested already by the sshkey tests? (Or, could they be, if those tests used UnprivilegedUser
?) (cc @plotnick)
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.
I think so, yes. It looks like creation, listing, and deletion are all covered by the tests below, but as you say not by an unprivileged user.
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.
Addressed in #1399.
I went ahead and sync'd this with "main" and will plan to land this PR. |
The last test run hit #962 (on buildomat only) and I've rerun it. |
Added a new package, crucible-dtrace that pulls from buildomat a package that contains a set of DTrace scripts. These scripts are extracted into the global zone at /opt/oxide/crucible_dtrace/ Update Crucible to latest includes these updates: Clean up dependency checking, fixing space leak (#1372) Make a DTrace package (#1367) Use a single context in all messages (#1363) Remove `DownstairsWork`, because it's redundant (#1371) Remove `WorkState`, because it's implicit (#1370) Do work immediately upon receipt of a job, if possible (#1366) Move 'do work for one job' into a helper function (#1365) Remove `DownstairsWork` from map when handling it (#1361) Using `block_in_place` for IO operations (#1357) update omicron deps; use re-exported dropshot types in oximeter-producer configuration (#1369) Parameterize more tests (#1364) Misc cleanup, remove sqlite references. (#1360) Fix `Extent::close` docstring (#1359) Make many `Region` functions synchronous (#1356) Remove `Workstate::Done` (unused) (#1355) Return a sorted `VecDeque` directly (#1354) Combine `proc_frame` and `do_work_for` (#1351) Move `do_work_for` and `do_work` into `ActiveConnection` (#1350) Support arbitrary Volumes during replace compare (#1349) Remove the SQLite backend (#1352) Add a custom timeout for buildomat tests (#1344) Move `proc_frame` into `ActiveConnection` (#1348) Remove `UpstairsConnection` from `DownstairsWork` (#1341) Move Work into ConnectionState (#1340) Make `ConnectionState` an enum type (#1339) Parameterize `test_repair.sh` directories (#1345) Remove `Arc<Mutex<Downstairs>>` (#1338) Send message to Downstairs directly (#1336) Consolidate `on_disconnected` and `remove_connection` (#1333) Move disconnect logic to the Downstairs (#1332) Remove invalid DTrace probes. (#1335) Fix outdated comments (#1331) Use message passing when a new connection starts (#1330) Move cancellation into Downstairs, using a token to kill IO tasks (#1329) Make the Downstairs own per-connection state (#1328) Move remaining local state into a `struct ConnectionState` (#1327) Consolidate negotiation + IO operations into one loop (#1322) Allow replacement of a target in a read_only_parent (#1281) Do all IO through IO tasks (#1321) Make `reqwest_client` only present if it's used (#1326) Move negotiation into Downstairs as well (#1320) Update Rust crate clap to v4.5.4 (#1301) Reuse a reqwest client when creating Nexus clients (#1317) Reuse a reqwest client when creating repair client (#1324) Add % to keep buildomat happy (#1323) Downstairs task cleanup (#1313) Update crutest replace test, and mismatch printing. (#1314) Added more DTrace scripts. (#1309) Update Rust crate async-trait to 0.1.80 (#1298)
Update Crucible and Propolis to the latest Added a new package, crucible-dtrace that pulls from buildomat a package that contains a set of DTrace scripts. These scripts are extracted into the global zone at /opt/oxide/crucible_dtrace/ Crucible latest includes these updates: Clean up dependency checking, fixing space leak (#1372) Make a DTrace package (#1367) Use a single context in all messages (#1363) Remove `DownstairsWork`, because it's redundant (#1371) Remove `WorkState`, because it's implicit (#1370) Do work immediately upon receipt of a job, if possible (#1366) Move 'do work for one job' into a helper function (#1365) Remove `DownstairsWork` from map when handling it (#1361) Using `block_in_place` for IO operations (#1357) update omicron deps; use re-exported dropshot types in oximeter-producer configuration (#1369) Parameterize more tests (#1364) Misc cleanup, remove sqlite references. (#1360) Fix `Extent::close` docstring (#1359) Make many `Region` functions synchronous (#1356) Remove `Workstate::Done` (unused) (#1355) Return a sorted `VecDeque` directly (#1354) Combine `proc_frame` and `do_work_for` (#1351) Move `do_work_for` and `do_work` into `ActiveConnection` (#1350) Support arbitrary Volumes during replace compare (#1349) Remove the SQLite backend (#1352) Add a custom timeout for buildomat tests (#1344) Move `proc_frame` into `ActiveConnection` (#1348) Remove `UpstairsConnection` from `DownstairsWork` (#1341) Move Work into ConnectionState (#1340) Make `ConnectionState` an enum type (#1339) Parameterize `test_repair.sh` directories (#1345) Remove `Arc<Mutex<Downstairs>>` (#1338) Send message to Downstairs directly (#1336) Consolidate `on_disconnected` and `remove_connection` (#1333) Move disconnect logic to the Downstairs (#1332) Remove invalid DTrace probes. (#1335) Fix outdated comments (#1331) Use message passing when a new connection starts (#1330) Move cancellation into Downstairs, using a token to kill IO tasks (#1329) Make the Downstairs own per-connection state (#1328) Move remaining local state into a `struct ConnectionState` (#1327) Consolidate negotiation + IO operations into one loop (#1322) Allow replacement of a target in a read_only_parent (#1281) Do all IO through IO tasks (#1321) Make `reqwest_client` only present if it's used (#1326) Move negotiation into Downstairs as well (#1320) Update Rust crate clap to v4.5.4 (#1301) Reuse a reqwest client when creating Nexus clients (#1317) Reuse a reqwest client when creating repair client (#1324) Add % to keep buildomat happy (#1323) Downstairs task cleanup (#1313) Update crutest replace test, and mismatch printing. (#1314) Added more DTrace scripts. (#1309) Update Rust crate async-trait to 0.1.80 (#1298) Propolis just has this one update: Allow boot order config in propolis-standalone --------- Co-authored-by: Alan Hanson <[email protected]>
Add polar statements so that any authenticated user can list and read
global images.
In order to test this, the set of integration tests that check endpoints
for their authz visibility had to change:
GET /images/
was nowaccessible by any authenticated user, even if they had no permissions at
all, meaning new type of visibility was required. But
POST /images
wasprotected by authz, meaning separate visibility settings were required
for different HTTP methods. This commit also adds per-method authz
visibility and updates each VerifyEndpoint struct accordingly.
A few of the endpoints in uncovered-authz-endpoints.txt were able to be
added in because of the new visibility variant, but not all. In order
to test those for authz visibility, more work is required to support
endpoints that redirect (for example) or endpoints with query
parameters.