Skip to content

Commit

Permalink
Slightly more structured error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
UMR1352 committed Nov 28, 2023
1 parent b23fd20 commit d2eab5e
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 27 deletions.
1 change: 1 addition & 0 deletions bindings/grpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ iota-sdk = { version = "1.1.2", features = ["stronghold"] }
serde_json = { version = "1.0.108", features = ["alloc"] }
thiserror = "1.0.50"
rand = "0.8.5"
serde = { version = "1.0.193", features = ["derive", "alloc"] }

[build-dependencies]
tonic-build = "0.10"
6 changes: 2 additions & 4 deletions bindings/grpc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@ const API_ENDPOINT: &str = "http://127.0.0.1:14265";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client: Client = Client::builder()
let client: Client = Client::builder()
.with_primary_node(API_ENDPOINT, None)?
.finish()
.await?;

let addr = "127.0.0.1:50051".parse()?;
println!("gRPC server listening on {}", addr);
GRpcServer::new(client)
.serve(addr)
.await?;
GRpcServer::new(client).serve(addr).await?;

Ok(())
}
8 changes: 3 additions & 5 deletions bindings/grpc/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::net::SocketAddr;

use tonic::transport::server::{Router, Server};
use iota_sdk::client::Client;
use tonic::transport::server::{Router, Server};

use crate::services;

Expand All @@ -13,14 +13,12 @@ pub struct GRpcServer {
impl GRpcServer {
pub fn new(client: Client) -> Self {
let router = Server::builder().add_routes(services::routes(client));
Self {
router,
}
Self { router }
}
pub async fn serve(self, addr: SocketAddr) -> Result<(), tonic::transport::Error> {
self.router.serve(addr).await
}
pub fn into_router(self) -> Router {
self.router
}
}
}
40 changes: 26 additions & 14 deletions bindings/grpc/src/services/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use identity_iota::{
prelude::{IotaDocument, Resolver},
};
use iota_sdk::client::Client;
use prost::bytes::Bytes;
use serde::{Deserialize, Serialize};

use thiserror::Error;
use tonic::{self, Request, Response};
Expand Down Expand Up @@ -46,35 +48,44 @@ mod credential_verification {
}
}

