Skip to content

Commit 1b7b3b9

Browse files
committed
add fleet and silo role to /v1/me response
1 parent 5f2f06d commit 1b7b3b9

File tree

9 files changed

+292
-30
lines changed

9 files changed

+292
-30
lines changed

nexus/auth/src/authz/context.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,20 @@ impl Context {
149149
}
150150
}
151151
}
152+
153+
/// Get the opctx actor's roles on a given resource
154+
pub async fn get_roles<Resource>(
155+
&self,
156+
opctx: &OpContext,
157+
resource: Resource,
158+
) -> Result<RoleSet, Error>
159+
where
160+
Resource: AuthorizedResource + Clone,
161+
{
162+
let mut roles = RoleSet::new();
163+
resource.load_roles(opctx, &self.authn, &mut roles).await?;
164+
Ok(roles)
165+
}
152166
}
153167

154168
pub trait AuthorizedResource: oso::ToPolar + Send + Sync + 'static {

nexus/auth/src/authz/roles.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use uuid::Uuid;
4949
/// For more on roles, see dbinit.rs.
5050
#[derive(Clone, Debug)]
5151
pub struct RoleSet {
52-
roles: BTreeSet<(ResourceType, Uuid, String)>,
52+
pub roles: BTreeSet<(ResourceType, Uuid, String)>,
5353
}
5454

5555
impl RoleSet {
@@ -82,6 +82,28 @@ impl RoleSet {
8282
String::from(role_name),
8383
));
8484
}
85+
86+
/// Effective, i.e., strongest, role in set for specified resource
87+
pub fn effective_role(
88+
&self,
89+
resource_type: &ResourceType,
90+
resource_id: &Uuid,
91+
) -> Option<String> {
92+
self.roles
93+
.iter()
94+
.filter(|(typ, id, _)| typ == resource_type && id == resource_id)
95+
.max_by_key(|(_, _, role)| role_num(role))
96+
.map(|(_, _, role)| role.clone())
97+
}
98+
}
99+
100+
fn role_num(role: &str) -> u8 {
101+
match role {
102+
"viewer" => 1,
103+
"collaborator" => 2,
104+
"admin" => 3,
105+
_ => 0,
106+
}
85107
}
86108

87109
pub async fn load_roles_for_resource_tree<R>(

nexus/auth/src/context.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use super::authz;
88
use crate::authn::ConsoleSessionWithSiloId;
99
use crate::authn::external::session_cookie::Session;
1010
use crate::authz::AuthorizedResource;
11+
use crate::authz::RoleSet;
1112
use crate::storage::Storage;
1213
use chrono::{DateTime, Utc};
1314
use omicron_common::api::external::Error;
@@ -285,6 +286,16 @@ impl OpContext {
285286
result
286287
}
287288

289+
pub async fn get_roles<Resource>(
290+
&self,
291+
resource: &Resource,
292+
) -> Result<RoleSet, Error>
293+
where
294+
Resource: AuthorizedResource + Debug + Clone,
295+
{
296+
self.authz.get_roles(self, resource.clone()).await
297+
}
298+
288299
/// Returns an error if we're currently in a context where expensive or
289300
/// complex operations should not be allowed
290301
///

nexus/src/external_api/http_entrypoints.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use dropshot::{WebsocketChannelResult, WebsocketConnection};
3939
use dropshot::{http_response_found, http_response_see_other};
4040
use http::{Response, StatusCode, header};
4141
use ipnetwork::IpNetwork;
42+
use nexus_auth::authz::{ApiResource, ApiResourceWithRoles};
4243
use nexus_db_lookup::lookup::ImageLookup;
4344
use nexus_db_lookup::lookup::ImageParentLookup;
4445
use nexus_db_queries::authn::external::session_cookie::{self, SessionStore};
@@ -6942,11 +6943,31 @@ impl NexusExternalApi for NexusExternalApiImpl {
69426943
let handler = async {
69436944
let opctx =
69446945
crate::context::op_context_for_external_api(&rqctx).await?;
6946+
69456947
let user = nexus.silo_user_fetch_self(&opctx).await?;
6946-
let (_, silo) = nexus.current_silo_lookup(&opctx)?.fetch().await?;
6948+
let (authz_silo, db_silo) =
6949+
nexus.current_silo_lookup(&opctx)?.fetch().await?;
6950+
6951+
let silo_roles = opctx.get_roles(&authz_silo).await;
69476952
Ok(HttpResponseOk(views::CurrentUser {
69486953
user: user.into(),
6949-
silo_name: silo.name().clone(),
6954+
silo_name: db_silo.name().clone(),
6955+
fleet_role: silo_roles.clone().map_or(None, |roleset| {
6956+
roleset.effective_role(
6957+
&authz::FLEET.resource_type(),
6958+
&authz::FLEET.resource_id(),
6959+
)
6960+
}),
6961+
// TODO: one weird wrinkle here is that the polar policy gives
6962+
// every user read perms on their own silo, but that is not
6963+
// equivalent to viewer because it's not inherited, so we are
6964+
// probably fine leaving the no-role case as None
6965+
silo_role: silo_roles.map_or(None, |roleset| {
6966+
roleset.effective_role(
6967+
&authz_silo.resource_type(),
6968+
&authz_silo.resource_id(),
6969+
)
6970+
}),
69506971
}))
69516972
};
69526973
apictx

nexus/test-utils/src/resource_helpers.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,11 +1106,8 @@ pub async fn grant_iam<T>(
11061106
let existing_policy: shared::Policy<T> =
11071107
NexusRequest::object_get(client, &policy_url)
11081108
.authn_as(run_as.clone())
1109-
.execute()
1110-
.await
1111-
.expect("failed to fetch policy")
1112-
.parsed_body()
1113-
.expect("failed to parse policy");
1109+
.execute_and_parse_unwrap()
1110+
.await;
11141111
let new_role_assignment = shared::RoleAssignment {
11151112
identity_type: IdentityType::SiloUser,
11161113
identity_id: grant_user,
@@ -1132,6 +1129,43 @@ pub async fn grant_iam<T>(
11321129
.expect("failed to update policy");
11331130
}
11341131

1132+
/// Same as `grant_iam`, except that instead of adding a role, we are
1133+
/// filtering out any matching roles for the user on that resource
1134+
pub async fn revoke_iam<T>(
1135+
client: &ClientTestContext,
1136+
grant_resource_url: &str,
1137+
grant_role: T,
1138+
grant_user: Uuid,
1139+
run_as: AuthnMode,
1140+
) where
1141+
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
1142+
{
1143+
let policy_url = format!("{}/policy", grant_resource_url);
1144+
let existing_policy: shared::Policy<T> =
1145+
NexusRequest::object_get(client, &policy_url)
1146+
.authn_as(run_as.clone())
1147+
.execute_and_parse_unwrap()
1148+
.await;
1149+
1150+
let filtered_role_assignments = existing_policy
1151+
.role_assignments
1152+
.into_iter()
1153+
.filter(|ra| {
1154+
!(ra.identity_id == grant_user && ra.role_name == grant_role)
1155+
})
1156+
.collect();
1157+
1158+
let new_policy =
1159+
shared::Policy { role_assignments: filtered_role_assignments };
1160+
1161+
// TODO-correctness use etag when we have it
1162+
NexusRequest::object_put(client, &policy_url, Some(&new_policy))
1163+
.authn_as(run_as)
1164+
.execute()
1165+
.await
1166+
.expect("failed to update policy");
1167+
}
1168+
11351169
pub async fn project_get(
11361170
client: &ClientTestContext,
11371171
project_url: &str,

0 commit comments

Comments
 (0)