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,315 changes: 0 additions & 5,315 deletions nexus/src/db/datastore.rs

This file was deleted.

147 changes: 147 additions & 0 deletions nexus/src/db/datastore/console_session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! [`DataStore`] methods related to [`ConsoleSession`]s.

use super::DataStore;
use crate::authn;
use crate::authz;
use crate::context::OpContext;
use crate::db;
use crate::db::lookup::LookupPath;
use crate::db::model::ConsoleSession;
use crate::db::model::IdentityType;
use async_bb8_diesel::AsyncRunQueryDsl;
use chrono::Utc;
use diesel::prelude::*;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DeleteResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::InternalContext;
use omicron_common::api::external::UpdateResult;

impl DataStore {
// TODO-correctness: fix session method errors. the map_errs turn all errors
// into 500s, most notably (and most frequently) session not found. they
// don't end up as 500 in the http response because they get turned into a
// 4xx error by calling code, the session cookie authn scheme. this is
// necessary for now in order to avoid the possibility of leaking out a
// too-friendly 404 to the client. once datastore has its own error type and
// the conversion to serializable user-facing errors happens elsewhere (see
// issue #347) these methods can safely return more accurate errors, and
// showing/hiding that info as appropriate will be handled higher up
// TODO-correctness this may apply at the Nexus level as well.

pub async fn session_create(
&self,
opctx: &OpContext,
session: ConsoleSession,
) -> CreateResult<ConsoleSession> {
opctx
.authorize(authz::Action::CreateChild, &authz::CONSOLE_SESSION_LIST)
.await?;

use db::schema::console_session::dsl;

diesel::insert_into(dsl::console_session)
.values(session)
.returning(ConsoleSession::as_returning())
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
Error::internal_error(&format!(
"error creating session: {:?}",
e
))
})
}

pub async fn session_update_last_used(
&self,
opctx: &OpContext,
authz_session: &authz::ConsoleSession,
) -> UpdateResult<authn::ConsoleSessionWithSiloId> {
opctx.authorize(authz::Action::Modify, authz_session).await?;

use db::schema::console_session::dsl;
let console_session = diesel::update(dsl::console_session)
.filter(dsl::token.eq(authz_session.id()))
.set((dsl::time_last_used.eq(Utc::now()),))
.returning(ConsoleSession::as_returning())
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
Error::internal_error(&format!(
"error renewing session: {:?}",
e
))
})?;

let (.., db_silo_user) = LookupPath::new(opctx, &self)
.silo_user_id(console_session.silo_user_id)
.fetch()
.await
.map_err(|e| {
Error::internal_error(&format!(
"error fetching silo id: {:?}",
e
))
})?;

Ok(authn::ConsoleSessionWithSiloId {
console_session,
silo_id: db_silo_user.silo_id,
})
}

// putting "hard" in the name because we don't do this with any other model
pub async fn session_hard_delete(
&self,
opctx: &OpContext,
authz_session: &authz::ConsoleSession,
) -> DeleteResult {
// We don't do a typical authz check here. Instead, knowing that every
// user is allowed to delete their own session, the query below filters
// on the session's silo_user_id matching the current actor's id.
//
// We could instead model this more like other authz checks. That would
// involve fetching the session record from the database, storing the
// associated silo_user_id into the `authz::ConsoleSession`, and having
// an Oso rule saying you can delete a session whose associated silo
// user matches the authenticated actor. This would be a fair bit more
// complicated and more work at runtime work than what we're doing here.
// The tradeoff is that we're effectively encoding policy here, but it
// seems worth it in this case.
let actor = opctx
.authn
.actor_required()
.internal_context("deleting current user's session")?;

// This check shouldn't be required in that there should be no overlap
// between silo user ids and other types of identity ids. But it's easy
// to check, and if we add another type of Actor, we'll be forced here
// to consider if they should be able to have console sessions and log
// out of them.
let silo_user_id = match actor.actor_type() {
IdentityType::SiloUser => actor.actor_id(),
IdentityType::UserBuiltin => {
return Err(Error::invalid_request("not a Silo user"))
}
};

use db::schema::console_session::dsl;
diesel::delete(dsl::console_session)
.filter(dsl::silo_user_id.eq(silo_user_id))
.filter(dsl::token.eq(authz_session.id()))
.execute_async(self.pool_authorized(opctx).await?)
.await
.map(|_rows_deleted| ())
.map_err(|e| {
Error::internal_error(&format!(
"error deleting session: {:?}",
e
))
})
}
}
87 changes: 87 additions & 0 deletions nexus/src/db/datastore/dataset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! [`DataStore`] methods on [`Dataset`]s.

use super::DataStore;
use super::RunnableQuery;
use super::REGION_REDUNDANCY_THRESHOLD;
use crate::db;
use crate::db::collection_insert::AsyncInsertError;
use crate::db::collection_insert::DatastoreCollection;
use crate::db::error::public_error_from_diesel_pool;
use crate::db::error::ErrorHandler;
use crate::db::identity::Asset;
use crate::db::model::Dataset;
use crate::db::model::DatasetKind;
use crate::db::model::Zpool;
use chrono::Utc;
use diesel::prelude::*;
use diesel::upsert::excluded;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::LookupType;
use omicron_common::api::external::ResourceType;

