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