Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 46 additions & 19 deletions crates/auth/src/policy/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! `ActionMatch` and `ResourceMatch` enums prevent invalid states where both
//! `Action` and `NotAction` (or `Resource` and `NotResource`) are present.

use extenddb_core::limits::DEFAULT_MAX_POLICY_DOCUMENT_BYTES;
use serde_json::Value;
use std::fmt;

Expand Down Expand Up @@ -144,23 +145,27 @@ impl fmt::Display for PolicyParseError {
impl std::error::Error for PolicyParseError {}

impl PolicyDocument {
/// Parse a policy document from a JSON string.
/// Parse a policy document from a JSON string, with the default size
/// limit ([`DEFAULT_MAX_POLICY_DOCUMENT_BYTES`]).
///
/// Enforces the default 6,144-byte size limit (matching AWS IAM inline
/// policy limit). This applies to both write-path validation and read-path
/// parsing of stored policies — a stored policy exceeding this limit will
/// fail to parse, triggering fail-closed denial (defense in depth).
/// The server parses with the configured `max_policy_document_bytes`
/// instead, through [`Self::from_json_with_size_limit`] on the read path
/// and [`Self::from_json_for_write`] on the write path.
///
/// # Errors
///
/// Returns `PolicyParseError` if the JSON is malformed or contains
/// invalid policy constructs (e.g., both Action and `NotAction`).
pub fn from_json(json: &str) -> Result<Self, PolicyParseError> {
Self::from_json_with_size_limit(json, 6_144)
Self::from_json_with_size_limit(json, DEFAULT_MAX_POLICY_DOCUMENT_BYTES)
}

/// Parse a policy document with an explicit size limit.
///
/// The read path parses a stored policy this way, with the same limit the
/// write path applies: a stored policy over the limit does not parse, and
/// its principal is denied (fail closed).
///
/// # Errors
///
/// Returns `PolicyParseError` if the document exceeds `max_bytes` or is malformed.
Expand All @@ -187,20 +192,21 @@ impl PolicyDocument {
/// a managed or boundary policy) and apply the checks the service applies
/// only when a policy is written.
///
/// Everything `from_json` checks, plus the `Version` value and the `Sid`
/// charset. Both run at write only, so a policy stored before either check
/// existed keeps parsing on the request path rather than failing the
/// principal closed: a `Sid` is a label that does not affect evaluation,
/// and a stored document with another `Version` matches its `Resource`
/// patterns as written, as it did before.
/// Everything [`Self::from_json_with_size_limit`] checks with `max_bytes`,
/// the configured `max_policy_document_bytes`, plus the `Version` value
/// and the `Sid` charset. Both run at write only, so a policy stored before
/// either check existed keeps parsing on the request path rather than
/// failing the principal closed: a `Sid` is a label that does not affect
/// evaluation, and a stored document with another `Version` matches its
/// `Resource` patterns as written, as it did before.
///
/// # Errors
///
/// Returns `PolicyParseError` if `from_json` rejects the document, its
/// `Version` is not `2012-10-17` or `2008-10-17`, or a statement `Sid` is
/// outside the service's charset.
pub fn from_json_for_write(json: &str) -> Result<Self, PolicyParseError> {
let doc = Self::from_json(json)?;
/// Returns `PolicyParseError` if the document exceeds `max_bytes` or does
/// not parse, its `Version` is not `2012-10-17` or `2008-10-17`, or a
/// statement `Sid` is outside the service's charset.
pub fn from_json_for_write(json: &str, max_bytes: usize) -> Result<Self, PolicyParseError> {
let doc = Self::from_json_with_size_limit(json, max_bytes)?;
super::variables::validate_version(json)?;
validate_statement_sids(&doc.statements)?;
Ok(doc)
Expand Down Expand Up @@ -687,7 +693,8 @@ mod tests {
r#"{{"Version":"2012-10-17","Statement":[{{"Sid":"{sid}","Effect":"Allow","Action":"dynamodb:GetItem","Resource":"*"}}]}}"#
);
assert!(
PolicyDocument::from_json_for_write(&json).is_err(),
PolicyDocument::from_json_for_write(&json, DEFAULT_MAX_POLICY_DOCUMENT_BYTES)
.is_err(),
"should reject Sid {sid:?} at write"
);
// A policy stored before the check existed still parses on the
Expand All @@ -706,12 +713,32 @@ mod tests {
r#"{{"Version":"2012-10-17","Statement":[{{"Sid":"{sid}","Effect":"Allow","Action":"dynamodb:GetItem","Resource":"*"}}]}}"#
);
assert!(
PolicyDocument::from_json_for_write(&json).is_ok(),
PolicyDocument::from_json_for_write(&json, DEFAULT_MAX_POLICY_DOCUMENT_BYTES)
.is_ok(),
"should accept Sid {sid:?}"
);
}
}

/// Both paths take the limit from the caller: a document of exactly
/// `max_bytes` parses, and one byte over does not.
#[test]
fn the_size_limit_is_the_one_the_caller_passes() {
let json = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:GetItem","Resource":"*"}]}"#;
let at = json.len();
assert!(PolicyDocument::from_json_with_size_limit(json, at).is_ok());
assert!(PolicyDocument::from_json_for_write(json, at).is_ok());
let over = PolicyDocument::from_json_for_write(json, at - 1).expect_err("one byte over");
assert_eq!(
over.to_string(),
format!(
"policy parse error: policy document exceeds maximum size ({} bytes)",
at - 1
)
);
assert!(PolicyDocument::from_json_with_size_limit(json, at - 1).is_err());
}

#[test]
fn reject_miscased_operator_names() {
// The live service treats operator names, set qualifiers and the
Expand Down
13 changes: 10 additions & 3 deletions crates/auth/src/policy/variables/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Unit tests for policy variable parsing, expansion and token matching.

use super::*;
use extenddb_core::limits::DEFAULT_MAX_POLICY_DOCUMENT_BYTES;
use std::collections::HashMap;

struct Ctx(HashMap<String, Vec<String>>);
Expand Down Expand Up @@ -72,7 +73,7 @@ fn a_write_refuses_a_version_that_is_not_one_of_the_two_values() {
r#""2012-10-17T""#,
] {
let json = format!(r#"{{"Version":{version},{stmt}}}"#);
let refused = PolicyDocument::from_json_for_write(&json)
let refused = PolicyDocument::from_json_for_write(&json, DEFAULT_MAX_POLICY_DOCUMENT_BYTES)
.err()
.map(|e| e.0);
let expected =
Expand All @@ -85,12 +86,18 @@ fn a_write_refuses_a_version_that_is_not_one_of_the_two_values() {
for version in ["2012-10-17", "2008-10-17"] {
let json = format!(r#"{{"Version":"{version}",{stmt}}}"#);
assert!(
PolicyDocument::from_json_for_write(&json).is_ok(),
PolicyDocument::from_json_for_write(&json, DEFAULT_MAX_POLICY_DOCUMENT_BYTES).is_ok(),
"{version}"
);
}
// A missing Version is refused by each write path before parsing.
assert!(PolicyDocument::from_json_for_write(&format!("{{{stmt}}}")).is_ok());
assert!(
PolicyDocument::from_json_for_write(
&format!("{{{stmt}}}"),
DEFAULT_MAX_POLICY_DOCUMENT_BYTES
)
.is_ok()
);
}

// --- parse_reference ---
Expand Down
8 changes: 6 additions & 2 deletions crates/core/src/limits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,13 @@ fn default_max_expression_length_bytes() -> usize {
fn default_max_expression_depth() -> usize {
150
}

/// The default `max_policy_document_bytes`: the AWS IAM limit of 6,144 bytes
/// for an inline policy.
pub const DEFAULT_MAX_POLICY_DOCUMENT_BYTES: usize = 6_144;

fn default_max_policy_document_bytes() -> usize {
// AWS IAM limit: 6,144 bytes for inline policies.
6_144
DEFAULT_MAX_POLICY_DOCUMENT_BYTES
}
fn default_max_import_file_bytes() -> u64 {
// 10 GB default.
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/validation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2026 ExtendDB contributors
// SPDX-License-Identifier: Apache-2.0
pub mod number;
pub mod provisioned_limits;
pub mod vector_item;

pub use vector_item::{
Expand Down
Loading
Loading