Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions nexus/src/app/iam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ impl super::Nexus {
pub async fn silo_user_fetch_groups_for_self(
&self,
opctx: &OpContext,
) -> ListResultVec<db::model::SiloGroupMembership> {
self.db_datastore.silo_group_membership_for_self(opctx).await
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<db::model::SiloGroup> {
self.db_datastore.silo_groups_for_self(opctx, pagparams).await
}

// Silo groups
Expand Down
15 changes: 9 additions & 6 deletions nexus/src/db/datastore/silo_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,11 @@ impl DataStore {
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

pub async fn silo_group_membership_for_self(
pub async fn silo_groups_for_self(
&self,
opctx: &OpContext,
) -> ListResultVec<SiloGroupMembership> {
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<SiloGroup> {
// Similar to session_hard_delete (see comment there), we do not do a
// typical authz check, instead effectively encoding the policy here
// that any user is allowed to fetch their own group memberships
Expand All @@ -118,10 +119,12 @@ impl DataStore {
.actor_required()
.internal_context("fetching current user's group memberships")?;

use db::schema::silo_group_membership::dsl;
dsl::silo_group_membership
.filter(dsl::silo_user_id.eq(actor.actor_id()))
.select(SiloGroupMembership::as_returning())
use db::schema::{silo_group as sg, silo_group_membership as sgm};
paginated(sg::dsl::silo_group, sg::id, pagparams)
.inner_join(sgm::table.on(sgm::silo_group_id.eq(sg::id)))
.filter(sgm::silo_user_id.eq(actor.actor_id()))
.filter(sg::time_deleted.is_null())
.select(SiloGroup::as_returning())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed with debug_query that this produces this (I commented out the paginated part because I don't know how to unbox it):

SELECT silo_group.id,
       silo_group.time_created,
       silo_group.time_modified,
       silo_group.silo_id,
       silo_group.external_id
  FROM silo_group
       INNER JOIN silo_group_membership ON
			silo_group_membership.silo_group_id
			= silo_group.id
 WHERE silo_group_membership.silo_user_id = $1
   AND (silo_group.time_deleted IS NULL);

.get_results_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
Expand Down
51 changes: 42 additions & 9 deletions nexus/src/external_api/console_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ use dropshot::{
endpoint, http_response_found, http_response_see_other, HttpError,
HttpResponseFound, HttpResponseHeaders, HttpResponseOk,
HttpResponseSeeOther, HttpResponseUpdatedNoContent, Path, Query,
RequestContext, TypedBody,
RequestContext, ResultsPage, TypedBody,
};
use http::{header, Response, StatusCode};
use hyper::Body;
use lazy_static::lazy_static;
use mime_guess;
use omicron_common::api::external::http_pagination::{
data_page_params_for, PaginatedById, ScanById, ScanParams,
};
use omicron_common::api::external::Error;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -592,7 +595,7 @@ pub async fn login_begin(
}]
pub async fn session_me(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
) -> Result<HttpResponseOk<views::SessionMe>, HttpError> {
) -> Result<HttpResponseOk<views::User>, HttpError> {
let apictx = rqctx.context();
let nexus = &apictx.nexus;
let handler = async {
Expand All @@ -601,13 +604,43 @@ pub async fn session_me(
// not clear what the advantage would be.
let opctx = OpContext::for_external_api(&rqctx).await?;
let user = nexus.silo_user_fetch_self(&opctx).await?;
let groups = nexus.silo_user_fetch_groups_for_self(&opctx).await?;
Ok(HttpResponseOk(views::SessionMe {
id: user.id(),
display_name: user.external_id,
silo_id: user.silo_id,
group_ids: groups.iter().map(|g| g.silo_group_id).collect(),
}))
Ok(HttpResponseOk(user.into()))
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Fetch the silo groups the current user belongs to
#[endpoint {
method = GET,
path = "/session/me/groups",
tags = ["hidden"],
}]
pub async fn session_me_groups(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
query_params: Query<PaginatedById>,
) -> Result<HttpResponseOk<ResultsPage<views::Group>>, HttpError> {
let apictx = rqctx.context();
let nexus = &apictx.nexus;
let query = query_params.into_inner();
let handler = async {
// We don't care about authentication method, as long as they are authed
// as _somebody_. We could restrict this to session auth only, but it's
// not clear what the advantage would be.
let opctx = OpContext::for_external_api(&rqctx).await?;
let groups = nexus
.silo_user_fetch_groups_for_self(
&opctx,
&data_page_params_for(&rqctx, &query)?,
)
.await?
.into_iter()
.map(|d| d.into())
.collect();
Ok(HttpResponseOk(ScanById::results_page(
&query,
groups,
&|_, group: &views::Group| group.id,
)?))
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}
Expand Down
1 change: 1 addition & 0 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ pub fn external_api() -> NexusApiDescription {
api.register(console_api::logout)?;

api.register(console_api::session_me)?;
api.register(console_api::session_me_groups)?;
api.register(console_api::console_page)?;
api.register(console_api::console_root)?;
api.register(console_api::console_settings_page)?;
Expand Down
46 changes: 40 additions & 6 deletions nexus/tests/integration_tests/console_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use dropshot::test_util::ClientTestContext;
use dropshot::ResultsPage;
use http::header::HeaderName;
use http::{header, method::Method, StatusCode};
use std::env::current_dir;
Expand Down Expand Up @@ -335,16 +336,15 @@ async fn test_session_me(cptestctx: &ControlPlaneTestContext) {
.execute()
.await
.expect("failed to get current user")
.parsed_body::<views::SessionMe>()
.parsed_body::<views::User>()
.unwrap();

assert_eq!(
priv_user,
views::SessionMe {
views::User {
id: USER_TEST_PRIVILEGED.id(),
display_name: USER_TEST_PRIVILEGED.external_id.clone(),
silo_id: DEFAULT_SILO.id(),
group_ids: vec![],
}
);

Expand All @@ -353,20 +353,54 @@ async fn test_session_me(cptestctx: &ControlPlaneTestContext) {
.execute()
.await
.expect("failed to get current user")
.parsed_body::<views::SessionMe>()
.parsed_body::<views::User>()
.unwrap();

assert_eq!(
unpriv_user,
views::SessionMe {
views::User {
id: USER_TEST_UNPRIVILEGED.id(),
display_name: USER_TEST_UNPRIVILEGED.external_id.clone(),
silo_id: DEFAULT_SILO.id(),
group_ids: vec![],
}
);
}

#[nexus_test]
async fn test_session_me_groups(cptestctx: &ControlPlaneTestContext) {
let testctx = &cptestctx.external_client;

// hitting /session/me without being logged in is a 401
RequestBuilder::new(&testctx, Method::GET, "/session/me/groups")
.expect_status(Some(StatusCode::UNAUTHORIZED))
.execute()
.await
.expect("failed to 401 on unauthed request");

// now make same request with auth
let priv_user_groups =
NexusRequest::object_get(testctx, "/session/me/groups")
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.expect("failed to get current user")
.parsed_body::<ResultsPage<views::Group>>()
.unwrap();

assert_eq!(priv_user_groups.items, vec![]);

let unpriv_user_groups =
NexusRequest::object_get(testctx, "/session/me/groups")
.authn_as(AuthnMode::UnprivilegedUser)
.execute()
.await
.expect("failed to get current user")
.parsed_body::<ResultsPage<views::Group>>()
.unwrap();

assert_eq!(unpriv_user_groups.items, vec![]);
}

#[nexus_test]
async fn test_login_redirect(cptestctx: &ControlPlaneTestContext) {
let testctx = &cptestctx.external_client;
Expand Down
8 changes: 8 additions & 0 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,14 @@ lazy_static! {
AllowedMethod::Get,
],
},
VerifyEndpoint {
url: "/session/me/groups",
visibility: Visibility::Public,
unprivileged_access: UnprivilegedAccess::ReadOnly,
allowed_methods: vec![
AllowedMethod::Get,
],
},

/* SSH keys */

Expand Down
21 changes: 18 additions & 3 deletions nexus/tests/integration_tests/saml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,9 +1030,9 @@ async fn test_post_saml_response(cptestctx: &ControlPlaneTestContext) {

assert_same_items(silo_group_names, vec!["SRE", "Admins"]);

let session_me: views::SessionMe = NexusRequest::new(
let session_me: views::User = NexusRequest::new(
RequestBuilder::new(client, Method::GET, "/session/me")
.header(http::header::COOKIE, session_cookie_value)
.header(http::header::COOKIE, session_cookie_value.clone())
.expect_status(Some(StatusCode::OK)),
)
.execute()
Expand All @@ -1042,7 +1042,22 @@ async fn test_post_saml_response(cptestctx: &ControlPlaneTestContext) {
.unwrap();

assert_eq!(session_me.display_name, "[email protected]");
assert_same_items(session_me.group_ids, silo_group_ids);

let session_me: ResultsPage<views::Group> = NexusRequest::new(
RequestBuilder::new(client, Method::GET, "/session/me/groups")
.header(http::header::COOKIE, session_cookie_value)
.expect_status(Some(StatusCode::OK)),
)
.execute()
.await
.expect("expected success")
.parsed_body()
.unwrap();

let session_me_group_ids =
session_me.items.iter().map(|g| g.id).collect::<Vec<_>>();

assert_same_items(session_me_group_ids, silo_group_ids);
}

/// Order-agnostic vec equality
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ device_auth_request /device/auth
login_spoof /login
logout /logout
session_me /session/me
session_me_groups /session/me/groups

API operations found with tag "images"
OPERATION ID URL PATH
Expand Down
13 changes: 0 additions & 13 deletions nexus/types/src/external_api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,19 +322,6 @@ pub struct User {
pub silo_id: Uuid,
}

/// Client view of a [`User`] and their groups
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, JsonSchema)]
pub struct SessionMe {
pub id: Uuid,
/** Human-readable name that can identify the user */
pub display_name: String,

/** Uuid of the silo to which this user belongs */
pub silo_id: Uuid,

pub group_ids: Vec<Uuid>,
}

// SILO GROUPS

/// Client view of a [`Group`]
Expand Down
Loading