veracode_platform/
identity.rs

1//! Identity API functionality for managing users, teams, roles, and API credentials.
2//!
3//! This module provides functionality to interact with the Veracode Identity API,
4//! allowing you to manage users, teams, roles, and API credentials programmatically.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
11use crate::{VeracodeClient, VeracodeError};
12
13/// Represents a Veracode user account
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct User {
16    /// Unique user ID
17    pub user_id: String,
18    /// Legacy user ID
19    pub user_legacy_id: Option<u32>,
20    /// Username for the account
21    pub user_name: String,
22    /// User's email address
23    pub email_address: String,
24    /// User's first name
25    pub first_name: String,
26    /// User's last name
27    pub last_name: String,
28    /// User account type (optional in basic response)
29    pub user_type: Option<UserType>,
30    /// Whether the user account is active (optional in basic response)
31    pub active: Option<bool>,
32    /// Whether login is enabled
33    pub login_enabled: Option<bool>,
34    /// Whether this is a SAML user
35    pub saml_user: Option<bool>,
36    /// List of roles assigned to the user (only in detailed response)
37    pub roles: Option<Vec<Role>>,
38    /// List of teams the user belongs to (only in detailed response)
39    pub teams: Option<Vec<Team>>,
40    /// Login status information (only in detailed response)
41    pub login_status: Option<LoginStatus>,
42    /// Date when the user was created (only in detailed response)
43    pub created_date: Option<DateTime<Utc>>,
44    /// Date when the user was last modified (only in detailed response)
45    pub modified_date: Option<DateTime<Utc>>,
46    /// API credentials information (for API service accounts, only in detailed response)
47    pub api_credentials: Option<Vec<ApiCredential>>,
48    /// Links for navigation
49    #[serde(rename = "_links")]
50    pub links: Option<serde_json::Value>,
51}
52
53/// User account types
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "UPPERCASE")]
56pub enum UserType {
57    /// Regular human user account
58    Human,
59    /// API service account
60    #[serde(rename = "API")]
61    ApiService,
62    /// SAML user account
63    Saml,
64    /// VOSP user account
65    #[serde(rename = "VOSP")]
66    Vosp,
67}
68
69/// Represents a user role
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Role {
72    /// Unique role ID
73    pub role_id: String,
74    /// Legacy role ID (optional)
75    pub role_legacy_id: Option<i32>,
76    /// Role name
77    pub role_name: String,
78    /// Role description
79    pub role_description: Option<String>,
80    /// Whether this is an internal Veracode role
81    pub is_internal: Option<bool>,
82    /// Whether the role requires a token
83    pub requires_token: Option<bool>,
84    /// Whether the role is assigned to proxy users
85    pub assigned_to_proxy_users: Option<bool>,
86    /// Whether team admins can manage this role
87    pub team_admin_manageable: Option<bool>,
88    /// Whether the role is JIT assignable
89    pub jit_assignable: Option<bool>,
90    /// Whether the role is JIT assignable by default
91    pub jit_assignable_default: Option<bool>,
92    /// Whether this is an API role
93    pub is_api: Option<bool>,
94    /// Whether this is a scan type role
95    pub is_scan_type: Option<bool>,
96    /// Whether the role ignores team restrictions
97    pub ignore_team_restrictions: Option<bool>,
98    /// Whether the role is HMAC only
99    pub is_hmac_only: Option<bool>,
100    /// Organization ID (for custom roles)
101    pub org_id: Option<String>,
102    /// Child roles (nested roles)
103    pub child_roles: Option<Vec<serde_json::Value>>,
104    /// Whether the role is disabled
105    pub role_disabled: Option<bool>,
106    /// List of permissions granted by this role
107    pub permissions: Option<Vec<Permission>>,
108    /// Links for navigation
109    #[serde(rename = "_links")]
110    pub links: Option<serde_json::Value>,
111}
112
113/// Represents a permission within a role
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct Permission {
116    /// Permission ID (returned from API)
117    pub permission_id: Option<String>,
118    /// Permission name
119    pub permission_name: String,
120    /// Permission description (optional)
121    pub description: Option<String>,
122    /// Whether this permission is API only
123    pub api_only: Option<bool>,
124    /// Whether this permission is UI only
125    pub ui_only: Option<bool>,
126}
127
128/// Represents a team
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Team {
131    /// Unique team ID
132    pub team_id: String,
133    /// Team name
134    pub team_name: String,
135    /// Team description
136    pub team_description: Option<String>,
137    /// List of users in the team
138    pub users: Option<Vec<User>>,
139    /// Business unit the team belongs to
140    pub business_unit: Option<BusinessUnit>,
141}
142
143/// Represents a business unit
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct BusinessUnit {
146    /// Unique business unit ID
147    pub bu_id: String,
148    /// Business unit name
149    pub bu_name: String,
150    /// Business unit description
151    pub bu_description: Option<String>,
152    /// List of teams in the business unit
153    pub teams: Option<Vec<Team>>,
154}
155
156/// Represents API credentials
157#[derive(Clone, Serialize, Deserialize)]
158pub struct ApiCredential {
159    /// Unique API credential ID
160    pub api_id: String,
161    /// API key (only shown when first created) - automatically redacted in debug output
162    pub api_key: Option<String>,
163    /// Expiration date of the credentials
164    pub expiration_ts: Option<DateTime<Utc>>,
165    /// Whether the credentials are active
166    pub active: Option<bool>,
167    /// Creation date
168    pub created_date: Option<DateTime<Utc>>,
169}
170
171///
172/// # Errors
173///
174/// Returns an error if the API request fails, the resource is not found,
175/// or authentication/authorization fails.
176/// Custom Debug implementation for `ApiCredential` that redacts sensitive information
177impl std::fmt::Debug for ApiCredential {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("ApiCredential")
180            .field("api_id", &self.api_id)
181            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
182            .field("expiration_ts", &self.expiration_ts)
183            .field("active", &self.active)
184            .field("created_date", &self.created_date)
185            .finish()
186    }
187}
188
189/// Login status information
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct LoginStatus {
192    /// Last login date
193    pub last_login_date: Option<DateTime<Utc>>,
194    /// Whether the user has ever logged in
195    pub never_logged_in: Option<bool>,
196    /// Number of failed login attempts
197    pub failed_login_attempts: Option<u32>,
198}
199
200/// Request structure for creating a new user
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct CreateUserRequest {
203    /// Email address (required)
204    pub email_address: String,
205    /// First name (required)
206    pub first_name: String,
207    /// Last name (required)
208    pub last_name: String,
209    /// Username (required)
210    pub user_name: Option<String>,
211    /// User type (defaults to HUMAN)
212    pub user_type: Option<UserType>,
213    /// Whether to send email invitation
214    pub send_email_invitation: Option<bool>,
215    /// List of role IDs to assign
216    pub role_ids: Option<Vec<String>>,
217    /// List of team IDs to assign (at least one required for human users)
218    pub team_ids: Option<Vec<String>>,
219    /// List of permissions to assign
220    pub permissions: Option<Vec<Permission>>,
221}
222
223/// Request structure for updating a user
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct UpdateUserRequest {
226    /// Email address (required)
227    pub email_address: String,
228    /// Username (required)
229    pub user_name: String,
230    /// First name
231    pub first_name: Option<String>,
232    /// Last name
233    pub last_name: Option<String>,
234    /// Whether the account is active
235    pub active: Option<bool>,
236    /// List of role IDs to assign (required)
237    pub role_ids: Vec<String>,
238    /// List of team IDs to assign (required)
239    pub team_ids: Vec<String>,
240}
241
242/// Request structure for creating a team
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct CreateTeamRequest {
245    /// Team name (required)
246    pub team_name: String,
247    /// Team description
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub team_description: Option<String>,
250    /// Business unit ID
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub business_unit_id: Option<String>,
253    /// List of user IDs to add to the team
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub user_ids: Option<Vec<String>>,
256}
257
258/// Request structure for updating a team
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct UpdateTeamRequest {
261    /// Team name
262    pub team_name: Option<String>,
263    /// Team description
264    pub team_description: Option<String>,
265    /// Business unit ID
266    pub business_unit_id: Option<String>,
267    /// List of user IDs to add to the team (when using incremental=true)
268    pub user_ids: Option<Vec<String>>,
269}
270
271/// Request structure for creating API credentials
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct CreateApiCredentialRequest {
274    /// User ID to create credentials for (optional, defaults to current user)
275    pub user_id: Option<String>,
276    /// Expiration date (optional)
277    pub expiration_ts: Option<DateTime<Utc>>,
278}
279
280/// Response wrapper for paginated user results
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct UsersResponse {
283    /// List of users (direct array) or embedded
284    #[serde(default, skip_serializing_if = "Vec::is_empty")]
285    pub users: Vec<User>,
286    /// Embedded users (alternative structure)
287    #[serde(rename = "_embedded")]
288    pub embedded: Option<EmbeddedUsers>,
289    /// Pagination information
290    pub page: Option<PageInfo>,
291    /// Response links
292    #[serde(rename = "_links")]
293    pub links: Option<HashMap<String, Link>>,
294}
295
296/// Embedded users in the response
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct EmbeddedUsers {
299    /// List of users
300    pub users: Vec<User>,
301}
302
303/// Response wrapper for paginated team results
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct TeamsResponse {
306    /// List of teams (direct array) or embedded
307    #[serde(default, skip_serializing_if = "Vec::is_empty")]
308    pub teams: Vec<Team>,
309    /// Embedded teams (alternative structure)
310    #[serde(rename = "_embedded")]
311    pub embedded: Option<EmbeddedTeams>,
312    /// Pagination information
313    pub page: Option<PageInfo>,
314}
315
316/// Embedded teams in the response
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct EmbeddedTeams {
319    /// List of teams
320    pub teams: Vec<Team>,
321}
322
323/// Response wrapper for paginated role results
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct RolesResponse {
326    /// List of roles (direct array) or embedded
327    #[serde(default, skip_serializing_if = "Vec::is_empty")]
328    pub roles: Vec<Role>,
329    /// Embedded roles (alternative structure)
330    #[serde(rename = "_embedded")]
331    pub embedded: Option<EmbeddedRoles>,
332    /// Pagination information
333    pub page: Option<PageInfo>,
334}
335
336/// Embedded roles in the response
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct EmbeddedRoles {
339    /// List of roles
340    pub roles: Vec<Role>,
341}
342
343/// Pagination information
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct PageInfo {
346    /// Current page number
347    pub number: Option<u32>,
348    /// Number of items per page
349    pub size: Option<u32>,
350    /// Total number of elements
351    pub total_elements: Option<u64>,
352    /// Total number of pages
353    pub total_pages: Option<u32>,
354}
355
356/// Link information for navigation
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct Link {
359    /// URL for the link
360    pub href: String,
361}
362
363/// Query parameters for user search
364#[derive(Debug, Clone, Default)]
365pub struct UserQuery {
366    /// Filter by username
367    pub user_name: Option<String>,
368    /// Filter by email address
369    pub email_address: Option<String>,
370    /// Filter by role ID
371    pub role_id: Option<String>,
372    /// Filter by user type
373    pub user_type: Option<UserType>,
374    /// Filter by login status
375    pub login_status: Option<String>,
376    /// Page number
377    pub page: Option<u32>,
378    /// Items per page
379    pub size: Option<u32>,
380}
381
382impl UserQuery {
383    /// Create a new empty user query
384    #[must_use]
385    pub fn new() -> Self {
386        Self::default()
387    }
388
389    /// Filter by username
390    pub fn with_username(mut self, username: impl Into<String>) -> Self {
391        self.user_name = Some(username.into());
392        self
393    }
394
395    /// Filter by email address
396    pub fn with_email(mut self, email: impl Into<String>) -> Self {
397        self.email_address = Some(email.into());
398        self
399    }
400
401    /// Filter by role ID
402    pub fn with_role_id(mut self, role_id: impl Into<String>) -> Self {
403        self.role_id = Some(role_id.into());
404        self
405    }
406
407    /// Filter by user type
408    #[must_use]
409    pub fn with_user_type(mut self, user_type: UserType) -> Self {
410        self.user_type = Some(user_type);
411        self
412    }
413
414    /// Set pagination
415    #[must_use]
416    pub fn with_pagination(mut self, page: u32, size: u32) -> Self {
417        self.page = Some(page);
418        self.size = Some(size);
419        self
420    }
421
422    /// Convert to query parameters
423    #[must_use]
424    pub fn to_query_params(&self) -> Vec<(String, String)> {
425        Vec::from(self) // Delegate to trait
426    }
427}
428
429// Trait implementations for memory optimization
430impl From<&UserQuery> for Vec<(String, String)> {
431    fn from(query: &UserQuery) -> Self {
432        let mut params = Vec::new();
433
434        if let Some(ref username) = query.user_name {
435            params.push(("user_name".to_string(), username.clone())); // Still clone for borrowing
436        }
437        if let Some(ref email) = query.email_address {
438            params.push(("email_address".to_string(), email.clone()));
439        }
440        if let Some(ref role_id) = query.role_id {
441            params.push(("role_id".to_string(), role_id.clone()));
442        }
443        if let Some(ref user_type) = query.user_type {
444            let type_str = match user_type {
445                UserType::Human => "HUMAN",
446                UserType::ApiService => "API",
447                UserType::Saml => "SAML",
448                UserType::Vosp => "VOSP",
449            };
450            params.push(("user_type".to_string(), type_str.to_string()));
451        }
452        if let Some(ref login_status) = query.login_status {
453            params.push(("login_status".to_string(), login_status.clone()));
454        }
455        if let Some(page) = query.page {
456            params.push(("page".to_string(), page.to_string()));
457        }
458        if let Some(size) = query.size {
459            params.push(("size".to_string(), size.to_string()));
460        }
461
462        params
463    }
464}
465
466impl From<UserQuery> for Vec<(String, String)> {
467    fn from(query: UserQuery) -> Self {
468        let mut params = Vec::new();
469
470        if let Some(username) = query.user_name {
471            params.push(("user_name".to_string(), username)); // MOVE - no clone!
472        }
473        if let Some(email) = query.email_address {
474            params.push(("email_address".to_string(), email)); // MOVE - no clone!
475        }
476        if let Some(role_id) = query.role_id {
477            params.push(("role_id".to_string(), role_id)); // MOVE - no clone!
478        }
479        if let Some(user_type) = query.user_type {
480            let type_str = match user_type {
481                UserType::Human => "HUMAN",
482                UserType::ApiService => "API",
483                UserType::Saml => "SAML",
484                UserType::Vosp => "VOSP",
485            };
486            params.push(("user_type".to_string(), type_str.to_string()));
487        }
488        if let Some(login_status) = query.login_status {
489            params.push(("login_status".to_string(), login_status)); // MOVE - no clone!
490        }
491        if let Some(page) = query.page {
492            params.push(("page".to_string(), page.to_string()));
493        }
494        if let Some(size) = query.size {
495            params.push(("size".to_string(), size.to_string()));
496        }
497
498        params
499    }
500}
501
502/// Identity-specific error types
503#[derive(Debug)]
504#[must_use = "Need to handle all error enum types."]
505pub enum IdentityError {
506    /// General API error
507    Api(VeracodeError),
508    /// User not found
509    UserNotFound,
510    /// Team not found
511    TeamNotFound,
512    /// Role not found
513    RoleNotFound,
514    /// Invalid input data
515    InvalidInput(String),
516    /// Permission denied
517    PermissionDenied(String),
518    /// User already exists
519    UserAlreadyExists(String),
520    /// Team already exists
521    TeamAlreadyExists(String),
522}
523
524impl std::fmt::Display for IdentityError {
525    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526        match self {
527            IdentityError::Api(err) => write!(f, "API error: {err}"),
528            IdentityError::UserNotFound => write!(f, "User not found"),
529            IdentityError::TeamNotFound => write!(f, "Team not found"),
530            IdentityError::RoleNotFound => write!(f, "Role not found"),
531            IdentityError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
532            IdentityError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
533            IdentityError::UserAlreadyExists(msg) => write!(f, "User already exists: {msg}"),
534            IdentityError::TeamAlreadyExists(msg) => write!(f, "Team already exists: {msg}"),
535        }
536    }
537}
538
539impl std::error::Error for IdentityError {}
540
541impl From<VeracodeError> for IdentityError {
542    fn from(err: VeracodeError) -> Self {
543        IdentityError::Api(err)
544    }
545}
546
547impl From<reqwest::Error> for IdentityError {
548    fn from(err: reqwest::Error) -> Self {
549        IdentityError::Api(VeracodeError::Http(err))
550    }
551}
552
553impl From<serde_json::Error> for IdentityError {
554    fn from(err: serde_json::Error) -> Self {
555        IdentityError::Api(VeracodeError::Serialization(err))
556    }
557}
558
559/// Identity API operations
560pub struct IdentityApi<'a> {
561    client: &'a VeracodeClient,
562}
563
564impl<'a> IdentityApi<'a> {
565    ///
566    /// # Errors
567    ///
568    /// Returns an error if the API request fails, the resource is not found,
569    /// or authentication/authorization fails.
570    /// Create a new `IdentityApi` instance
571    #[must_use]
572    pub fn new(client: &'a VeracodeClient) -> Self {
573        Self { client }
574    }
575
576    /// Sanitize error messages to prevent information disclosure
577    ///
578    /// Logs detailed error information server-side while returning
579    /// a generic, safe error message to callers.
580    ///
581    /// # Arguments
582    ///
583    /// * `raw_error` - The raw error text from the API response
584    /// * `status` - The HTTP status code
585    /// * `context` - Context about the operation for logging (e.g., "`list_users`")
586    fn sanitize_error(raw_error: &str, status: u16, context: &str) -> String {
587        // Log detailed error server-side for debugging
588        log::warn!("API error in {} - HTTP {}: {}", context, status, raw_error);
589
590        // Return sanitized message based on status code
591        match status {
592            400 => {
593                // For 400 errors, check for specific known safe error patterns
594                if raw_error.contains("already exists") {
595                    "Resource already exists".to_string()
596                } else {
597                    "Invalid request parameters".to_string()
598                }
599            }
600            401 => "Authentication required".to_string(),
601            403 => "Insufficient permissions for this operation".to_string(),
602            404 => "Resource not found".to_string(),
603            415 => "Unsupported media type".to_string(),
604            429 => "Rate limit exceeded. Please try again later".to_string(),
605            500..=599 => "Internal server error occurred".to_string(),
606            _ => format!("Request failed with status {}", status),
607        }
608    }
609
610    /// List users with optional filtering
611    ///
612    /// # Arguments
613    ///
614    /// * `query` - Optional query parameters for filtering users
615    ///
616    /// # Returns
617    ///
618    /// A `Result` containing a list of users or an error
619    ///
620    /// # Errors
621    ///
622    /// Returns an error if the API request fails, the identity resource is not found,
623    /// or authentication/authorization fails.
624    pub async fn list_users(&self, query: Option<UserQuery>) -> Result<Vec<User>, IdentityError> {
625        let endpoint = "/api/authn/v2/users";
626        let query_params = query.as_ref().map(Vec::from);
627
628        let response = self.client.get(endpoint, query_params.as_deref()).await?;
629
630        let status = response.status().as_u16();
631        match status {
632            200 => {
633                let response_text = response.text().await?;
634
635                // Validate JSON depth before parsing to prevent DoS attacks
636                validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
637                    IdentityError::Api(VeracodeError::InvalidResponse(format!(
638                        "JSON validation failed: {}",
639                        e
640                    )))
641                })?;
642
643                // Try embedded response format first
644                if let Ok(users_response) = serde_json::from_str::<UsersResponse>(&response_text) {
645                    let users = if !users_response.users.is_empty() {
646                        users_response.users
647                    } else if let Some(embedded) = users_response.embedded {
648                        embedded.users
649                    } else {
650                        Vec::new()
651                    };
652                    return Ok(users);
653                }
654
655                // Try direct array as fallback
656                if let Ok(users) = serde_json::from_str::<Vec<User>>(&response_text) {
657                    return Ok(users);
658                }
659
660                Err(IdentityError::Api(VeracodeError::InvalidResponse(
661                    "Unable to parse users response".to_string(),
662                )))
663            }
664            404 => Err(IdentityError::UserNotFound),
665            _ => {
666                let error_text = response.text().await.unwrap_or_default();
667                let sanitized = Self::sanitize_error(&error_text, status, "list_users");
668                Err(IdentityError::Api(VeracodeError::InvalidResponse(
669                    sanitized,
670                )))
671            }
672        }
673    }
674
675    /// Get a specific user by ID
676    ///
677    /// # Arguments
678    ///
679    /// * `user_id` - The ID of the user to retrieve
680    ///
681    /// # Returns
682    ///
683    /// A `Result` containing the user or an error
684    ///
685    /// # Errors
686    ///
687    /// Returns an error if the API request fails, the identity resource is not found,
688    /// or authentication/authorization fails.
689    pub async fn get_user(&self, user_id: &str) -> Result<User, IdentityError> {
690        let endpoint = format!("/api/authn/v2/users/{user_id}");
691
692        let response = self.client.get(&endpoint, None).await?;
693
694        let status = response.status().as_u16();
695        match status {
696            200 => {
697                let user: User = response.json().await?;
698                Ok(user)
699            }
700            404 => Err(IdentityError::UserNotFound),
701            _ => {
702                let error_text = response.text().await.unwrap_or_default();
703                let sanitized = Self::sanitize_error(&error_text, status, "get_user");
704                Err(IdentityError::Api(VeracodeError::InvalidResponse(
705                    sanitized,
706                )))
707            }
708        }
709    }
710
711    /// Create a new user
712    ///
713    /// # Arguments
714    ///
715    /// * `request` - The user creation request
716    ///
717    /// # Returns
718    ///
719    /// A `Result` containing the created user or an error
720    ///
721    /// # Errors
722    ///
723    /// Returns an error if the API request fails, the identity resource is not found,
724    /// or authentication/authorization fails.
725    pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, IdentityError> {
726        let endpoint = "/api/authn/v2/users";
727
728        let mut fixed_request = request.clone();
729
730        // Both email_address and user_name are required by the API
731        if fixed_request.user_name.is_none() {
732            return Err(IdentityError::InvalidInput(
733                "user_name is required".to_string(),
734            ));
735        }
736
737        // Team membership validation: ALL users must either have team assignment OR "No Team Restrictions" role
738        let has_teams = fixed_request
739            .team_ids
740            .as_ref()
741            .is_some_and(|teams| !teams.is_empty());
742
743        if !has_teams {
744            // Check if user has "No Team Restrictions" role (works for both human and API users)
745            let has_no_team_restriction_role = if let Some(ref role_ids) = fixed_request.role_ids {
746                // Get available roles to check role descriptions
747                let roles = self.list_roles().await?;
748                role_ids.iter().any(|role_id| {
749                    roles.iter().any(|r| {
750                        &r.role_id == role_id
751                            && (r
752                                .role_description
753                                .as_ref()
754                                .is_some_and(|desc| desc == "No Team Restriction API")
755                                || r.role_name.to_lowercase() == "noteamrestrictionapi")
756                    })
757                })
758            } else {
759                false
760            };
761
762            if !has_no_team_restriction_role {
763                return Err(IdentityError::InvalidInput(
764                    "You must select at least one team for this user, or select No Team Restrictions role".to_string()
765                ));
766            }
767        }
768
769        // Determine user type flags early
770        let is_api_user = matches!(fixed_request.user_type, Some(UserType::ApiService));
771        let is_saml_user = matches!(fixed_request.user_type, Some(UserType::Saml));
772
773        // Validate role assignments for API users
774        if is_api_user && let Some(ref provided_role_ids) = fixed_request.role_ids {
775            let roles = self.list_roles().await?;
776
777            // Define human-only role descriptions (from userrolesbydescription file)
778            let human_role_descriptions = [
779                "Creator",
780                "Executive",
781                "Mitigation Approver",
782                "Reviewer",
783                "Sandbox User",
784                "Security Lead",
785                "Team Admin",
786                "Workspace Editor",
787                "Analytics Creator",
788                "Delete Scans",
789                "Greenlight IDE User",
790                "Policy Administrator",
791                "Sandbox Administrator",
792                "Security Insights",
793                "Submitter",
794                "Workspace Administrator",
795            ];
796
797            for role_id in provided_role_ids {
798                if let Some(role) = roles.iter().find(|r| &r.role_id == role_id) {
799                    // Check if this is a human-only role
800                    if let Some(ref desc) = role.role_description
801                        && human_role_descriptions.contains(&desc.as_str())
802                    {
803                        return Err(IdentityError::InvalidInput(format!(
804                            "Role '{}' (description: '{}') is a human-only role and cannot be assigned to API users.",
805                            role.role_name, desc
806                        )));
807                    }
808
809                    // API users can only be assigned roles where is_api is true
810                    if role.is_api != Some(true) {
811                        return Err(IdentityError::InvalidInput(format!(
812                            "Role '{}' (is_api: {}) cannot be assigned to API users. API users can only be assigned API roles.",
813                            role.role_name,
814                            role.is_api.map_or("None".to_string(), |b| b.to_string())
815                        )));
816                    }
817                }
818            }
819        }
820
821        // If no roles provided, assign default roles (any scan and submitter)
822        if fixed_request
823            .role_ids
824            .as_ref()
825            .is_none_or(|roles| roles.is_empty())
826        {
827            // Get available roles to find default ones
828            let roles = self.list_roles().await?;
829            let mut default_role_ids = Vec::new();
830
831            // Based on Veracode documentation, assign appropriate default roles
832            if is_api_user {
833                // For API service accounts, assign apisubmitanyscan role
834                if let Some(api_submit_role) = roles
835                    .iter()
836                    .find(|r| r.role_name.to_lowercase() == "apisubmitanyscan")
837                {
838                    default_role_ids.push(api_submit_role.role_id.clone());
839                }
840
841                // Always assign noteamrestrictionapi role for API users (required for team restrictions)
842                if let Some(noteam_role) = roles
843                    .iter()
844                    .find(|r| r.role_name.to_lowercase() == "noteamrestrictionapi")
845                {
846                    default_role_ids.push(noteam_role.role_id.clone());
847                }
848            } else {
849                // For human users (Human/SAML/VOSP), start with Submitter as it's the most basic role
850                if let Some(submitter_role) = roles.iter().find(|r| {
851                    r.role_description
852                        .as_ref()
853                        .is_some_and(|desc| desc == "Submitter")
854                }) {
855                    default_role_ids.push(submitter_role.role_id.clone());
856                } else if let Some(creator_role) = roles.iter().find(|r| {
857                    r.role_description
858                        .as_ref()
859                        .is_some_and(|desc| desc == "Creator")
860                }) {
861                    default_role_ids.push(creator_role.role_id.clone());
862                } else if let Some(reviewer_role) = roles.iter().find(|r| {
863                    r.role_description
864                        .as_ref()
865                        .is_some_and(|desc| desc == "Reviewer")
866                }) {
867                    default_role_ids.push(reviewer_role.role_id.clone());
868                }
869
870                // If user has no teams, also assign "No Team Restrictions" role
871                if !has_teams
872                    && let Some(no_team_role) = roles.iter().find(|r| {
873                        r.role_description
874                            .as_ref()
875                            .is_some_and(|desc| desc == "No Team Restriction API")
876                            || r.role_name.to_lowercase() == "noteamrestrictionapi"
877                    })
878                {
879                    default_role_ids.push(no_team_role.role_id.clone());
880                }
881            }
882
883            // If we found default roles, use them
884            if !default_role_ids.is_empty() {
885                fixed_request.role_ids = Some(default_role_ids);
886            }
887        }
888
889        // If no permissions provided, assign default permissions based on user type
890        if fixed_request
891            .permissions
892            .as_ref()
893            .is_none_or(|p| p.is_empty())
894        {
895            if is_api_user {
896                // For API users, assign "apiUser" permission (from defaultapiuserperm file)
897                let api_user_permission = Permission {
898                    permission_id: None,
899                    permission_name: "apiUser".to_string(),
900                    description: Some("API User".to_string()),
901                    api_only: Some(false),
902                    ui_only: Some(false),
903                };
904                fixed_request.permissions = Some(vec![api_user_permission]);
905            } else {
906                // For human users, assign "humanUser" permission
907                let human_user_permission = Permission {
908                    permission_id: None,
909                    permission_name: "humanUser".to_string(),
910                    description: Some("Human User".to_string()),
911                    api_only: Some(false),
912                    ui_only: Some(false),
913                };
914                fixed_request.permissions = Some(vec![human_user_permission]);
915            }
916        }
917
918        // Create the payload with the correct role structure
919        let roles_payload = if let Some(ref role_ids) = fixed_request.role_ids {
920            role_ids
921                .iter()
922                .map(|id| serde_json::json!({"role_id": id}))
923                .collect::<Vec<_>>()
924        } else {
925            Vec::new()
926        };
927
928        // Create the payload with the correct team structure
929        let teams_payload = if let Some(ref team_ids) = fixed_request.team_ids {
930            team_ids
931                .iter()
932                .map(|id| serde_json::json!({"team_id": id}))
933                .collect::<Vec<_>>()
934        } else {
935            Vec::new()
936        };
937
938        // Create the payload with the correct permissions structure
939        let permissions_payload = if let Some(ref permissions) = fixed_request.permissions {
940            permissions
941                .iter()
942                .map(|p| {
943                    serde_json::json!({
944                        "permission_name": p.permission_name,
945                        "api_only": p.api_only.unwrap_or(false),
946                        "ui_only": p.ui_only.unwrap_or(false)
947                    })
948                })
949                .collect::<Vec<_>>()
950        } else {
951            Vec::new()
952        };
953
954        // Build payload conditionally to exclude null fields and user_type for API users
955        let mut payload = serde_json::json!({
956            "email_address": fixed_request.email_address,
957            "first_name": fixed_request.first_name,
958            "last_name": fixed_request.last_name,
959            "apiUser": is_api_user,
960            "samlUser": is_saml_user,
961            "active": true, // New users are active by default
962            "send_email_invitation": fixed_request.send_email_invitation.unwrap_or(false)
963        });
964
965        // Add user_name only if it's not None
966        if let Some(ref user_name) = fixed_request.user_name
967            && let Some(obj) = payload.as_object_mut()
968        {
969            obj.insert("user_name".to_string(), serde_json::json!(user_name));
970        }
971
972        // Add roles only if not empty
973        if !roles_payload.is_empty()
974            && let Some(obj) = payload.as_object_mut()
975        {
976            obj.insert("roles".to_string(), serde_json::json!(roles_payload));
977        }
978
979        // Add teams only if not empty
980        if !teams_payload.is_empty()
981            && let Some(obj) = payload.as_object_mut()
982        {
983            obj.insert("teams".to_string(), serde_json::json!(teams_payload));
984        }
985
986        // Add permissions only if not empty
987        if !permissions_payload.is_empty()
988            && let Some(obj) = payload.as_object_mut()
989        {
990            obj.insert(
991                "permissions".to_string(),
992                serde_json::json!(permissions_payload),
993            );
994        }
995
996        let response = self.client.post(endpoint, Some(&payload)).await?;
997
998        let status = response.status().as_u16();
999        match status {
1000            200 | 201 => {
1001                let user: User = response.json().await?;
1002                Ok(user)
1003            }
1004            400 => {
1005                let error_text = response.text().await.unwrap_or_default();
1006                let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1007                if error_text.contains("already exists") {
1008                    Err(IdentityError::UserAlreadyExists(sanitized))
1009                } else {
1010                    Err(IdentityError::InvalidInput(sanitized))
1011                }
1012            }
1013            403 => {
1014                let error_text = response.text().await.unwrap_or_default();
1015                let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1016                Err(IdentityError::PermissionDenied(sanitized))
1017            }
1018            415 => {
1019                let error_text = response.text().await.unwrap_or_default();
1020                let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1021                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1022                    sanitized,
1023                )))
1024            }
1025            _ => {
1026                let error_text = response.text().await.unwrap_or_default();
1027                let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1028                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1029                    sanitized,
1030                )))
1031            }
1032        }
1033    }
1034
1035    /// Update an existing user
1036    ///
1037    /// # Arguments
1038    ///
1039    /// * `user_id` - The ID of the user to update
1040    /// * `request` - The user update request
1041    ///
1042    /// # Returns
1043    ///
1044    /// A `Result` containing the updated user or an error
1045    ///
1046    /// # Errors
1047    ///
1048    /// Returns an error if the API request fails, the identity resource is not found,
1049    /// or authentication/authorization fails.
1050    pub async fn update_user(
1051        &self,
1052        user_id: &str,
1053        request: UpdateUserRequest,
1054    ) -> Result<User, IdentityError> {
1055        let endpoint = format!("/api/authn/v2/users/{user_id}");
1056
1057        // Create the payload with the correct role and team structure
1058        let roles_payload = request
1059            .role_ids
1060            .iter()
1061            .map(|id| serde_json::json!({"role_id": id}))
1062            .collect::<Vec<_>>();
1063
1064        let teams_payload = request
1065            .team_ids
1066            .iter()
1067            .map(|id| serde_json::json!({"team_id": id}))
1068            .collect::<Vec<_>>();
1069
1070        let payload = serde_json::json!({
1071            "email_address": request.email_address,
1072            "user_name": request.user_name,
1073            "first_name": request.first_name,
1074            "last_name": request.last_name,
1075            "active": request.active,
1076            "roles": roles_payload,
1077            "teams": teams_payload
1078        });
1079
1080        let response = self.client.put(&endpoint, Some(&payload)).await?;
1081
1082        let status = response.status().as_u16();
1083        match status {
1084            200 => {
1085                let user: User = response.json().await?;
1086                Ok(user)
1087            }
1088            400 => {
1089                let error_text = response.text().await.unwrap_or_default();
1090                let sanitized = Self::sanitize_error(&error_text, status, "update_user");
1091                Err(IdentityError::InvalidInput(sanitized))
1092            }
1093            403 => {
1094                let error_text = response.text().await.unwrap_or_default();
1095                let sanitized = Self::sanitize_error(&error_text, status, "update_user");
1096                Err(IdentityError::PermissionDenied(sanitized))
1097            }
1098            404 => Err(IdentityError::UserNotFound),
1099            _ => {
1100                let error_text = response.text().await.unwrap_or_default();
1101                let sanitized = Self::sanitize_error(&error_text, status, "update_user");
1102                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1103                    sanitized,
1104                )))
1105            }
1106        }
1107    }
1108
1109    /// Delete a user
1110    ///
1111    /// # Arguments
1112    ///
1113    /// * `user_id` - The ID of the user to delete
1114    ///
1115    /// # Returns
1116    ///
1117    /// A `Result` indicating success or failure
1118    ///
1119    /// # Errors
1120    ///
1121    /// Returns an error if the API request fails, the identity resource is not found,
1122    /// or authentication/authorization fails.
1123    pub async fn delete_user(&self, user_id: &str) -> Result<(), IdentityError> {
1124        let endpoint = format!("/api/authn/v2/users/{user_id}");
1125
1126        let response = self.client.delete(&endpoint).await?;
1127
1128        let status = response.status().as_u16();
1129        match status {
1130            200 | 204 => Ok(()),
1131            403 => {
1132                let error_text = response.text().await.unwrap_or_default();
1133                let sanitized = Self::sanitize_error(&error_text, status, "delete_user");
1134                Err(IdentityError::PermissionDenied(sanitized))
1135            }
1136            404 => Err(IdentityError::UserNotFound),
1137            _ => {
1138                let error_text = response.text().await.unwrap_or_default();
1139                let sanitized = Self::sanitize_error(&error_text, status, "delete_user");
1140                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1141                    sanitized,
1142                )))
1143            }
1144        }
1145    }
1146
1147    /// List all roles
1148    ///
1149    /// # Returns
1150    ///
1151    /// A `Result` containing a list of roles or an error
1152    ///
1153    /// # Errors
1154    ///
1155    /// Returns an error if the API request fails, the identity resource is not found,
1156    /// or authentication/authorization fails.
1157    pub async fn list_roles(&self) -> Result<Vec<Role>, IdentityError> {
1158        let endpoint = "/api/authn/v2/roles";
1159        let mut all_roles = Vec::new();
1160        let mut page: u32 = 0;
1161        let page_size = 500;
1162
1163        // Simple pagination loop - fetch pages until empty
1164        loop {
1165            let query_params = vec![
1166                ("page".to_string(), page.to_string()),
1167                ("size".to_string(), page_size.to_string()),
1168            ];
1169
1170            let response = self.client.get(endpoint, Some(&query_params)).await?;
1171            let status = response.status().as_u16();
1172
1173            match status {
1174                200 => {
1175                    let response_text = response.text().await?;
1176
1177                    // Validate JSON depth before parsing to prevent DoS attacks
1178                    validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
1179                        IdentityError::Api(VeracodeError::InvalidResponse(format!(
1180                            "JSON validation failed: {}",
1181                            e
1182                        )))
1183                    })?;
1184
1185                    // Try embedded response format
1186                    if let Ok(roles_response) =
1187                        serde_json::from_str::<RolesResponse>(&response_text)
1188                    {
1189                        let page_roles = if !roles_response.roles.is_empty() {
1190                            roles_response.roles
1191                        } else if let Some(embedded) = roles_response.embedded {
1192                            embedded.roles
1193                        } else {
1194                            Vec::new()
1195                        };
1196
1197                        if page_roles.is_empty() {
1198                            break; // No more roles to fetch
1199                        }
1200
1201                        all_roles.extend(page_roles);
1202                        page = page.saturating_add(1);
1203
1204                        // Check pagination info if available
1205                        if let Some(page_info) = roles_response.page
1206                            && let (Some(current_page), Some(total_pages)) =
1207                                (page_info.number, page_info.total_pages)
1208                            && current_page.saturating_add(1) >= total_pages
1209                        {
1210                            break;
1211                        }
1212
1213                        continue;
1214                    }
1215
1216                    // Try direct array as fallback
1217                    if let Ok(roles) = serde_json::from_str::<Vec<Role>>(&response_text) {
1218                        if roles.is_empty() {
1219                            break;
1220                        }
1221                        all_roles.extend(roles);
1222                        page = page.saturating_add(1);
1223                        continue;
1224                    }
1225
1226                    // If we can't parse, maybe it's the first page without pagination
1227                    if page == 0 {
1228                        log::warn!("Unable to parse roles response: {}", response_text);
1229                        return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1230                            "Unable to parse roles response".to_string(),
1231                        )));
1232                    }
1233                    break; // End of pages
1234                }
1235                _ => {
1236                    let error_text = response.text().await.unwrap_or_default();
1237                    let sanitized = Self::sanitize_error(&error_text, status, "list_roles");
1238                    return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1239                        sanitized,
1240                    )));
1241                }
1242            }
1243        }
1244
1245        Ok(all_roles)
1246    }
1247
1248    /// List all teams with pagination support
1249    ///
1250    /// # Returns
1251    ///
1252    /// A `Result` containing a list of all teams across all pages or an error
1253    ///
1254    /// # Errors
1255    ///
1256    /// Returns an error if the API request fails, the identity resource is not found,
1257    /// or authentication/authorization fails.
1258    pub async fn list_teams(&self) -> Result<Vec<Team>, IdentityError> {
1259        let endpoint = "/api/authn/v2/teams";
1260        let mut all_teams = Vec::new();
1261        let mut page: u32 = 0;
1262        let page_size = 500;
1263
1264        // Simple pagination loop - fetch pages until empty
1265        loop {
1266            // Safety check to prevent infinite loops
1267            if page > 100 {
1268                break;
1269            }
1270
1271            let query_params = vec![
1272                ("page".to_string(), page.to_string()),
1273                ("size".to_string(), page_size.to_string()),
1274            ];
1275
1276            let response = self.client.get(endpoint, Some(&query_params)).await?;
1277            let status = response.status().as_u16();
1278
1279            match status {
1280                200 => {
1281                    let response_text = response.text().await?;
1282
1283                    // Validate JSON depth before parsing to prevent DoS attacks
1284                    validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
1285                        IdentityError::Api(VeracodeError::InvalidResponse(format!(
1286                            "JSON validation failed: {}",
1287                            e
1288                        )))
1289                    })?;
1290
1291                    // Try embedded response format first
1292                    if let Ok(teams_response) =
1293                        serde_json::from_str::<TeamsResponse>(&response_text)
1294                    {
1295                        let page_teams = if !teams_response.teams.is_empty() {
1296                            teams_response.teams
1297                        } else if let Some(embedded) = teams_response.embedded {
1298                            embedded.teams
1299                        } else {
1300                            Vec::new()
1301                        };
1302
1303                        if page_teams.is_empty() {
1304                            break; // No more teams to fetch
1305                        }
1306
1307                        all_teams.extend(page_teams);
1308                        page = page.saturating_add(1);
1309
1310                        // Check pagination info if available
1311                        if let Some(page_info) = teams_response.page
1312                            && let (Some(current_page), Some(total_pages)) =
1313                                (page_info.number, page_info.total_pages)
1314                            && current_page.saturating_add(1) >= total_pages
1315                        {
1316                            break; // Last page reached
1317                        }
1318
1319                        continue;
1320                    }
1321
1322                    // Try direct array as fallback
1323                    if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1324                        if teams.is_empty() {
1325                            break;
1326                        }
1327                        all_teams.extend(teams);
1328                        page = page.saturating_add(1);
1329                        continue;
1330                    }
1331
1332                    // If we can't parse, maybe it's the first page without pagination
1333                    if page == 0 {
1334                        return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1335                            "Unable to parse teams response".to_string(),
1336                        )));
1337                    }
1338                    // We've gotten some teams, but this page failed - break
1339                    break;
1340                }
1341                _ => {
1342                    let error_text = response.text().await.unwrap_or_default();
1343                    let sanitized = Self::sanitize_error(&error_text, status, "list_teams");
1344                    return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1345                        sanitized,
1346                    )));
1347                }
1348            }
1349        }
1350
1351        Ok(all_teams)
1352    }
1353
1354    /// Create a new team
1355    ///
1356    /// # Arguments
1357    ///
1358    /// * `request` - The team creation request
1359    ///
1360    /// # Returns
1361    ///
1362    /// A `Result` containing the created team or an error
1363    ///
1364    /// # Errors
1365    ///
1366    /// Returns an error if the API request fails, the identity resource is not found,
1367    /// or authentication/authorization fails.
1368    pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team, IdentityError> {
1369        let endpoint = "/api/authn/v2/teams";
1370
1371        let response = self.client.post(endpoint, Some(&request)).await?;
1372
1373        let status = response.status().as_u16();
1374        match status {
1375            200 | 201 => {
1376                let team: Team = response.json().await?;
1377                Ok(team)
1378            }
1379            400 => {
1380                let error_text = response.text().await.unwrap_or_default();
1381                let sanitized = Self::sanitize_error(&error_text, status, "create_team");
1382                if error_text.contains("already exists") {
1383                    Err(IdentityError::TeamAlreadyExists(sanitized))
1384                } else {
1385                    Err(IdentityError::InvalidInput(sanitized))
1386                }
1387            }
1388            403 => {
1389                let error_text = response.text().await.unwrap_or_default();
1390                let sanitized = Self::sanitize_error(&error_text, status, "create_team");
1391                Err(IdentityError::PermissionDenied(sanitized))
1392            }
1393            _ => {
1394                let error_text = response.text().await.unwrap_or_default();
1395                let sanitized = Self::sanitize_error(&error_text, status, "create_team");
1396                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1397                    sanitized,
1398                )))
1399            }
1400        }
1401    }
1402
1403    /// Delete a team
1404    ///
1405    /// # Arguments
1406    ///
1407    /// * `team_id` - The ID of the team to delete
1408    ///
1409    /// # Returns
1410    ///
1411    /// A `Result` indicating success or failure
1412    ///
1413    /// # Errors
1414    ///
1415    /// Returns an error if the API request fails, the identity resource is not found,
1416    /// or authentication/authorization fails.
1417    pub async fn delete_team(&self, team_id: &str) -> Result<(), IdentityError> {
1418        let endpoint = format!("/api/authn/v2/teams/{team_id}");
1419
1420        let response = self.client.delete(&endpoint).await?;
1421
1422        let status = response.status().as_u16();
1423        match status {
1424            200 | 204 => Ok(()),
1425            403 => {
1426                let error_text = response.text().await.unwrap_or_default();
1427                let sanitized = Self::sanitize_error(&error_text, status, "delete_team");
1428                Err(IdentityError::PermissionDenied(sanitized))
1429            }
1430            404 => Err(IdentityError::TeamNotFound),
1431            _ => {
1432                let error_text = response.text().await.unwrap_or_default();
1433                let sanitized = Self::sanitize_error(&error_text, status, "delete_team");
1434                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1435                    sanitized,
1436                )))
1437            }
1438        }
1439    }
1440
1441    /// Get a team by its name
1442    ///
1443    /// Searches for a team by exact name match. The API may return multiple teams
1444    /// that match the search criteria, so this method performs an exact string
1445    /// comparison to find the specific team requested.
1446    ///
1447    /// # Arguments
1448    ///
1449    /// * `team_name` - The exact name of the team to find
1450    ///
1451    /// # Returns
1452    ///
1453    /// A `Result` containing an `Option<Team>` if found, or an error
1454    ///
1455    /// # Errors
1456    ///
1457    /// Returns an error if the API request fails, the identity resource is not found,
1458    /// or authentication/authorization fails.
1459    pub async fn get_team_by_name(&self, team_name: &str) -> Result<Option<Team>, IdentityError> {
1460        let endpoint = "/api/authn/v2/teams";
1461
1462        let query_params = vec![
1463            ("page".to_string(), "0".to_string()),
1464            ("size".to_string(), "50".to_string()),
1465            ("team_name".to_string(), team_name.to_string()),
1466            ("ignore_self_teams".to_string(), "false".to_string()),
1467            ("only_manageable".to_string(), "false".to_string()),
1468            ("deleted".to_string(), "false".to_string()),
1469        ];
1470
1471        log::debug!("🔍 Team lookup request - endpoint: {}", endpoint);
1472        log::debug!(
1473            "🔍 Team lookup request - query parameters: [{}]",
1474            query_params
1475                .iter()
1476                .map(|(k, v)| format!("{}={}", k, v))
1477                .collect::<Vec<_>>()
1478                .join(", ")
1479        );
1480        log::debug!(
1481            "🔍 Team lookup request - searching for team: '{}'",
1482            team_name
1483        );
1484
1485        let response = self.client.get(endpoint, Some(&query_params)).await?;
1486        let status = response.status().as_u16();
1487
1488        log::debug!("🔍 Team lookup response - HTTP status: {}", status);
1489
1490        match status {
1491            200 => {
1492                let response_text = response.text().await?;
1493                log::debug!(
1494                    "🔍 Team lookup response - content length: {} bytes",
1495                    response_text.len()
1496                );
1497
1498                // Validate JSON depth before parsing to prevent DoS attacks
1499                validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
1500                    IdentityError::Api(VeracodeError::InvalidResponse(format!(
1501                        "JSON validation failed: {}",
1502                        e
1503                    )))
1504                })?;
1505
1506                // Helper closure to process teams list and find exact case-insensitive match
1507                let process_teams = |teams: Vec<Team>, format_description: &str| -> Option<Team> {
1508                    let team_count = teams.len();
1509                    log::debug!(
1510                        "🔍 Team lookup response - found {} teams total ({})",
1511                        team_count,
1512                        format_description
1513                    );
1514                    for (i, team) in teams.iter().enumerate() {
1515                        log::debug!(
1516                            "🔍 Team lookup response - team {}: '{}' (GUID: {})",
1517                            i.saturating_add(1),
1518                            team.team_name,
1519                            team.team_id
1520                        );
1521                    }
1522
1523                    // Find exact case-insensitive match by team name
1524                    // Note: API search is case-insensitive, so we match case-insensitively too
1525                    let found_team = teams
1526                        .into_iter()
1527                        .find(|team| team.team_name.to_lowercase() == team_name.to_lowercase());
1528                    if let Some(ref team) = found_team {
1529                        log::debug!(
1530                            "🔍 Team lookup result - found case-insensitive match: '{}' (searched for '{}') with GUID: {}",
1531                            team.team_name,
1532                            team_name,
1533                            team.team_id
1534                        );
1535                    } else {
1536                        log::debug!(
1537                            "🔍 Team lookup result - no case-insensitive match for '{}' among {} teams",
1538                            team_name,
1539                            team_count
1540                        );
1541                    }
1542                    found_team
1543                };
1544
1545                // Parse response - prioritize embedded format as shown in your example
1546                if let Ok(teams_response) = serde_json::from_str::<TeamsResponse>(&response_text) {
1547                    let teams = if let Some(embedded) = teams_response.embedded {
1548                        embedded.teams
1549                    } else if !teams_response.teams.is_empty() {
1550                        teams_response.teams
1551                    } else {
1552                        Vec::new()
1553                    };
1554                    Ok(process_teams(teams, "embedded format"))
1555                } else if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1556                    // Fallback for direct array (less common based on your example)
1557                    Ok(process_teams(teams, "direct array"))
1558                } else {
1559                    Err(IdentityError::Api(VeracodeError::InvalidResponse(
1560                        "Unable to parse team response".to_string(),
1561                    )))
1562                }
1563            }
1564            404 => {
1565                log::debug!(
1566                    "🔍 Team lookup result - HTTP 404: team '{}' not found",
1567                    team_name
1568                );
1569                Ok(None) // Team not found
1570            }
1571            403 => {
1572                let error_text = response.text().await.unwrap_or_default();
1573                log::debug!("🔍 Team lookup error - HTTP 403: permission denied");
1574                let sanitized = Self::sanitize_error(&error_text, status, "get_team_by_name");
1575                Err(IdentityError::PermissionDenied(sanitized))
1576            }
1577            _ => {
1578                let error_text = response.text().await.unwrap_or_default();
1579                log::debug!("🔍 Team lookup error - HTTP {}", status);
1580                let sanitized = Self::sanitize_error(&error_text, status, "get_team_by_name");
1581                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1582                    sanitized,
1583                )))
1584            }
1585        }
1586    }
1587
1588    /// Get a team's GUID by its name
1589    ///
1590    /// This is a convenience method for application creation workflows where
1591    /// only the team GUID is needed.
1592    ///
1593    /// # Arguments
1594    ///
1595    /// * `team_name` - The exact name of the team to find
1596    ///
1597    /// # Returns
1598    ///
1599    /// A `Result` containing an `Option<String>` with the team's GUID if found, or an error
1600    ///
1601    /// # Errors
1602    ///
1603    /// Returns an error if the API request fails, the identity resource is not found,
1604    /// or authentication/authorization fails.
1605    pub async fn get_team_guid_by_name(
1606        &self,
1607        team_name: &str,
1608    ) -> Result<Option<String>, IdentityError> {
1609        match self.get_team_by_name(team_name).await? {
1610            Some(team) => Ok(Some(team.team_id)),
1611            None => Ok(None),
1612        }
1613    }
1614
1615    /// Create API credentials
1616    ///
1617    /// # Arguments
1618    ///
1619    /// * `request` - The API credential creation request
1620    ///
1621    /// # Returns
1622    ///
1623    /// A `Result` containing the created API credentials or an error
1624    ///
1625    /// # Errors
1626    ///
1627    /// Returns an error if the API request fails, the identity resource is not found,
1628    /// or authentication/authorization fails.
1629    pub async fn create_api_credentials(
1630        &self,
1631        request: CreateApiCredentialRequest,
1632    ) -> Result<ApiCredential, IdentityError> {
1633        let endpoint = "/api/authn/v2/api_credentials";
1634
1635        let response = self.client.post(endpoint, Some(&request)).await?;
1636
1637        let status = response.status().as_u16();
1638        match status {
1639            200 | 201 => {
1640                let credentials: ApiCredential = response.json().await?;
1641                Ok(credentials)
1642            }
1643            400 => {
1644                let error_text = response.text().await.unwrap_or_default();
1645                let sanitized = Self::sanitize_error(&error_text, status, "create_api_credentials");
1646                Err(IdentityError::InvalidInput(sanitized))
1647            }
1648            403 => {
1649                let error_text = response.text().await.unwrap_or_default();
1650                let sanitized = Self::sanitize_error(&error_text, status, "create_api_credentials");
1651                Err(IdentityError::PermissionDenied(sanitized))
1652            }
1653            _ => {
1654                let error_text = response.text().await.unwrap_or_default();
1655                let sanitized = Self::sanitize_error(&error_text, status, "create_api_credentials");
1656                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1657                    sanitized,
1658                )))
1659            }
1660        }
1661    }
1662
1663    /// Revoke API credentials
1664    ///
1665    /// # Arguments
1666    ///
1667    /// * `api_creds_id` - The ID of the API credentials to revoke
1668    ///
1669    /// # Returns
1670    ///
1671    /// A `Result` indicating success or failure
1672    ///
1673    /// # Errors
1674    ///
1675    /// Returns an error if the API request fails, the identity resource is not found,
1676    /// or authentication/authorization fails.
1677    pub async fn revoke_api_credentials(&self, api_creds_id: &str) -> Result<(), IdentityError> {
1678        let endpoint = format!("/api/authn/v2/api_credentials/{api_creds_id}");
1679
1680        let response = self.client.delete(&endpoint).await?;
1681
1682        let status = response.status().as_u16();
1683        match status {
1684            200 | 204 => Ok(()), // Accept both 200 OK and 204 No Content as success
1685            403 => {
1686                let error_text = response.text().await.unwrap_or_default();
1687                let sanitized = Self::sanitize_error(&error_text, status, "revoke_api_credentials");
1688                Err(IdentityError::PermissionDenied(sanitized))
1689            }
1690            404 => Err(IdentityError::UserNotFound), // API credentials not found
1691            _ => {
1692                let error_text = response.text().await.unwrap_or_default();
1693                let sanitized = Self::sanitize_error(&error_text, status, "revoke_api_credentials");
1694                Err(IdentityError::Api(VeracodeError::InvalidResponse(
1695                    sanitized,
1696                )))
1697            }
1698        }
1699    }
1700}
1701
1702/// Convenience methods for common operations
1703impl<'a> IdentityApi<'a> {
1704    /// Find a user by email address
1705    ///
1706    /// # Arguments
1707    ///
1708    /// * `email` - The email address to search for
1709    ///
1710    /// # Returns
1711    ///
1712    /// A `Result` containing the user if found, or None if not found
1713    ///
1714    /// # Errors
1715    ///
1716    /// Returns an error if the API request fails, the identity resource is not found,
1717    /// or authentication/authorization fails.
1718    pub async fn find_user_by_email(&self, email: &str) -> Result<Option<User>, IdentityError> {
1719        let query = UserQuery::new().with_email(email);
1720        let users = self.list_users(Some(query)).await?;
1721        Ok(users.into_iter().find(|u| u.email_address == email))
1722    }
1723
1724    /// Find a user by username
1725    ///
1726    /// # Arguments
1727    ///
1728    /// * `username` - The username to search for
1729    ///
1730    /// # Returns
1731    ///
1732    /// A `Result` containing the user if found, or None if not found
1733    ///
1734    /// # Errors
1735    ///
1736    /// Returns an error if the API request fails, the identity resource is not found,
1737    /// or authentication/authorization fails.
1738    pub async fn find_user_by_username(
1739        &self,
1740        username: &str,
1741    ) -> Result<Option<User>, IdentityError> {
1742        let query = UserQuery::new().with_username(username);
1743        let users = self.list_users(Some(query)).await?;
1744        Ok(users.into_iter().find(|u| u.user_name == username))
1745    }
1746
1747    /// Create a simple user with basic information
1748    ///
1749    /// # Arguments
1750    ///
1751    /// * `email` - User's email address
1752    /// * `username` - User's username
1753    /// * `first_name` - User's first name
1754    /// * `last_name` - User's last name
1755    /// * `team_ids` - List of team IDs to assign (at least one required)
1756    ///
1757    /// # Returns
1758    ///
1759    /// A `Result` containing the created user or an error
1760    ///
1761    /// # Errors
1762    ///
1763    /// Returns an error if the API request fails, the identity resource is not found,
1764    /// or authentication/authorization fails.
1765    pub async fn create_simple_user(
1766        &self,
1767        email: &str,
1768        username: &str,
1769        first_name: &str,
1770        last_name: &str,
1771        team_ids: Vec<String>,
1772    ) -> Result<User, IdentityError> {
1773        let request = CreateUserRequest {
1774            email_address: email.to_string(),
1775            first_name: first_name.to_string(),
1776            last_name: last_name.to_string(),
1777            user_name: Some(username.to_string()),
1778            user_type: Some(UserType::Human),
1779            send_email_invitation: Some(true),
1780            role_ids: None, // Will be auto-assigned to "any scan" and "submitter" roles
1781            team_ids: Some(team_ids),
1782            permissions: None, // Will use default permissions for human users
1783        };
1784
1785        self.create_user(request).await
1786    }
1787
1788    /// Create an API service account
1789    ///
1790    /// # Arguments
1791    ///
1792    /// * `email` - Service account email address
1793    /// * `username` - Service account username
1794    /// * `first_name` - Service account first name
1795    /// * `last_name` - Service account last name
1796    /// * `role_ids` - List of role IDs to assign
1797    /// * `team_ids` - Optional list of team IDs to assign
1798    ///
1799    /// # Returns
1800    ///
1801    /// A `Result` containing the created user or an error
1802    ///
1803    /// # Errors
1804    ///
1805    /// Returns an error if the API request fails, the identity resource is not found,
1806    /// or authentication/authorization fails.
1807    pub async fn create_api_service_account(
1808        &self,
1809        email: &str,
1810        username: &str,
1811        first_name: &str,
1812        last_name: &str,
1813        role_ids: Vec<String>,
1814        team_ids: Option<Vec<String>>,
1815    ) -> Result<User, IdentityError> {
1816        let request = CreateUserRequest {
1817            email_address: email.to_string(),
1818            first_name: first_name.to_string(),
1819            last_name: last_name.to_string(),
1820            user_name: Some(username.to_string()),
1821            user_type: Some(UserType::ApiService), // Still needed for internal logic
1822            send_email_invitation: Some(false),
1823            role_ids: Some(role_ids),
1824            team_ids,          // Use the provided team IDs
1825            permissions: None, // Will auto-assign "apiUser" permission for API users
1826        };
1827
1828        self.create_user(request).await
1829    }
1830}
1831
1832#[cfg(test)]
1833#[allow(clippy::expect_used)]
1834mod tests {
1835    use super::*;
1836
1837    #[test]
1838    fn test_user_query_params() {
1839        let query = UserQuery::new()
1840            .with_username("testuser")
1841            .with_email("test@example.com")
1842            .with_user_type(UserType::Human)
1843            .with_pagination(1, 50);
1844
1845        let params: Vec<_> = query.into();
1846        assert_eq!(params.len(), 5); // username, email, user_type, page, size
1847        assert!(params.contains(&("user_name".to_string(), "testuser".to_string())));
1848        assert!(params.contains(&("email_address".to_string(), "test@example.com".to_string())));
1849        assert!(params.contains(&("user_type".to_string(), "HUMAN".to_string())));
1850        assert!(params.contains(&("page".to_string(), "1".to_string())));
1851        assert!(params.contains(&("size".to_string(), "50".to_string())));
1852    }
1853
1854    #[test]
1855    fn test_user_type_serialization() {
1856        assert_eq!(
1857            serde_json::to_string(&UserType::Human).expect("should serialize to json"),
1858            "\"HUMAN\""
1859        );
1860        assert_eq!(
1861            serde_json::to_string(&UserType::ApiService).expect("should serialize to json"),
1862            "\"API\""
1863        );
1864        assert_eq!(
1865            serde_json::to_string(&UserType::Saml).expect("should serialize to json"),
1866            "\"SAML\""
1867        );
1868        assert_eq!(
1869            serde_json::to_string(&UserType::Vosp).expect("should serialize to json"),
1870            "\"VOSP\""
1871        );
1872    }
1873}
1874
1875// ============================================================================
1876// SECURITY PROPERTY TESTS
1877// ============================================================================
1878// These tests verify security-critical properties of the identity module:
1879// - API credential redaction in debug output
1880// - Error message sanitization to prevent information disclosure
1881// - Input validation for identity fields (email, username, etc.)
1882// - Memory safety in query parameter building
1883// - Proper bounds checking in pagination
1884// ============================================================================
1885
1886#[cfg(test)]
1887mod security_tests {
1888    use super::*;
1889    use proptest::prelude::*;
1890
1891    // ========================================================================
1892    // Test 1: API Credential Redaction (CRITICAL SECURITY)
1893    // ========================================================================
1894    // Ensures that API keys are NEVER leaked in debug output, logs, or error messages.
1895    // This is essential because API keys provide full authentication access.
1896
1897    proptest! {
1898        #![proptest_config(ProptestConfig {
1899            cases: if cfg!(miri) { 5 } else { 1000 },
1900            failure_persistence: None,
1901            .. ProptestConfig::default()
1902        })]
1903
1904        /// Property: ApiCredential Debug output must NEVER contain the actual API key
1905        ///
1906        /// Security concern: API keys exposed in logs/debug output could lead to:
1907        /// - Unauthorized access to Veracode accounts
1908        /// - Privilege escalation
1909        /// - Account compromise
1910        #[test]
1911        fn api_credential_debug_redacts_sensitive_data(
1912            api_id in "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
1913            api_key in "[a-f0-9]{64}",
1914            active in any::<bool>(),
1915        ) {
1916            let credential = ApiCredential {
1917                api_id: api_id.clone(),
1918                api_key: Some(api_key.clone()),
1919                expiration_ts: None,
1920                active: Some(active),
1921                created_date: None,
1922            };
1923
1924            // Get debug string representation
1925            let debug_output = format!("{:?}", credential);
1926
1927            // CRITICAL: API key must NEVER appear in debug output
1928            assert!(
1929                !debug_output.contains(&api_key),
1930                "API key leaked in debug output! This is a critical security vulnerability. Output: {}",
1931                debug_output
1932            );
1933
1934            // Verify it shows [REDACTED] instead
1935            assert!(
1936                debug_output.contains("[REDACTED]"),
1937                "Debug output should show [REDACTED] for API key. Output: {}",
1938                debug_output
1939            );
1940
1941            // Verify the api_id is still present (needed for identification)
1942            assert!(
1943                debug_output.contains(&api_id),
1944                "API ID should be visible in debug output for identification purposes"
1945            );
1946        }
1947
1948        /// Property: ApiCredential with None api_key should not leak information
1949        #[test]
1950        fn api_credential_debug_handles_none_safely(
1951            api_id in "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
1952        ) {
1953            let credential = ApiCredential {
1954                api_id: api_id.clone(),
1955                api_key: None,
1956                expiration_ts: None,
1957                active: Some(true),
1958                created_date: None,
1959            };
1960
1961            let debug_output = format!("{:?}", credential);
1962
1963            // Should still show the field but as None/redacted
1964            assert!(
1965                debug_output.contains("api_key"),
1966                "Debug output should show api_key field even when None"
1967            );
1968        }
1969    }
1970
1971    // ========================================================================
1972    // Test 2: Error Message Sanitization
1973    // ========================================================================
1974    // Ensures error messages don't leak sensitive information like:
1975    // - Internal server details
1976    // - Database schema information
1977    // - User enumeration data
1978    // - Stack traces
1979
1980    proptest! {
1981        #![proptest_config(ProptestConfig {
1982            cases: if cfg!(miri) { 5 } else { 1000 },
1983            failure_persistence: None,
1984            .. ProptestConfig::default()
1985        })]
1986
1987        /// Property: Sanitized errors must not leak internal details
1988        ///
1989        /// Security concern: Information disclosure via error messages
1990        #[test]
1991        fn sanitize_error_prevents_information_disclosure(
1992            raw_error in ".*",
1993            status in 400u16..600u16,
1994        ) {
1995            let context = "test_operation";
1996            let sanitized = IdentityApi::sanitize_error(&raw_error, status, context);
1997
1998            // Sanitized message should NOT contain potentially sensitive patterns
1999            let sensitive_patterns = [
2000                "password",
2001                "token",
2002                "secret",
2003                "key",
2004                "credential",
2005                "database",
2006                "sql",
2007                "stack trace",
2008                "at line",
2009                "error:",
2010                "exception:",
2011                "/usr/",
2012                "/var/",
2013                "C:\\",
2014            ];
2015
2016            for pattern in &sensitive_patterns {
2017                assert!(
2018                    !sanitized.to_lowercase().contains(pattern),
2019                    "Sanitized error message contains sensitive pattern '{}': {}",
2020                    pattern,
2021                    sanitized
2022                );
2023            }
2024
2025            // Should NOT be the raw error (unless it's already safe)
2026            if raw_error.len() > 50 || raw_error.contains("Exception") {
2027                assert_ne!(
2028                    sanitized, raw_error,
2029                    "Raw error should be sanitized, not passed through directly"
2030                );
2031            }
2032
2033            // Should return a generic message appropriate for the status code
2034            assert!(
2035                sanitized.len() < 200,
2036                "Sanitized error should be concise, got: {}",
2037                sanitized
2038            );
2039        }
2040
2041        /// Property: Common HTTP status codes get appropriate generic messages
2042        #[test]
2043        fn sanitize_error_returns_appropriate_messages(
2044            raw_error in ".*",
2045        ) {
2046            // Test specific status codes
2047            let test_cases = vec![
2048                (400, "invalid"),
2049                (401, "authentication"),
2050                (403, "permission"),
2051                (404, "not found"),
2052                (429, "rate limit"),
2053                (500, "server error"),
2054            ];
2055
2056            for (status, expected_substring) in test_cases {
2057                let sanitized = IdentityApi::sanitize_error(&raw_error, status, "test");
2058                assert!(
2059                    sanitized.to_lowercase().contains(expected_substring),
2060                    "Status {} should mention '{}', got: {}",
2061                    status, expected_substring, sanitized
2062                );
2063            }
2064        }
2065    }
2066
2067    // ========================================================================
2068    // Test 3: Input Validation for Identity Fields
2069    // ========================================================================
2070    // Tests that identity fields (email, username, team names) are properly validated
2071    // and don't allow injection attacks or excessive memory consumption.
2072
2073    proptest! {
2074        #![proptest_config(ProptestConfig {
2075            cases: if cfg!(miri) { 5 } else { 1000 },
2076            failure_persistence: None,
2077            .. ProptestConfig::default()
2078        })]
2079
2080        /// Property: UserQuery builder must handle arbitrary strings safely
2081        ///
2082        /// Security concern: SQL injection, command injection, or buffer overflow
2083        /// via malicious username/email strings
2084        #[test]
2085        fn user_query_handles_malicious_input_safely(
2086            username in ".*",
2087            email in ".*",
2088            role_id in ".*",
2089        ) {
2090            // Should not panic with any input
2091            let query = UserQuery::new()
2092                .with_username(&username)
2093                .with_email(&email)
2094                .with_role_id(&role_id);
2095
2096            // Convert to query params should not panic
2097            let params = query.to_query_params();
2098
2099            // Verify all expected fields are present
2100            let has_username = params.iter().any(|(k, v)| k == "user_name" && v == &username);
2101            let has_email = params.iter().any(|(k, v)| k == "email_address" && v == &email);
2102            let has_role = params.iter().any(|(k, v)| k == "role_id" && v == &role_id);
2103
2104            assert!(has_username, "Username should be in query params");
2105            assert!(has_email, "Email should be in query params");
2106            assert!(has_role, "Role ID should be in query params");
2107        }
2108
2109        /// Property: Query parameter building must not cause excessive allocations
2110        ///
2111        /// Security concern: Memory exhaustion DoS via large inputs
2112        #[test]
2113        fn user_query_params_bounded_memory(
2114            username in ".*",
2115            email in ".*",
2116        ) {
2117            let query = UserQuery::new()
2118                .with_username(&username)
2119                .with_email(&email)
2120                .with_pagination(0, 100);
2121
2122            let params = query.to_query_params();
2123
2124            // Total parameter count should be bounded
2125            assert!(
2126                params.len() <= 10,
2127                "Query should not generate excessive parameters: {} params",
2128                params.len()
2129            );
2130
2131            // Total serialized size should be bounded to prevent memory exhaustion
2132            let total_size: usize = params.iter().map(|(k, v)| k.len().saturating_add(v.len())).sum();
2133            let max_expected = username.len().saturating_add(email.len()).saturating_add(200);
2134
2135            assert!(
2136                total_size <= max_expected,
2137                "Query params should not cause excessive memory allocation: {} bytes (max: {})",
2138                total_size, max_expected
2139            );
2140        }
2141    }
2142
2143    // ========================================================================
2144    // Test 4: CreateUserRequest Validation
2145    // ========================================================================
2146    // Ensures user creation requests properly validate inputs and prevent
2147    // malicious data from being submitted to the API.
2148
2149    proptest! {
2150        #![proptest_config(ProptestConfig {
2151            cases: if cfg!(miri) { 5 } else { 500 },
2152            failure_persistence: None,
2153            .. ProptestConfig::default()
2154        })]
2155
2156        /// Property: CreateUserRequest must handle arbitrary string inputs without panic
2157        ///
2158        /// Security concern: Injection attacks via user creation fields
2159        #[test]
2160        fn create_user_request_handles_arbitrary_input(
2161            email in ".*",
2162            first_name in ".*",
2163            last_name in ".*",
2164            username in ".*",
2165        ) {
2166            let request = CreateUserRequest {
2167                email_address: email.clone(),
2168                first_name: first_name.clone(),
2169                last_name: last_name.clone(),
2170                user_name: Some(username.clone()),
2171                user_type: Some(UserType::Human),
2172                send_email_invitation: Some(true),
2173                role_ids: None,
2174                team_ids: None,
2175                permissions: None,
2176            };
2177
2178            // Serialization should not panic
2179            let serialized = serde_json::to_string(&request);
2180            assert!(
2181                serialized.is_ok(),
2182                "CreateUserRequest serialization should not panic with arbitrary input"
2183            );
2184
2185            // JSON should be valid and non-empty
2186            if let Ok(json_str) = serialized {
2187                assert!(!json_str.is_empty(), "Serialized JSON should not be empty");
2188                assert!(json_str.starts_with('{'), "Should serialize to JSON object");
2189            }
2190        }
2191
2192        /// Property: CreateTeamRequest must handle arbitrary team names safely
2193        ///
2194        /// Security concern: Injection attacks or memory exhaustion via team names
2195        #[test]
2196        fn create_team_request_handles_arbitrary_input(
2197            team_name in ".*",
2198            team_description in ".*",
2199        ) {
2200            let request = CreateTeamRequest {
2201                team_name: team_name.clone(),
2202                team_description: Some(team_description.clone()),
2203                business_unit_id: None,
2204                user_ids: None,
2205            };
2206
2207            // Serialization should not panic
2208            let serialized = serde_json::to_string(&request);
2209            assert!(
2210                serialized.is_ok(),
2211                "CreateTeamRequest serialization should not panic"
2212            );
2213        }
2214    }
2215
2216    // ========================================================================
2217    // Test 5: Pagination Safety
2218    // ========================================================================
2219    // Ensures pagination logic doesn't have off-by-one errors or integer overflow
2220    // that could lead to infinite loops or resource exhaustion.
2221
2222    proptest! {
2223        #![proptest_config(ProptestConfig {
2224            cases: if cfg!(miri) { 5 } else { 1000 },
2225            failure_persistence: None,
2226            .. ProptestConfig::default()
2227        })]
2228
2229        /// Property: Pagination parameters must handle edge cases safely
2230        ///
2231        /// Security concern: Integer overflow or off-by-one errors causing
2232        /// infinite loops or resource exhaustion
2233        #[test]
2234        fn pagination_handles_edge_cases(
2235            page in 0u32..1000u32,
2236            size in 1u32..1000u32,
2237        ) {
2238            let query = UserQuery::new().with_pagination(page, size);
2239            let params = query.to_query_params();
2240
2241            // Find page and size in params
2242            let page_param = params.iter().find(|(k, _)| k == "page");
2243            let size_param = params.iter().find(|(k, _)| k == "size");
2244
2245            assert!(page_param.is_some(), "Page parameter should be present");
2246            assert!(size_param.is_some(), "Size parameter should be present");
2247
2248            // Verify conversion to string doesn't cause issues
2249            if let Some((_, page_str)) = page_param {
2250                let parsed: Result<u32, _> = page_str.parse();
2251                assert!(parsed.is_ok(), "Page should be valid u32");
2252                assert_eq!(parsed.expect("Page parsing verified above"), page, "Page value should match");
2253            }
2254
2255            if let Some((_, size_str)) = size_param {
2256                let parsed: Result<u32, _> = size_str.parse();
2257                assert!(parsed.is_ok(), "Size should be valid u32");
2258                assert_eq!(parsed.expect("Size parsing verified above"), size, "Size value should match");
2259            }
2260        }
2261
2262        /// Property: PageInfo calculations must not overflow
2263        #[test]
2264        fn page_info_calculations_safe(
2265            number in 0u32..1000u32,
2266            size in 1u32..1000u32,
2267            total_elements in 0u64..1_000_000u64,
2268            total_pages in 0u32..10000u32,
2269        ) {
2270            let page_info = PageInfo {
2271                number: Some(number),
2272                size: Some(size),
2273                total_elements: Some(total_elements),
2274                total_pages: Some(total_pages),
2275            };
2276
2277            // Serialization should not panic
2278            let serialized = serde_json::to_string(&page_info);
2279            assert!(serialized.is_ok(), "PageInfo serialization should not panic");
2280
2281            // Check for overflow in calculations (current page + 1 >= total pages)
2282            if let (Some(current), Some(total)) = (page_info.number, page_info.total_pages) {
2283                // Use saturating_add to prevent overflow
2284                let next_page = current.saturating_add(1);
2285                assert!(
2286                    next_page >= current,
2287                    "Page increment should not wrap around"
2288                );
2289
2290                // Verify no arithmetic overflow when checking page bounds
2291                // Pages can be zero-indexed or one-indexed, so we just verify no overflow
2292                let _ = current.saturating_add(1);
2293                let _ = total.saturating_sub(current);
2294            }
2295        }
2296    }
2297
2298    // ========================================================================
2299    // Test 6: Error Type Safety
2300    // ========================================================================
2301    // Ensures error types don't leak sensitive information and convert properly.
2302
2303    proptest! {
2304        #![proptest_config(ProptestConfig {
2305            cases: if cfg!(miri) { 5 } else { 500 },
2306            failure_persistence: None,
2307            .. ProptestConfig::default()
2308        })]
2309
2310        /// Property: IdentityError Display should not leak sensitive info
2311        ///
2312        /// Security concern: Error messages might expose internal details
2313        #[test]
2314        fn identity_error_display_safe(
2315            error_msg in ".*",
2316        ) {
2317            let errors = vec![
2318                IdentityError::UserNotFound,
2319                IdentityError::TeamNotFound,
2320                IdentityError::RoleNotFound,
2321                IdentityError::InvalidInput(error_msg.clone()),
2322                IdentityError::PermissionDenied(error_msg.clone()),
2323                IdentityError::UserAlreadyExists(error_msg.clone()),
2324                IdentityError::TeamAlreadyExists(error_msg.clone()),
2325            ];
2326
2327            for error in errors {
2328                let display_str = format!("{}", error);
2329
2330                // Error messages should be bounded in size
2331                assert!(
2332                    display_str.len() < 500,
2333                    "Error message should be concise: {} chars",
2334                    display_str.len()
2335                );
2336
2337                // Should not contain certain sensitive patterns
2338                assert!(
2339                    !display_str.contains("password"),
2340                    "Error should not mention passwords"
2341                );
2342                assert!(
2343                    !display_str.contains("token"),
2344                    "Error should not mention tokens"
2345                );
2346            }
2347        }
2348    }
2349
2350    // ========================================================================
2351    // Test 7: Memory Safety in Query Parameter Conversion
2352    // ========================================================================
2353    // Ensures the From trait implementations don't cause memory issues.
2354
2355    proptest! {
2356        #![proptest_config(ProptestConfig {
2357            cases: if cfg!(miri) { 5 } else { 500 },
2358            failure_persistence: None,
2359            .. ProptestConfig::default()
2360        })]
2361
2362        /// Property: Converting UserQuery to Vec should be memory-safe
2363        ///
2364        /// Security concern: Move semantics must not cause use-after-free
2365        #[test]
2366        fn user_query_conversion_memory_safe(
2367            username in ".*",
2368            email in ".*",
2369        ) {
2370            let query = UserQuery::new()
2371                .with_username(&username)
2372                .with_email(&email);
2373
2374            // Test borrowing conversion
2375            let params_borrowed: Vec<(String, String)> = Vec::from(&query);
2376            assert!(params_borrowed.len() >= 2);
2377
2378            // Original query should still be usable
2379            let params_again = query.to_query_params();
2380            assert_eq!(params_borrowed.len(), params_again.len());
2381
2382            // Test move conversion
2383            let query2 = UserQuery::new()
2384                .with_username(&username)
2385                .with_email(&email);
2386            let params_moved: Vec<(String, String)> = Vec::from(query2);
2387            assert!(params_moved.len() >= 2);
2388
2389            // Both conversions should produce same result
2390            assert_eq!(params_borrowed.len(), params_moved.len());
2391        }
2392    }
2393
2394    // ========================================================================
2395    // Test 8: UserType Serialization Safety
2396    // ========================================================================
2397    // Ensures UserType enum serialization doesn't introduce vulnerabilities.
2398
2399    proptest! {
2400        #![proptest_config(ProptestConfig {
2401            cases: if cfg!(miri) { 5 } else { 500 },
2402            failure_persistence: None,
2403            .. ProptestConfig::default()
2404        })]
2405
2406        /// Property: UserType serialization must be consistent and safe
2407        #[test]
2408        fn user_type_serialization_safe(
2409            user_type_idx in 0usize..4,
2410        ) {
2411            let user_types = [
2412                UserType::Human,
2413                UserType::ApiService,
2414                UserType::Saml,
2415                UserType::Vosp,
2416            ];
2417            let expected_strings = ["\"HUMAN\"", "\"API\"", "\"SAML\"", "\"VOSP\""];
2418
2419            let user_type = user_types.get(user_type_idx).expect("user_type_idx should be in bounds");
2420            let expected = expected_strings.get(user_type_idx).expect("user_type_idx should be in bounds");
2421
2422            // Serialize
2423            let serialized = serde_json::to_string(user_type);
2424            assert!(serialized.is_ok(), "UserType serialization should not fail");
2425
2426            let serialized_str = serialized.expect("UserType serialization verified above");
2427            assert_eq!(serialized_str, *expected, "UserType should serialize to expected format");
2428
2429            // Deserialize back
2430            let deserialized: Result<UserType, _> = serde_json::from_str(&serialized_str);
2431            assert!(deserialized.is_ok(), "UserType deserialization should not fail");
2432            assert_eq!(&deserialized.expect("UserType deserialization verified above"), user_type, "Roundtrip should preserve value");
2433        }
2434    }
2435}