-
Notifications
You must be signed in to change notification settings - Fork 41
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
/policy
for current silo, /global/policy
for fleet
#1573
Conversation
.silo_required() | ||
.internal_context("loading current silo")?; | ||
let policy = | ||
nexus.silo_fetch_policy_by_id(&opctx, authz_silo.id()).await?; |
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.
Added this by_id
fetcher because we have the ID on hand and it seems silly to fetch the silo to get the name just so we can use the existing silo_fetch_policy
.
@@ -316,10 +319,10 @@ pub fn external_api() -> NexusApiDescription { | |||
/// Fetch the top-level IAM policy | |||
#[endpoint { | |||
method = GET, | |||
path = "/policy", | |||
path = "/global/policy", |
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.
nexus/src/app/silo.rs
Outdated
silo_id: Uuid, | ||
) -> LookupResult<shared::Policy<authz::SiloRole>> { | ||
let (.., authz_silo) = LookupPath::new(opctx, &self.db_datastore) | ||
.silo_id(silo_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.
Feels a bit sad to copy silo_fetch_policy
with only this line being different, may extract a private helper that takes the authz_silo
instead.
policy_update /policy | ||
policy_view /policy | ||
silo_create /silos | ||
silo_delete /silos/{silo_name} | ||
silo_identity_provider_create /silos/{silo_name}/saml-identity-providers |
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'm not certain this tag grouping is really correct. I'm not recommending a change in this PR, but broadly it's worth us talking about what we're going to call "global" in the context of the silo. If we're going to use the tag silo (and thus silo as a term to describe this global context) then we're going to need to educate users on it even before they really need to know what a silo is.
Eh, on second thought most of the stuff here will be grouped under the global tag eventually so it's probably moot. Still unsure if silo
is the right tag for things at the root that aren't under /organizations
but also aren't under /global
, but I don't know what else we'd call it.
).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.
This fails with the following. It's making a request with the unprivileged user that it expects to 404 but it's 200ing instead. I don't understand why the unprivileged user would 200 — we are doing operations that require ReadPolicy
on the silo.
G GET PUT POST DEL TRCE G URL
0thread 'integration_tests::unauthorized::test_unauthorized' panicked at 'called `Result::unwrap()` on an `Err` value: expected status code 404 Not Found, found 200 OK
omicron/nexus/tests/integration_tests/unauthorized.rs
Lines 444 to 457 in 000333b
if do_test_unprivileged { | |
let expected_status = match allowed { | |
Some(_) => unauthz_status, | |
None => StatusCode::METHOD_NOT_ALLOWED, | |
}; | |
let response = NexusRequest::new( | |
RequestBuilder::new(client, method.clone(), &uri) | |
.body(body.as_ref()) | |
.expect_status(Some(expected_status)), | |
) | |
.authn_as(AuthnMode::UnprivilegedUser) | |
.execute() | |
.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.
Given the /policy
API is known and documented if you didn't have access to that it'd more likely be a 401. I think you want to change the visibility
in your test to public
instead of protected
. That might not be the original issue, but it is one thing.
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.
First pass at debugging using the lovely guide. Seems like the unprivileged user does have the permission in question, which is weird?
~/oxide/omicron $ cat /var/folders/lk/vsx86g0545g3g2tsmvs50py40000gn/T/test_all-af18dffa1640b050-test_unauthorized.49638.0.log | rg "authorize result" | rg "/policy" | rg "ReadPolicy" | rg "60001" | jq '{ msg, req_id, authenticated, method, uri, actor, action, result }'
{
"msg": "authorize result",
"req_id": "49c6298b-e273-41f4-b3ac-a79de7259074",
"authenticated": true,
"method": "GET",
"uri": "/global/policy",
"actor": "Some(Actor::SiloUser { silo_user_id: 001de000-05e4-4000-8000-000000060001, silo_id: 001de000-5110-4000-8000-000000000000, .. })",
"action": "ReadPolicy",
"result": "Err(Forbidden)"
}
{
"msg": "authorize result",
"req_id": "9b1a600f-70bc-4fd8-8103-5bde60c970bc",
"authenticated": true,
"method": "GET",
"uri": "/silos/demo-silo/policy",
"actor": "Some(Actor::SiloUser { silo_user_id: 001de000-05e4-4000-8000-000000060001, silo_id: 001de000-5110-4000-8000-000000000000, .. })",
"action": "ReadPolicy",
"result": "Err(ObjectNotFound { type_name: Silo, lookup_type: ByName(\"demo-silo\") })"
}
{
"msg": "authorize result",
"req_id": "2ba41eb1-bf4f-42d8-9ee4-a1fbe8899082",
"authenticated": true,
"method": "GET",
"uri": "/policy",
"actor": "Some(Actor::SiloUser { silo_user_id: 001de000-05e4-4000-8000-000000060001, silo_id: 001de000-5110-4000-8000-000000000000, .. })",
"action": "ReadPolicy",
"result": "Ok(())"
}
{
"msg": "authorize result",
"req_id": "2ba41eb1-bf4f-42d8-9ee4-a1fbe8899082",
"authenticated": true,
"method": "GET",
"uri": "/policy",
"actor": "Some(Actor::SiloUser { silo_user_id: 001de000-05e4-4000-8000-000000060001, silo_id: 001de000-5110-4000-8000-000000000000, .. })",
"action": "ReadPolicy",
"result": "Ok(())"
}
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 was the explanation of what I was seeing. The existing silo policy test was not asking about the user's own silo, which is why the unprivileged user didn't have access. Even the unpriv user has access to their own silo. I am writing an issue to think about how we could do this differently if we wanted to.
omicron/nexus/src/authz/omicron.polar
Lines 148 to 163 in 4528641
# As a special case, all authenticated users can read their own Silo. That's | |
# not quite the same as having the "viewer" role. For example, they cannot list | |
# Organizations in the Silo. | |
# | |
# One reason this is necessary is because if an unprivileged user tries to | |
# create an Organization using "POST /organizations", they should get back a 403 | |
# (which implies they're able to see /organizations, which is essentially seeing | |
# the Silo itself) rather than a 404. This behavior isn't a hard constraint | |
# (i.e., you could reasonably get a 404 for an API you're not allowed to call). | |
# Nor is the implementation (i.e., we could special-case this endpoint somehow). | |
# But granting this permission is the simplest way to keep this endpoint's | |
# behavior consistent with the rest of the API. | |
# | |
# It's unclear what else would break if users couldn't see their own Silo. | |
has_permission(actor: AuthenticatedActor, "read", silo: Silo) | |
if silo in actor.silo; |
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.
Good stuff! The silo visibility stuff was tricky but educational.
.lookup_for(authz::Action::ModifyPolicy) | ||
.await?; | ||
let (.., authz_silo) = | ||
silo_lookup.lookup_for(authz::Action::ModifyPolicy).await?; |
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.
Assuming the static lifetime isn't wrong, this approach of taking a lookup instead of a name or ID is rad. I like it.
opctx: &'a OpContext, | ||
) -> db::lookup::LookupPath { | ||
db::lookup::LookupPath::new(opctx, &self.db_datastore) | ||
} |
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.
As I say in the comment, we can do without this, but having to refer explicitly to nexus.datastore()
inside an endpoint handler to build the lookup feels like it violates the abstraction boundary of the Nexus
object. No caller will ever have to make a lookup with a different datastore than the one that hangs of nexus
.
Crucible Changes: Send deactivate flush if `need_flush` is set (#1573) Improve DTrace scripts. (#1574) Move reconcile-specific data into `ReconcileData` struct (#1567) Remove unprinted header item from sled_upstairs_info (#1566) Downgrade warning about skipping IO on all 3x Downstairs (#1564) Propolis changes: Wire up viona params from illumos#16738
Crucible Changes: Send deactivate flush if `need_flush` is set (#1573) Improve DTrace scripts. (#1574) Move reconcile-specific data into `ReconcileData` struct (#1567) Remove unprinted header item from sled_upstairs_info (#1566) Downgrade warning about skipping IO on all 3x Downstairs (#1564) Propolis changes: Wire up viona params from illumos#16738 Co-authored-by: Alan Hanson <[email protected]>
Fixes #1571
GET/PUT /policy
, which was previously about the fleet policy, to `/global/policyGET /policy
andPUT /policy
are now about the user's current silofetch_silo_policy
andupdate_silo_policy
to take adb::lookup::Silo
instead of a name or ID so they are agnostic about whether you're working with name or ID at the call site