8923bc0
use crate::protos::auth::common::v1::{Code, ErrorDetail};
use crate::protos::google::rpc::Status as RpcStatus;
use prost::Message;
use prost_types::Any;
use tonic::Status;
use crate::errors::AuthError;
pub fn code_of(error: &AuthError) -> Code {
match error {
AuthError::Expired => Code::ExpiredChallenge,
AuthError::InvalidPayload => Code::InvalidPayload,
AuthError::InvalidProof => Code::InvalidProof,
AuthError::MalformedRequest => Code::MalformedRequest,
AuthError::InvalidUsername => Code::InvalidUsername,
AuthError::InvalidDisplayname => Code::InvalidDisplayname,
AuthError::InvalidEmail => Code::InvalidEmail,
AuthError::EmailUnchanged => Code::EmailUnchanged,
AuthError::InvalidBirthDate => Code::InvalidBirthDate,
AuthError::UnderMinimumAge { .. } => Code::UnderMinimumAge,
AuthError::InvalidPassword => Code::InvalidPassword,
AuthError::PasswordHash(_) => Code::Internal,
AuthError::UsernameTaken => Code::UsernameTaken,
AuthError::EmailTaken => Code::EmailTaken,
AuthError::InvalidEmailCode => Code::InvalidEmailCode,
AuthError::EmailCodeAttemptsExceeded => Code::EmailCodeAttemptsExceeded,
AuthError::InvalidResetToken => Code::InvalidResetToken,
AuthError::Unauthenticated => Code::Unauthenticated,
AuthError::Forbidden => Code::Forbidden,
AuthError::BadRequest(_) => Code::MalformedRequest,
AuthError::Conflict(_) => Code::VersionMismatch,
AuthError::InvalidCredentials => Code::InvalidCredentials,
AuthError::LoginForbidden { .. } => Code::LoginForbidden,
AuthError::AccountUnavailable { .. } => Code::UserDisabled,
AuthError::MfaRequired => Code::MfaRequired,
AuthError::TotpNotConfigured => Code::MfaNotConfigured,
AuthError::TotpAlreadyEnabled => Code::TotpAlreadyEnabled,
AuthError::TotpCodeInvalid => Code::InvalidCredentials,
AuthError::TotpStepReused => Code::TotpStepReused,
AuthError::RecoveryCodeInvalid => Code::InvalidCredentials,
AuthError::RecoveryCodesExhausted => Code::RecoveryCodesExhausted,
AuthError::InvalidAccessToken => Code::InvalidAccessToken,
AuthError::AccessTokenNotFound => Code::AccessKeyNotFound,
AuthError::InvalidAccessKeyName => Code::InvalidName,
AuthError::InvalidScope => Code::InvalidScope,
AuthError::InvalidExpiry => Code::InvalidExpiry,
AuthError::TooManyAccessKeys => Code::TooManyAccessKeys,
AuthError::SshKeyInvalid => Code::InvalidPublicKey,
AuthError::InvalidPermission { .. } => Code::InvalidPermission,
AuthError::OrganizationNotFound => Code::OrganizationNotFound,
AuthError::OrganizationMemberNotFound => Code::OrganizationMemberNotFound,
AuthError::OrganizationRoleNotFound => Code::OrganizationRoleNotFound,
AuthError::OrganizationInviteNotFound => Code::OrganizationInviteNotFound,
AuthError::OrganizationSlugTaken => Code::OrganizationSlugTaken,
AuthError::OrganizationMemberExists => Code::OrganizationMemberExists,
AuthError::OrganizationRoleKeyTaken => Code::OrganizationRoleKeyTaken,
AuthError::OrganizationRoleInUse => Code::OrganizationRoleInUse,
AuthError::OrganizationRoleSystem => Code::OrganizationRoleSystem,
AuthError::OrganizationInviteExhausted => Code::OrganizationInviteExhausted,
AuthError::OrganizationInviteExpired => Code::OrganizationInviteExpired,
AuthError::OrganizationOwnerRequired => Code::OrganizationOwnerRequired,
AuthError::OrganizationHasMembers => Code::OrganizationHasMembers,
AuthError::OrganizationInactive => Code::OrganizationInactive,
AuthError::OrganizationMemberBanned => Code::OrganizationMemberBanned,
AuthError::VersionMismatch { .. } => Code::VersionMismatch,
AuthError::SshKeyTaken => Code::SshKeyTaken,
AuthError::SshKeyNameInvalid => Code::InvalidName,
AuthError::SshKeyNotFound => Code::SshKeyNotFound,
AuthError::AgreementNotFound { .. } => Code::AgreementNotFound,
AuthError::AgreementNotAccepted { .. } => Code::AgreementNotAccepted,
AuthError::DeletionAlreadyRequested => Code::DeletionAlreadyRequested,
AuthError::DeletionNotRequested => Code::DeletionNotRequested,
AuthError::RateLimited { .. } => Code::RateLimited,
AuthError::UserNotFound => Code::UserNotFound,
AuthError::AppNotFound => Code::NotFound,
AuthError::SessionNotFound => Code::SessionNotFound,
AuthError::TokenNotFound => Code::TokenNotFound,
AuthError::TokenExpired => Code::TokenExpired,
AuthError::TokenRevoked => Code::TokenRevoked,
AuthError::SecurityVersionStale { .. } => Code::SecurityVersionStale,
AuthError::ScopeInsufficient { .. } => Code::ScopeInsufficient,
AuthError::Unavailable(_) => Code::Unavailable,
AuthError::Internal(_) => Code::Internal,
AuthError::Database(_) => Code::DatabaseError,
AuthError::Cache(_) => Code::CacheError,
AuthError::Session(_) => Code::CacheError,
AuthError::Email(_) => Code::EmailError,
AuthError::Serialization(_) => Code::SerializationError,
}
}
pub fn tonic_code(code: Code) -> tonic::Code {
match code {
Code::Unspecified => tonic::Code::Unknown,
Code::InvalidArgument
| Code::MalformedRequest
| Code::ExpiredChallenge
| Code::InvalidProof
| Code::InvalidPayload
| Code::InvalidEmail
| Code::InvalidUsername
| Code::InvalidDisplayname
| Code::InvalidBirthDate
| Code::InvalidPassword
| Code::InvalidEmailCode
| Code::InvalidResetToken
| Code::InvalidScope
| Code::InvalidExpiry
| Code::InvalidName
| Code::InvalidPublicKey => tonic::Code::InvalidArgument,
Code::Unauthenticated
| Code::InvalidCredentials
| Code::InvalidAccessToken
| Code::TokenExpired
| Code::TokenRevoked
| Code::SecurityVersionStale
| Code::MfaRequired => tonic::Code::Unauthenticated,
Code::Forbidden
| Code::LoginForbidden
| Code::UserDisabled
| Code::UnderMinimumAge
| Code::ScopeInsufficient
| Code::AgreementNotAccepted => tonic::Code::PermissionDenied,
Code::MfaNotConfigured
| Code::TooManyAccessKeys
| Code::TotpAlreadyEnabled
| Code::TotpStepReused
| Code::DeletionAlreadyRequested
| Code::DeletionNotRequested
| Code::OrganizationRoleInUse
| Code::OrganizationRoleSystem
| Code::OrganizationInviteExhausted
| Code::OrganizationInviteExpired
| Code::OrganizationOwnerRequired => tonic::Code::FailedPrecondition,
Code::Conflict
| Code::UsernameTaken
| Code::EmailTaken
| Code::SshKeyTaken
| Code::OrganizationSlugTaken
| Code::OrganizationMemberExists
| Code::OrganizationRoleKeyTaken
| Code::OrganizationHasMembers
| Code::VersionMismatch
| Code::EmailUnchanged => tonic::Code::AlreadyExists,
Code::NotFound
| Code::UserNotFound
| Code::SessionNotFound
| Code::AccessKeyNotFound
| Code::SshKeyNotFound
| Code::AgreementNotFound
| Code::OrganizationNotFound
| Code::OrganizationMemberNotFound
| Code::OrganizationRoleNotFound
| Code::OrganizationInviteNotFound
| Code::TokenNotFound => tonic::Code::NotFound,
Code::OrganizationInactive | Code::OrganizationMemberBanned => {
tonic::Code::PermissionDenied
}
Code::InvalidPermission => tonic::Code::InvalidArgument,
Code::RateLimited
| Code::EmailCodeAttemptsExceeded
| Code::RecoveryCodesExhausted
| Code::PayloadTooLarge => tonic::Code::ResourceExhausted,
Code::Internal
| Code::DatabaseError
| Code::CacheError
| Code::SerializationError
| Code::EmailError => tonic::Code::Internal,
Code::Unavailable => tonic::Code::Unavailable,
}
}
pub fn detail(error: &AuthError, request_id: Option<&str>) -> ErrorDetail {
let retry_after_ms = match error {
AuthError::RateLimited { retry_after_secs } => (*retry_after_secs).min(3600) as u32 * 1000,
_ => 0,
};
let current_security_version = match error {
AuthError::SecurityVersionStale { current } => *current,
_ => 0,
};
ErrorDetail {
code: code_of(error) as i32,
message: error.message(),
retry_after_ms,
current_security_version,
request_id: request_id.unwrap_or_default().to_owned(),
offending_fields: Vec::new(),
}
}
/// 业务错误详情的 type_url(放进 `google.rpc.Status.details`)。
pub const DETAIL_TYPE_URL: &str = "type.googleapis.com/auth.common.v1.ErrorDetail";
/// 组装 `grpc-status-details-bin`。
///
/// 按 gRPC 规范,这个 trailer 必须是 `google.rpc.Status`:外层 `code` 与 `grpc-status`
/// 一致,业务错误码放在 `details` 的 `Any` 里。直接把自定义 message 塞进去会被严格客户端
/// (grpcurl、Go 的 status.FromError)解析成错误的 code。
fn with_details(code: tonic::Code, message: String, detail: ErrorDetail) -> Status {
let outer = RpcStatus {
code: code as i32,
message: message.clone(),
details: vec![Any {
type_url: DETAIL_TYPE_URL.to_owned(),
value: detail.encode_to_vec(),
}],
};
Status::with_details(code, message, outer.encode_to_vec().into())
}
pub fn status(error: &AuthError, request_id: Option<&str>) -> Status {
let code = code_of(error);
let detail = detail(error, request_id);
with_details(tonic_code(code), error.message(), detail)
}
pub fn status_of(code: Code, message: impl Into<String>, request_id: Option<&str>) -> Status {
let detail = ErrorDetail {
code: code as i32,
message: message.into(),
retry_after_ms: 0,
current_security_version: 0,
request_id: request_id.unwrap_or_default().to_owned(),
offending_fields: Vec::new(),
};
let message = detail.message.clone();
with_details(tonic_code(code), message, detail)
}
pub fn unauthenticated(message: impl Into<String>, request_id: Option<&str>) -> Status {
status_of(Code::Unauthenticated, message, request_id)
}
pub fn invalid_argument(message: impl Into<String>, request_id: Option<&str>) -> Status {
status_of(Code::InvalidArgument, message, request_id)
}
pub fn forbidden(message: impl Into<String>, request_id: Option<&str>) -> Status {
status_of(Code::Forbidden, message, request_id)
}
pub fn unavailable(message: impl Into<String>, request_id: Option<&str>) -> Status {
status_of(Code::Unavailable, message, request_id)
}