Skip to content
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

Refactor device auth schema #1344

Merged
merged 10 commits into from
Jul 7, 2022
1 change: 1 addition & 0 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,7 @@ CREATE TABLE omicron.public.device_access_token (
client_id UUID NOT NULL,
device_code STRING(40) NOT NULL,
silo_user_id UUID NOT NULL,
time_requested TIMESTAMPTZ NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
davepacheco marked this conversation as resolved.
Show resolved Hide resolved
time_expires TIMESTAMPTZ
);
Expand Down
34 changes: 23 additions & 11 deletions nexus/src/app/device_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,11 @@ impl super::Nexus {
.await?;
assert_eq!(authz_user.id(), silo_user_id);

// Delete the request regardless of whether it's still valid.
self.db_datastore
.device_auth_request_hard_delete(opctx, &authz_request)
.await?;

// Create an access token record.
let token = DeviceAccessToken::new(
db_request.client_id,
db_request.device_code,
db_request.time_created,
silo_user_id,
);

Expand All @@ -111,15 +107,25 @@ impl super::Nexus {
// can get a proper "denied" message on its next poll.
let token = token.expires(db_request.time_expires);
self.db_datastore
.device_access_token_create(opctx, &authz_user, token)
.device_access_token_create(
opctx,
&authz_request,
&authz_user,
token,
)
.await?;
Err(Error::InvalidRequest {
message: "device authorization request expired".to_string(),
})
} else {
// TODO-security: set an expiration time for the valid token.
self.db_datastore
.device_access_token_create(opctx, &authz_user, token)
.device_access_token_create(
opctx,
&authz_request,
&authz_user,
token,
)
.await
}
}
Expand Down Expand Up @@ -155,17 +161,23 @@ impl super::Nexus {
.device_access_token(&token)
.fetch()
.await
.map_err(|_| Reason::UnknownActor {
actor: "from bearer token".to_string(),
.map_err(|e| match e {
Error::ObjectNotFound { .. } => Reason::UnknownActor {
actor: "from device access token".to_string(),
},
e => Reason::UnknownError { source: e },
})?;

let silo_user_id = db_access_token.silo_user_id;
let (.., db_silo_user) = LookupPath::new(opctx, &self.db_datastore)
.silo_user_id(silo_user_id)
.fetch()
.await
.map_err(|_| Reason::UnknownActor {
actor: silo_user_id.to_string(),
.map_err(|e| match e {
Error::ObjectNotFound { .. } => {
Reason::UnknownActor { actor: silo_user_id.to_string() }
}
e => Reason::UnknownError { source: e },
})?;
let silo_id = db_silo_user.silo_id;

Expand Down
42 changes: 19 additions & 23 deletions nexus/src/db/datastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4300,39 +4300,35 @@ impl DataStore {
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

/// Delete a device authorization request.
pub async fn device_auth_request_hard_delete(
&self,
opctx: &OpContext,
authz_request: &authz::DeviceAuthRequest,
) -> DeleteResult {
opctx.authorize(authz::Action::Delete, authz_request).await?;

use db::schema::device_auth_request::dsl;
diesel::delete(dsl::device_auth_request)
.filter(dsl::user_code.eq(authz_request.id()))
.execute_async(self.pool_authorized(opctx).await?)
.await
.map(|rows_deleted| assert_eq!(rows_deleted, 1))
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

/// Create a device access token record. The token may already
/// be expired if the device auth flow was not completed in time.
/// 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?;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This check feels a little weird, since DeviceAccessToken isn't declared by lookup_resource! as a child of SiloUser. Suggestions for a better check would be welcome.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We could create another synthetic resource analogous to ConsoleSessionList (DeviceAccessTokenList) but I admit the boilerplate-to-value ratio on those is getting worse...

To be clear, would we expect this function to be invoked by the user themselves or by Nexus's external-authenticator? I assumed the latter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this function is called as part of the confirmation in the browser, so it's made by an authenticated user.


use db::schema::device_access_token::dsl;
diesel::insert_into(dsl::device_access_token)
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())
.get_result_async(self.pool_authorized(opctx).await?)
.returning(DeviceAccessToken::as_returning());

self.pool_authorized(opctx)
.await?
.transaction(move |conn| {
davepacheco marked this conversation as resolved.
Show resolved Hide resolved
delete_request.execute(conn)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe there's a window here where this could happen:

  1. request A comes in to verify the request and create a new token
  2. request A fetches the device auth request
  3. request B comes in to verify the request and create a new token, fetches the device auth request row, and executes this transaction
  4. request A executes this transaction

I think the result would be two tokens created from the same device auth request.

I think the easiest way to fix that might be to bail out at L4329 if execute() reports having deleted any number of rows other than 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, fixed in 152fd2a. Please LMK if I got the error handling wrong.

Ok(insert_token.get_result(conn)?)
})
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}
Expand Down
7 changes: 6 additions & 1 deletion nexus/src/db/model/device_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub struct DeviceAccessToken {
pub client_id: Uuid,
pub device_code: String,
pub silo_user_id: Uuid,
pub time_requested: DateTime<Utc>,
pub time_created: DateTime<Utc>,
pub time_expires: Option<DateTime<Utc>>,
}
Expand All @@ -108,14 +109,18 @@ impl DeviceAccessToken {
pub fn new(
client_id: Uuid,
device_code: String,
time_requested: DateTime<Utc>,
silo_user_id: Uuid,
) -> Self {
let now = Utc::now();
assert!(time_requested < now);
davepacheco marked this conversation as resolved.
Show resolved Hide resolved
Self {
token: generate_token(),
client_id,
device_code,
silo_user_id,
time_created: Utc::now(),
time_requested,
time_created: now,
time_expires: None,
}
}
Expand Down
1 change: 1 addition & 0 deletions nexus/src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ table! {
client_id -> Uuid,
device_code -> Text,
silo_user_id -> Uuid,
time_requested -> Timestamptz,
time_created -> Timestamptz,
time_expires -> Nullable<Timestamptz>,
}
Expand Down