use std::collections::{HashMap, HashSet};
use std::io::{self, Write};
use serde::{Deserialize, Serialize};
use super::action::Action;
use super::principal::Principal;
use super::resource::AttrValue;
use super::resource::Resource;
use super::status::RequestLimits;
use super::validation::{RequestId, ValidationError, validate_attribute_name};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Request {
principal: Principal,
action: Action,
resource: Resource,
}
impl Request {
pub fn new(principal: impl Into<Principal>, action: Action, resource: Resource) -> Self {
Self {
principal: principal.into(),
action,
resource,
}
}
pub fn principal(&self) -> &Principal {
&self.principal
}
pub fn action(&self) -> &Action {
&self.action
}
pub fn resource(&self) -> &Resource {
&self.resource
}
pub fn validate(&self) -> Result<(), ValidationError> {
self.principal.validate()?;
self.action.validate()?;
self.resource.validate()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(try_from = "AuthRequestWire")]
pub struct AuthRequest {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<RequestId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
context: Option<HashMap<String, AttrValue>>,
#[serde(flatten)]
request: Request,
}
impl AuthRequest {
pub fn new(request: Request) -> Self {
Self {
id: None,
context: None,
request,
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Result<Self, ValidationError> {
let id = RequestId::new(id);
id.validate()?;
self.id = Some(id);
Ok(self)
}
pub fn with_context(
mut self,
context: HashMap<String, AttrValue>,
) -> Result<Self, ValidationError> {
for key in context.keys() {
validate_attribute_name(key, "auth_request.context")?;
}
if !context.is_empty() {
self.context = Some(context);
}
Ok(self)
}
#[deprecated(since = "0.0.2", note = "use AuthRequest::new(request).with_id(id)")]
pub fn try_with_id(id: impl Into<String>, request: Request) -> Result<Self, ValidationError> {
Self::new(request).with_id(id)
}
#[deprecated(
since = "0.0.2",
note = "AuthRequest::with_context now validates its input"
)]
pub fn try_with_context(
self,
context: HashMap<String, AttrValue>,
) -> Result<Self, ValidationError> {
self.with_context(context)
}
pub fn id(&self) -> Option<&str> {
self.id.as_ref().map(RequestId::as_str)
}
pub fn context(&self) -> Option<&HashMap<String, AttrValue>> {
self.context.as_ref()
}
pub fn request(&self) -> &Request {
&self.request
}
pub fn validate(&self) -> Result<(), ValidationError> {
if let Some(id) = &self.id {
id.validate()?;
}
if let Some(context) = &self.context {
for key in context.keys() {
validate_attribute_name(key, "auth_request.context")?;
}
}
self.request.validate()
}
pub fn validate_context(&self, limits: RequestLimits) -> Result<(), ValidationError> {
let Some(context) = &self.context else {
return Ok(());
};
if context.len() > limits.max_context_keys {
return Err(ValidationError::ContextTooManyKeys {
actual: context.len(),
limit: limits.max_context_keys,
});
}
let depth = context.values().map(context_value_depth).max().unwrap_or(0);
if depth > limits.max_context_depth {
return Err(ValidationError::ContextTooDeep {
actual: depth,
limit: limits.max_context_depth,
});
}
let mut counter = ByteCounter::new(limits.max_context_bytes);
if let Err(error) = serde_json::to_writer(&mut counter, context) {
if counter.exceeded {
return Err(ValidationError::ContextTooLarge {
actual: counter.bytes,
limit: limits.max_context_bytes,
});
}
return Err(ValidationError::ContextSerialization {
message: error.to_string(),
});
}
Ok(())
}
}
#[derive(Deserialize)]
struct AuthRequestWire {
#[serde(default)]
id: Option<RequestId>,
#[serde(default)]
context: Option<HashMap<String, AttrValue>>,
#[serde(flatten)]
request: Request,
}
impl TryFrom<AuthRequestWire> for AuthRequest {
type Error = ValidationError;
fn try_from(wire: AuthRequestWire) -> Result<Self, Self::Error> {
let request = Self {
id: wire.id,
context: wire.context.filter(|context| !context.is_empty()),
request: wire.request,
};
request.validate()?;
Ok(request)
}
}
impl From<Request> for AuthRequest {
fn from(request: Request) -> Self {
Self::new(request)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(try_from = "AuthorizeRequestWire")]
pub struct AuthorizeRequest {
requests: Vec<AuthRequest>,
}
impl AuthorizeRequest {
pub fn new() -> Self {
Self {
requests: Vec::new(),
}
}
pub fn single(request: Request) -> Self {
Self {
requests: vec![AuthRequest::new(request)],
}
}
pub fn from_requests(requests: impl IntoIterator<Item = Request>) -> Self {
Self {
requests: requests.into_iter().map(AuthRequest::from).collect(),
}
}
pub fn from_auth_requests(
requests: impl IntoIterator<Item = AuthRequest>,
) -> Result<Self, ValidationError> {
let request = Self {
requests: requests.into_iter().collect(),
};
request.validate()?;
Ok(request)
}
pub fn add_request(mut self, request: Request) -> Self {
self.requests.push(AuthRequest::new(request));
self
}
pub fn add_request_with_id(
self,
id: impl Into<String>,
request: Request,
) -> Result<Self, ValidationError> {
self.add_auth_request(AuthRequest::new(request).with_id(id)?)
}
pub fn add_auth_request(mut self, request: AuthRequest) -> Result<Self, ValidationError> {
if let Some(id) = request.id() {
if self
.requests
.iter()
.any(|existing| existing.id() == Some(id))
{
return Err(ValidationError::DuplicateRequestId {
value: id.to_string(),
});
}
}
self.requests.push(request);
Ok(self)
}
#[deprecated(
since = "0.0.2",
note = "AuthorizeRequest::add_request_with_id now validates its input"
)]
pub fn try_add_request_with_id(
self,
id: impl Into<String>,
request: Request,
) -> Result<Self, ValidationError> {
self.add_request_with_id(id, request)
}
pub fn requests(&self) -> &[AuthRequest] {
&self.requests
}
pub fn len(&self) -> usize {
self.requests.len()
}
pub fn is_empty(&self) -> bool {
self.requests.is_empty()
}
pub fn validate(&self) -> Result<(), ValidationError> {
let mut request_ids = HashSet::new();
for request in &self.requests {
request.validate()?;
if let Some(id) = request.id() {
if !request_ids.insert(id) {
return Err(ValidationError::DuplicateRequestId {
value: id.to_string(),
});
}
}
}
Ok(())
}
pub fn validate_context(&self, limits: RequestLimits) -> Result<(), ValidationError> {
for request in &self.requests {
request.validate_context(limits)?;
}
Ok(())
}
}
#[derive(Deserialize)]
struct AuthorizeRequestWire {
requests: Vec<AuthRequest>,
}
impl TryFrom<AuthorizeRequestWire> for AuthorizeRequest {
type Error = ValidationError;
fn try_from(wire: AuthorizeRequestWire) -> Result<Self, Self::Error> {
Self::from_auth_requests(wire.requests)
}
}
fn context_value_depth(value: &AttrValue) -> usize {
let mut maximum = 0;
let mut pending = vec![(value, 1usize)];
while let Some((value, depth)) = pending.pop() {
maximum = maximum.max(depth);
if let AttrValue::Set(values) = value {
pending.extend(values.iter().map(|value| (value, depth.saturating_add(1))));
}
}
maximum
}
struct ByteCounter {
bytes: usize,
limit: usize,
exceeded: bool,
}
impl ByteCounter {
fn new(limit: usize) -> Self {
Self {
bytes: 0,
limit,
exceeded: false,
}
}
}
impl Write for ByteCounter {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.bytes = self.bytes.saturating_add(buffer.len());
if self.bytes > self.limit {
self.exceeded = true;
return Err(io::Error::other(
"serialized context exceeds configured limit",
));
}
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Action, AttrValue, Resource, User};
fn sample_request() -> Request {
Request::new(
User::new("alice").unwrap(),
Action::new("create").unwrap(),
Resource::new("Host", "web-01").unwrap(),
)
}
#[test]
fn request_serialization_matches_wire_format() {
let request = sample_request();
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["principal"]["User"]["id"], "alice");
assert_eq!(json["action"]["id"], "create");
assert_eq!(json["resource"]["kind"], "Host");
assert_eq!(json["resource"]["id"], "web-01");
}
#[test]
fn auth_request_flattens_request() {
let auth = AuthRequest::new(sample_request()).with_id("req-1").unwrap();
let json = serde_json::to_value(&auth).unwrap();
assert_eq!(json["id"], "req-1");
assert_eq!(json["principal"]["User"]["id"], "alice");
assert_eq!(json["action"]["id"], "create");
}
#[test]
fn auth_request_with_context_serializes_context() {
let mut context = HashMap::new();
context.insert("env".to_string(), AttrValue::String("prod".to_string()));
let auth = AuthRequest::new(sample_request())
.with_context(context)
.unwrap();
let json = serde_json::to_value(&auth).unwrap();
assert_eq!(json["context"]["env"]["type"], "String");
assert_eq!(json["context"]["env"]["value"], "prod");
}
#[test]
fn auth_request_empty_context_is_omitted() {
let auth = AuthRequest::new(sample_request())
.with_context(HashMap::new())
.unwrap();
let json = serde_json::to_value(&auth).unwrap();
assert!(json.get("context").is_none());
}
#[test]
fn authorize_request_builder() {
let req = AuthorizeRequest::new()
.add_request(sample_request())
.add_request_with_id("req-2", sample_request())
.unwrap();
assert_eq!(req.requests().len(), 2);
assert!(req.requests()[0].id().is_none());
assert_eq!(req.requests()[1].id(), Some("req-2"));
}
#[test]
fn authorize_request_single() {
let req = AuthorizeRequest::single(sample_request());
assert_eq!(req.requests().len(), 1);
}
#[test]
fn authorize_request_rejects_duplicate_ids() {
let request = AuthorizeRequest::new()
.add_request_with_id("duplicate", sample_request())
.unwrap()
.add_request_with_id("duplicate", sample_request());
assert!(matches!(
request,
Err(ValidationError::DuplicateRequestId { value }) if value == "duplicate"
));
}
#[test]
fn request_roundtrip() {
let request = sample_request();
let json = serde_json::to_value(&request).unwrap();
let deserialized: Request = serde_json::from_value(json).unwrap();
assert_eq!(request, deserialized);
}
}