1use proc_macro::TokenStream;
43use proc_macro2::Span;
44use quote::quote;
45use syn::{DeriveInput, LitStr, parse_macro_input};
46
47const KNOWN_PERMISSIONS: &[&str] = &[
52 "read",
53 "write",
54 "delete",
55 "execute",
56 "delegate",
57 "read_sensitive",
58 "write_sensitive",
59 "declassify",
60 "ai:infer",
61 "ai:train",
62 "ai:exfiltrate",
63];
64
65fn check_permission(name: &str, span: Span) -> Result<(), syn::Error> {
66 if KNOWN_PERMISSIONS.contains(&name) {
67 Ok(())
68 } else {
69 Err(syn::Error::new(
70 span,
71 format!(
72 "unknown permission '{name}' (expected one of: {})",
73 KNOWN_PERMISSIONS.join(", ")
74 ),
75 ))
76 }
77}
78
79#[proc_macro_derive(TypesecRole, attributes(role))]
83pub fn derive_typesec_role(input: TokenStream) -> TokenStream {
84 let input = parse_macro_input!(input as DeriveInput);
85 match derive_typesec_role_impl(input) {
86 Ok(ts) => ts.into(),
87 Err(e) => e.to_compile_error().into(),
88 }
89}
90
91fn derive_typesec_role_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
92 let struct_name = &input.ident;
93 let struct_name_str = struct_name.to_string().to_lowercase();
94
95 let role_attr = input
97 .attrs
98 .iter()
99 .find(|a| a.path().is_ident("role"))
100 .ok_or_else(|| {
101 syn::Error::new(
102 Span::call_site(),
103 "TypesecRole requires a #[role(permissions = \"...\", resources = \"...\")] attribute",
104 )
105 })?;
106
107 let mut permissions: Vec<String> = Vec::new();
109 let mut resources: Vec<String> = Vec::new();
110
111 role_attr.parse_nested_meta(|meta| {
112 if meta.path.is_ident("permissions") {
113 let value: LitStr = meta.value()?.parse()?;
114 permissions = value
115 .value()
116 .split(',')
117 .map(|s| s.trim().to_owned())
118 .filter(|s| !s.is_empty())
119 .collect();
120 for permission in &permissions {
121 check_permission(permission, value.span())?;
122 }
123 Ok(())
124 } else if meta.path.is_ident("resources") {
125 let value: LitStr = meta.value()?.parse()?;
126 resources = value
127 .value()
128 .split(',')
129 .map(|s| s.trim().to_owned())
130 .filter(|s| !s.is_empty())
131 .collect();
132 Ok(())
133 } else {
134 Err(meta.error("unknown role attribute key (expected 'permissions' or 'resources')"))
135 }
136 })?;
137
138 let perm_lits: Vec<LitStr> = permissions
139 .iter()
140 .map(|p| LitStr::new(p, Span::call_site()))
141 .collect();
142
143 let resource_lits: Vec<LitStr> = resources
144 .iter()
145 .map(|r| LitStr::new(r, Span::call_site()))
146 .collect();
147
148 let name_lit = LitStr::new(&struct_name_str, Span::call_site());
149
150 Ok(quote! {
151 impl typesec_core::role::Role for #struct_name {
152 fn name() -> &'static str {
153 #name_lit
154 }
155 fn permission_names() -> &'static [&'static str] {
156 &[#(#perm_lits),*]
157 }
158 fn resource_patterns() -> &'static [&'static str] {
159 &[#(#resource_lits),*]
160 }
161 }
162 })
163}
164
165#[proc_macro]
180pub fn policy(input: TokenStream) -> TokenStream {
181 match policy_impl(input.into()) {
182 Ok(ts) => ts.into(),
183 Err(e) => e.to_compile_error().into(),
184 }
185}
186
187fn policy_impl(input: proc_macro2::TokenStream) -> Result<proc_macro2::TokenStream, syn::Error> {
188 use syn::{
189 Ident, Token, braced,
190 parse::{Parse, ParseStream},
191 punctuated::Punctuated,
192 };
193
194 struct PolicyParser(Vec<(Ident, Vec<Ident>, Vec<LitStr>)>);
196
197 impl Parse for PolicyParser {
198 fn parse(input: ParseStream) -> syn::Result<Self> {
199 let mut roles = Vec::new();
200
201 while !input.is_empty() {
202 let kw: Ident = input.parse()?;
204 if kw != "role" {
205 return Err(syn::Error::new(kw.span(), "expected `role`"));
206 }
207
208 let name: Ident = input.parse()?;
210
211 let content;
213 braced!(content in input);
214
215 let can_kw: Ident = content.parse()?;
217 if can_kw != "can" {
218 return Err(syn::Error::new(can_kw.span(), "expected `can`"));
219 }
220
221 let perm_content;
223 syn::bracketed!(perm_content in content);
224 let perms: Punctuated<Ident, Token![,]> =
225 perm_content.parse_terminated(Ident::parse, Token![,])?;
226
227 let on_kw: Ident = content.parse()?;
229 if on_kw != "on" {
230 return Err(syn::Error::new(on_kw.span(), "expected `on`"));
231 }
232
233 let res_content;
235 syn::bracketed!(res_content in content);
236 let resources: Punctuated<LitStr, Token![,]> =
237 res_content.parse_terminated(Parse::parse, Token![,])?;
238
239 let _ = content.parse::<Token![;]>();
241
242 roles.push((
243 name,
244 perms.into_iter().collect(),
245 resources.into_iter().collect(),
246 ));
247 }
248
249 Ok(PolicyParser(roles))
250 }
251 }
252
253 let parsed: PolicyParser = syn::parse2(input)?;
254 let mut output = proc_macro2::TokenStream::new();
255
256 for (name, perms, resources) in parsed.0 {
257 let name_str = name.to_string().to_lowercase();
258 for perm in &perms {
259 check_permission(&perm.to_string(), perm.span())?;
260 }
261 let perm_strs: Vec<String> = perms.iter().map(|p| p.to_string()).collect();
262 let perm_lits: Vec<LitStr> = perm_strs
263 .iter()
264 .map(|s| LitStr::new(s, Span::call_site()))
265 .collect();
266
267 let name_lit = LitStr::new(&name_str, Span::call_site());
268
269 output.extend(quote! {
270 #[derive(Debug, Clone, Copy)]
271 pub struct #name;
272
273 impl typesec_core::role::Role for #name {
274 fn name() -> &'static str { #name_lit }
275 fn permission_names() -> &'static [&'static str] { &[#(#perm_lits),*] }
276 fn resource_patterns() -> &'static [&'static str] { &[#(#resources),*] }
277 }
278 });
279 }
280
281 Ok(output)
282}