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::{VeracodeClient, VeracodeError};
11
12/// Represents a Veracode user account
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct User {
15    /// Unique user ID
16    pub user_id: String,
17    /// Legacy user ID
18    pub user_legacy_id: Option<u32>,
19    /// Username for the account
20    pub user_name: String,
21    /// User's email address
22    pub email_address: String,
23    /// User's first name
24    pub first_name: String,
25    /// User's last name
26    pub last_name: String,
27    /// User account type (optional in basic response)
28    pub user_type: Option<UserType>,
29    /// Whether the user account is active (optional in basic response)
30    pub active: Option<bool>,
31    /// Whether login is enabled
32    pub login_enabled: Option<bool>,
33    /// Whether this is a SAML user
34    pub saml_user: Option<bool>,
35    /// List of roles assigned to the user (only in detailed response)
36    pub roles: Option<Vec<Role>>,
37    /// List of teams the user belongs to (only in detailed response)
38    pub teams: Option<Vec<Team>>,
39    /// Login status information (only in detailed response)
40    pub login_status: Option<LoginStatus>,
41    /// Date when the user was created (only in detailed response)
42    pub created_date: Option<DateTime<Utc>>,
43    /// Date when the user was last modified (only in detailed response)
44    pub modified_date: Option<DateTime<Utc>>,
45    /// API credentials information (for API service accounts, only in detailed response)
46    pub api_credentials: Option<Vec<ApiCredential>>,
47    /// Links for navigation
48    #[serde(rename = "_links")]
49    pub links: Option<serde_json::Value>,
50}
51
52/// User account types
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(rename_all = "UPPERCASE")]
55pub enum UserType {
56    /// Regular human user account
57    Human,
58    /// API service account
59    #[serde(rename = "API")]
60    ApiService,
61    /// SAML user account
62    Saml,
63    /// VOSP user account
64    #[serde(rename = "VOSP")]
65    Vosp,
66}
67
68/// Represents a user role
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Role {
71    /// Unique role ID
72    pub role_id: String,
73    /// Legacy role ID (optional)
74    pub role_legacy_id: Option<i32>,
75    /// Role name
76    pub role_name: String,
77    /// Role description
78    pub role_description: Option<String>,
79    /// Whether this is an internal Veracode role
80    pub is_internal: Option<bool>,
81    /// Whether the role requires a token
82    pub requires_token: Option<bool>,
83    /// Whether the role is assigned to proxy users
84    pub assigned_to_proxy_users: Option<bool>,
85    /// Whether team admins can manage this role
86    pub team_admin_manageable: Option<bool>,
87    /// Whether the role is JIT assignable
88    pub jit_assignable: Option<bool>,
89    /// Whether the role is JIT assignable by default
90    pub jit_assignable_default: Option<bool>,
91    /// Whether this is an API role
92    pub is_api: Option<bool>,
93    /// Whether this is a scan type role
94    pub is_scan_type: Option<bool>,
95    /// Whether the role ignores team restrictions
96    pub ignore_team_restrictions: Option<bool>,
97    /// Whether the role is HMAC only
98    pub is_hmac_only: Option<bool>,
99    /// Organization ID (for custom roles)
100    pub org_id: Option<String>,
101    /// Child roles (nested roles)
102    pub child_roles: Option<Vec<serde_json::Value>>,
103    /// Whether the role is disabled
104    pub role_disabled: Option<bool>,
105    /// List of permissions granted by this role
106    pub permissions: Option<Vec<Permission>>,
107    /// Links for navigation
108    #[serde(rename = "_links")]
109    pub links: Option<serde_json::Value>,
110}
111
112/// Represents a permission within a role
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Permission {
115    /// Permission ID (returned from API)
116    pub permission_id: Option<String>,
117    /// Permission name
118    pub permission_name: String,
119    /// Permission description (optional)
120    pub description: Option<String>,
121    /// Whether this permission is API only
122    pub api_only: Option<bool>,
123    /// Whether this permission is UI only
124    pub ui_only: Option<bool>,
125}
126
127/// Represents a team
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Team {
130    /// Unique team ID
131    pub team_id: String,
132    /// Team name
133    pub team_name: String,
134    /// Team description
135    pub team_description: Option<String>,
136    /// List of users in the team
137    pub users: Option<Vec<User>>,
138    /// Business unit the team belongs to
139    pub business_unit: Option<BusinessUnit>,
140}
141
142/// Represents a business unit
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct BusinessUnit {
145    /// Unique business unit ID
146    pub bu_id: String,
147    /// Business unit name
148    pub bu_name: String,
149    /// Business unit description
150    pub bu_description: Option<String>,
151    /// List of teams in the business unit
152    pub teams: Option<Vec<Team>>,
153}
154
155/// Represents API credentials
156#[derive(Clone, Serialize, Deserialize)]
157pub struct ApiCredential {
158    /// Unique API credential ID
159    pub api_id: String,
160    /// API key (only shown when first created) - automatically redacted in debug output
161    pub api_key: Option<String>,
162    /// Expiration date of the credentials
163    pub expiration_ts: Option<DateTime<Utc>>,
164    /// Whether the credentials are active
165    pub active: Option<bool>,
166    /// Creation date
167    pub created_date: Option<DateTime<Utc>>,
168}
169
170///
171/// # Errors
172///
173/// Returns an error if the API request fails, the resource is not found,
174/// or authentication/authorization fails.
175/// Custom Debug implementation for `ApiCredential` that redacts sensitive information
176impl std::fmt::Debug for ApiCredential {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        f.debug_struct("ApiCredential")
179            .field("api_id", &self.api_id)
180            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
181            .field("expiration_ts", &self.expiration_ts)
182            .field("active", &self.active)
183            .field("created_date", &self.created_date)
184            .finish()
185    }
186}
187
188/// Login status information
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct LoginStatus {
191    /// Last login date
192    pub last_login_date: Option<DateTime<Utc>>,
193    /// Whether the user has ever logged in
194    pub never_logged_in: Option<bool>,
195    /// Number of failed login attempts
196    pub failed_login_attempts: Option<u32>,
197}
198
199/// Request structure for creating a new user
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct CreateUserRequest {
202    /// Email address (required)
203    pub email_address: String,
204    /// First name (required)
205    pub first_name: String,
206    /// Last name (required)
207    pub last_name: String,
208    /// Username (required)
209    pub user_name: Option<String>,
210    /// User type (defaults to HUMAN)
211    pub user_type: Option<UserType>,
212    /// Whether to send email invitation
213    pub send_email_invitation: Option<bool>,
214    /// List of role IDs to assign
215    pub role_ids: Option<Vec<String>>,
216    /// List of team IDs to assign (at least one required for human users)
217    pub team_ids: Option<Vec<String>>,
218    /// List of permissions to assign
219    pub permissions: Option<Vec<Permission>>,
220}
221
222/// Request structure for updating a user
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct UpdateUserRequest {
225    /// Email address (required)
226    pub email_address: String,
227    /// Username (required)
228    pub user_name: String,
229    /// First name
230    pub first_name: Option<String>,
231    /// Last name
232    pub last_name: Option<String>,
233    /// Whether the account is active
234    pub active: Option<bool>,
235    /// List of role IDs to assign (required)
236    pub role_ids: Vec<String>,
237    /// List of team IDs to assign (required)
238    pub team_ids: Vec<String>,
239}
240
241/// Request structure for creating a team
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct CreateTeamRequest {
244    /// Team name (required)
245    pub team_name: String,
246    /// Team description
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub team_description: Option<String>,
249    /// Business unit ID
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub business_unit_id: Option<String>,
252    /// List of user IDs to add to the team
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub user_ids: Option<Vec<String>>,
255}
256
257/// Request structure for updating a team
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct UpdateTeamRequest {
260    /// Team name
261    pub team_name: Option<String>,
262    /// Team description
263    pub team_description: Option<String>,
264    /// Business unit ID
265    pub business_unit_id: Option<String>,
266    /// List of user IDs to add to the team (when using incremental=true)
267    pub user_ids: Option<Vec<String>>,
268}
269
270/// Request structure for creating API credentials
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct CreateApiCredentialRequest {
273    /// User ID to create credentials for (optional, defaults to current user)
274    pub user_id: Option<String>,
275    /// Expiration date (optional)
276    pub expiration_ts: Option<DateTime<Utc>>,
277}
278
279/// Response wrapper for paginated user results
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct UsersResponse {
282    /// List of users (direct array) or embedded
283    #[serde(default, skip_serializing_if = "Vec::is_empty")]
284    pub users: Vec<User>,
285    /// Embedded users (alternative structure)
286    #[serde(rename = "_embedded")]
287    pub embedded: Option<EmbeddedUsers>,
288    /// Pagination information
289    pub page: Option<PageInfo>,
290    /// Response links
291    #[serde(rename = "_links")]
292    pub links: Option<HashMap<String, Link>>,
293}
294
295/// Embedded users in the response
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct EmbeddedUsers {
298    /// List of users
299    pub users: Vec<User>,
300}
301
302/// Response wrapper for paginated team results
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct TeamsResponse {
305    /// List of teams (direct array) or embedded
306    #[serde(default, skip_serializing_if = "Vec::is_empty")]
307    pub teams: Vec<Team>,
308    /// Embedded teams (alternative structure)
309    #[serde(rename = "_embedded")]
310    pub embedded: Option<EmbeddedTeams>,
311    /// Pagination information
312    pub page: Option<PageInfo>,
313}
314
315/// Embedded teams in the response
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct EmbeddedTeams {
318    /// List of teams
319    pub teams: Vec<Team>,
320}
321
322/// Response wrapper for paginated role results
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct RolesResponse {
325    /// List of roles (direct array) or embedded
326    #[serde(default, skip_serializing_if = "Vec::is_empty")]
327    pub roles: Vec<Role>,
328    /// Embedded roles (alternative structure)
329    #[serde(rename = "_embedded")]
330    pub embedded: Option<EmbeddedRoles>,
331    /// Pagination information
332    pub page: Option<PageInfo>,
333}
334
335/// Embedded roles in the response
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct EmbeddedRoles {
338    /// List of roles
339    pub roles: Vec<Role>,
340}
341
342/// Pagination information
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct PageInfo {
345    /// Current page number
346    pub number: Option<u32>,
347    /// Number of items per page
348    pub size: Option<u32>,
349    /// Total number of elements
350    pub total_elements: Option<u64>,
351    /// Total number of pages
352    pub total_pages: Option<u32>,
353}
354
355/// Link information for navigation
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct Link {
358    /// URL for the link
359    pub href: String,
360}
361
362/// Query parameters for user search
363#[derive(Debug, Clone, Default)]
364pub struct UserQuery {
365    /// Filter by username
366    pub user_name: Option<String>,
367    /// Filter by email address
368    pub email_address: Option<String>,
369    /// Filter by role ID
370    pub role_id: Option<String>,
371    /// Filter by user type
372    pub user_type: Option<UserType>,
373    /// Filter by login status
374    pub login_status: Option<String>,
375    /// Page number
376    pub page: Option<u32>,
377    /// Items per page
378    pub size: Option<u32>,
379}
380
381impl UserQuery {
382    /// Create a new empty user query
383    #[must_use]
384    pub fn new() -> Self {
385        Self::default()
386    }
387
388    /// Filter by username
389    pub fn with_username(mut self, username: impl Into<String>) -> Self {
390        self.user_name = Some(username.into());
391        self
392    }
393
394    /// Filter by email address
395    pub fn with_email(mut self, email: impl Into<String>) -> Self {
396        self.email_address = Some(email.into());
397        self
398    }
399
400    /// Filter by role ID
401    pub fn with_role_id(mut self, role_id: impl Into<String>) -> Self {
402        self.role_id = Some(role_id.into());
403        self
404    }
405
406    /// Filter by user type
407    #[must_use]
408    pub fn with_user_type(mut self, user_type: UserType) -> Self {
409        self.user_type = Some(user_type);
410        self
411    }
412
413    /// Set pagination
414    #[must_use]
415    pub fn with_pagination(mut self, page: u32, size: u32) -> Self {
416        self.page = Some(page);
417        self.size = Some(size);
418        self
419    }
420
421    /// Convert to query parameters
422    #[must_use]
423    pub fn to_query_params(&self) -> Vec<(String, String)> {
424        Vec::from(self) // Delegate to trait
425    }
426}
427
428// Trait implementations for memory optimization
429impl From<&UserQuery> for Vec<(String, String)> {
430    fn from(query: &UserQuery) -> Self {
431        let mut params = Vec::new();
432
433        if let Some(ref username) = query.user_name {
434            params.push(("user_name".to_string(), username.clone())); // Still clone for borrowing
435        }
436        if let Some(ref email) = query.email_address {
437            params.push(("email_address".to_string(), email.clone()));
438        }
439        if let Some(ref role_id) = query.role_id {
440            params.push(("role_id".to_string(), role_id.clone()));
441        }
442        if let Some(ref user_type) = query.user_type {
443            let type_str = match user_type {
444                UserType::Human => "HUMAN",
445                UserType::ApiService => "API",
446                UserType::Saml => "SAML",
447                UserType::Vosp => "VOSP",
448            };
449            params.push(("user_type".to_string(), type_str.to_string()));
450        }
451        if let Some(ref login_status) = query.login_status {
452            params.push(("login_status".to_string(), login_status.clone()));
453        }
454        if let Some(page) = query.page {
455            params.push(("page".to_string(), page.to_string()));
456        }
457        if let Some(size) = query.size {
458            params.push(("size".to_string(), size.to_string()));
459        }
460
461        params
462    }
463}
464
465impl From<UserQuery> for Vec<(String, String)> {
466    fn from(query: UserQuery) -> Self {
467        let mut params = Vec::new();
468
469        if let Some(username) = query.user_name {
470            params.push(("user_name".to_string(), username)); // MOVE - no clone!
471        }
472        if let Some(email) = query.email_address {
473            params.push(("email_address".to_string(), email)); // MOVE - no clone!
474        }
475        if let Some(role_id) = query.role_id {
476            params.push(("role_id".to_string(), role_id)); // MOVE - no clone!
477        }
478        if let Some(user_type) = query.user_type {
479            let type_str = match user_type {
480                UserType::Human => "HUMAN",
481                UserType::ApiService => "API",
482                UserType::Saml => "SAML",
483                UserType::Vosp => "VOSP",
484            };
485            params.push(("user_type".to_string(), type_str.to_string()));
486        }
487        if let Some(login_status) = query.login_status {
488            params.push(("login_status".to_string(), login_status)); // MOVE - no clone!
489        }
490        if let Some(page) = query.page {
491            params.push(("page".to_string(), page.to_string()));
492        }
493        if let Some(size) = query.size {
494            params.push(("size".to_string(), size.to_string()));
495        }
496
497        params
498    }
499}
500
501/// Identity-specific error types
502#[derive(Debug)]
503#[must_use = "Need to handle all error enum types."]
504pub enum IdentityError {
505    /// General API error
506    Api(VeracodeError),
507    /// User not found
508    UserNotFound,
509    /// Team not found
510    TeamNotFound,
511    /// Role not found
512    RoleNotFound,
513    /// Invalid input data
514    InvalidInput(String),
515    /// Permission denied
516    PermissionDenied(String),
517    /// User already exists
518    UserAlreadyExists(String),
519    /// Team already exists
520    TeamAlreadyExists(String),
521}
522
523impl std::fmt::Display for IdentityError {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        match self {
526            IdentityError::Api(err) => write!(f, "API error: {err}"),
527            IdentityError::UserNotFound => write!(f, "User not found"),
528            IdentityError::TeamNotFound => write!(f, "Team not found"),
529            IdentityError::RoleNotFound => write!(f, "Role not found"),
530            IdentityError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
531            IdentityError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
532            IdentityError::UserAlreadyExists(msg) => write!(f, "User already exists: {msg}"),
533            IdentityError::TeamAlreadyExists(msg) => write!(f, "Team already exists: {msg}"),
534        }
535    }
536}
537
538impl std::error::Error for IdentityError {}
539
540impl From<VeracodeError> for IdentityError {
541    fn from(err: VeracodeError) -> Self {
542        IdentityError::Api(err)
543    }
544}
545
546impl From<reqwest::Error> for IdentityError {
547    fn from(err: reqwest::Error) -> Self {
548        IdentityError::Api(VeracodeError::Http(err))
549    }
550}
551
552impl From<serde_json::Error> for IdentityError {
553    fn from(err: serde_json::Error) -> Self {
554        IdentityError::Api(VeracodeError::Serialization(err))
555    }
556}
557
558/// Identity API operations
559pub struct IdentityApi<'a> {
560    client: &'a VeracodeClient,
561}
562
563impl<'a> IdentityApi<'a> {
564    ///
565    /// # Errors
566    ///
567    /// Returns an error if the API request fails, the resource is not found,
568    /// or authentication/authorization fails.
569    /// Create a new `IdentityApi` instance
570    #[must_use]
571    pub fn new(client: &'a VeracodeClient) -> Self {
572        Self { client }
573    }
574
575    /// List users with optional filtering
576    ///
577    /// # Arguments
578    ///
579    /// * `query` - Optional query parameters for filtering users
580    ///
581    /// # Returns
582    ///
583    /// A `Result` containing a list of users or an error
584    ///
585    /// # Errors
586    ///
587    /// Returns an error if the API request fails, the identity resource is not found,
588    /// or authentication/authorization fails.
589    pub async fn list_users(&self, query: Option<UserQuery>) -> Result<Vec<User>, IdentityError> {
590        let endpoint = "/api/authn/v2/users";
591        let query_params = query.as_ref().map(Vec::from);
592
593        let response = self.client.get(endpoint, query_params.as_deref()).await?;
594
595        let status = response.status().as_u16();
596        match status {
597            200 => {
598                let response_text = response.text().await?;
599
600                // Try embedded response format first
601                if let Ok(users_response) = serde_json::from_str::<UsersResponse>(&response_text) {
602                    let users = if !users_response.users.is_empty() {
603                        users_response.users
604                    } else if let Some(embedded) = users_response.embedded {
605                        embedded.users
606                    } else {
607                        Vec::new()
608                    };
609                    return Ok(users);
610                }
611
612                // Try direct array as fallback
613                if let Ok(users) = serde_json::from_str::<Vec<User>>(&response_text) {
614                    return Ok(users);
615                }
616
617                Err(IdentityError::Api(VeracodeError::InvalidResponse(
618                    "Unable to parse users response".to_string(),
619                )))
620            }
621            404 => Err(IdentityError::UserNotFound),
622            _ => {
623                let error_text = response.text().await.unwrap_or_default();
624                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
625                    "HTTP {status}: {error_text}"
626                ))))
627            }
628        }
629    }
630
631    /// Get a specific user by ID
632    ///
633    /// # Arguments
634    ///
635    /// * `user_id` - The ID of the user to retrieve
636    ///
637    /// # Returns
638    ///
639    /// A `Result` containing the user or an error
640    ///
641    /// # Errors
642    ///
643    /// Returns an error if the API request fails, the identity resource is not found,
644    /// or authentication/authorization fails.
645    pub async fn get_user(&self, user_id: &str) -> Result<User, IdentityError> {
646        let endpoint = format!("/api/authn/v2/users/{user_id}");
647
648        let response = self.client.get(&endpoint, None).await?;
649
650        let status = response.status().as_u16();
651        match status {
652            200 => {
653                let user: User = response.json().await?;
654                Ok(user)
655            }
656            404 => Err(IdentityError::UserNotFound),
657            _ => {
658                let error_text = response.text().await.unwrap_or_default();
659                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
660                    "HTTP {status}: {error_text}"
661                ))))
662            }
663        }
664    }
665
666    /// Create a new user
667    ///
668    /// # Arguments
669    ///
670    /// * `request` - The user creation request
671    ///
672    /// # Returns
673    ///
674    /// A `Result` containing the created user or an error
675    ///
676    /// # Errors
677    ///
678    /// Returns an error if the API request fails, the identity resource is not found,
679    /// or authentication/authorization fails.
680    pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, IdentityError> {
681        let endpoint = "/api/authn/v2/users";
682
683        let mut fixed_request = request.clone();
684
685        // Both email_address and user_name are required by the API
686        if fixed_request.user_name.is_none() {
687            return Err(IdentityError::InvalidInput(
688                "user_name is required".to_string(),
689            ));
690        }
691
692        // Team membership validation: ALL users must either have team assignment OR "No Team Restrictions" role
693        let has_teams = fixed_request
694            .team_ids
695            .as_ref()
696            .is_some_and(|teams| !teams.is_empty());
697
698        if !has_teams {
699            // Check if user has "No Team Restrictions" role (works for both human and API users)
700            let has_no_team_restriction_role = if let Some(ref role_ids) = fixed_request.role_ids {
701                // Get available roles to check role descriptions
702                let roles = self.list_roles().await?;
703                role_ids.iter().any(|role_id| {
704                    roles.iter().any(|r| {
705                        &r.role_id == role_id
706                            && (r
707                                .role_description
708                                .as_ref()
709                                .is_some_and(|desc| desc == "No Team Restriction API")
710                                || r.role_name.to_lowercase() == "noteamrestrictionapi")
711                    })
712                })
713            } else {
714                false
715            };
716
717            if !has_no_team_restriction_role {
718                return Err(IdentityError::InvalidInput(
719                    "You must select at least one team for this user, or select No Team Restrictions role".to_string()
720                ));
721            }
722        }
723
724        // Determine user type flags early
725        let is_api_user = matches!(fixed_request.user_type, Some(UserType::ApiService));
726        let is_saml_user = matches!(fixed_request.user_type, Some(UserType::Saml));
727
728        // Validate role assignments for API users
729        if is_api_user && let Some(ref provided_role_ids) = fixed_request.role_ids {
730            let roles = self.list_roles().await?;
731
732            // Define human-only role descriptions (from userrolesbydescription file)
733            let human_role_descriptions = [
734                "Creator",
735                "Executive",
736                "Mitigation Approver",
737                "Reviewer",
738                "Sandbox User",
739                "Security Lead",
740                "Team Admin",
741                "Workspace Editor",
742                "Analytics Creator",
743                "Delete Scans",
744                "Greenlight IDE User",
745                "Policy Administrator",
746                "Sandbox Administrator",
747                "Security Insights",
748                "Submitter",
749                "Workspace Administrator",
750            ];
751
752            for role_id in provided_role_ids {
753                if let Some(role) = roles.iter().find(|r| &r.role_id == role_id) {
754                    // Check if this is a human-only role
755                    if let Some(ref desc) = role.role_description
756                        && human_role_descriptions.contains(&desc.as_str())
757                    {
758                        return Err(IdentityError::InvalidInput(format!(
759                            "Role '{}' (description: '{}') is a human-only role and cannot be assigned to API users.",
760                            role.role_name, desc
761                        )));
762                    }
763
764                    // API users can only be assigned roles where is_api is true
765                    if role.is_api != Some(true) {
766                        return Err(IdentityError::InvalidInput(format!(
767                            "Role '{}' (is_api: {}) cannot be assigned to API users. API users can only be assigned API roles.",
768                            role.role_name,
769                            role.is_api.map_or("None".to_string(), |b| b.to_string())
770                        )));
771                    }
772                }
773            }
774        }
775
776        // If no roles provided, assign default roles (any scan and submitter)
777        if fixed_request
778            .role_ids
779            .as_ref()
780            .is_none_or(|roles| roles.is_empty())
781        {
782            // Get available roles to find default ones
783            let roles = self.list_roles().await?;
784            let mut default_role_ids = Vec::new();
785
786            // Based on Veracode documentation, assign appropriate default roles
787            if is_api_user {
788                // For API service accounts, assign apisubmitanyscan role
789                if let Some(api_submit_role) = roles
790                    .iter()
791                    .find(|r| r.role_name.to_lowercase() == "apisubmitanyscan")
792                {
793                    default_role_ids.push(api_submit_role.role_id.clone());
794                }
795
796                // Always assign noteamrestrictionapi role for API users (required for team restrictions)
797                if let Some(noteam_role) = roles
798                    .iter()
799                    .find(|r| r.role_name.to_lowercase() == "noteamrestrictionapi")
800                {
801                    default_role_ids.push(noteam_role.role_id.clone());
802                }
803            } else {
804                // For human users (Human/SAML/VOSP), start with Submitter as it's the most basic role
805                if let Some(submitter_role) = roles.iter().find(|r| {
806                    r.role_description
807                        .as_ref()
808                        .is_some_and(|desc| desc == "Submitter")
809                }) {
810                    default_role_ids.push(submitter_role.role_id.clone());
811                } else if let Some(creator_role) = roles.iter().find(|r| {
812                    r.role_description
813                        .as_ref()
814                        .is_some_and(|desc| desc == "Creator")
815                }) {
816                    default_role_ids.push(creator_role.role_id.clone());
817                } else if let Some(reviewer_role) = roles.iter().find(|r| {
818                    r.role_description
819                        .as_ref()
820                        .is_some_and(|desc| desc == "Reviewer")
821                }) {
822                    default_role_ids.push(reviewer_role.role_id.clone());
823                }
824
825                // If user has no teams, also assign "No Team Restrictions" role
826                if !has_teams
827                    && let Some(no_team_role) = roles.iter().find(|r| {
828                        r.role_description
829                            .as_ref()
830                            .is_some_and(|desc| desc == "No Team Restriction API")
831                            || r.role_name.to_lowercase() == "noteamrestrictionapi"
832                    })
833                {
834                    default_role_ids.push(no_team_role.role_id.clone());
835                }
836            }
837
838            // If we found default roles, use them
839            if !default_role_ids.is_empty() {
840                fixed_request.role_ids = Some(default_role_ids);
841            }
842        }
843
844        // If no permissions provided, assign default permissions based on user type
845        if fixed_request
846            .permissions
847            .as_ref()
848            .is_none_or(|p| p.is_empty())
849        {
850            if is_api_user {
851                // For API users, assign "apiUser" permission (from defaultapiuserperm file)
852                let api_user_permission = Permission {
853                    permission_id: None,
854                    permission_name: "apiUser".to_string(),
855                    description: Some("API User".to_string()),
856                    api_only: Some(false),
857                    ui_only: Some(false),
858                };
859                fixed_request.permissions = Some(vec![api_user_permission]);
860            } else {
861                // For human users, assign "humanUser" permission
862                let human_user_permission = Permission {
863                    permission_id: None,
864                    permission_name: "humanUser".to_string(),
865                    description: Some("Human User".to_string()),
866                    api_only: Some(false),
867                    ui_only: Some(false),
868                };
869                fixed_request.permissions = Some(vec![human_user_permission]);
870            }
871        }
872
873        // Create the payload with the correct role structure
874        let roles_payload = if let Some(ref role_ids) = fixed_request.role_ids {
875            role_ids
876                .iter()
877                .map(|id| serde_json::json!({"role_id": id}))
878                .collect::<Vec<_>>()
879        } else {
880            Vec::new()
881        };
882
883        // Create the payload with the correct team structure
884        let teams_payload = if let Some(ref team_ids) = fixed_request.team_ids {
885            team_ids
886                .iter()
887                .map(|id| serde_json::json!({"team_id": id}))
888                .collect::<Vec<_>>()
889        } else {
890            Vec::new()
891        };
892
893        // Create the payload with the correct permissions structure
894        let permissions_payload = if let Some(ref permissions) = fixed_request.permissions {
895            permissions
896                .iter()
897                .map(|p| {
898                    serde_json::json!({
899                        "permission_name": p.permission_name,
900                        "api_only": p.api_only.unwrap_or(false),
901                        "ui_only": p.ui_only.unwrap_or(false)
902                    })
903                })
904                .collect::<Vec<_>>()
905        } else {
906            Vec::new()
907        };
908
909        // Build payload conditionally to exclude null fields and user_type for API users
910        let mut payload = serde_json::json!({
911            "email_address": fixed_request.email_address,
912            "first_name": fixed_request.first_name,
913            "last_name": fixed_request.last_name,
914            "apiUser": is_api_user,
915            "samlUser": is_saml_user,
916            "active": true, // New users are active by default
917            "send_email_invitation": fixed_request.send_email_invitation.unwrap_or(false)
918        });
919
920        // Add user_name only if it's not None
921        if let Some(ref user_name) = fixed_request.user_name
922            && let Some(obj) = payload.as_object_mut()
923        {
924            obj.insert("user_name".to_string(), serde_json::json!(user_name));
925        }
926
927        // Add roles only if not empty
928        if !roles_payload.is_empty()
929            && let Some(obj) = payload.as_object_mut()
930        {
931            obj.insert("roles".to_string(), serde_json::json!(roles_payload));
932        }
933
934        // Add teams only if not empty
935        if !teams_payload.is_empty()
936            && let Some(obj) = payload.as_object_mut()
937        {
938            obj.insert("teams".to_string(), serde_json::json!(teams_payload));
939        }
940
941        // Add permissions only if not empty
942        if !permissions_payload.is_empty()
943            && let Some(obj) = payload.as_object_mut()
944        {
945            obj.insert(
946                "permissions".to_string(),
947                serde_json::json!(permissions_payload),
948            );
949        }
950
951        let response = self.client.post(endpoint, Some(&payload)).await?;
952
953        let status = response.status().as_u16();
954        match status {
955            200 | 201 => {
956                let user: User = response.json().await?;
957                Ok(user)
958            }
959            400 => {
960                let error_text = response.text().await.unwrap_or_default();
961                if error_text.contains("already exists") {
962                    Err(IdentityError::UserAlreadyExists(error_text))
963                } else {
964                    Err(IdentityError::InvalidInput(error_text))
965                }
966            }
967            403 => {
968                let error_text = response.text().await.unwrap_or_default();
969                Err(IdentityError::PermissionDenied(error_text))
970            }
971            415 => {
972                let error_text = response.text().await.unwrap_or_default();
973                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
974                    "HTTP 415 Unsupported Media Type: {error_text}"
975                ))))
976            }
977            _ => {
978                let error_text = response.text().await.unwrap_or_default();
979                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
980                    "HTTP {status}: {error_text}"
981                ))))
982            }
983        }
984    }
985
986    /// Update an existing user
987    ///
988    /// # Arguments
989    ///
990    /// * `user_id` - The ID of the user to update
991    /// * `request` - The user update request
992    ///
993    /// # Returns
994    ///
995    /// A `Result` containing the updated user or an error
996    ///
997    /// # Errors
998    ///
999    /// Returns an error if the API request fails, the identity resource is not found,
1000    /// or authentication/authorization fails.
1001    pub async fn update_user(
1002        &self,
1003        user_id: &str,
1004        request: UpdateUserRequest,
1005    ) -> Result<User, IdentityError> {
1006        let endpoint = format!("/api/authn/v2/users/{user_id}");
1007
1008        // Create the payload with the correct role and team structure
1009        let roles_payload = request
1010            .role_ids
1011            .iter()
1012            .map(|id| serde_json::json!({"role_id": id}))
1013            .collect::<Vec<_>>();
1014
1015        let teams_payload = request
1016            .team_ids
1017            .iter()
1018            .map(|id| serde_json::json!({"team_id": id}))
1019            .collect::<Vec<_>>();
1020
1021        let payload = serde_json::json!({
1022            "email_address": request.email_address,
1023            "user_name": request.user_name,
1024            "first_name": request.first_name,
1025            "last_name": request.last_name,
1026            "active": request.active,
1027            "roles": roles_payload,
1028            "teams": teams_payload
1029        });
1030
1031        let response = self.client.put(&endpoint, Some(&payload)).await?;
1032
1033        let status = response.status().as_u16();
1034        match status {
1035            200 => {
1036                let user: User = response.json().await?;
1037                Ok(user)
1038            }
1039            400 => {
1040                let error_text = response.text().await.unwrap_or_default();
1041                Err(IdentityError::InvalidInput(error_text))
1042            }
1043            403 => {
1044                let error_text = response.text().await.unwrap_or_default();
1045                Err(IdentityError::PermissionDenied(error_text))
1046            }
1047            404 => Err(IdentityError::UserNotFound),
1048            _ => {
1049                let error_text = response.text().await.unwrap_or_default();
1050                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1051                    "HTTP {status}: {error_text}"
1052                ))))
1053            }
1054        }
1055    }
1056
1057    /// Delete a user
1058    ///
1059    /// # Arguments
1060    ///
1061    /// * `user_id` - The ID of the user to delete
1062    ///
1063    /// # Returns
1064    ///
1065    /// A `Result` indicating success or failure
1066    ///
1067    /// # Errors
1068    ///
1069    /// Returns an error if the API request fails, the identity resource is not found,
1070    /// or authentication/authorization fails.
1071    pub async fn delete_user(&self, user_id: &str) -> Result<(), IdentityError> {
1072        let endpoint = format!("/api/authn/v2/users/{user_id}");
1073
1074        let response = self.client.delete(&endpoint).await?;
1075
1076        let status = response.status().as_u16();
1077        match status {
1078            200 | 204 => Ok(()),
1079            403 => {
1080                let error_text = response.text().await.unwrap_or_default();
1081                Err(IdentityError::PermissionDenied(error_text))
1082            }
1083            404 => Err(IdentityError::UserNotFound),
1084            _ => {
1085                let error_text = response.text().await.unwrap_or_default();
1086                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1087                    "HTTP {status}: {error_text}"
1088                ))))
1089            }
1090        }
1091    }
1092
1093    /// List all roles
1094    ///
1095    /// # Returns
1096    ///
1097    /// A `Result` containing a list of roles or an error
1098    ///
1099    /// # Errors
1100    ///
1101    /// Returns an error if the API request fails, the identity resource is not found,
1102    /// or authentication/authorization fails.
1103    pub async fn list_roles(&self) -> Result<Vec<Role>, IdentityError> {
1104        let endpoint = "/api/authn/v2/roles";
1105        let mut all_roles = Vec::new();
1106        let mut page: u32 = 0;
1107        let page_size = 500;
1108
1109        // Simple pagination loop - fetch pages until empty
1110        loop {
1111            let query_params = vec![
1112                ("page".to_string(), page.to_string()),
1113                ("size".to_string(), page_size.to_string()),
1114            ];
1115
1116            let response = self.client.get(endpoint, Some(&query_params)).await?;
1117            let status = response.status().as_u16();
1118
1119            match status {
1120                200 => {
1121                    let response_text = response.text().await?;
1122
1123                    // Try embedded response format
1124                    if let Ok(roles_response) =
1125                        serde_json::from_str::<RolesResponse>(&response_text)
1126                    {
1127                        let page_roles = if !roles_response.roles.is_empty() {
1128                            roles_response.roles
1129                        } else if let Some(embedded) = roles_response.embedded {
1130                            embedded.roles
1131                        } else {
1132                            Vec::new()
1133                        };
1134
1135                        if page_roles.is_empty() {
1136                            break; // No more roles to fetch
1137                        }
1138
1139                        all_roles.extend(page_roles);
1140                        page = page.saturating_add(1);
1141
1142                        // Check pagination info if available
1143                        if let Some(page_info) = roles_response.page
1144                            && let (Some(current_page), Some(total_pages)) =
1145                                (page_info.number, page_info.total_pages)
1146                            && current_page.saturating_add(1) >= total_pages
1147                        {
1148                            break;
1149                        }
1150
1151                        continue;
1152                    }
1153
1154                    // Try direct array as fallback
1155                    if let Ok(roles) = serde_json::from_str::<Vec<Role>>(&response_text) {
1156                        if roles.is_empty() {
1157                            break;
1158                        }
1159                        all_roles.extend(roles);
1160                        page = page.saturating_add(1);
1161                        continue;
1162                    }
1163
1164                    // If we can't parse, maybe it's the first page without pagination
1165                    if page == 0 {
1166                        return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1167                            "Unable to parse roles response: {response_text}"
1168                        ))));
1169                    }
1170                    break; // End of pages
1171                }
1172                _ => {
1173                    let error_text = response.text().await.unwrap_or_default();
1174                    return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1175                        "HTTP {status}: {error_text}"
1176                    ))));
1177                }
1178            }
1179        }
1180
1181        Ok(all_roles)
1182    }
1183
1184    /// List all teams with pagination support
1185    ///
1186    /// # Returns
1187    ///
1188    /// A `Result` containing a list of all teams across all pages or an error
1189    ///
1190    /// # Errors
1191    ///
1192    /// Returns an error if the API request fails, the identity resource is not found,
1193    /// or authentication/authorization fails.
1194    pub async fn list_teams(&self) -> Result<Vec<Team>, IdentityError> {
1195        let endpoint = "/api/authn/v2/teams";
1196        let mut all_teams = Vec::new();
1197        let mut page: u32 = 0;
1198        let page_size = 500;
1199
1200        // Simple pagination loop - fetch pages until empty
1201        loop {
1202            // Safety check to prevent infinite loops
1203            if page > 100 {
1204                break;
1205            }
1206
1207            let query_params = vec![
1208                ("page".to_string(), page.to_string()),
1209                ("size".to_string(), page_size.to_string()),
1210            ];
1211
1212            let response = self.client.get(endpoint, Some(&query_params)).await?;
1213            let status = response.status().as_u16();
1214
1215            match status {
1216                200 => {
1217                    let response_text = response.text().await?;
1218
1219                    // Try embedded response format first
1220                    if let Ok(teams_response) =
1221                        serde_json::from_str::<TeamsResponse>(&response_text)
1222                    {
1223                        let page_teams = if !teams_response.teams.is_empty() {
1224                            teams_response.teams
1225                        } else if let Some(embedded) = teams_response.embedded {
1226                            embedded.teams
1227                        } else {
1228                            Vec::new()
1229                        };
1230
1231                        if page_teams.is_empty() {
1232                            break; // No more teams to fetch
1233                        }
1234
1235                        all_teams.extend(page_teams);
1236                        page = page.saturating_add(1);
1237
1238                        // Check pagination info if available
1239                        if let Some(page_info) = teams_response.page
1240                            && let (Some(current_page), Some(total_pages)) =
1241                                (page_info.number, page_info.total_pages)
1242                            && current_page.saturating_add(1) >= total_pages
1243                        {
1244                            break; // Last page reached
1245                        }
1246
1247                        continue;
1248                    }
1249
1250                    // Try direct array as fallback
1251                    if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1252                        if teams.is_empty() {
1253                            break;
1254                        }
1255                        all_teams.extend(teams);
1256                        page = page.saturating_add(1);
1257                        continue;
1258                    }
1259
1260                    // If we can't parse, maybe it's the first page without pagination
1261                    if page == 0 {
1262                        return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1263                            "Unable to parse teams response".to_string(),
1264                        )));
1265                    }
1266                    // We've gotten some teams, but this page failed - break
1267                    break;
1268                }
1269                _ => {
1270                    let error_text = response.text().await.unwrap_or_default();
1271                    return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1272                        "HTTP {status}: {error_text}"
1273                    ))));
1274                }
1275            }
1276        }
1277
1278        Ok(all_teams)
1279    }
1280
1281    /// Create a new team
1282    ///
1283    /// # Arguments
1284    ///
1285    /// * `request` - The team creation request
1286    ///
1287    /// # Returns
1288    ///
1289    /// A `Result` containing the created team or an error
1290    ///
1291    /// # Errors
1292    ///
1293    /// Returns an error if the API request fails, the identity resource is not found,
1294    /// or authentication/authorization fails.
1295    pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team, IdentityError> {
1296        let endpoint = "/api/authn/v2/teams";
1297
1298        let response = self.client.post(endpoint, Some(&request)).await?;
1299
1300        let status = response.status().as_u16();
1301        match status {
1302            200 | 201 => {
1303                let team: Team = response.json().await?;
1304                Ok(team)
1305            }
1306            400 => {
1307                let error_text = response.text().await.unwrap_or_default();
1308                if error_text.contains("already exists") {
1309                    Err(IdentityError::TeamAlreadyExists(error_text))
1310                } else {
1311                    Err(IdentityError::InvalidInput(error_text))
1312                }
1313            }
1314            403 => {
1315                let error_text = response.text().await.unwrap_or_default();
1316                Err(IdentityError::PermissionDenied(error_text))
1317            }
1318            _ => {
1319                let error_text = response.text().await.unwrap_or_default();
1320                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1321                    "HTTP {status}: {error_text}"
1322                ))))
1323            }
1324        }
1325    }
1326
1327    /// Delete a team
1328    ///
1329    /// # Arguments
1330    ///
1331    /// * `team_id` - The ID of the team to delete
1332    ///
1333    /// # Returns
1334    ///
1335    /// A `Result` indicating success or failure
1336    ///
1337    /// # Errors
1338    ///
1339    /// Returns an error if the API request fails, the identity resource is not found,
1340    /// or authentication/authorization fails.
1341    pub async fn delete_team(&self, team_id: &str) -> Result<(), IdentityError> {
1342        let endpoint = format!("/api/authn/v2/teams/{team_id}");
1343
1344        let response = self.client.delete(&endpoint).await?;
1345
1346        let status = response.status().as_u16();
1347        match status {
1348            200 | 204 => Ok(()),
1349            403 => {
1350                let error_text = response.text().await.unwrap_or_default();
1351                Err(IdentityError::PermissionDenied(error_text))
1352            }
1353            404 => Err(IdentityError::TeamNotFound),
1354            _ => {
1355                let error_text = response.text().await.unwrap_or_default();
1356                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1357                    "HTTP {status}: {error_text}"
1358                ))))
1359            }
1360        }
1361    }
1362
1363    /// Get a team by its name
1364    ///
1365    /// Searches for a team by exact name match. The API may return multiple teams
1366    /// that match the search criteria, so this method performs an exact string
1367    /// comparison to find the specific team requested.
1368    ///
1369    /// # Arguments
1370    ///
1371    /// * `team_name` - The exact name of the team to find
1372    ///
1373    /// # Returns
1374    ///
1375    /// A `Result` containing an `Option<Team>` if found, or an error
1376    ///
1377    /// # Errors
1378    ///
1379    /// Returns an error if the API request fails, the identity resource is not found,
1380    /// or authentication/authorization fails.
1381    pub async fn get_team_by_name(&self, team_name: &str) -> Result<Option<Team>, IdentityError> {
1382        let endpoint = "/api/authn/v2/teams";
1383
1384        let query_params = vec![
1385            ("page".to_string(), "0".to_string()),
1386            ("size".to_string(), "50".to_string()),
1387            ("team_name".to_string(), team_name.to_string()),
1388            ("ignore_self_teams".to_string(), "false".to_string()),
1389            ("only_manageable".to_string(), "false".to_string()),
1390            ("deleted".to_string(), "false".to_string()),
1391        ];
1392
1393        log::debug!("🔍 Team lookup request - endpoint: {}", endpoint);
1394        log::debug!(
1395            "🔍 Team lookup request - query parameters: [{}]",
1396            query_params
1397                .iter()
1398                .map(|(k, v)| format!("{}={}", k, v))
1399                .collect::<Vec<_>>()
1400                .join(", ")
1401        );
1402        log::debug!(
1403            "🔍 Team lookup request - searching for team: '{}'",
1404            team_name
1405        );
1406
1407        let response = self.client.get(endpoint, Some(&query_params)).await?;
1408        let status = response.status().as_u16();
1409
1410        log::debug!("🔍 Team lookup response - HTTP status: {}", status);
1411
1412        match status {
1413            200 => {
1414                let response_text = response.text().await?;
1415                log::debug!("🔍 Team lookup response - body: {}", response_text);
1416
1417                // Helper closure to process teams list and find exact case-insensitive match
1418                let process_teams = |teams: Vec<Team>, format_description: &str| -> Option<Team> {
1419                    let team_count = teams.len();
1420                    log::debug!(
1421                        "🔍 Team lookup response - found {} teams total ({})",
1422                        team_count,
1423                        format_description
1424                    );
1425                    for (i, team) in teams.iter().enumerate() {
1426                        log::debug!(
1427                            "🔍 Team lookup response - team {}: '{}' (GUID: {})",
1428                            i.saturating_add(1),
1429                            team.team_name,
1430                            team.team_id
1431                        );
1432                    }
1433
1434                    // Find exact case-insensitive match by team name
1435                    // Note: API search is case-insensitive, so we match case-insensitively too
1436                    let found_team = teams
1437                        .into_iter()
1438                        .find(|team| team.team_name.to_lowercase() == team_name.to_lowercase());
1439                    if let Some(ref team) = found_team {
1440                        log::debug!(
1441                            "🔍 Team lookup result - found case-insensitive match: '{}' (searched for '{}') with GUID: {}",
1442                            team.team_name,
1443                            team_name,
1444                            team.team_id
1445                        );
1446                    } else {
1447                        log::debug!(
1448                            "🔍 Team lookup result - no case-insensitive match for '{}' among {} teams",
1449                            team_name,
1450                            team_count
1451                        );
1452                    }
1453                    found_team
1454                };
1455
1456                // Parse response - prioritize embedded format as shown in your example
1457                if let Ok(teams_response) = serde_json::from_str::<TeamsResponse>(&response_text) {
1458                    let teams = if let Some(embedded) = teams_response.embedded {
1459                        embedded.teams
1460                    } else if !teams_response.teams.is_empty() {
1461                        teams_response.teams
1462                    } else {
1463                        Vec::new()
1464                    };
1465                    Ok(process_teams(teams, "embedded format"))
1466                } else if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1467                    // Fallback for direct array (less common based on your example)
1468                    Ok(process_teams(teams, "direct array"))
1469                } else {
1470                    Err(IdentityError::Api(VeracodeError::InvalidResponse(
1471                        "Unable to parse team response".to_string(),
1472                    )))
1473                }
1474            }
1475            404 => {
1476                log::debug!(
1477                    "🔍 Team lookup result - HTTP 404: team '{}' not found",
1478                    team_name
1479                );
1480                Ok(None) // Team not found
1481            }
1482            403 => {
1483                let error_text = response.text().await.unwrap_or_default();
1484                log::debug!(
1485                    "🔍 Team lookup error - HTTP 403: permission denied - {}",
1486                    error_text
1487                );
1488                Err(IdentityError::PermissionDenied(error_text))
1489            }
1490            _ => {
1491                let error_text = response.text().await.unwrap_or_default();
1492                log::debug!("🔍 Team lookup error - HTTP {}: {}", status, error_text);
1493                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1494                    "HTTP {status}: {error_text}"
1495                ))))
1496            }
1497        }
1498    }
1499
1500    /// Get a team's GUID by its name
1501    ///
1502    /// This is a convenience method for application creation workflows where
1503    /// only the team GUID is needed.
1504    ///
1505    /// # Arguments
1506    ///
1507    /// * `team_name` - The exact name of the team to find
1508    ///
1509    /// # Returns
1510    ///
1511    /// A `Result` containing an `Option<String>` with the team's GUID if found, or an error
1512    ///
1513    /// # Errors
1514    ///
1515    /// Returns an error if the API request fails, the identity resource is not found,
1516    /// or authentication/authorization fails.
1517    pub async fn get_team_guid_by_name(
1518        &self,
1519        team_name: &str,
1520    ) -> Result<Option<String>, IdentityError> {
1521        match self.get_team_by_name(team_name).await? {
1522            Some(team) => Ok(Some(team.team_id)),
1523            None => Ok(None),
1524        }
1525    }
1526
1527    /// Create API credentials
1528    ///
1529    /// # Arguments
1530    ///
1531    /// * `request` - The API credential creation request
1532    ///
1533    /// # Returns
1534    ///
1535    /// A `Result` containing the created API credentials or an error
1536    ///
1537    /// # Errors
1538    ///
1539    /// Returns an error if the API request fails, the identity resource is not found,
1540    /// or authentication/authorization fails.
1541    pub async fn create_api_credentials(
1542        &self,
1543        request: CreateApiCredentialRequest,
1544    ) -> Result<ApiCredential, IdentityError> {
1545        let endpoint = "/api/authn/v2/api_credentials";
1546
1547        let response = self.client.post(endpoint, Some(&request)).await?;
1548
1549        let status = response.status().as_u16();
1550        match status {
1551            200 | 201 => {
1552                let credentials: ApiCredential = response.json().await?;
1553                Ok(credentials)
1554            }
1555            400 => {
1556                let error_text = response.text().await.unwrap_or_default();
1557                Err(IdentityError::InvalidInput(error_text))
1558            }
1559            403 => {
1560                let error_text = response.text().await.unwrap_or_default();
1561                Err(IdentityError::PermissionDenied(error_text))
1562            }
1563            _ => {
1564                let error_text = response.text().await.unwrap_or_default();
1565                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1566                    "HTTP {status}: {error_text}"
1567                ))))
1568            }
1569        }
1570    }
1571
1572    /// Revoke API credentials
1573    ///
1574    /// # Arguments
1575    ///
1576    /// * `api_creds_id` - The ID of the API credentials to revoke
1577    ///
1578    /// # Returns
1579    ///
1580    /// A `Result` indicating success or failure
1581    ///
1582    /// # Errors
1583    ///
1584    /// Returns an error if the API request fails, the identity resource is not found,
1585    /// or authentication/authorization fails.
1586    pub async fn revoke_api_credentials(&self, api_creds_id: &str) -> Result<(), IdentityError> {
1587        let endpoint = format!("/api/authn/v2/api_credentials/{api_creds_id}");
1588
1589        let response = self.client.delete(&endpoint).await?;
1590
1591        let status = response.status().as_u16();
1592        match status {
1593            200 | 204 => Ok(()), // Accept both 200 OK and 204 No Content as success
1594            403 => {
1595                let error_text = response.text().await.unwrap_or_default();
1596                Err(IdentityError::PermissionDenied(error_text))
1597            }
1598            404 => Err(IdentityError::UserNotFound), // API credentials not found
1599            _ => {
1600                let error_text = response.text().await.unwrap_or_default();
1601                Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1602                    "HTTP {status}: {error_text}"
1603                ))))
1604            }
1605        }
1606    }
1607}
1608
1609/// Convenience methods for common operations
1610impl<'a> IdentityApi<'a> {
1611    /// Find a user by email address
1612    ///
1613    /// # Arguments
1614    ///
1615    /// * `email` - The email address to search for
1616    ///
1617    /// # Returns
1618    ///
1619    /// A `Result` containing the user if found, or None if not found
1620    ///
1621    /// # Errors
1622    ///
1623    /// Returns an error if the API request fails, the identity resource is not found,
1624    /// or authentication/authorization fails.
1625    pub async fn find_user_by_email(&self, email: &str) -> Result<Option<User>, IdentityError> {
1626        let query = UserQuery::new().with_email(email);
1627        let users = self.list_users(Some(query)).await?;
1628        Ok(users.into_iter().find(|u| u.email_address == email))
1629    }
1630
1631    /// Find a user by username
1632    ///
1633    /// # Arguments
1634    ///
1635    /// * `username` - The username to search for
1636    ///
1637    /// # Returns
1638    ///
1639    /// A `Result` containing the user if found, or None if not found
1640    ///
1641    /// # Errors
1642    ///
1643    /// Returns an error if the API request fails, the identity resource is not found,
1644    /// or authentication/authorization fails.
1645    pub async fn find_user_by_username(
1646        &self,
1647        username: &str,
1648    ) -> Result<Option<User>, IdentityError> {
1649        let query = UserQuery::new().with_username(username);
1650        let users = self.list_users(Some(query)).await?;
1651        Ok(users.into_iter().find(|u| u.user_name == username))
1652    }
1653
1654    /// Create a simple user with basic information
1655    ///
1656    /// # Arguments
1657    ///
1658    /// * `email` - User's email address
1659    /// * `username` - User's username
1660    /// * `first_name` - User's first name
1661    /// * `last_name` - User's last name
1662    /// * `team_ids` - List of team IDs to assign (at least one required)
1663    ///
1664    /// # Returns
1665    ///
1666    /// A `Result` containing the created user or an error
1667    ///
1668    /// # Errors
1669    ///
1670    /// Returns an error if the API request fails, the identity resource is not found,
1671    /// or authentication/authorization fails.
1672    pub async fn create_simple_user(
1673        &self,
1674        email: &str,
1675        username: &str,
1676        first_name: &str,
1677        last_name: &str,
1678        team_ids: Vec<String>,
1679    ) -> Result<User, IdentityError> {
1680        let request = CreateUserRequest {
1681            email_address: email.to_string(),
1682            first_name: first_name.to_string(),
1683            last_name: last_name.to_string(),
1684            user_name: Some(username.to_string()),
1685            user_type: Some(UserType::Human),
1686            send_email_invitation: Some(true),
1687            role_ids: None, // Will be auto-assigned to "any scan" and "submitter" roles
1688            team_ids: Some(team_ids),
1689            permissions: None, // Will use default permissions for human users
1690        };
1691
1692        self.create_user(request).await
1693    }
1694
1695    /// Create an API service account
1696    ///
1697    /// # Arguments
1698    ///
1699    /// * `email` - Service account email address
1700    /// * `username` - Service account username
1701    /// * `first_name` - Service account first name
1702    /// * `last_name` - Service account last name
1703    /// * `role_ids` - List of role IDs to assign
1704    /// * `team_ids` - Optional list of team IDs to assign
1705    ///
1706    /// # Returns
1707    ///
1708    /// A `Result` containing the created user or an error
1709    ///
1710    /// # Errors
1711    ///
1712    /// Returns an error if the API request fails, the identity resource is not found,
1713    /// or authentication/authorization fails.
1714    pub async fn create_api_service_account(
1715        &self,
1716        email: &str,
1717        username: &str,
1718        first_name: &str,
1719        last_name: &str,
1720        role_ids: Vec<String>,
1721        team_ids: Option<Vec<String>>,
1722    ) -> Result<User, IdentityError> {
1723        let request = CreateUserRequest {
1724            email_address: email.to_string(),
1725            first_name: first_name.to_string(),
1726            last_name: last_name.to_string(),
1727            user_name: Some(username.to_string()),
1728            user_type: Some(UserType::ApiService), // Still needed for internal logic
1729            send_email_invitation: Some(false),
1730            role_ids: Some(role_ids),
1731            team_ids,          // Use the provided team IDs
1732            permissions: None, // Will auto-assign "apiUser" permission for API users
1733        };
1734
1735        self.create_user(request).await
1736    }
1737}
1738
1739#[cfg(test)]
1740#[allow(clippy::expect_used)]
1741mod tests {
1742    use super::*;
1743
1744    #[test]
1745    fn test_user_query_params() {
1746        let query = UserQuery::new()
1747            .with_username("testuser")
1748            .with_email("test@example.com")
1749            .with_user_type(UserType::Human)
1750            .with_pagination(1, 50);
1751
1752        let params: Vec<_> = query.into();
1753        assert_eq!(params.len(), 5); // username, email, user_type, page, size
1754        assert!(params.contains(&("user_name".to_string(), "testuser".to_string())));
1755        assert!(params.contains(&("email_address".to_string(), "test@example.com".to_string())));
1756        assert!(params.contains(&("user_type".to_string(), "HUMAN".to_string())));
1757        assert!(params.contains(&("page".to_string(), "1".to_string())));
1758        assert!(params.contains(&("size".to_string(), "50".to_string())));
1759    }
1760
1761    #[test]
1762    fn test_user_type_serialization() {
1763        assert_eq!(
1764            serde_json::to_string(&UserType::Human).expect("should serialize to json"),
1765            "\"HUMAN\""
1766        );
1767        assert_eq!(
1768            serde_json::to_string(&UserType::ApiService).expect("should serialize to json"),
1769            "\"API\""
1770        );
1771        assert_eq!(
1772            serde_json::to_string(&UserType::Saml).expect("should serialize to json"),
1773            "\"SAML\""
1774        );
1775        assert_eq!(
1776            serde_json::to_string(&UserType::Vosp).expect("should serialize to json"),
1777            "\"VOSP\""
1778        );
1779    }
1780}