impl DataStore {
/// Stores a new dataset in the database.
pub async fn dataset_upsert(
&self,
dataset: Dataset,
) -> CreateResult<Dataset> {
use db::schema::dataset::dsl;

let zpool_id = dataset.pool_id;
Zpool::insert_resource(
zpool_id,
diesel::insert_into(dsl::dataset)
.values(dataset.clone())
.on_conflict(dsl::id)
.do_update()
.set((
dsl::time_modified.eq(Utc::now()),
dsl::pool_id.eq(excluded(dsl::pool_id)),
dsl::ip.eq(excluded(dsl::ip)),
dsl::port.eq(excluded(dsl::port)),
dsl::kind.eq(excluded(dsl::kind)),
)),
)
.insert_and_get_result_async(self.pool())
.await
.map_err(|e| match e {
AsyncInsertError::CollectionNotFound => Error::ObjectNotFound {
type_name: ResourceType::Zpool,
lookup_type: LookupType::ById(zpool_id),
},
AsyncInsertError::DatabaseError(e) => {
public_error_from_diesel_pool(
e,
ErrorHandler::Conflict(
ResourceType::Dataset,
&dataset.id().to_string(),
),
)
}
})
}

pub(super) fn get_allocatable_datasets_query() -> impl RunnableQuery<Dataset>
{
use db::schema::dataset::dsl;

dsl::dataset
// We look for valid datasets (non-deleted crucible datasets).
.filter(dsl::size_used.is_not_null())
.filter(dsl::time_deleted.is_null())
.filter(dsl::kind.eq(DatasetKind::Crucible))
.order(dsl::size_used.asc())
// TODO: We admittedly don't actually *fail* any request for
// running out of space - we try to send the request down to
// crucible agents, and expect them to fail on our behalf in
// out-of-storage conditions. This should undoubtedly be
// handled more explicitly.
.select(Dataset::as_select())
.limit(REGION_REDUNDANCY_THRESHOLD.try_into().unwrap())
}
}
142 changes: 142 additions & 0 deletions nexus/src/db/datastore/device_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! [`DataStore`] methods related to OAuth 2.0 Device Authorization Grants.

use super::DataStore;
use crate::authz;
use crate::context::OpContext;
use crate::db;
use crate::db::error::public_error_from_diesel_pool;
use crate::db::error::ErrorHandler;
use crate::db::error::TransactionError;
use crate::db::model::DeviceAccessToken;
use crate::db::model::DeviceAuthRequest;
use async_bb8_diesel::AsyncConnection;
use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::prelude::*;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::LookupType;
use omicron_common::api::external::ResourceType;
use uuid::Uuid;

impl DataStore {
/// Start a device authorization grant flow by recording the request
/// and initial response parameters.
pub async fn device_auth_request_create(
&self,
opctx: &OpContext,
auth_request: DeviceAuthRequest,
) -> CreateResult<DeviceAuthRequest> {
opctx
.authorize(
authz::Action::CreateChild,
&authz::DEVICE_AUTH_REQUEST_LIST,
)
.await?;

use db::schema::device_auth_request::dsl;
diesel::insert_into(dsl::device_auth_request)
.values(auth_request)
.returning(DeviceAuthRequest::as_returning())
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

/// Remove the device authorization request and create a new device
/// access token record. The token may already be expired if the flow
/// was not completed in time.
pub async fn device_access_token_create(
&self,
opctx: &OpContext,
authz_request: &authz::DeviceAuthRequest,
authz_user: &authz::SiloUser,
access_token: DeviceAccessToken,
) -> CreateResult<DeviceAccessToken> {
assert_eq!(authz_user.id(), access_token.silo_user_id);
opctx.authorize(authz::Action::Delete, authz_request).await?;
opctx.authorize(authz::Action::CreateChild, authz_user).await?;

use db::schema::device_auth_request::dsl as request_dsl;
let delete_request = diesel::delete(request_dsl::device_auth_request)
.filter(request_dsl::user_code.eq(authz_request.id()));

use db::schema::device_access_token::dsl as token_dsl;
let insert_token = diesel::insert_into(token_dsl::device_access_token)
.values(access_token)
.returning(DeviceAccessToken::as_returning());

#[derive(Debug)]
enum TokenGrantError {
RequestNotFound,
TooManyRequests,
}
type TxnError = TransactionError<TokenGrantError>;

self.pool_authorized(opctx)
.await?
.transaction(move |conn| match delete_request.execute(conn)? {
0 => {
Err(TxnError::CustomError(TokenGrantError::RequestNotFound))
}
1 => Ok(insert_token.get_result(conn)?),
_ => Err(TxnError::CustomError(
TokenGrantError::TooManyRequests,
)),
})
.await
.map_err(|e| match e {
TxnError::CustomError(TokenGrantError::RequestNotFound) => {
Error::ObjectNotFound {
type_name: ResourceType::DeviceAuthRequest,
lookup_type: LookupType::ByCompositeId(
authz_request.id(),
),
}
}
TxnError::CustomError(TokenGrantError::TooManyRequests) => {
Error::internal_error("unexpectedly found multiple device auth requests for the same user code")
}
TxnError::Pool(e) => {
public_error_from_diesel_pool(e, ErrorHandler::Server)
}
})
}

/// Look up a granted device access token.
/// Note: since this lookup is not by a primary key or name,
/// (though it does use a unique index), it does not fit the
/// usual lookup machinery pattern. It therefore does include
/// any authz checks. However, the device code is a single-use
/// high-entropy random token, and so should not be guessable
/// by an attacker.
pub async fn device_access_token_fetch(
&self,
opctx: &OpContext,
client_id: Uuid,
device_code: String,
) -> LookupResult<DeviceAccessToken> {
use db::schema::device_access_token::dsl;
dsl::device_access_token
.filter(dsl::client_id.eq(client_id))
.filter(dsl::device_code.eq(device_code))
.select(DeviceAccessToken::as_select())
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
public_error_from_diesel_pool(
e,
ErrorHandler::NotFoundByLookup(
ResourceType::DeviceAccessToken,
LookupType::ByCompositeId(
"client_id, device_code".to_string(),
),
),
)
})
}
}
Loading