Skip to content
Merged
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
5 changes: 2 additions & 3 deletions crates/auth/src/credential_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl CachedCredentialStore {
) -> Result<(), extenddb_cache::PredicateError> {
let acct = account_id.to_owned();
self.cache
.invalidate_if_value(move |v| v.map(|cred| cred.account_id == acct).unwrap_or(false))
.invalidate_if_value(move |v| v.is_some_and(|cred| cred.account_id == acct))
}

/// Drop every cached credential for `principal_name` in `account_id`.
Expand All @@ -140,8 +140,7 @@ impl CachedCredentialStore {
let acct = account_id.to_owned();
let principal = principal_name.to_owned();
self.cache.invalidate_if_value(move |v| {
v.map(|cred| cred.account_id == acct && cred.principal_name == principal)
.unwrap_or(false)
v.is_some_and(|cred| cred.account_id == acct && cred.principal_name == principal)
})
}

Expand Down
8 changes: 4 additions & 4 deletions crates/auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! Authentication and authorization for extenddb.
//!
//! Defines the `AuthProvider` trait for pluggable auth backends. Ships with
//! `BuiltinAuthProvider` (full SigV4 verification with local credential store).
//! `BuiltinAuthProvider` (full `SigV4` verification with local credential store).

pub mod cache_registry;
pub mod credential_cache;
Expand All @@ -20,7 +20,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};

/// Auth provider trait — pluggable authentication.
///
/// `BuiltinAuthProvider` performs SigV4 verification.
/// `BuiltinAuthProvider` performs `SigV4` verification.
/// Fix #11: Accept `&HeaderMap` directly to avoid per-request `HashMap` allocation.
#[async_trait::async_trait]
pub trait AuthProvider: Send + Sync {
Expand Down Expand Up @@ -102,10 +102,10 @@ pub trait CredentialStore: Send + Sync {
) -> Result<Option<StoredCredential>, DynamoDbError>;
}