#[derive(Debug, Error)]
#[derive(Debug, Error, Serialize, Deserialize)]
#[serde(tag = "error_type", content = "reason")]
pub enum RevocationCheckError {
#[error("Unknown revocation type {0}")]
UnknownRevocationType(String),
#[error("Could not parse {0} into a valid URL")]
InvalidRevocationUrl(String),
#[error("Properties isn't a valid JSON object")]
MalformedPropertiesObject,
#[error("Invalid credential status")]
InvalidCredentialStatus(#[source] credential::Error),
#[error("Issuer's DID resolution error")]
ResolutionError(#[source] identity_iota::resolver::Error),
#[error("Invalid credential status: {0}")]
InvalidCredentialStatus(String),
#[error("Issuer's DID resolution error: {0}")]
ResolutionError(String),
#[error("Revocation map not found")]
RevocationMapNotFound(#[source] credential::JwtValidationError),
RevocationMapNotFound,
}

impl From<RevocationCheckError> for tonic::Status {
fn from(e: RevocationCheckError) -> Self {
let message = e.to_string();
let code = match &e {
RevocationCheckError::InvalidCredentialStatus(_)
| RevocationCheckError::MalformedPropertiesObject
| RevocationCheckError::UnknownRevocationType(_)
| RevocationCheckError::InvalidRevocationUrl(_) => tonic::Code::InvalidArgument,
| RevocationCheckError::MalformedPropertiesObject
| RevocationCheckError::UnknownRevocationType(_)
| RevocationCheckError::InvalidRevocationUrl(_) => tonic::Code::InvalidArgument,
RevocationCheckError::ResolutionError(_) => tonic::Code::Internal,
RevocationCheckError::RevocationMapNotFound(_) => tonic::Code::NotFound,
RevocationCheckError::RevocationMapNotFound => tonic::Code::NotFound,
};
let error_json = serde_json::to_vec(&e).unwrap_or_default();

tonic::Status::new(code, message)
tonic::Status::with_details(code, message, Bytes::from(error_json))
}
}

impl TryFrom<tonic::Status> for RevocationCheckError {
type Error = ();
fn try_from(value: tonic::Status) -> Result<Self, Self::Error> {
serde_json::from_slice(value.details()).map_err(|_| ())
}
}

Expand All @@ -99,14 +110,15 @@ impl CredentialRevocation for CredentialVerifier {
) -> Result<Response<RevocationCheckResponse>, tonic::Status> {
let credential_revocation_status = {
let revocation_status = credential::Status::try_from(req.into_inner())?;
RevocationBitmapStatus::try_from(revocation_status).map_err(RevocationCheckError::InvalidCredentialStatus)?
RevocationBitmapStatus::try_from(revocation_status)
.map_err(|e| RevocationCheckError::InvalidCredentialStatus(e.to_string()))?
};
let issuer_did = credential_revocation_status.id().unwrap(); // Safety: already parsed as a valid URL
let issuer_doc = self
.resolver
.resolve(issuer_did.did())
.await
.map_err(RevocationCheckError::ResolutionError)?;
.map_err(|e| RevocationCheckError::ResolutionError(e.to_string()))?;

if let Err(e) =
JwtCredentialValidatorUtils::check_revocation_bitmap_status(&issuer_doc, credential_revocation_status)
Expand All @@ -115,7 +127,7 @@ impl CredentialRevocation for CredentialVerifier {
JwtValidationError::Revoked => Ok(Response::new(RevocationCheckResponse {
status: RevocationStatus::Revoked.into(),
})),
_ => Err(RevocationCheckError::RevocationMapNotFound(e).into()),
_ => Err(RevocationCheckError::RevocationMapNotFound.into()),
}
} else {
Ok(Response::new(RevocationCheckResponse {
Expand Down
4 changes: 2 additions & 2 deletions bindings/grpc/src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod health_check;
mod credential;
pub mod credential;
pub mod health_check;

use iota_sdk::client::Client;
use tonic::transport::server::{Routes, RoutesBuilder};
Expand Down
28 changes: 27 additions & 1 deletion bindings/grpc/tests/api/credential_revocation_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use identity_iota::{
credential::{self, RevocationBitmap, RevocationBitmapStatus},
did::DID,
};
use serde_json::json;

use crate::{
credential_revocation_check::credentials::RevocationCheckRequest,
Expand Down Expand Up @@ -51,7 +52,6 @@ async fn checking_status_of_credential_works() -> anyhow::Result<()> {
.map(|(k, v)| (k, v.to_string().trim_matches('"').to_owned()))
.collect(),
};
dbg!(&req);
let res = grpc_client.check(tonic::Request::new(req.clone())).await?.into_inner();

assert_eq!(res.status(), RevocationStatus::Valid);
Expand All @@ -68,3 +68,29 @@ async fn checking_status_of_credential_works() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn checking_status_of_valid_but_unresolvable_url_fails() -> anyhow::Result<()> {
use identity_grpc::services::credential::RevocationCheckError;
let server = TestServer::new().await;

let mut grpc_client = CredentialRevocationClient::connect(server.endpoint()).await?;
let properties = json!({
"revocationBitmapIndex": "3"
});
let req = RevocationCheckRequest {
r#type: RevocationBitmap::TYPE.to_owned(),
url: "did:example:1234567890#my-revocation-service".to_owned(),
properties: properties
.as_object()
.unwrap()
.into_iter()
.map(|(k, v)| (k.clone(), v.to_string().trim_matches('"').to_owned()))
.collect(),
};
let res_error = grpc_client.check(tonic::Request::new(req.clone())).await;

assert!(res_error.is_err_and(|e| matches!(e.try_into().unwrap(), RevocationCheckError::ResolutionError(_))));

Ok(())
}
2 changes: 1 addition & 1 deletion bindings/grpc/tests/api/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mod credential_revocation_check;
mod health_check;
mod helpers;
mod credential_revocation_check;

0 comments on commit d2eab5e

Please sign in to comment.