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