/// SigV4 auth provider with local credential store.
/// `SigV4` auth provider with local credential store.
///
/// Parses the `Authorization` header, looks up the access key, decrypts the
/// secret, verifies the SigV4 signature, and validates the request timestamp.
/// secret, verifies the `SigV4` signature, and validates the request timestamp.
/// Handles both long-lived (AKIA*) and temporary (ASIA* + X-Amz-Security-Token)
/// credentials.
pub struct BuiltinAuthProvider<C: CredentialStore> {
Expand Down
6 changes: 3 additions & 3 deletions crates/auth/src/policy/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//!
//! Evaluates condition blocks against a `ConditionContext`. Supports all IAM
//! condition operators: String*, Numeric*, Date*, Bool, Null, Arn*, and the
//! set operators ForAllValues/ForAnyValue with optional IfExists suffix.
//! set operators ForAllValues/ForAnyValue with optional `IfExists` suffix.

use super::context::ConditionContext;
use super::document::{Condition, ConditionOperator};
Expand Down Expand Up @@ -160,10 +160,10 @@ fn unwrap_if_exists(op: &ConditionOperator) -> (bool, &ConditionOperator) {
/// For multi-valued keys (e.g., `dynamodb:LeadingKeys`), all context values
/// must satisfy the condition (implicit AND).
///
/// For positive operators (StringEquals, NumericEquals, etc.): each context
/// For positive operators (`StringEquals`, `NumericEquals`, etc.): each context
/// value must match at least one policy value (OR semantics — "value in set").
///
/// For negative operators (StringNotEquals, NumericNotEquals, etc.): each
/// For negative operators (`StringNotEquals`, `NumericNotEquals`, etc.): each
/// context value must satisfy the negative comparison against ALL policy
/// values (AND semantics — "value not in set"). This matches AWS IAM behavior
/// where `StringNotEquals` with `["a", "b"]` means "value is neither a nor b".
Expand Down
35 changes: 18 additions & 17 deletions crates/auth/src/policy/context.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Copyright 2026 ExtendDB contributors
// SPDX-License-Identifier: Apache-2.0

//! Condition context trait and DynamoDB request context.
//! Condition context trait and `DynamoDB` request context.
//!
//! `ConditionContext` is the shared trait for resolving condition keys during
//! policy evaluation. `RequestContext` implements it for DynamoDB operations;
//! policy evaluation. `RequestContext` implements it for `DynamoDB` operations;
//! `AssumeRoleContext` implements it for trust policy evaluation.

use std::collections::HashMap;

/// Trait for resolving condition keys during policy evaluation.
///
/// Implemented by `RequestContext` (DynamoDB operations) and
/// `AssumeRoleContext` (trust policy / AssumeRole).
/// Implemented by `RequestContext` (`DynamoDB` operations) and
/// `AssumeRoleContext` (trust policy / `AssumeRole`).
pub trait ConditionContext {
/// Resolve a condition key to its value(s).
///
Expand All @@ -21,29 +21,29 @@ pub trait ConditionContext {
fn resolve_key(&self, key: &str) -> Option<Vec<&str>>;
}

/// Request parameters extracted from a DynamoDB operation for condition evaluation.
/// Request parameters extracted from a `DynamoDB` operation for condition evaluation.
#[derive(Debug, Default)]
pub struct RequestParams {
/// Partition key values being accessed (for `dynamodb:LeadingKeys`).
/// `None` for table-level operations (CreateTable, etc.).
/// `None` for table-level operations (`CreateTable`, etc.).
pub leading_keys: Option<Vec<String>>,
/// Attribute names being read/written (for `dynamodb:Attributes`).
/// `None` when not applicable.
pub attributes: Option<Vec<String>>,
/// The Select parameter value (for `dynamodb:Select`).
pub select: Option<String>,
/// The ReturnValues parameter value (for `dynamodb:ReturnValues`).
/// The `ReturnValues` parameter value (for `dynamodb:ReturnValues`).
pub return_values: Option<String>,
/// The ReturnConsumedCapacity parameter value.
/// The `ReturnConsumedCapacity` parameter value.
pub return_consumed_capacity: Option<String>,
/// The enclosing operation for batch/transact sub-operations.
pub enclosing_operation: Option<String>,
}

/// Context for evaluating conditions on DynamoDB operations.
/// Context for evaluating conditions on `DynamoDB` operations.
///
/// Built by the server middleware before policy evaluation. Contains all
/// condition keys that IAM policies can reference for DynamoDB access control.
/// condition keys that IAM policies can reference for `DynamoDB` access control.
#[derive(Debug)]
pub struct RequestContext {
/// Tags on the authenticated principal (`aws:PrincipalTag/*`).
Expand All @@ -56,9 +56,9 @@ pub struct RequestContext {
pub attributes: Option<Vec<String>>,
/// The Select parameter value.
pub select: Option<String>,
/// The ReturnValues parameter value.
/// The `ReturnValues` parameter value.
pub return_values: Option<String>,
/// The ReturnConsumedCapacity parameter value.
/// The `ReturnConsumedCapacity` parameter value.
pub return_consumed_capacity: Option<String>,
/// Whether this is a Scan operation.
pub full_table_scan: Option<bool>,
Expand All @@ -67,11 +67,12 @@ pub struct RequestContext {
}

impl RequestContext {
/// Build context for a DynamoDB operation.
/// Build context for a `DynamoDB` operation.
///
/// `principal_tags` and `resource_tags` come from the identity and target
/// table respectively. `is_scan` should be true for Scan operations.
/// `params` carries operation-specific request parameters.
#[must_use]
pub fn build(
principal_tags: HashMap<String, String>,
resource_tags: HashMap<String, String>,
Expand Down Expand Up @@ -103,11 +104,11 @@ impl ConditionContext for RequestContext {
"dynamodb:LeadingKeys" => self
.leading_keys
.as_ref()
.map(|v| v.iter().map(|s| s.as_str()).collect()),
.map(|v| v.iter().map(std::string::String::as_str).collect()),
"dynamodb:Attributes" => self
.attributes
.as_ref()
.map(|v| v.iter().map(|s| s.as_str()).collect()),
.map(|v| v.iter().map(std::string::String::as_str).collect()),
"dynamodb:Select" => self.select.as_deref().map(|v| vec![v]),
"dynamodb:ReturnValues" => self.return_values.as_deref().map(|v| vec![v]),
"dynamodb:ReturnConsumedCapacity" => {
Expand All @@ -125,15 +126,15 @@ impl ConditionContext for RequestContext {
}
}

/// Context for evaluating trust policy conditions during AssumeRole.
/// Context for evaluating trust policy conditions during `AssumeRole`.
///
/// Trust policies can reference `aws:PrincipalTag/*` and `sts:ExternalId`.
/// DynamoDB-specific keys are not applicable.
#[derive(Debug)]
pub struct AssumeRoleContext {
/// Tags on the calling principal.
pub principal_tags: HashMap<String, String>,
/// The external ID provided in the AssumeRole call (if any).
/// The external ID provided in the `AssumeRole` call (if any).
pub external_id: Option<String>,
}

Expand Down
18 changes: 9 additions & 9 deletions crates/auth/src/policy/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ pub struct Statement {
pub sid: Option<String>,
/// Allow or Deny.
pub effect: Effect,
/// Action or NotAction matching.
/// Action or `NotAction` matching.
pub action_match: ActionMatch,
/// Resource or NotResource matching.
/// Resource or `NotResource` matching.
pub resource_match: ResourceMatch,
/// Conditions that must all be true for the statement to apply.
pub conditions: Vec<Condition>,
Expand All @@ -43,7 +43,7 @@ pub enum Effect {
Deny,
}

/// Action matching: either Action (include list) or NotAction (exclude list).
/// Action matching: either Action (include list) or `NotAction` (exclude list).
/// A statement uses exactly one — never both.
#[derive(Debug, Clone)]
pub enum ActionMatch {
Expand All @@ -53,7 +53,7 @@ pub enum ActionMatch {
NotActions(Vec<String>),
}

/// Resource matching: either Resource (include list) or NotResource (exclude list).
/// Resource matching: either Resource (include list) or `NotResource` (exclude list).
#[derive(Debug, Clone)]
pub enum ResourceMatch {
/// Matches listed resources.
Expand Down Expand Up @@ -82,7 +82,7 @@ pub struct Condition {
pub values: Vec<String>,
}

/// All IAM condition operators relevant to DynamoDB access control.
/// All IAM condition operators relevant to `DynamoDB` access control.
///
/// Set operators (`ForAllValues`, `ForAnyValue`) and `IfExists` wrap a base
/// operator. Valid nestings: `ForAllValues(IfExists(base))`,
Expand Down Expand Up @@ -148,7 +148,7 @@ impl PolicyDocument {
/// # Errors
///
/// Returns `PolicyParseError` if the JSON is malformed or contains
/// invalid policy constructs (e.g., both Action and NotAction).
/// 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)
}
Expand Down Expand Up @@ -214,7 +214,7 @@ fn parse_statement(value: &Value) -> Result<Statement, PolicyParseError> {
})
}

/// Parse Action or NotAction (mutually exclusive).
/// Parse Action or `NotAction` (mutually exclusive).
fn parse_action_match(value: &Value) -> Result<ActionMatch, PolicyParseError> {
let has_action = !value["Action"].is_null();
let has_not_action = !value["NotAction"].is_null();
Expand All @@ -234,7 +234,7 @@ fn parse_action_match(value: &Value) -> Result<ActionMatch, PolicyParseError> {
}
}

/// Parse Resource or NotResource (mutually exclusive).
/// Parse Resource or `NotResource` (mutually exclusive).
fn parse_resource_match(value: &Value) -> Result<ResourceMatch, PolicyParseError> {
let has_resource = !value["Resource"].is_null();
let has_not_resource = !value["NotResource"].is_null();
Expand All @@ -254,7 +254,7 @@ fn parse_resource_match(value: &Value) -> Result<ResourceMatch, PolicyParseError
}
}

/// Parse Principal or NotPrincipal (optional, for trust policies).
/// Parse Principal or `NotPrincipal` (optional, for trust policies).
fn parse_principal_match(value: &Value) -> Result<Option<PrincipalMatch>, PolicyParseError> {
let has_principal = !value["Principal"].is_null();
let has_not_principal = !value["NotPrincipal"].is_null();
Expand Down
4 changes: 2 additions & 2 deletions crates/auth/src/policy/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ pub enum AuthzDecision {
///
/// - `identity_policies`: user + group policies, or role policies.
/// - `permissions_boundary`: optional boundary policy on the user or role.
/// - `session_policy`: optional inline policy from AssumeRole.
/// - `action`: the DynamoDB action (e.g., "dynamodb:PutItem").
/// - `session_policy`: optional inline policy from `AssumeRole`.
/// - `action`: the `DynamoDB` action (e.g., "dynamodb:PutItem").
/// - `resource_arn`: the target resource ARN.
/// - `context`: condition context for evaluating condition blocks.
pub fn evaluate_policies(
Expand Down
3 changes: 3 additions & 0 deletions crates/auth/src/policy/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
/// assert!(!wildcard_match("dynamodb:Get*", "dynamodb:PutItem"));
/// assert!(wildcard_match("s?s", "sis"));
/// ```
#[must_use]
pub fn wildcard_match(pattern: &str, value: &str) -> bool {
wildcard_match_impl(pattern.as_bytes(), value.as_bytes(), false)
}
Expand All @@ -40,6 +41,7 @@ pub fn wildcard_match(pattern: &str, value: &str) -> bool {
/// assert!(wildcard_match_ignore_case("dynamodb:Get*", "dynamodb:getitem"));
/// assert!(!wildcard_match_ignore_case("dynamodb:Get*", "dynamodb:PutItem"));
/// ```
#[must_use]
pub fn wildcard_match_ignore_case(pattern: &str, value: &str) -> bool {
wildcard_match_impl(pattern.as_bytes(), value.as_bytes(), true)
}
Expand Down Expand Up @@ -104,6 +106,7 @@ fn wildcard_match_impl(p: &[u8], v: &[u8], ignore_case: bool) -> bool {
/// "arn:aws:dynamodb:us-east-1:123456789012:table/Users"
/// ));
/// ```
#[must_use]
pub fn arn_match(pattern: &str, value: &str) -> bool {
// "*" as a pattern matches any ARN
if pattern == "*" {
Expand Down
12 changes: 6 additions & 6 deletions crates/auth/src/sigv4/canonical.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright 2026 ExtendDB contributors
// SPDX-License-Identifier: Apache-2.0

//! Canonical request construction for SigV4 verification.
//! Canonical request construction for `SigV4` verification.
//!
//! Builds the canonical request string from the HTTP method, URI path,
//! query string, signed headers, and body hash per the AWS SigV4 spec.
//! query string, signed headers, and body hash per the AWS `SigV4` spec.

use axum::http::HeaderMap;
use sha2::{Digest, Sha256};
Expand All @@ -21,7 +21,7 @@ use sha2::{Digest, Sha256};
/// HashedPayload
/// ```
///
/// For DynamoDB, the URI is always `/` and there is no query string.
/// For `DynamoDB`, the URI is always `/` and there is no query string.
pub fn canonical_request(
method: &str,
uri_path: &str,
Expand All @@ -39,15 +39,14 @@ pub fn canonical_request(
let payload_hash = headers
.get("x-amz-content-sha256")
.and_then(|v| v.to_str().ok())
.map(str::to_owned)
.unwrap_or_else(|| sha256_hex(body));
.map_or_else(|| sha256_hex(body), str::to_owned);

format!(
"{method}\n{uri_path}\n{query_string}\n{canonical_headers}\n{signed_lower}\n{payload_hash}"
)
}

/// Build the string-to-sign for SigV4.
/// Build the string-to-sign for `SigV4`.
///
/// Format:
/// ```text
Expand All @@ -56,6 +55,7 @@ pub fn canonical_request(
/// <scope>\n
/// Hex(SHA256(canonical_request))
/// ```
#[must_use]
pub fn string_to_sign(timestamp: &str, scope: &str, canonical_request: &str) -> String {
let hashed = sha256_hex(canonical_request.as_bytes());
format!("AWS4-HMAC-SHA256\n{timestamp}\n{scope}\n{hashed}")
Expand Down
2 changes: 1 addition & 1 deletion crates/auth/src/sigv4/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! AWS Signature Version 4 verification.
//!
//! Implements server-side SigV4 verification: parsing the `Authorization` header,
//! Implements server-side `SigV4` verification: parsing the `Authorization` header,
//! reconstructing the canonical request, deriving the signing key, and performing
//! constant-time signature comparison.

Expand Down
6 changes: 3 additions & 3 deletions crates/auth/src/sigv4/parse.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2026 ExtendDB contributors
// SPDX-License-Identifier: Apache-2.0

//! Parse the AWS SigV4 `Authorization` header.
//! Parse the AWS `SigV4` `Authorization` header.
//!
//! Format:
//! ```text
Expand All @@ -12,7 +12,7 @@

use extenddb_core::error::DynamoDbError;

/// Parsed components of a SigV4 `Authorization` header.
/// Parsed components of a `SigV4` `Authorization` header.
#[derive(Debug, PartialEq)]
pub struct ParsedAuthorization {
/// The access key ID (e.g. `AKIAIOSFODNN7EXAMPLE`).
Expand All @@ -29,7 +29,7 @@ pub struct ParsedAuthorization {
pub signature: String,
}

/// Parse a SigV4 `Authorization` header value.
/// Parse a `SigV4` `Authorization` header value.
///
/// Returns `IncompleteSignature` if the header is malformed.
/// S-2: Rejects headers exceeding 8 KB to prevent heap abuse.
Expand Down
Loading
Loading