use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{DeriveInput, LitStr, parse_macro_input};
const KNOWN_PERMISSIONS: &[&str] = &[
"read",
"write",
"delete",
"execute",
"delegate",
"read_sensitive",
"write_sensitive",
"declassify",
"ai:infer",
"ai:train",
"ai:exfiltrate",
];
fn check_permission(name: &str, span: Span) -> Result<(), syn::Error> {
if KNOWN_PERMISSIONS.contains(&name) {
Ok(())
} else {
Err(syn::Error::new(
span,
format!(
"unknown permission '{name}' (expected one of: {})",
KNOWN_PERMISSIONS.join(", ")
),
))
}
}
#[proc_macro_derive(TypesecRole, attributes(role))]
pub fn derive_typesec_role(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match derive_typesec_role_impl(input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn derive_typesec_role_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
let struct_name = &input.ident;
let struct_name_str = struct_name.to_string().to_lowercase();
let role_attr = input
.attrs
.iter()
.find(|a| a.path().is_ident("role"))
.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"TypesecRole requires a #[role(permissions = \"...\", resources = \"...\")] attribute",
)
})?;
let mut permissions: Vec<String> = Vec::new();
let mut resources: Vec<String> = Vec::new();
role_attr.parse_nested_meta(|meta| {
if meta.path.is_ident("permissions") {
let value: LitStr = meta.value()?.parse()?;
permissions = value
.value()
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();
for permission in &permissions {
check_permission(permission, value.span())?;
}
Ok(())
} else if meta.path.is_ident("resources") {
let value: LitStr = meta.value()?.parse()?;
resources = value
.value()
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();
Ok(())
} else {
Err(meta.error("unknown role attribute key (expected 'permissions' or 'resources')"))
}
})?;
let perm_lits: Vec<LitStr> = permissions
.iter()
.map(|p| LitStr::new(p, Span::call_site()))
.collect();
let resource_lits: Vec<LitStr> = resources
.iter()
.map(|r| LitStr::new(r, Span::call_site()))
.collect();
let name_lit = LitStr::new(&struct_name_str, Span::call_site());
Ok(quote! {
impl typesec_core::role::Role for #struct_name {
fn name() -> &'static str {
#name_lit
}
fn permission_names() -> &'static [&'static str] {
&[#(#perm_lits),*]
}
fn resource_patterns() -> &'static [&'static str] {
&[#(#resource_lits),*]
}
}
})
}
#[proc_macro]
pub fn policy(input: TokenStream) -> TokenStream {
match policy_impl(input.into()) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn policy_impl(input: proc_macro2::TokenStream) -> Result<proc_macro2::TokenStream, syn::Error> {
use syn::{
Ident, Token, braced,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
struct PolicyParser(Vec<(Ident, Vec<Ident>, Vec<LitStr>)>);
impl Parse for PolicyParser {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut roles = Vec::new();
while !input.is_empty() {
let kw: Ident = input.parse()?;
if kw != "role" {
return Err(syn::Error::new(kw.span(), "expected `role`"));
}
let name: Ident = input.parse()?;
let content;
braced!(content in input);
let can_kw: Ident = content.parse()?;
if can_kw != "can" {
return Err(syn::Error::new(can_kw.span(), "expected `can`"));
}
let perm_content;
syn::bracketed!(perm_content in content);
let perms: Punctuated<Ident, Token![,]> =
perm_content.parse_terminated(Ident::parse, Token![,])?;
let on_kw: Ident = content.parse()?;
if on_kw != "on" {
return Err(syn::Error::new(on_kw.span(), "expected `on`"));
}
let res_content;
syn::bracketed!(res_content in content);
let resources: Punctuated<LitStr, Token![,]> =
res_content.parse_terminated(Parse::parse, Token![,])?;
let _ = content.parse::<Token![;]>();
roles.push((
name,
perms.into_iter().collect(),
resources.into_iter().collect(),
));
}
Ok(PolicyParser(roles))
}
}
let parsed: PolicyParser = syn::parse2(input)?;
let mut output = proc_macro2::TokenStream::new();
for (name, perms, resources) in parsed.0 {
let name_str = name.to_string().to_lowercase();
for perm in &perms {
check_permission(&perm.to_string(), perm.span())?;
}
let perm_strs: Vec<String> = perms.iter().map(|p| p.to_string()).collect();
let perm_lits: Vec<LitStr> = perm_strs
.iter()
.map(|s| LitStr::new(s, Span::call_site()))
.collect();
let name_lit = LitStr::new(&name_str, Span::call_site());
output.extend(quote! {
#[derive(Debug, Clone, Copy)]
pub struct #name;
impl typesec_core::role::Role for #name {
fn name() -> &'static str { #name_lit }
fn permission_names() -> &'static [&'static str] { &[#(#perm_lits),*] }
fn resource_patterns() -> &'static [&'static str] { &[#(#resources),*] }
}
});
}
Ok(output)
}