crates/gunnar-ui-server/src/passkeys_at_gunnar/fixtures/mod.rs
//! **The fake gunnar's IdentityService**: edda's real `IdentityGrpc` over
//! the fake's host, every call heard as `Identity.<Method>`, and the faults a
//! real gunnar answers injected where a test asks for them. A test fixture:
//! the adapter's method names are the identity core's own.
use super::*;
/// The fake's IdentityService: edda's `IdentityGrpc` over [`FakeHost`],
/// every call heard as `Identity.<Method>`, and the faults a real gunnar
/// answers injected where the tests ask for them.
#[derive(Clone)]
pub(crate) struct Heard {
pub(super) fake: Fake,
pub(super) inner: nornir_identity::grpc::IdentityGrpc<FakeHost>,
}
type Reply<T> = Result<Response<T>, Status>;
#[tonic::async_trait]
impl ipb::identity_service_server::IdentityService for Heard {
async fn sign_up(&self, r: Request<ipb::SignUpRequest>) -> Reply<ipb::SignUpReply> {
self.fake.said("Identity.SignUp".to_owned());
self.inner.sign_up(r).await
}
async fn create_invite(&self, r: Request<ipb::CreateInviteRequest>) -> Reply<ipb::CreateInviteReply> {
let by = self.fake.core().session(&r.get_ref().session).map(|v| v.user_id).unwrap_or_default();
self.fake.said(format!("Identity.CreateInvite by {by}"));
if self.fake.stale_at_core.load(Ordering::SeqCst) {
return Err(nornir_identity::grpc::status(nornir_identity::Refused::new(
nornir_identity::Code::NeedsStepUp,
"this needs a fresh touch of your own passkey",
)));
}
let person = r.get_ref().person.clone();
let answer = self.inner.create_invite(r).await?;
lock(&self.fake.invite_requests).push(person);
Ok(answer)
}
async fn list_invites(&self, r: Request<ipb::ListInvitesRequest>) -> Reply<ipb::ListInvitesReply> {
let by = self.fake.core().session(&r.get_ref().session).map(|v| v.user_id).unwrap_or_default();
self.fake.said(format!("Identity.ListInvites by {by}"));
self.inner.list_invites(r).await
}
async fn revoke_invite(&self, r: Request<ipb::RevokeInviteRequest>) -> Reply<ipb::RevokeInviteReply> {
let by = self.fake.core().session(&r.get_ref().session).map(|v| v.user_id).unwrap_or_default();
self.fake.said(format!("Identity.RevokeInvite by {by}"));
self.inner.revoke_invite(r).await
}
async fn redeem_start(&self, r: Request<ipb::RedeemStartRequest>) -> Reply<ipb::CeremonyReply> {
self.fake.said("Identity.RedeemStart".to_owned());
if let Some(fault) = self.fake.fault() {
return Err(fault);
}
self.fake.delay().await;
lock(&self.fake.starts).push(r.get_ref().client.clone());
self.fake.issued.fetch_add(1, Ordering::SeqCst);
self.inner.redeem_start(r).await
}
async fn redeem_finish(&self, r: Request<ipb::RedeemFinishRequest>) -> Reply<ipb::SessionReply> {
self.fake.said("Identity.RedeemFinish".to_owned());
let delay = *lock(&self.fake.register_delay);
if let Some(d) = delay {
tokio::time::sleep(d).await;
}
attestation_carries(&r.get_ref().attestation_object, &r.get_ref().public_key)?;
let asked = r.get_ref().clone();
let answer = self.inner.redeem_finish(r).await?;
lock(&self.fake.registrations).push(Registered {
principal_id: answer.get_ref().session.as_ref().map(|s| s.user_id.clone()).unwrap_or_default(),
credential_id: asked.credential_id,
public_key: asked.public_key,
attestation_object: asked.attestation_object,
rp_id: asked.rp_id,
origin: asked.origin,
});
Ok(answer)
}
async fn login_start(&self, r: Request<ipb::LoginStartRequest>) -> Reply<ipb::CeremonyReply> {
self.fake.said("Identity.LoginStart".to_owned());
if let Some(fault) = self.fake.fault() {
return Err(fault);
}
self.fake.delay().await;
lock(&self.fake.starts).push(r.get_ref().client.clone());
self.fake.issued.fetch_add(1, Ordering::SeqCst);
self.inner.login_start(r).await
}
async fn login_finish(&self, r: Request<ipb::LoginFinishRequest>) -> Reply<ipb::SessionReply> {
self.fake.said("Identity.LoginFinish".to_owned());
lock(&self.fake.logins).push(r.get_ref().clone());
if self.fake.refuse_logins.load(Ordering::SeqCst) {
return Err(nornir_identity::grpc::status(nornir_identity::Refused::new(nornir_identity::Code::Ceremony, "the signature does not verify")));
}
if let Some((code, message)) = lock(&self.fake.login_status).clone() {
return Err(Status::new(code, message));
}
if self.fake.growing.load(Ordering::SeqCst) {
return Err(nornir_identity::grpc::status(nornir_identity::Refused::new(
nornir_identity::Code::Store,
"the control store is off the stripe while the data set grows (the transaction log is released for the remount); writes are refused until /data is mounted again — retry in a few minutes",
)));
}
self.inner.login_finish(r).await
}
async fn whoami(&self, r: Request<ipb::WhoamiRequest>) -> Reply<ipb::SessionInfo> {
if let Some(fault) = self.fake.fault() {
return Err(fault);
}
self.inner.whoami(r).await
}
async fn step_up_start(&self, r: Request<ipb::StepUpStartRequest>) -> Reply<ipb::ChallengeReply> {
self.fake.said("Identity.StepUpStart".to_owned());
self.inner.step_up_start(r).await
}
async fn step_up_finish(&self, r: Request<ipb::StepUpFinishRequest>) -> Reply<ipb::SessionInfo> {
self.fake.said("Identity.StepUpFinish".to_owned());
let answer = self.inner.step_up_finish(r).await?;
self.fake.stale_at_core.store(false, Ordering::SeqCst);
Ok(answer)
}
async fn logout(&self, r: Request<ipb::LogoutRequest>) -> Reply<ipb::LogoutReply> {
let by = self.fake.core().session(&r.get_ref().session).map(|v| v.user_id).unwrap_or_default();
self.fake.said(format!("Identity.Logout by {by} everywhere={}", r.get_ref().everywhere));
self.inner.logout(r).await
}
async fn deactivate(&self, r: Request<ipb::DeactivateRequest>) -> Reply<ipb::DeactivateReply> {
self.inner.deactivate(r).await
}
async fn end_all_sessions(&self, r: Request<ipb::EndAllSessionsRequest>) -> Reply<ipb::EndAllSessionsReply> {
self.fake.said("Identity.EndAllSessions".to_owned());
self.inner.end_all_sessions(r).await
}
}