139 lines
4.8 KiB
Rust
139 lines
4.8 KiB
Rust
//! Auth behavior: hashed-only tokens, uniform authentication refusal,
|
|
//! explicit capability grants, and credentials-never-stored.
|
|
|
|
mod common;
|
|
use common::*;
|
|
use makepad_asset_store::{token_hash, Capability, Scope, ServerError};
|
|
use std::fs;
|
|
|
|
const SECRET: &[u8] = b"THE-DISTINCTIVE-TRANSPORT-SECRET-0123456789abcdef";
|
|
|
|
#[test]
|
|
fn token_roundtrip_and_uniform_refusals() {
|
|
let (_root, core) = open_core("token");
|
|
let auth = core.auth();
|
|
let p = pid_n(1);
|
|
auth.create_principal(&p, "uploader", NOW).unwrap();
|
|
auth.register_token(&p, &token_hash(SECRET), NOW + 100_000, NOW).unwrap();
|
|
|
|
assert_eq!(auth.authenticate(SECRET, NOW + 1).unwrap(), p);
|
|
|
|
// Wrong secret, expired, revoked, disabled: all identical refusals.
|
|
assert!(matches!(
|
|
auth.authenticate(b"wrong secret", NOW + 1).unwrap_err(),
|
|
ServerError::Unauthenticated
|
|
));
|
|
assert!(matches!(
|
|
auth.authenticate(SECRET, NOW + 100_000).unwrap_err(),
|
|
ServerError::Unauthenticated
|
|
));
|
|
auth.revoke_token(&token_hash(SECRET)).unwrap();
|
|
assert!(matches!(
|
|
auth.authenticate(SECRET, NOW + 1).unwrap_err(),
|
|
ServerError::Unauthenticated
|
|
));
|
|
|
|
let secret2: &[u8] = b"second-secret";
|
|
auth.register_token(&p, &token_hash(secret2), NOW + 100_000, NOW).unwrap();
|
|
auth.disable_principal(&p).unwrap();
|
|
assert!(matches!(
|
|
auth.authenticate(secret2, NOW + 1).unwrap_err(),
|
|
ServerError::Unauthenticated
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn token_admission_is_fail_closed() {
|
|
let (_root, core) = open_core("token_admission");
|
|
let auth = core.auth();
|
|
// Token for an unknown principal refuses.
|
|
assert!(matches!(
|
|
auth.register_token(&pid_n(9), &token_hash(SECRET), NOW + 10, NOW).unwrap_err(),
|
|
ServerError::NotFound { what: "principal" }
|
|
));
|
|
let p = pid_n(1);
|
|
auth.create_principal(&p, "u", NOW).unwrap();
|
|
// Already-expired token refuses.
|
|
assert!(matches!(
|
|
auth.register_token(&p, &token_hash(SECRET), NOW, NOW).unwrap_err(),
|
|
ServerError::InvalidInput { what: "token already expired" }
|
|
));
|
|
// Duplicate principal id refuses.
|
|
assert!(matches!(
|
|
auth.create_principal(&p, "other-name", NOW).unwrap_err(),
|
|
ServerError::Conflict { what: "principal id" }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn secret_bytes_never_reach_the_catalog_files() {
|
|
let (root, core) = open_core("no_secret");
|
|
let auth = core.auth();
|
|
let p = pid_n(1);
|
|
auth.create_principal(&p, "uploader", NOW).unwrap();
|
|
auth.register_token(&p, &token_hash(SECRET), NOW + 100_000, NOW).unwrap();
|
|
auth.authenticate(SECRET, NOW + 1).unwrap();
|
|
drop(core); // close the connection so WAL contents are final on disk
|
|
|
|
// Scan every byte the server persisted: the secret must appear nowhere
|
|
// (only its SHA-256 does).
|
|
let mut scanned = 0;
|
|
for name in ["catalog.sqlite3", "catalog.sqlite3-wal", "catalog.sqlite3-shm"] {
|
|
let path = root.join(name);
|
|
if !path.exists() {
|
|
continue;
|
|
}
|
|
let bytes = fs::read(&path).unwrap();
|
|
scanned += 1;
|
|
assert!(
|
|
!bytes.windows(SECRET.len()).any(|w| w == SECRET),
|
|
"secret bytes found in {name}"
|
|
);
|
|
}
|
|
assert!(scanned > 0, "no catalog files scanned");
|
|
}
|
|
|
|
#[test]
|
|
fn grants_are_explicit_and_scoped() {
|
|
let (_root, core) = open_core("grants");
|
|
let auth = core.auth();
|
|
let p = pid_n(1);
|
|
auth.create_principal(&p, "worker", NOW).unwrap();
|
|
|
|
// No grant at all: denied.
|
|
assert!(matches!(
|
|
auth.require(&p, Capability::BlobWrite, "rik2").unwrap_err(),
|
|
ServerError::Denied { capability: "blob_write" }
|
|
));
|
|
|
|
// Namespace-scoped grant works only in that namespace.
|
|
auth.grant(&p, Capability::BlobWrite, Scope::Namespace("rik2"), NOW).unwrap();
|
|
auth.require(&p, Capability::BlobWrite, "rik2").unwrap();
|
|
assert!(matches!(
|
|
auth.require(&p, Capability::BlobWrite, "other").unwrap_err(),
|
|
ServerError::Denied { .. }
|
|
));
|
|
// A different capability in the same namespace is still denied.
|
|
assert!(matches!(
|
|
auth.require(&p, Capability::AssetPublish, "rik2").unwrap_err(),
|
|
ServerError::Denied { capability: "asset_publish" }
|
|
));
|
|
|
|
// Wildcard grant spans namespaces.
|
|
auth.grant(&p, Capability::JobWorker, Scope::All, NOW).unwrap();
|
|
auth.require(&p, Capability::JobWorker, "rik2").unwrap();
|
|
auth.require(&p, Capability::JobWorker, "other").unwrap();
|
|
|
|
// Revocation returns to denied.
|
|
auth.revoke_grant(&p, Capability::BlobWrite, Scope::Namespace("rik2")).unwrap();
|
|
assert!(matches!(
|
|
auth.require(&p, Capability::BlobWrite, "rik2").unwrap_err(),
|
|
ServerError::Denied { .. }
|
|
));
|
|
|
|
// Granting to an unknown principal refuses.
|
|
assert!(matches!(
|
|
auth.grant(&pid_n(9), Capability::BlobWrite, Scope::All, NOW).unwrap_err(),
|
|
ServerError::NotFound { what: "principal" }
|
|
));
|
|
}
|