1use proc_macro::TokenStream;
2use quote::quote;
3use serde_json;
4use syn::{Data, DeriveInput, Fields, Lit, Meta, parse_macro_input};
5
6fn extract_entity_type_from_attrs(attrs: &[syn::Attribute]) -> Option<String> {
9 for attr in attrs {
10 if attr.path().is_ident("entity_type") {
11 if let Meta::NameValue(meta) = &attr.meta {
12 if let syn::Expr::Lit(expr_lit) = &meta.value {
13 if let Lit::Str(lit_str) = &expr_lit.lit {
14 return Some(lit_str.value());
15 }
16 }
17 }
18 }
19 }
20 None
21}
22
23#[proc_macro_derive(HodeiEntity, attributes(hodei, entity_type))]
27pub fn hodei_entity_derive(input: TokenStream) -> TokenStream {
28 let ast = parse_macro_input!(input as DeriveInput);
29 let struct_name = &ast.ident;
30
31 let mut entity_type_str: Option<String> = None;
32 for attr in &ast.attrs {
33 if attr.path().is_ident("hodei") {
34 let _ = attr.parse_nested_meta(|meta| {
35 if meta.path.is_ident("entity_type") {
36 if let Ok(Lit::Str(s)) = meta.value()?.parse() {
37 entity_type_str = Some(s.value());
38 }
39 }
40 Ok(())
41 });
42 }
43 }
44 let entity_type_str =
45 entity_type_str.expect("#[derive(HodeiEntity)] requiere #[hodei(entity_type = \"...\")]");
46
47 let mut attributes = serde_json::Map::new();
48 let mut attr_map_assignments: Vec<proc_macro2::TokenStream> = Vec::new();
49
50 if let Data::Struct(data_struct) = &ast.data {
51 if let Fields::Named(fields) = &data_struct.fields {
52 for field in &fields.named {
53 let field_ident = field.ident.as_ref().unwrap();
54 let field_name = field_ident.to_string();
55 if field_name == "id" {
56 continue;
57 }
58
59 let type_ident = match &field.ty {
60 syn::Type::Path(tp) => tp
61 .path
62 .segments
63 .last()
64 .map(|s| s.ident.to_string())
65 .unwrap_or_else(|| "Complex".to_string()),
66 _ => "Complex".to_string(),
67 };
68
69 let schema_type = if type_ident == "Hrn" {
71 let entity_type = extract_entity_type_from_attrs(&field.attrs)
74 .unwrap_or_else(|| {
75 panic!(
76 "Campo '{}' de tipo Hrn requiere el atributo #[entity_type = \"Namespace::EntityType\"]\n\
77 Ejemplo: #[entity_type = \"MyApp::User\"]",
78 field_name
79 )
80 });
81 serde_json::json!({
82 "type": "Entity",
83 "name": entity_type,
84 "required": true
85 })
86 } else {
87 let cedar_type = match type_ident.as_str() {
88 "String" => "String",
89 "i64" | "u64" | "i32" | "u32" | "usize" => "Long",
90 "bool" => "Boolean",
91 _ => "String",
92 };
93 serde_json::json!({ "type": cedar_type, "required": true })
94 };
95
96 attributes.insert(field_name.clone(), schema_type);
97
98 let value_expr = if type_ident == "Hrn" {
99 let entity_type = extract_entity_type_from_attrs(&field.attrs)
102 .unwrap_or_else(|| {
103 panic!(
104 "Campo '{}' de tipo Hrn requiere el atributo #[entity_type = \"Namespace::EntityType\"]\n\
105 Ejemplo: #[entity_type = \"MyApp::User\"]",
106 field_name
107 )
108 });
109 let entity_type_lit =
110 syn::LitStr::new(&entity_type, proc_macro2::Span::call_site());
111 quote! {
112 {
113 let euid = cedar_policy::EntityUid::from_type_name_and_id(
114 #entity_type_lit.parse().unwrap(),
115 self.#field_ident.to_string().parse().unwrap(),
116 );
117 cedar_policy::RestrictedExpression::new_entity_uid(euid)
118 }
119 }
120 } else if type_ident == "String" {
121 quote! { cedar_policy::RestrictedExpression::new_string(self.#field_ident.clone()) }
122 } else if type_ident == "bool" {
123 quote! { cedar_policy::RestrictedExpression::new_bool(self.#field_ident) }
124 } else {
125 quote! { cedar_policy::RestrictedExpression::new_long(self.#field_ident as i64) }
126 };
127
128 attr_map_assignments.push(quote! {
129 attrs.insert(#field_name.into(), #value_expr);
130 });
131 }
132 }
133 }
134
135 attributes.insert(
136 "tenant_id".to_string(),
137 serde_json::json!({ "type": "String" }),
138 );
139 attributes.insert(
140 "service".to_string(),
141 serde_json::json!({ "type": "String" }),
142 );
143
144 let schema_fragment_json = serde_json::json!({
146 "memberOfTypes": [],
147 "shape": {
148 "type": "Record",
149 "attributes": attributes
150 }
151 });
152 let schema_fragment_str = serde_json::to_string(&schema_fragment_json).unwrap();
153
154 let expanded = quote! {
155 impl hodei_authz::RuntimeHodeiEntityMapper for #struct_name {
156 fn hodei_type_name(&self) -> &'static str { #entity_type_str }
157 fn hodei_id(&self) -> String { self.id.resource_id.clone() }
158 fn hodei_hrn(&self) -> &hodei_hrn::api::Hrn { &self.id }
159 fn to_cedar_entity(&self) -> cedar_policy::Entity {
160 let euid = self.to_cedar_euid();
161 let mut attrs = std::collections::HashMap::new();
162 #(#attr_map_assignments)*
163 attrs.insert("tenant_id".into(), cedar_policy::RestrictedExpression::new_string(self.id.tenant_id.clone()));
164 attrs.insert("service".into(), cedar_policy::RestrictedExpression::new_string(self.id.service.clone()));
165 cedar_policy::Entity::new(euid, attrs, std::collections::HashSet::new()).unwrap()
166 }
167 }
168 #[cfg(feature = "schema-discovery")]
169 hodei_authz::inventory::submit! {
170 hodei_authz::EntitySchemaFragment { entity_type: #entity_type_str, fragment_json: #schema_fragment_str, }
171 }
172 };
173 expanded.into()
174}
175
176#[proc_macro_derive(HodeiAction, attributes(hodei))]
177pub fn hodei_action_derive(input: TokenStream) -> TokenStream {
178 let ast = parse_macro_input!(input as DeriveInput);
179 let enum_name = &ast.ident;
180
181 let mut namespace: Option<String> = None;
182 for attr in &ast.attrs {
183 if attr.path().is_ident("hodei") {
184 let _ = attr.parse_nested_meta(|meta| {
185 if meta.path.is_ident("namespace") {
186 if let Ok(Lit::Str(s)) = meta.value()?.parse() {
187 namespace = Some(s.value());
188 }
189 }
190 Ok(())
191 });
192 }
193 }
194 let _namespace =
195 namespace.expect("#[derive(HodeiAction)] requiere #[hodei(namespace = \"...\")]");
196
197 let data_enum = match &ast.data {
198 Data::Enum(de) => de,
199 _ => panic!("HodeiAction solo se puede derivar en enums"),
200 };
201
202 let mut inventory_submissions: Vec<proc_macro2::TokenStream> = Vec::new();
203 let mut euid_match_arms: Vec<proc_macro2::TokenStream> = Vec::new();
204 let mut creates_resource_match_arms: Vec<proc_macro2::TokenStream> = Vec::new();
205 let mut virtual_entity_match_arms: Vec<proc_macro2::TokenStream> = Vec::new();
206
207 for variant in &data_enum.variants {
208 let variant_name = &variant.ident;
209 let action_name_str = variant_name.to_string();
210 let mut principal_types: Vec<String> = Vec::new();
211 let mut resource_types: Vec<String> = Vec::new();
212 let mut is_create_action = false;
213
214 for attr in &variant.attrs {
215 if attr.path().is_ident("hodei") {
216 let _ = attr.parse_nested_meta(|meta| {
217 if meta.path.is_ident("principal") {
218 if let Ok(Lit::Str(s)) = meta.value()?.parse() {
219 principal_types.push(s.value());
220 }
221 } else if meta.path.is_ident("resource") {
222 if let Ok(Lit::Str(s)) = meta.value()?.parse() {
223 resource_types.push(s.value());
224 }
225 } else if meta.path.is_ident("creates_resource") {
226 is_create_action = true;
227 }
228 Ok(())
229 });
230 }
231 }
232
233 let resource_type = resource_types
236 .first()
237 .expect("Action must have at least one resource type");
238 let full_action_name = format!("{}::{}", resource_type, action_name_str);
239 let action_euid_str = format!("Action::\"{}\"", full_action_name);
240
241 let action_schema_json = serde_json::json!({
242 "appliesTo": {
243 "principalTypes": principal_types,
244 "resourceTypes": resource_types
245 }
246 });
247 let action_schema_str = serde_json::to_string(&action_schema_json).unwrap();
248
249 inventory_submissions.push(quote! {
250 #[cfg(feature = "schema-discovery")]
251 hodei_authz::inventory::submit! {
252 hodei_authz::ActionSchemaFragment {
253 name: #full_action_name,
254 fragment_json: #action_schema_str
255 }
256 }
257 });
258
259 let fields_pattern = match &variant.fields {
260 Fields::Named(_) => quote! { {..} },
261 Fields::Unnamed(_) => quote! { (..) },
262 Fields::Unit => quote! {},
263 };
264
265 euid_match_arms.push(
266 quote! { Self::#variant_name #fields_pattern => #action_euid_str.parse().unwrap() },
267 );
268
269 if is_create_action {
270 creates_resource_match_arms
271 .push(quote! { Self::#variant_name #fields_pattern => true });
272 match &variant.fields {
273 Fields::Unnamed(f) if f.unnamed.len() == 1 => {
274 virtual_entity_match_arms.push(quote! {
275 Self::#variant_name(payload) => {
276 let ctx = context.downcast_ref::<crate::RequestContext>().expect("Invalid context type");
277 Some(payload.to_virtual_entity(ctx))
278 }
279 });
280 }
281 _ => {
282 virtual_entity_match_arms
283 .push(quote! { Self::#variant_name #fields_pattern => None });
284 }
285 }
286 } else {
287 creates_resource_match_arms
288 .push(quote! { Self::#variant_name #fields_pattern => false });
289 }
290 }
291
292 virtual_entity_match_arms.push(quote! { _ => None });
293
294 let expanded = quote! {
295 #(#inventory_submissions)*
296 impl hodei_authz::RuntimeHodeiActionMapper for #enum_name {
297 fn to_cedar_action_euid(&self) -> cedar_policy::EntityUid {
298 match self { #(#euid_match_arms,)* }
299 }
300 fn creates_resource_from_payload(&self) -> bool {
301 match self { #(#creates_resource_match_arms,)* }
302 }
303 fn get_payload_as_virtual_entity(&self, context: &dyn std::any::Any) -> Option<cedar_policy::Entity> {
304 match self { #(#virtual_entity_match_arms,)* }
305 }
306 }
307 };
308 expanded.into()
309}