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