pub mod capture;
pub mod logout;
pub mod renewal;
use std::fmt;
use std::fs::File;
use asupersync::Cx;
use fastmcp_core::partition::{CredentialStoreKey, PartitionAuthorization};
use super::{
MAX_CONFIGURATION_BYTES, MAX_ENCODED_REFRESH_GRANT_BYTES, MAX_PROTECTED_REFRESH_GRANT_BYTES,
OAuthClient, OAuthCredentials, OAuthGrantProtector, OAuthRefreshGrant, OAuthRefreshStore,
OAuthRefreshStoreError, configuration_digest,
};
use crate::http_auth::secure_file::SecureAtomicFile;
use crate::http_auth::secure_file::slot::coordinator::asynchronous::{
CredentialIoError, CredentialIoLane, CredentialSlotTask, composed::ComposedCredentialIo,
};
use crate::http_auth::secure_file::slot::coordinator::{
CoordinatedSlotError, CredentialCommitAnchor,
};
use crate::http_auth::secure_file::slot::{CredentialSlotError, SlotRecoveryOutcome, SlotRevision};
const EXTRA_WORK_BYTES: usize = 4 * MAX_CONFIGURATION_BYTES
+ 4 * MAX_ENCODED_REFRESH_GRANT_BYTES
+ 2 * MAX_PROTECTED_REFRESH_GRANT_BYTES;
const MAX_FILE_BYTES: usize = MAX_PROTECTED_REFRESH_GRANT_BYTES + 256;
#[derive(Debug)]
pub enum AsyncOAuthRefreshError {
Io(CredentialIoError),
Store(OAuthRefreshStoreError),
}
impl fmt::Display for AsyncOAuthRefreshError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => error.fmt(f),
Self::Store(error) => error.fmt(f),
}
}
}
impl std::error::Error for AsyncOAuthRefreshError {}
impl From<CredentialIoError> for AsyncOAuthRefreshError {
fn from(error: CredentialIoError) -> Self {
Self::Io(error)
}
}
impl From<OAuthRefreshStoreError> for AsyncOAuthRefreshError {
fn from(error: OAuthRefreshStoreError) -> Self {
Self::Store(error)
}
}
pub struct OAuthRefreshSubmissionFailure<A, P, I> {
cause: AsyncOAuthRefreshError,
retained: Option<Box<(AsyncOAuthRefreshStore<A, P>, I)>>,
}
impl<A, P, I> OAuthRefreshSubmissionFailure<A, P, I> {
pub fn cause(&self) -> &AsyncOAuthRefreshError {
&self.cause
}
pub fn into_parts(
self,
) -> (
AsyncOAuthRefreshError,
Option<(AsyncOAuthRefreshStore<A, P>, I)>,
) {
(self.cause, self.retained.map(|retained| *retained))
}
}
impl<A, P, I> fmt::Debug for OAuthRefreshSubmissionFailure<A, P, I> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OAuthRefreshSubmissionFailure")
.field("cause", &self.cause)
.field("ownership_retained", &self.retained.is_some())
.finish()
}
}
impl<A, P, I> fmt::Display for OAuthRefreshSubmissionFailure<A, P, I> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.cause, f)
}
}
impl<A, P, I> std::error::Error for OAuthRefreshSubmissionFailure<A, P, I> {}
pub struct OAuthRefreshCompletion<A, P, T> {
owner: AsyncOAuthRefreshStore<A, P>,
outcome: T,
}
impl<A, P, T> OAuthRefreshCompletion<A, P, T> {
pub fn into_parts(self) -> (AsyncOAuthRefreshStore<A, P>, T) {
(self.owner, self.outcome)
}
}
pub type OAuthRefreshOpen<A, P> =
Result<(AsyncOAuthRefreshStore<A, P>, Option<SlotRecoveryOutcome>), OAuthRefreshStoreError>;
pub type OAuthRefreshWrite<A, P> = OAuthRefreshCompletion<
A,
P,
(
OAuthCredentials,
Result<SlotRevision, OAuthRefreshStoreError>,
),
>;
pub type OAuthRefreshTake<A, P> =
OAuthRefreshCompletion<A, P, Result<Option<OAuthRefreshGrant>, OAuthRefreshStoreError>>;
pub struct AsyncOAuthRefreshStore<A, P> {
store: OAuthRefreshStore<A, P>,
io: ComposedCredentialIo,
}
impl<A, P> AsyncOAuthRefreshStore<A, P>
where
A: CredentialCommitAnchor + 'static,
P: OAuthGrantProtector + 'static,
{
#[allow(clippy::too_many_arguments)]
pub fn open(
cx: &Cx,
lane: &CredentialIoLane,
directory: File,
leaf: String,
key: CredentialStoreKey,
authorization: PartitionAuthorization,
namespace: String,
anchor: A,
protector: P,
client: &OAuthClient,
) -> Result<CredentialSlotTask<OAuthRefreshOpen<A, P>>, AsyncOAuthRefreshError> {
if leaf.is_empty() || leaf.len() > 96 || namespace.is_empty() || namespace.len() > 128 {
return Err(CredentialIoError::InvalidSlotConfiguration.into());
}
configuration_digest(&client.configuration)?;
let _ = super::CredentialAnchorBinding::for_store(&namespace, &key, &authorization)
.map_err(OAuthRefreshStoreError::from)?;
let io = ComposedCredentialIo::reserve(cx, lane, MAX_FILE_BYTES, EXTRA_WORK_BYTES)?;
let client = client.clone();
let leaf = leaf.into_boxed_str();
let namespace = namespace.into_boxed_str();
Ok(io.submit(cx, move |worker, io| {
let file = SecureAtomicFile::open(worker, directory, &leaf, MAX_FILE_BYTES).map_err(
|error| {
OAuthRefreshStoreError::Storage(CoordinatedSlotError::Slot(
CredentialSlotError::Storage(error),
))
},
)?;
let (store, recovery) = OAuthRefreshStore::open(
worker,
file,
&key,
&authorization,
&namespace,
anchor,
protector,
&client,
)?;
Ok((Self { store, io }, recovery))
})?)
}
pub fn revision(&self) -> Option<SlotRevision> {
self.store.revision()
}
pub fn requires_recovery(&self) -> bool {
self.store.requires_recovery()
}
pub fn store_refresh(
self,
cx: &Cx,
authorization: PartitionAuthorization,
expected: Option<SlotRevision>,
credentials: OAuthCredentials,
) -> Result<CredentialSlotTask<OAuthRefreshWrite<A, P>>, AsyncOAuthRefreshError> {
self.try_store_refresh(cx, authorization, expected, credentials)
.map_err(|failure| failure.cause)
}
pub fn try_store_refresh(
self,
cx: &Cx,
authorization: PartitionAuthorization,
expected: Option<SlotRevision>,
mut credentials: OAuthCredentials,
) -> Result<
CredentialSlotTask<OAuthRefreshWrite<A, P>>,
OAuthRefreshSubmissionFailure<A, P, OAuthCredentials>,
> {
if let Err(error) = configuration_digest(&credentials.configuration) {
return Err(OAuthRefreshSubmissionFailure {
cause: error.into(),
retained: Some(Box::new((self, credentials))),
});
}
if let Some(token) = &mut credentials.refresh_token {
token.shrink_to_fit();
}
for scope in &mut credentials.scopes {
scope.shrink_to_fit();
}
credentials.scopes.shrink_to_fit();
self.try_operate(cx, credentials, move |store, worker, mut credentials| {
let result = store.store_refresh(worker, &authorization, expected, &mut credentials);
(credentials, result)
})
}
pub fn take_refresh(
self,
cx: &Cx,
authorization: PartitionAuthorization,
) -> Result<CredentialSlotTask<OAuthRefreshTake<A, P>>, AsyncOAuthRefreshError> {
self.try_take_refresh(cx, authorization)
.map_err(|failure| failure.cause)
}
pub fn try_take_refresh(
self,
cx: &Cx,
authorization: PartitionAuthorization,
) -> Result<CredentialSlotTask<OAuthRefreshTake<A, P>>, OAuthRefreshSubmissionFailure<A, P, ()>>
{
self.try_operate(cx, (), move |store, worker, ()| {
store.take_refresh(worker, &authorization)
})
}
pub fn invalidate(
self,
cx: &Cx,
authorization: PartitionAuthorization,
) -> Result<
CredentialSlotTask<
OAuthRefreshCompletion<A, P, Result<SlotRevision, OAuthRefreshStoreError>>,
>,
AsyncOAuthRefreshError,
> {
self.try_invalidate(cx, authorization)
.map_err(|failure| failure.cause)
}
pub fn try_invalidate(
self,
cx: &Cx,
authorization: PartitionAuthorization,
) -> Result<
CredentialSlotTask<
OAuthRefreshCompletion<A, P, Result<SlotRevision, OAuthRefreshStoreError>>,
>,
OAuthRefreshSubmissionFailure<A, P, ()>,
> {
self.try_operate(cx, (), move |store, worker, ()| {
store.invalidate(worker, &authorization)
})
}
fn try_operate<I, T, F>(
self,
cx: &Cx,
input: I,
operation: F,
) -> Result<
CredentialSlotTask<OAuthRefreshCompletion<A, P, T>>,
OAuthRefreshSubmissionFailure<A, P, I>,
>
where
I: Send + 'static,
T: Send + 'static,
F: FnOnce(&mut OAuthRefreshStore<A, P>, &Cx, I) -> T + Send + 'static,
{
let Self { store, io } = self;
io.try_submit(cx, (store, input), move |worker, io, (store, input)| {
let mut owner = Self { store, io };
let outcome = operation(&mut owner.store, worker, input);
OAuthRefreshCompletion { owner, outcome }
})
.map_err(|(error, retained)| OAuthRefreshSubmissionFailure {
cause: error.into(),
retained: retained.map(|(io, (store, input))| Box::new((Self { store, io }, input))),
})
}
pub fn close(self, cx: &Cx) -> Result<CredentialSlotTask<()>, AsyncOAuthRefreshError> {
let Self { store, io } = self;
Ok(io.close(cx, store)?)
}
}
#[cfg(test)]
mod tests;