Skip to main content

netget/client/saml/
actions.rs

1//! SAML client protocol actions implementation
2
3use crate::llm::actions::{
4    client_trait::{Client, ClientActionResult},
5    protocol_trait::Protocol,
6    ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::log_template::LogTemplate;
9use crate::protocol::EventType;
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::LazyLock;
14
15/// SAML client connected event
16pub static SAML_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17    EventType::new(
18        "saml_connected",
19        "SAML client initialized and ready to authenticate",
20        json!({
21            "type": "parse_assertion",
22            "response_xml": "<samlp:Response...>"
23        }),
24    )
25    .with_parameters(vec![Parameter {
26        name: "idp_url".to_string(),
27        type_hint: "string".to_string(),
28        description: "Identity Provider URL".to_string(),
29        required: true,
30    }])
31});
32
33/// SAML authentication response received event
34pub static SAML_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35    EventType::new(
36        "saml_response_received",
37        "SAML authentication response received from IdP",
38        json!({
39            "type": "parse_assertion",
40            "response_xml": "<samlp:Response...>"
41        }),
42    )
43    .with_parameters(vec![
44        Parameter {
45            name: "success".to_string(),
46            type_hint: "boolean".to_string(),
47            description: "Whether authentication was successful".to_string(),
48            required: true,
49        },
50        Parameter {
51            name: "status_code".to_string(),
52            type_hint: "string".to_string(),
53            description: "SAML status code".to_string(),
54            required: true,
55        },
56        Parameter {
57            name: "assertion".to_string(),
58            type_hint: "object".to_string(),
59            description: "SAML assertion data if successful".to_string(),
60            required: false,
61        },
62        Parameter {
63            name: "attributes".to_string(),
64            type_hint: "object".to_string(),
65            description: "User attributes from IdP".to_string(),
66            required: false,
67        },
68    ])
69});
70
71/// SAML client protocol action handler
72pub struct SamlClientProtocol;
73
74impl SamlClientProtocol {
75    pub fn new() -> Self {
76        Self
77    }
78}
79
80// Implement Protocol trait (common functionality)
81impl Protocol for SamlClientProtocol {
82    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
83        vec![
84            ParameterDefinition {
85                name: "entity_id".to_string(),
86                description: "Service Provider entity ID".to_string(),
87                type_hint: "string".to_string(),
88                required: false,
89                example: json!("https://example.com/saml/sp"),
90                default: None,
91            },
92            ParameterDefinition {
93                name: "acs_url".to_string(),
94                description: "Assertion Consumer Service URL".to_string(),
95                type_hint: "string".to_string(),
96                required: false,
97                example: json!("https://example.com/saml/acs"),
98                default: None,
99            },
100            ParameterDefinition {
101                name: "binding".to_string(),
102                description: "SAML binding type (redirect or post)".to_string(),
103                type_hint: "string".to_string(),
104                required: false,
105                example: json!("redirect"),
106                default: None,
107            },
108        ]
109    }
110    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
111        vec![
112            ActionDefinition {
113                name: "initiate_sso".to_string(),
114                description: "Initiate SAML Single Sign-On with IdP".to_string(),
115                parameters: vec![
116                    Parameter {
117                        name: "relay_state".to_string(),
118                        type_hint: "string".to_string(),
119                        description: "Optional relay state to preserve across authentication"
120                            .to_string(),
121                        required: false,
122                    },
123                    Parameter {
124                        name: "force_authn".to_string(),
125                        type_hint: "boolean".to_string(),
126                        description: "Force re-authentication at IdP".to_string(),
127                        required: false,
128                    },
129                ],
130                example: json!({
131                    "type": "initiate_sso",
132                    "relay_state": "/protected/resource",
133                    "force_authn": false
134                }),
135                log_template: None,
136            },
137            ActionDefinition {
138                name: "validate_assertion".to_string(),
139                description: "Validate a SAML assertion received from IdP".to_string(),
140                parameters: vec![Parameter {
141                    name: "saml_response".to_string(),
142                    type_hint: "string".to_string(),
143                    description: "Base64-encoded SAML response".to_string(),
144                    required: true,
145                }],
146                example: json!({
147                    "type": "validate_assertion",
148                    "saml_response": "PHNhbWxwOlJlc3BvbnNlLi4uPg=="
149                }),
150                log_template: None,
151            },
152            ActionDefinition {
153                name: "disconnect".to_string(),
154                description: "Disconnect from SAML IdP".to_string(),
155                parameters: vec![],
156                example: json!({
157                    "type": "disconnect"
158                }),
159                log_template: None,
160            },
161        ]
162    }
163    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
164        vec![ActionDefinition {
165            name: "parse_assertion".to_string(),
166            description: "Parse SAML assertion from response".to_string(),
167            parameters: vec![Parameter {
168                name: "response_xml".to_string(),
169                type_hint: "string".to_string(),
170                description: "SAML response XML".to_string(),
171                required: true,
172            }],
173            example: json!({
174                "type": "parse_assertion",
175                "response_xml": "<samlp:Response...>"
176            }),
177            log_template: Some(
178                LogTemplate::new()
179                    .with_info("-> SAML parse assertion")
180                    .with_debug("SAML parse_assertion"),
181            ),
182        }]
183    }
184    fn protocol_name(&self) -> &'static str {
185        "SAML"
186    }
187    fn get_event_types(&self) -> Vec<EventType> {
188        vec![
189            EventType::new(
190                "saml_connected",
191                "Triggered when SAML client is initialized",
192                json!({"type": "placeholder", "event_id": "saml_connected"}),
193            ),
194            EventType::new(
195                "saml_response_received",
196                "Triggered when SAML client receives an authentication response",
197                json!({"type": "placeholder", "event_id": "saml_response_received"}),
198            ),
199        ]
200    }
201    fn stack_name(&self) -> &'static str {
202        "ETH>IP>TCP>HTTP>SAML"
203    }
204    fn keywords(&self) -> Vec<&'static str> {
205        vec![
206            "saml",
207            "saml client",
208            "connect to saml",
209            "sso",
210            "single sign-on",
211        ]
212    }
213    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
214        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
215
216        ProtocolMetadataV2::builder()
217            .state(DevelopmentState::Experimental)
218            .implementation("Custom SAML SP client with XML parsing")
219            .llm_control("SSO initiation, assertion validation, attribute extraction")
220            .e2e_testing("SAML test IdP or simplesamlphp")
221            .build()
222    }
223    fn description(&self) -> &'static str {
224        "SAML Service Provider client for federated authentication"
225    }
226    fn example_prompt(&self) -> &'static str {
227        "Connect to SAML IdP at https://idp.example.com/saml and authenticate user"
228    }
229    fn group_name(&self) -> &'static str {
230        "Authentication"
231    }
232    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
233        use crate::llm::actions::StartupExamples;
234        use serde_json::json;
235
236        StartupExamples::new(
237            // LLM mode: LLM handles SAML SSO flow
238            json!({
239                "type": "open_client",
240                "remote_addr": "https://idp.example.com/saml/sso",
241                "base_stack": "saml",
242                "instruction": "Initiate SAML SSO authentication with the Identity Provider",
243                "startup_params": {
244                    "entity_id": "https://myapp.example.com/saml/sp",
245                    "acs_url": "https://myapp.example.com/saml/acs"
246                }
247            }),
248            // Script mode: Code-based SAML handling
249            json!({
250                "type": "open_client",
251                "remote_addr": "https://idp.example.com/saml/sso",
252                "base_stack": "saml",
253                "startup_params": {
254                    "entity_id": "https://myapp.example.com/saml/sp"
255                },
256                "event_handlers": [{
257                    "event_pattern": "saml_connected",
258                    "handler": {
259                        "type": "script",
260                        "language": "python",
261                        "code": "<saml_client_handler>"
262                    }
263                }]
264            }),
265            // Static mode: Fixed SAML action
266            json!({
267                "type": "open_client",
268                "remote_addr": "https://idp.example.com/saml/sso",
269                "base_stack": "saml",
270                "startup_params": {
271                    "entity_id": "https://myapp.example.com/saml/sp"
272                },
273                "event_handlers": [{
274                    "event_pattern": "saml_connected",
275                    "handler": {
276                        "type": "static",
277                        "actions": [{
278                            "type": "initiate_sso",
279                            "relay_state": "/dashboard",
280                            "force_authn": false
281                        }]
282                    }
283                }]
284            }),
285        )
286    }
287}
288
289// Implement Client trait (client-specific functionality)
290impl Client for SamlClientProtocol {
291    fn connect(
292        &self,
293        ctx: crate::protocol::ConnectContext,
294    ) -> std::pin::Pin<
295        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
296    > {
297        Box::pin(async move {
298            use crate::client::saml::SamlClient;
299            SamlClient::connect_with_llm_actions(
300                ctx.remote_addr,
301                ctx.llm_client,
302                ctx.state,
303                ctx.status_tx,
304                ctx.client_id,
305                ctx.startup_params,
306            )
307            .await
308        })
309    }
310    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
311        let action_type = action
312            .get("type")
313            .and_then(|v| v.as_str())
314            .context("Missing 'type' field in action")?;
315
316        match action_type {
317            "initiate_sso" => {
318                let relay_state = action
319                    .get("relay_state")
320                    .and_then(|v| v.as_str())
321                    .map(|s| s.to_string());
322
323                let force_authn = action
324                    .get("force_authn")
325                    .and_then(|v| v.as_bool())
326                    .unwrap_or(false);
327
328                Ok(ClientActionResult::Custom {
329                    name: "saml_initiate_sso".to_string(),
330                    data: json!({
331                        "relay_state": relay_state,
332                        "force_authn": force_authn,
333                    }),
334                })
335            }
336            "validate_assertion" => {
337                let saml_response = action
338                    .get("saml_response")
339                    .and_then(|v| v.as_str())
340                    .context("Missing 'saml_response' field")?
341                    .to_string();
342
343                Ok(ClientActionResult::Custom {
344                    name: "saml_validate_assertion".to_string(),
345                    data: json!({
346                        "saml_response": saml_response,
347                    }),
348                })
349            }
350            "parse_assertion" => {
351                let response_xml = action
352                    .get("response_xml")
353                    .and_then(|v| v.as_str())
354                    .context("Missing 'response_xml' field")?
355                    .to_string();
356
357                Ok(ClientActionResult::Custom {
358                    name: "saml_parse_assertion".to_string(),
359                    data: json!({
360                        "response_xml": response_xml,
361                    }),
362                })
363            }
364            "disconnect" => Ok(ClientActionResult::Disconnect),
365            _ => Err(anyhow::anyhow!(
366                "Unknown SAML client action: {}",
367                action_type
368            )),
369        }
370    }
371}