use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt;
#[derive(Debug, Clone)]
pub struct AccessDenied {
subject: Cow<'static, str>,
permission: Cow<'static, str>,
resource: Cow<'static, str>,
reason: Option<Cow<'static, str>>,
request_id: Option<String>,
}
impl AccessDenied {
pub fn new(
subject: impl Into<Cow<'static, str>>,
permission: impl Into<Cow<'static, str>>,
resource: impl Into<Cow<'static, str>>,
) -> Self {
Self {
subject: subject.into(),
permission: permission.into(),
resource: resource.into(),
reason: None,
request_id: None,
}
}
#[inline]
pub fn subject(&self) -> &str {
&self.subject
}
#[inline]
pub fn permission(&self) -> &str {
&self.permission
}
#[inline]
pub fn resource(&self) -> &str {
&self.resource
}
#[inline]
pub fn reason(&self) -> Option<&str> {
self.reason.as_deref()
}
#[inline]
pub fn request_id(&self) -> Option<&str> {
self.request_id.as_deref()
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<Cow<'static, str>>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
self.request_id = Some(request_id.into());
self
}
pub fn to_log_string(&self) -> String {
let mut parts = vec![
format!("subject={}", self.subject),
format!("permission={}", self.permission),
format!("resource={}", self.resource),
];
if let Some(ref reason) = self.reason {
parts.push(format!("reason={}", reason));
}
if let Some(ref request_id) = self.request_id {
parts.push(format!("request_id={}", request_id));
}
format!("access_denied: {}", parts.join(" "))
}
}
impl fmt::Display for AccessDenied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"access denied: {} cannot {} {}",
self.subject, self.permission, self.resource
)?;
if let Some(ref reason) = self.reason {
write!(f, " ({})", reason)?;
}
Ok(())
}
}
impl StdError for AccessDenied {}
impl From<AccessDenied> for super::Error {
fn from(denied: AccessDenied) -> Self {
super::Error::new(
super::ErrorKind::Forbidden,
format!(
"{} cannot {} {}",
denied.subject, denied.permission, denied.resource
),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_access_denied_new() {
let denied = AccessDenied::new("user:alice", "delete", "document:secret");
assert_eq!(denied.subject(), "user:alice");
assert_eq!(denied.permission(), "delete");
assert_eq!(denied.resource(), "document:secret");
assert!(denied.reason().is_none());
assert!(denied.request_id().is_none());
}
#[test]
fn test_access_denied_with_reason() {
let denied = AccessDenied::new("user:bob", "view", "folder:private")
.with_reason("no viewer relationship");
assert_eq!(denied.reason(), Some("no viewer relationship"));
}
#[test]
fn test_access_denied_with_request_id() {
let denied =
AccessDenied::new("user:charlie", "edit", "doc:1").with_request_id("req_abc123");
assert_eq!(denied.request_id(), Some("req_abc123"));
}
#[test]
fn test_access_denied_display() {
let denied = AccessDenied::new("user:alice", "delete", "document:readme");
let display = denied.to_string();
assert!(display.contains("user:alice"));
assert!(display.contains("delete"));
assert!(display.contains("document:readme"));
}
#[test]
fn test_access_denied_display_with_reason() {
let denied = AccessDenied::new("user:alice", "delete", "document:readme")
.with_reason("permission not granted");
let display = denied.to_string();
assert!(display.contains("permission not granted"));
}
#[test]
fn test_access_denied_to_log_string() {
let denied = AccessDenied::new("user:alice", "view", "doc:1")
.with_reason("no access")
.with_request_id("req_xyz");
let log = denied.to_log_string();
assert!(log.contains("subject=user:alice"));
assert!(log.contains("permission=view"));
assert!(log.contains("resource=doc:1"));
assert!(log.contains("reason=no access"));
assert!(log.contains("request_id=req_xyz"));
}
#[test]
fn test_access_denied_into_error() {
use crate::{Error, ErrorKind};
let denied = AccessDenied::new("user:alice", "delete", "doc:1");
let err: Error = denied.into();
assert_eq!(err.kind(), ErrorKind::Forbidden);
}
#[test]
fn test_access_denied_with_owned_strings() {
let subject = String::from("user:dynamic");
let permission = String::from("custom_perm");
let resource = String::from("resource:123");
let denied = AccessDenied::new(subject, permission, resource);
assert_eq!(denied.subject(), "user:dynamic");
assert_eq!(denied.permission(), "custom_perm");
assert_eq!(denied.resource(), "resource:123");
}
#[test]
fn test_access_denied_is_error() {
fn takes_error<E: std::error::Error>(_: &E) {}
let denied = AccessDenied::new("user:test", "view", "doc:1");
takes_error(&denied);
}
#[test]
fn test_access_denied_clone() {
let denied = AccessDenied::new("user:alice", "delete", "doc:1")
.with_reason("no permission")
.with_request_id("req-123");
let cloned = denied.clone();
assert_eq!(cloned.subject(), "user:alice");
assert_eq!(cloned.permission(), "delete");
assert_eq!(cloned.resource(), "doc:1");
assert_eq!(cloned.reason(), Some("no permission"));
assert_eq!(cloned.request_id(), Some("req-123"));
}
#[test]
fn test_access_denied_debug() {
let denied = AccessDenied::new("user:bob", "edit", "folder:private");
let debug = format!("{:?}", denied);
assert!(debug.contains("AccessDenied"));
assert!(debug.contains("user:bob"));
}
#[test]
fn test_access_denied_to_log_string_minimal() {
let denied = AccessDenied::new("user:alice", "view", "doc:1");
let log = denied.to_log_string();
assert!(log.contains("subject=user:alice"));
assert!(log.contains("permission=view"));
assert!(log.contains("resource=doc:1"));
assert!(!log.contains("reason="));
assert!(!log.contains("request_id="));
}
#[test]
fn test_access_denied_source_is_none() {
let denied = AccessDenied::new("user:test", "view", "doc:1");
assert!(denied.source().is_none());
}
}