Files
core/test/rust/compiler/authorization/mod.rs
T
2026-11-03 03:32:07 +00:00

172 lines
5.7 KiB
Rust

use super::{APP, ERROR};
use dever::{
compiler::{check, model::Program},
syntax::{diagnostic::Diagnostic, source::Source},
};
const API_ERROR: &str = "public type Error { error Invalid\nerror Unauthorized\nerror Forbidden\nerror NotFound\nerror Conflict\nerror TooManyRequests }";
fn checked(app: &str, api: &str) -> Result<Program, Diagnostic> {
let app = format!("{APP}\n{app}");
let api = format!("{api}\npublic get health = app.health");
let inputs = [
("dever.database", "database.dever", ERROR),
("dever.api", "api.dever", API_ERROR),
(
"dever.auth",
"values.dever",
include_str!("../../../../lib/dever/auth/values.dever"),
),
(
"dever.auth",
"capability.dever",
include_str!("../../../../lib/dever/auth/capability.dever"),
),
(
"demo.account.model",
"model.dever",
"global type Account { subject: Text\nsession: Text }",
),
("demo.account.app", "app.dever", app.as_str()),
("demo.account.api", "api.dever", api.as_str()),
];
let sources = inputs
.iter()
.enumerate()
.map(|(index, (_, path, text))| Source::new(index, *path, (*text).into()))
.collect::<Vec<_>>();
let modules = sources
.iter()
.map(dever::syntax::parse)
.collect::<Result<Vec<_>, _>>()?;
check::check(
modules,
&inputs
.iter()
.map(|(name, _, _)| (*name).into())
.collect::<Vec<_>>(),
)
}
#[test]
fn capability_operations_keep_original_types_and_write_effects() {
let program = checked("", "").unwrap();
for function in program.functions.iter().filter(|function| {
matches!(
function.signature.name.as_str(),
"register_capability" | "remove_capability"
)
}) {
let effects = function.signature.effects;
assert!(effects.identity && effects.authorization_write && effects.database_write);
}
let error = checked(
"register(id: Text) (key: Text) { key = dever.auth.registerCapability(id) }",
"post register = app.register",
)
.unwrap_err();
assert_eq!(error.code, "C005");
let error = checked(
"register(id: Uuid) (value: Bool) { value = dever.auth.registerCapability(id) }",
"post register = app.register",
)
.unwrap_err();
assert_eq!(error.code, "C005");
}
#[test]
fn capability_writes_require_protected_http_and_keep_transaction_rules() {
for api in [
"public post register = app.register",
"authenticated post register = app.register",
"get register = app.register",
"cmd register = app.register",
"long post register = app.register",
] {
let error = checked(
"register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
api,
)
.unwrap_err();
assert_eq!(error.code, "C014", "{api}: {error:?}");
}
let program = checked(
"transaction register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
"long post register = app.register",
)
.unwrap();
assert_tenant_requirement(&program, true);
}
#[test]
fn protected_role_query_is_authenticated_read_only_and_not_a_tenant_selector() {
let app =
"protected(user_id: Int) (value: Bool) { value = dever.auth.hasProtectedRoles(user_id) }";
for api in [
"get protected = app.protected",
"authenticated get protected = app.protected",
] {
let program = checked(app, api).unwrap();
assert_tenant_requirement(&program, true);
}
for api in [
"public get protected = app.protected",
"cmd protected = app.protected",
] {
assert_eq!(checked(app, api).unwrap_err().code, "C014");
}
assert_eq!(checked(
"protected(user_id: Int) (value: Bool) { value = dever.auth.hasProtectedRoles(user_id, 42) }",
"get protected = app.protected",
).unwrap_err().code, "C005");
}
fn assert_tenant_requirement(program: &Program, required: bool) {
let entry = program
.http
.iter()
.find(|entry| entry.access != dever::compiler::model::auth::Access::Public)
.unwrap();
assert_eq!(program.tenant_access(entry.function).required, required);
}
#[test]
fn protected_write_entries_cannot_replace_the_required_tenant() {
for (app, api) in [
(
"register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
"post register = app.register",
),
(
"remove(id: Uuid) () { dever.auth.removeCapability(id) }",
"delete remove = app.remove",
),
] {
assert_tenant_requirement(&checked(app, api).unwrap(), true);
}
}
#[test]
fn tenant_requirement_follows_reachable_calls_and_static_handlers() {
for (app, required) in [
(
"register(id: Uuid) (key: Text) { key = helper(id) }\nhelper(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
true,
),
(
"register(id: Uuid) (key: Text) { key = apply(dever.auth.registerCapability, id) }\napply(action: handler(id: Uuid) (key: Text), id: Uuid) (key: Text) { key = action(id) }",
true,
),
] {
assert_tenant_requirement(
&checked(app, "post register = app.register").unwrap(),
required,
);
}
let error = checked(
"register(id: Uuid) (key: Text) { key = \"unused\" }\nunused(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
"post register = app.register",
).unwrap_err();
assert_eq!(error.code, "C016");
}