1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
11use crate::{VeracodeClient, VeracodeError};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct User {
16 pub user_id: String,
18 pub user_legacy_id: Option<u32>,
20 pub user_name: String,
22 pub email_address: String,
24 pub first_name: String,
26 pub last_name: String,
28 pub user_type: Option<UserType>,
30 pub active: Option<bool>,
32 pub login_enabled: Option<bool>,
34 pub saml_user: Option<bool>,
36 pub roles: Option<Vec<Role>>,
38 pub teams: Option<Vec<Team>>,
40 pub login_status: Option<LoginStatus>,
42 pub created_date: Option<DateTime<Utc>>,
44 pub modified_date: Option<DateTime<Utc>>,
46 pub api_credentials: Option<Vec<ApiCredential>>,
48 #[serde(rename = "_links")]
50 pub links: Option<serde_json::Value>,
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "UPPERCASE")]
56pub enum UserType {
57 Human,
59 #[serde(rename = "API")]
61 ApiService,
62 Saml,
64 #[serde(rename = "VOSP")]
66 Vosp,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Role {
72 pub role_id: String,
74 pub role_legacy_id: Option<i32>,
76 pub role_name: String,
78 pub role_description: Option<String>,
80 pub is_internal: Option<bool>,
82 pub requires_token: Option<bool>,
84 pub assigned_to_proxy_users: Option<bool>,
86 pub team_admin_manageable: Option<bool>,
88 pub jit_assignable: Option<bool>,
90 pub jit_assignable_default: Option<bool>,
92 pub is_api: Option<bool>,
94 pub is_scan_type: Option<bool>,
96 pub ignore_team_restrictions: Option<bool>,
98 pub is_hmac_only: Option<bool>,
100 pub org_id: Option<String>,
102 pub child_roles: Option<Vec<serde_json::Value>>,
104 pub role_disabled: Option<bool>,
106 pub permissions: Option<Vec<Permission>>,
108 #[serde(rename = "_links")]
110 pub links: Option<serde_json::Value>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct Permission {
116 pub permission_id: Option<String>,
118 pub permission_name: String,
120 pub description: Option<String>,
122 pub api_only: Option<bool>,
124 pub ui_only: Option<bool>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Team {
131 pub team_id: String,
133 pub team_name: String,
135 pub team_description: Option<String>,
137 pub users: Option<Vec<User>>,
139 pub business_unit: Option<BusinessUnit>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct BusinessUnit {
146 pub bu_id: String,
148 pub bu_name: String,
150 pub bu_description: Option<String>,
152 pub teams: Option<Vec<Team>>,
154}
155
156#[derive(Clone, Serialize, Deserialize)]
158pub struct ApiCredential {
159 pub api_id: String,
161 pub api_key: Option<String>,
163 pub expiration_ts: Option<DateTime<Utc>>,
165 pub active: Option<bool>,
167 pub created_date: Option<DateTime<Utc>>,
169}
170
171impl std::fmt::Debug for ApiCredential {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 f.debug_struct("ApiCredential")
180 .field("api_id", &self.api_id)
181 .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
182 .field("expiration_ts", &self.expiration_ts)
183 .field("active", &self.active)
184 .field("created_date", &self.created_date)
185 .finish()
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct LoginStatus {
192 pub last_login_date: Option<DateTime<Utc>>,
194 pub never_logged_in: Option<bool>,
196 pub failed_login_attempts: Option<u32>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct CreateUserRequest {
203 pub email_address: String,
205 pub first_name: String,
207 pub last_name: String,
209 pub user_name: Option<String>,
211 pub user_type: Option<UserType>,
213 pub send_email_invitation: Option<bool>,
215 pub role_ids: Option<Vec<String>>,
217 pub team_ids: Option<Vec<String>>,
219 pub permissions: Option<Vec<Permission>>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct UpdateUserRequest {
226 pub email_address: String,
228 pub user_name: String,
230 pub first_name: Option<String>,
232 pub last_name: Option<String>,
234 pub active: Option<bool>,
236 pub role_ids: Vec<String>,
238 pub team_ids: Vec<String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct CreateTeamRequest {
245 pub team_name: String,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub team_description: Option<String>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub business_unit_id: Option<String>,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub user_ids: Option<Vec<String>>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct UpdateTeamRequest {
261 pub team_name: Option<String>,
263 pub team_description: Option<String>,
265 pub business_unit_id: Option<String>,
267 pub user_ids: Option<Vec<String>>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct CreateApiCredentialRequest {
274 pub user_id: Option<String>,
276 pub expiration_ts: Option<DateTime<Utc>>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct UsersResponse {
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 pub users: Vec<User>,
286 #[serde(rename = "_embedded")]
288 pub embedded: Option<EmbeddedUsers>,
289 pub page: Option<PageInfo>,
291 #[serde(rename = "_links")]
293 pub links: Option<HashMap<String, Link>>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct EmbeddedUsers {
299 pub users: Vec<User>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct TeamsResponse {
306 #[serde(default, skip_serializing_if = "Vec::is_empty")]
308 pub teams: Vec<Team>,
309 #[serde(rename = "_embedded")]
311 pub embedded: Option<EmbeddedTeams>,
312 pub page: Option<PageInfo>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct EmbeddedTeams {
319 pub teams: Vec<Team>,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct RolesResponse {
326 #[serde(default, skip_serializing_if = "Vec::is_empty")]
328 pub roles: Vec<Role>,
329 #[serde(rename = "_embedded")]
331 pub embedded: Option<EmbeddedRoles>,
332 pub page: Option<PageInfo>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct EmbeddedRoles {
339 pub roles: Vec<Role>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct PageInfo {
346 pub number: Option<u32>,
348 pub size: Option<u32>,
350 pub total_elements: Option<u64>,
352 pub total_pages: Option<u32>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct Link {
359 pub href: String,
361}
362
363#[derive(Debug, Clone, Default)]
365pub struct UserQuery {
366 pub user_name: Option<String>,
368 pub email_address: Option<String>,
370 pub role_id: Option<String>,
372 pub user_type: Option<UserType>,
374 pub login_status: Option<String>,
376 pub page: Option<u32>,
378 pub size: Option<u32>,
380}
381
382impl UserQuery {
383 #[must_use]
385 pub fn new() -> Self {
386 Self::default()
387 }
388
389 pub fn with_username(mut self, username: impl Into<String>) -> Self {
391 self.user_name = Some(username.into());
392 self
393 }
394
395 pub fn with_email(mut self, email: impl Into<String>) -> Self {
397 self.email_address = Some(email.into());
398 self
399 }
400
401 pub fn with_role_id(mut self, role_id: impl Into<String>) -> Self {
403 self.role_id = Some(role_id.into());
404 self
405 }
406
407 #[must_use]
409 pub fn with_user_type(mut self, user_type: UserType) -> Self {
410 self.user_type = Some(user_type);
411 self
412 }
413
414 #[must_use]
416 pub fn with_pagination(mut self, page: u32, size: u32) -> Self {
417 self.page = Some(page);
418 self.size = Some(size);
419 self
420 }
421
422 #[must_use]
424 pub fn to_query_params(&self) -> Vec<(String, String)> {
425 Vec::from(self) }
427}
428
429impl From<&UserQuery> for Vec<(String, String)> {
431 fn from(query: &UserQuery) -> Self {
432 let mut params = Vec::new();
433
434 if let Some(ref username) = query.user_name {
435 params.push(("user_name".to_string(), username.clone())); }
437 if let Some(ref email) = query.email_address {
438 params.push(("email_address".to_string(), email.clone()));
439 }
440 if let Some(ref role_id) = query.role_id {
441 params.push(("role_id".to_string(), role_id.clone()));
442 }
443 if let Some(ref user_type) = query.user_type {
444 let type_str = match user_type {
445 UserType::Human => "HUMAN",
446 UserType::ApiService => "API",
447 UserType::Saml => "SAML",
448 UserType::Vosp => "VOSP",
449 };
450 params.push(("user_type".to_string(), type_str.to_string()));
451 }
452 if let Some(ref login_status) = query.login_status {
453 params.push(("login_status".to_string(), login_status.clone()));
454 }
455 if let Some(page) = query.page {
456 params.push(("page".to_string(), page.to_string()));
457 }
458 if let Some(size) = query.size {
459 params.push(("size".to_string(), size.to_string()));
460 }
461
462 params
463 }
464}
465
466impl From<UserQuery> for Vec<(String, String)> {
467 fn from(query: UserQuery) -> Self {
468 let mut params = Vec::new();
469
470 if let Some(username) = query.user_name {
471 params.push(("user_name".to_string(), username)); }
473 if let Some(email) = query.email_address {
474 params.push(("email_address".to_string(), email)); }
476 if let Some(role_id) = query.role_id {
477 params.push(("role_id".to_string(), role_id)); }
479 if let Some(user_type) = query.user_type {
480 let type_str = match user_type {
481 UserType::Human => "HUMAN",
482 UserType::ApiService => "API",
483 UserType::Saml => "SAML",
484 UserType::Vosp => "VOSP",
485 };
486 params.push(("user_type".to_string(), type_str.to_string()));
487 }
488 if let Some(login_status) = query.login_status {
489 params.push(("login_status".to_string(), login_status)); }
491 if let Some(page) = query.page {
492 params.push(("page".to_string(), page.to_string()));
493 }
494 if let Some(size) = query.size {
495 params.push(("size".to_string(), size.to_string()));
496 }
497
498 params
499 }
500}
501
502#[derive(Debug)]
504#[must_use = "Need to handle all error enum types."]
505pub enum IdentityError {
506 Api(VeracodeError),
508 UserNotFound,
510 TeamNotFound,
512 RoleNotFound,
514 InvalidInput(String),
516 PermissionDenied(String),
518 UserAlreadyExists(String),
520 TeamAlreadyExists(String),
522}
523
524impl std::fmt::Display for IdentityError {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 match self {
527 IdentityError::Api(err) => write!(f, "API error: {err}"),
528 IdentityError::UserNotFound => write!(f, "User not found"),
529 IdentityError::TeamNotFound => write!(f, "Team not found"),
530 IdentityError::RoleNotFound => write!(f, "Role not found"),
531 IdentityError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
532 IdentityError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
533 IdentityError::UserAlreadyExists(msg) => write!(f, "User already exists: {msg}"),
534 IdentityError::TeamAlreadyExists(msg) => write!(f, "Team already exists: {msg}"),
535 }
536 }
537}
538
539impl std::error::Error for IdentityError {}
540
541impl From<VeracodeError> for IdentityError {
542 fn from(err: VeracodeError) -> Self {
543 IdentityError::Api(err)
544 }
545}
546
547impl From<reqwest::Error> for IdentityError {
548 fn from(err: reqwest::Error) -> Self {
549 IdentityError::Api(VeracodeError::Http(err))
550 }
551}
552
553impl From<serde_json::Error> for IdentityError {
554 fn from(err: serde_json::Error) -> Self {
555 IdentityError::Api(VeracodeError::Serialization(err))
556 }
557}
558
559pub struct IdentityApi<'a> {
561 client: &'a VeracodeClient,
562}
563
564impl<'a> IdentityApi<'a> {
565 #[must_use]
572 pub fn new(client: &'a VeracodeClient) -> Self {
573 Self { client }
574 }
575
576 fn sanitize_error(raw_error: &str, status: u16, context: &str) -> String {
587 log::warn!("API error in {} - HTTP {}: {}", context, status, raw_error);
589
590 match status {
592 400 => {
593 if raw_error.contains("already exists") {
595 "Resource already exists".to_string()
596 } else {
597 "Invalid request parameters".to_string()
598 }
599 }
600 401 => "Authentication required".to_string(),
601 403 => "Insufficient permissions for this operation".to_string(),
602 404 => "Resource not found".to_string(),
603 415 => "Unsupported media type".to_string(),
604 429 => "Rate limit exceeded. Please try again later".to_string(),
605 500..=599 => "Internal server error occurred".to_string(),
606 _ => format!("Request failed with status {}", status),
607 }
608 }
609
610 pub async fn list_users(&self, query: Option<UserQuery>) -> Result<Vec<User>, IdentityError> {
625 let endpoint = "/api/authn/v2/users";
626 let query_params = query.as_ref().map(Vec::from);
627
628 let response = self.client.get(endpoint, query_params.as_deref()).await?;
629
630 let status = response.status().as_u16();
631 match status {
632 200 => {
633 let response_text = response.text().await?;
634
635 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
637 IdentityError::Api(VeracodeError::InvalidResponse(format!(
638 "JSON validation failed: {}",
639 e
640 )))
641 })?;
642
643 if let Ok(users_response) = serde_json::from_str::<UsersResponse>(&response_text) {
645 let users = if !users_response.users.is_empty() {
646 users_response.users
647 } else if let Some(embedded) = users_response.embedded {
648 embedded.users
649 } else {
650 Vec::new()
651 };
652 return Ok(users);
653 }
654
655 if let Ok(users) = serde_json::from_str::<Vec<User>>(&response_text) {
657 return Ok(users);
658 }
659
660 Err(IdentityError::Api(VeracodeError::InvalidResponse(
661 "Unable to parse users response".to_string(),
662 )))
663 }
664 404 => Err(IdentityError::UserNotFound),
665 _ => {
666 let error_text = response.text().await.unwrap_or_default();
667 let sanitized = Self::sanitize_error(&error_text, status, "list_users");
668 Err(IdentityError::Api(VeracodeError::InvalidResponse(
669 sanitized,
670 )))
671 }
672 }
673 }
674
675 pub async fn get_user(&self, user_id: &str) -> Result<User, IdentityError> {
690 let endpoint = format!("/api/authn/v2/users/{user_id}");
691
692 let response = self.client.get(&endpoint, None).await?;
693
694 let status = response.status().as_u16();
695 match status {
696 200 => {
697 let user: User = response.json().await?;
698 Ok(user)
699 }
700 404 => Err(IdentityError::UserNotFound),
701 _ => {
702 let error_text = response.text().await.unwrap_or_default();
703 let sanitized = Self::sanitize_error(&error_text, status, "get_user");
704 Err(IdentityError::Api(VeracodeError::InvalidResponse(
705 sanitized,
706 )))
707 }
708 }
709 }
710
711 pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, IdentityError> {
726 let endpoint = "/api/authn/v2/users";
727
728 let mut fixed_request = request.clone();
729
730 if fixed_request.user_name.is_none() {
732 return Err(IdentityError::InvalidInput(
733 "user_name is required".to_string(),
734 ));
735 }
736
737 let has_teams = fixed_request
739 .team_ids
740 .as_ref()
741 .is_some_and(|teams| !teams.is_empty());
742
743 if !has_teams {
744 let has_no_team_restriction_role = if let Some(ref role_ids) = fixed_request.role_ids {
746 let roles = self.list_roles().await?;
748 role_ids.iter().any(|role_id| {
749 roles.iter().any(|r| {
750 &r.role_id == role_id
751 && (r
752 .role_description
753 .as_ref()
754 .is_some_and(|desc| desc == "No Team Restriction API")
755 || r.role_name.to_lowercase() == "noteamrestrictionapi")
756 })
757 })
758 } else {
759 false
760 };
761
762 if !has_no_team_restriction_role {
763 return Err(IdentityError::InvalidInput(
764 "You must select at least one team for this user, or select No Team Restrictions role".to_string()
765 ));
766 }
767 }
768
769 let is_api_user = matches!(fixed_request.user_type, Some(UserType::ApiService));
771 let is_saml_user = matches!(fixed_request.user_type, Some(UserType::Saml));
772
773 if is_api_user && let Some(ref provided_role_ids) = fixed_request.role_ids {
775 let roles = self.list_roles().await?;
776
777 let human_role_descriptions = [
779 "Creator",
780 "Executive",
781 "Mitigation Approver",
782 "Reviewer",
783 "Sandbox User",
784 "Security Lead",
785 "Team Admin",
786 "Workspace Editor",
787 "Analytics Creator",
788 "Delete Scans",
789 "Greenlight IDE User",
790 "Policy Administrator",
791 "Sandbox Administrator",
792 "Security Insights",
793 "Submitter",
794 "Workspace Administrator",
795 ];
796
797 for role_id in provided_role_ids {
798 if let Some(role) = roles.iter().find(|r| &r.role_id == role_id) {
799 if let Some(ref desc) = role.role_description
801 && human_role_descriptions.contains(&desc.as_str())
802 {
803 return Err(IdentityError::InvalidInput(format!(
804 "Role '{}' (description: '{}') is a human-only role and cannot be assigned to API users.",
805 role.role_name, desc
806 )));
807 }
808
809 if role.is_api != Some(true) {
811 return Err(IdentityError::InvalidInput(format!(
812 "Role '{}' (is_api: {}) cannot be assigned to API users. API users can only be assigned API roles.",
813 role.role_name,
814 role.is_api.map_or("None".to_string(), |b| b.to_string())
815 )));
816 }
817 }
818 }
819 }
820
821 if fixed_request
823 .role_ids
824 .as_ref()
825 .is_none_or(|roles| roles.is_empty())
826 {
827 let roles = self.list_roles().await?;
829 let mut default_role_ids = Vec::new();
830
831 if is_api_user {
833 if let Some(api_submit_role) = roles
835 .iter()
836 .find(|r| r.role_name.to_lowercase() == "apisubmitanyscan")
837 {
838 default_role_ids.push(api_submit_role.role_id.clone());
839 }
840
841 if let Some(noteam_role) = roles
843 .iter()
844 .find(|r| r.role_name.to_lowercase() == "noteamrestrictionapi")
845 {
846 default_role_ids.push(noteam_role.role_id.clone());
847 }
848 } else {
849 if let Some(submitter_role) = roles.iter().find(|r| {
851 r.role_description
852 .as_ref()
853 .is_some_and(|desc| desc == "Submitter")
854 }) {
855 default_role_ids.push(submitter_role.role_id.clone());
856 } else if let Some(creator_role) = roles.iter().find(|r| {
857 r.role_description
858 .as_ref()
859 .is_some_and(|desc| desc == "Creator")
860 }) {
861 default_role_ids.push(creator_role.role_id.clone());
862 } else if let Some(reviewer_role) = roles.iter().find(|r| {
863 r.role_description
864 .as_ref()
865 .is_some_and(|desc| desc == "Reviewer")
866 }) {
867 default_role_ids.push(reviewer_role.role_id.clone());
868 }
869
870 if !has_teams
872 && let Some(no_team_role) = roles.iter().find(|r| {
873 r.role_description
874 .as_ref()
875 .is_some_and(|desc| desc == "No Team Restriction API")
876 || r.role_name.to_lowercase() == "noteamrestrictionapi"
877 })
878 {
879 default_role_ids.push(no_team_role.role_id.clone());
880 }
881 }
882
883 if !default_role_ids.is_empty() {
885 fixed_request.role_ids = Some(default_role_ids);
886 }
887 }
888
889 if fixed_request
891 .permissions
892 .as_ref()
893 .is_none_or(|p| p.is_empty())
894 {
895 if is_api_user {
896 let api_user_permission = Permission {
898 permission_id: None,
899 permission_name: "apiUser".to_string(),
900 description: Some("API User".to_string()),
901 api_only: Some(false),
902 ui_only: Some(false),
903 };
904 fixed_request.permissions = Some(vec![api_user_permission]);
905 } else {
906 let human_user_permission = Permission {
908 permission_id: None,
909 permission_name: "humanUser".to_string(),
910 description: Some("Human User".to_string()),
911 api_only: Some(false),
912 ui_only: Some(false),
913 };
914 fixed_request.permissions = Some(vec![human_user_permission]);
915 }
916 }
917
918 let roles_payload = if let Some(ref role_ids) = fixed_request.role_ids {
920 role_ids
921 .iter()
922 .map(|id| serde_json::json!({"role_id": id}))
923 .collect::<Vec<_>>()
924 } else {
925 Vec::new()
926 };
927
928 let teams_payload = if let Some(ref team_ids) = fixed_request.team_ids {
930 team_ids
931 .iter()
932 .map(|id| serde_json::json!({"team_id": id}))
933 .collect::<Vec<_>>()
934 } else {
935 Vec::new()
936 };
937
938 let permissions_payload = if let Some(ref permissions) = fixed_request.permissions {
940 permissions
941 .iter()
942 .map(|p| {
943 serde_json::json!({
944 "permission_name": p.permission_name,
945 "api_only": p.api_only.unwrap_or(false),
946 "ui_only": p.ui_only.unwrap_or(false)
947 })
948 })
949 .collect::<Vec<_>>()
950 } else {
951 Vec::new()
952 };
953
954 let mut payload = serde_json::json!({
956 "email_address": fixed_request.email_address,
957 "first_name": fixed_request.first_name,
958 "last_name": fixed_request.last_name,
959 "apiUser": is_api_user,
960 "samlUser": is_saml_user,
961 "active": true, "send_email_invitation": fixed_request.send_email_invitation.unwrap_or(false)
963 });
964
965 if let Some(ref user_name) = fixed_request.user_name
967 && let Some(obj) = payload.as_object_mut()
968 {
969 obj.insert("user_name".to_string(), serde_json::json!(user_name));
970 }
971
972 if !roles_payload.is_empty()
974 && let Some(obj) = payload.as_object_mut()
975 {
976 obj.insert("roles".to_string(), serde_json::json!(roles_payload));
977 }
978
979 if !teams_payload.is_empty()
981 && let Some(obj) = payload.as_object_mut()
982 {
983 obj.insert("teams".to_string(), serde_json::json!(teams_payload));
984 }
985
986 if !permissions_payload.is_empty()
988 && let Some(obj) = payload.as_object_mut()
989 {
990 obj.insert(
991 "permissions".to_string(),
992 serde_json::json!(permissions_payload),
993 );
994 }
995
996 let response = self.client.post(endpoint, Some(&payload)).await?;
997
998 let status = response.status().as_u16();
999 match status {
1000 200 | 201 => {
1001 let user: User = response.json().await?;
1002 Ok(user)
1003 }
1004 400 => {
1005 let error_text = response.text().await.unwrap_or_default();
1006 let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1007 if error_text.contains("already exists") {
1008 Err(IdentityError::UserAlreadyExists(sanitized))
1009 } else {
1010 Err(IdentityError::InvalidInput(sanitized))
1011 }
1012 }
1013 403 => {
1014 let error_text = response.text().await.unwrap_or_default();
1015 let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1016 Err(IdentityError::PermissionDenied(sanitized))
1017 }
1018 415 => {
1019 let error_text = response.text().await.unwrap_or_default();
1020 let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1021 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1022 sanitized,
1023 )))
1024 }
1025 _ => {
1026 let error_text = response.text().await.unwrap_or_default();
1027 let sanitized = Self::sanitize_error(&error_text, status, "create_user");
1028 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1029 sanitized,
1030 )))
1031 }
1032 }
1033 }
1034
1035 pub async fn update_user(
1051 &self,
1052 user_id: &str,
1053 request: UpdateUserRequest,
1054 ) -> Result<User, IdentityError> {
1055 let endpoint = format!("/api/authn/v2/users/{user_id}");
1056
1057 let roles_payload = request
1059 .role_ids
1060 .iter()
1061 .map(|id| serde_json::json!({"role_id": id}))
1062 .collect::<Vec<_>>();
1063
1064 let teams_payload = request
1065 .team_ids
1066 .iter()
1067 .map(|id| serde_json::json!({"team_id": id}))
1068 .collect::<Vec<_>>();
1069
1070 let payload = serde_json::json!({
1071 "email_address": request.email_address,
1072 "user_name": request.user_name,
1073 "first_name": request.first_name,
1074 "last_name": request.last_name,
1075 "active": request.active,
1076 "roles": roles_payload,
1077 "teams": teams_payload
1078 });
1079
1080 let response = self.client.put(&endpoint, Some(&payload)).await?;
1081
1082 let status = response.status().as_u16();
1083 match status {
1084 200 => {
1085 let user: User = response.json().await?;
1086 Ok(user)
1087 }
1088 400 => {
1089 let error_text = response.text().await.unwrap_or_default();
1090 let sanitized = Self::sanitize_error(&error_text, status, "update_user");
1091 Err(IdentityError::InvalidInput(sanitized))
1092 }
1093 403 => {
1094 let error_text = response.text().await.unwrap_or_default();
1095 let sanitized = Self::sanitize_error(&error_text, status, "update_user");
1096 Err(IdentityError::PermissionDenied(sanitized))
1097 }
1098 404 => Err(IdentityError::UserNotFound),
1099 _ => {
1100 let error_text = response.text().await.unwrap_or_default();
1101 let sanitized = Self::sanitize_error(&error_text, status, "update_user");
1102 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1103 sanitized,
1104 )))
1105 }
1106 }
1107 }
1108
1109 pub async fn delete_user(&self, user_id: &str) -> Result<(), IdentityError> {
1124 let endpoint = format!("/api/authn/v2/users/{user_id}");
1125
1126 let response = self.client.delete(&endpoint).await?;
1127
1128 let status = response.status().as_u16();
1129 match status {
1130 200 | 204 => Ok(()),
1131 403 => {
1132 let error_text = response.text().await.unwrap_or_default();
1133 let sanitized = Self::sanitize_error(&error_text, status, "delete_user");
1134 Err(IdentityError::PermissionDenied(sanitized))
1135 }
1136 404 => Err(IdentityError::UserNotFound),
1137 _ => {
1138 let error_text = response.text().await.unwrap_or_default();
1139 let sanitized = Self::sanitize_error(&error_text, status, "delete_user");
1140 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1141 sanitized,
1142 )))
1143 }
1144 }
1145 }
1146
1147 pub async fn list_roles(&self) -> Result<Vec<Role>, IdentityError> {
1158 let endpoint = "/api/authn/v2/roles";
1159 let mut all_roles = Vec::new();
1160 let mut page: u32 = 0;
1161 let page_size = 500;
1162
1163 loop {
1165 let query_params = vec![
1166 ("page".to_string(), page.to_string()),
1167 ("size".to_string(), page_size.to_string()),
1168 ];
1169
1170 let response = self.client.get(endpoint, Some(&query_params)).await?;
1171 let status = response.status().as_u16();
1172
1173 match status {
1174 200 => {
1175 let response_text = response.text().await?;
1176
1177 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
1179 IdentityError::Api(VeracodeError::InvalidResponse(format!(
1180 "JSON validation failed: {}",
1181 e
1182 )))
1183 })?;
1184
1185 if let Ok(roles_response) =
1187 serde_json::from_str::<RolesResponse>(&response_text)
1188 {
1189 let page_roles = if !roles_response.roles.is_empty() {
1190 roles_response.roles
1191 } else if let Some(embedded) = roles_response.embedded {
1192 embedded.roles
1193 } else {
1194 Vec::new()
1195 };
1196
1197 if page_roles.is_empty() {
1198 break; }
1200
1201 all_roles.extend(page_roles);
1202 page = page.saturating_add(1);
1203
1204 if let Some(page_info) = roles_response.page
1206 && let (Some(current_page), Some(total_pages)) =
1207 (page_info.number, page_info.total_pages)
1208 && current_page.saturating_add(1) >= total_pages
1209 {
1210 break;
1211 }
1212
1213 continue;
1214 }
1215
1216 if let Ok(roles) = serde_json::from_str::<Vec<Role>>(&response_text) {
1218 if roles.is_empty() {
1219 break;
1220 }
1221 all_roles.extend(roles);
1222 page = page.saturating_add(1);
1223 continue;
1224 }
1225
1226 if page == 0 {
1228 log::warn!("Unable to parse roles response: {}", response_text);
1229 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1230 "Unable to parse roles response".to_string(),
1231 )));
1232 }
1233 break; }
1235 _ => {
1236 let error_text = response.text().await.unwrap_or_default();
1237 let sanitized = Self::sanitize_error(&error_text, status, "list_roles");
1238 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1239 sanitized,
1240 )));
1241 }
1242 }
1243 }
1244
1245 Ok(all_roles)
1246 }
1247
1248 pub async fn list_teams(&self) -> Result<Vec<Team>, IdentityError> {
1259 let endpoint = "/api/authn/v2/teams";
1260 let mut all_teams = Vec::new();
1261 let mut page: u32 = 0;
1262 let page_size = 500;
1263
1264 loop {
1266 if page > 100 {
1268 break;
1269 }
1270
1271 let query_params = vec![
1272 ("page".to_string(), page.to_string()),
1273 ("size".to_string(), page_size.to_string()),
1274 ];
1275
1276 let response = self.client.get(endpoint, Some(&query_params)).await?;
1277 let status = response.status().as_u16();
1278
1279 match status {
1280 200 => {
1281 let response_text = response.text().await?;
1282
1283 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
1285 IdentityError::Api(VeracodeError::InvalidResponse(format!(
1286 "JSON validation failed: {}",
1287 e
1288 )))
1289 })?;
1290
1291 if let Ok(teams_response) =
1293 serde_json::from_str::<TeamsResponse>(&response_text)
1294 {
1295 let page_teams = if !teams_response.teams.is_empty() {
1296 teams_response.teams
1297 } else if let Some(embedded) = teams_response.embedded {
1298 embedded.teams
1299 } else {
1300 Vec::new()
1301 };
1302
1303 if page_teams.is_empty() {
1304 break; }
1306
1307 all_teams.extend(page_teams);
1308 page = page.saturating_add(1);
1309
1310 if let Some(page_info) = teams_response.page
1312 && let (Some(current_page), Some(total_pages)) =
1313 (page_info.number, page_info.total_pages)
1314 && current_page.saturating_add(1) >= total_pages
1315 {
1316 break; }
1318
1319 continue;
1320 }
1321
1322 if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1324 if teams.is_empty() {
1325 break;
1326 }
1327 all_teams.extend(teams);
1328 page = page.saturating_add(1);
1329 continue;
1330 }
1331
1332 if page == 0 {
1334 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1335 "Unable to parse teams response".to_string(),
1336 )));
1337 }
1338 break;
1340 }
1341 _ => {
1342 let error_text = response.text().await.unwrap_or_default();
1343 let sanitized = Self::sanitize_error(&error_text, status, "list_teams");
1344 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1345 sanitized,
1346 )));
1347 }
1348 }
1349 }
1350
1351 Ok(all_teams)
1352 }
1353
1354 pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team, IdentityError> {
1369 let endpoint = "/api/authn/v2/teams";
1370
1371 let response = self.client.post(endpoint, Some(&request)).await?;
1372
1373 let status = response.status().as_u16();
1374 match status {
1375 200 | 201 => {
1376 let team: Team = response.json().await?;
1377 Ok(team)
1378 }
1379 400 => {
1380 let error_text = response.text().await.unwrap_or_default();
1381 let sanitized = Self::sanitize_error(&error_text, status, "create_team");
1382 if error_text.contains("already exists") {
1383 Err(IdentityError::TeamAlreadyExists(sanitized))
1384 } else {
1385 Err(IdentityError::InvalidInput(sanitized))
1386 }
1387 }
1388 403 => {
1389 let error_text = response.text().await.unwrap_or_default();
1390 let sanitized = Self::sanitize_error(&error_text, status, "create_team");
1391 Err(IdentityError::PermissionDenied(sanitized))
1392 }
1393 _ => {
1394 let error_text = response.text().await.unwrap_or_default();
1395 let sanitized = Self::sanitize_error(&error_text, status, "create_team");
1396 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1397 sanitized,
1398 )))
1399 }
1400 }
1401 }
1402
1403 pub async fn delete_team(&self, team_id: &str) -> Result<(), IdentityError> {
1418 let endpoint = format!("/api/authn/v2/teams/{team_id}");
1419
1420 let response = self.client.delete(&endpoint).await?;
1421
1422 let status = response.status().as_u16();
1423 match status {
1424 200 | 204 => Ok(()),
1425 403 => {
1426 let error_text = response.text().await.unwrap_or_default();
1427 let sanitized = Self::sanitize_error(&error_text, status, "delete_team");
1428 Err(IdentityError::PermissionDenied(sanitized))
1429 }
1430 404 => Err(IdentityError::TeamNotFound),
1431 _ => {
1432 let error_text = response.text().await.unwrap_or_default();
1433 let sanitized = Self::sanitize_error(&error_text, status, "delete_team");
1434 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1435 sanitized,
1436 )))
1437 }
1438 }
1439 }
1440
1441 pub async fn get_team_by_name(&self, team_name: &str) -> Result<Option<Team>, IdentityError> {
1460 let endpoint = "/api/authn/v2/teams";
1461
1462 let query_params = vec![
1463 ("page".to_string(), "0".to_string()),
1464 ("size".to_string(), "50".to_string()),
1465 ("team_name".to_string(), team_name.to_string()),
1466 ("ignore_self_teams".to_string(), "false".to_string()),
1467 ("only_manageable".to_string(), "false".to_string()),
1468 ("deleted".to_string(), "false".to_string()),
1469 ];
1470
1471 log::debug!("🔍 Team lookup request - endpoint: {}", endpoint);
1472 log::debug!(
1473 "🔍 Team lookup request - query parameters: [{}]",
1474 query_params
1475 .iter()
1476 .map(|(k, v)| format!("{}={}", k, v))
1477 .collect::<Vec<_>>()
1478 .join(", ")
1479 );
1480 log::debug!(
1481 "🔍 Team lookup request - searching for team: '{}'",
1482 team_name
1483 );
1484
1485 let response = self.client.get(endpoint, Some(&query_params)).await?;
1486 let status = response.status().as_u16();
1487
1488 log::debug!("🔍 Team lookup response - HTTP status: {}", status);
1489
1490 match status {
1491 200 => {
1492 let response_text = response.text().await?;
1493 log::debug!(
1494 "🔍 Team lookup response - content length: {} bytes",
1495 response_text.len()
1496 );
1497
1498 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
1500 IdentityError::Api(VeracodeError::InvalidResponse(format!(
1501 "JSON validation failed: {}",
1502 e
1503 )))
1504 })?;
1505
1506 let process_teams = |teams: Vec<Team>, format_description: &str| -> Option<Team> {
1508 let team_count = teams.len();
1509 log::debug!(
1510 "🔍 Team lookup response - found {} teams total ({})",
1511 team_count,
1512 format_description
1513 );
1514 for (i, team) in teams.iter().enumerate() {
1515 log::debug!(
1516 "🔍 Team lookup response - team {}: '{}' (GUID: {})",
1517 i.saturating_add(1),
1518 team.team_name,
1519 team.team_id
1520 );
1521 }
1522
1523 let found_team = teams
1526 .into_iter()
1527 .find(|team| team.team_name.to_lowercase() == team_name.to_lowercase());
1528 if let Some(ref team) = found_team {
1529 log::debug!(
1530 "🔍 Team lookup result - found case-insensitive match: '{}' (searched for '{}') with GUID: {}",
1531 team.team_name,
1532 team_name,
1533 team.team_id
1534 );
1535 } else {
1536 log::debug!(
1537 "🔍 Team lookup result - no case-insensitive match for '{}' among {} teams",
1538 team_name,
1539 team_count
1540 );
1541 }
1542 found_team
1543 };
1544
1545 if let Ok(teams_response) = serde_json::from_str::<TeamsResponse>(&response_text) {
1547 let teams = if let Some(embedded) = teams_response.embedded {
1548 embedded.teams
1549 } else if !teams_response.teams.is_empty() {
1550 teams_response.teams
1551 } else {
1552 Vec::new()
1553 };
1554 Ok(process_teams(teams, "embedded format"))
1555 } else if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1556 Ok(process_teams(teams, "direct array"))
1558 } else {
1559 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1560 "Unable to parse team response".to_string(),
1561 )))
1562 }
1563 }
1564 404 => {
1565 log::debug!(
1566 "🔍 Team lookup result - HTTP 404: team '{}' not found",
1567 team_name
1568 );
1569 Ok(None) }
1571 403 => {
1572 let error_text = response.text().await.unwrap_or_default();
1573 log::debug!("🔍 Team lookup error - HTTP 403: permission denied");
1574 let sanitized = Self::sanitize_error(&error_text, status, "get_team_by_name");
1575 Err(IdentityError::PermissionDenied(sanitized))
1576 }
1577 _ => {
1578 let error_text = response.text().await.unwrap_or_default();
1579 log::debug!("🔍 Team lookup error - HTTP {}", status);
1580 let sanitized = Self::sanitize_error(&error_text, status, "get_team_by_name");
1581 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1582 sanitized,
1583 )))
1584 }
1585 }
1586 }
1587
1588 pub async fn get_team_guid_by_name(
1606 &self,
1607 team_name: &str,
1608 ) -> Result<Option<String>, IdentityError> {
1609 match self.get_team_by_name(team_name).await? {
1610 Some(team) => Ok(Some(team.team_id)),
1611 None => Ok(None),
1612 }
1613 }
1614
1615 pub async fn create_api_credentials(
1630 &self,
1631 request: CreateApiCredentialRequest,
1632 ) -> Result<ApiCredential, IdentityError> {
1633 let endpoint = "/api/authn/v2/api_credentials";
1634
1635 let response = self.client.post(endpoint, Some(&request)).await?;
1636
1637 let status = response.status().as_u16();
1638 match status {
1639 200 | 201 => {
1640 let credentials: ApiCredential = response.json().await?;
1641 Ok(credentials)
1642 }
1643 400 => {
1644 let error_text = response.text().await.unwrap_or_default();
1645 let sanitized = Self::sanitize_error(&error_text, status, "create_api_credentials");
1646 Err(IdentityError::InvalidInput(sanitized))
1647 }
1648 403 => {
1649 let error_text = response.text().await.unwrap_or_default();
1650 let sanitized = Self::sanitize_error(&error_text, status, "create_api_credentials");
1651 Err(IdentityError::PermissionDenied(sanitized))
1652 }
1653 _ => {
1654 let error_text = response.text().await.unwrap_or_default();
1655 let sanitized = Self::sanitize_error(&error_text, status, "create_api_credentials");
1656 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1657 sanitized,
1658 )))
1659 }
1660 }
1661 }
1662
1663 pub async fn revoke_api_credentials(&self, api_creds_id: &str) -> Result<(), IdentityError> {
1678 let endpoint = format!("/api/authn/v2/api_credentials/{api_creds_id}");
1679
1680 let response = self.client.delete(&endpoint).await?;
1681
1682 let status = response.status().as_u16();
1683 match status {
1684 200 | 204 => Ok(()), 403 => {
1686 let error_text = response.text().await.unwrap_or_default();
1687 let sanitized = Self::sanitize_error(&error_text, status, "revoke_api_credentials");
1688 Err(IdentityError::PermissionDenied(sanitized))
1689 }
1690 404 => Err(IdentityError::UserNotFound), _ => {
1692 let error_text = response.text().await.unwrap_or_default();
1693 let sanitized = Self::sanitize_error(&error_text, status, "revoke_api_credentials");
1694 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1695 sanitized,
1696 )))
1697 }
1698 }
1699 }
1700}
1701
1702impl<'a> IdentityApi<'a> {
1704 pub async fn find_user_by_email(&self, email: &str) -> Result<Option<User>, IdentityError> {
1719 let query = UserQuery::new().with_email(email);
1720 let users = self.list_users(Some(query)).await?;
1721 Ok(users.into_iter().find(|u| u.email_address == email))
1722 }
1723
1724 pub async fn find_user_by_username(
1739 &self,
1740 username: &str,
1741 ) -> Result<Option<User>, IdentityError> {
1742 let query = UserQuery::new().with_username(username);
1743 let users = self.list_users(Some(query)).await?;
1744 Ok(users.into_iter().find(|u| u.user_name == username))
1745 }
1746
1747 pub async fn create_simple_user(
1766 &self,
1767 email: &str,
1768 username: &str,
1769 first_name: &str,
1770 last_name: &str,
1771 team_ids: Vec<String>,
1772 ) -> Result<User, IdentityError> {
1773 let request = CreateUserRequest {
1774 email_address: email.to_string(),
1775 first_name: first_name.to_string(),
1776 last_name: last_name.to_string(),
1777 user_name: Some(username.to_string()),
1778 user_type: Some(UserType::Human),
1779 send_email_invitation: Some(true),
1780 role_ids: None, team_ids: Some(team_ids),
1782 permissions: None, };
1784
1785 self.create_user(request).await
1786 }
1787
1788 pub async fn create_api_service_account(
1808 &self,
1809 email: &str,
1810 username: &str,
1811 first_name: &str,
1812 last_name: &str,
1813 role_ids: Vec<String>,
1814 team_ids: Option<Vec<String>>,
1815 ) -> Result<User, IdentityError> {
1816 let request = CreateUserRequest {
1817 email_address: email.to_string(),
1818 first_name: first_name.to_string(),
1819 last_name: last_name.to_string(),
1820 user_name: Some(username.to_string()),
1821 user_type: Some(UserType::ApiService), send_email_invitation: Some(false),
1823 role_ids: Some(role_ids),
1824 team_ids, permissions: None, };
1827
1828 self.create_user(request).await
1829 }
1830}
1831
1832#[cfg(test)]
1833#[allow(clippy::expect_used)]
1834mod tests {
1835 use super::*;
1836
1837 #[test]
1838 fn test_user_query_params() {
1839 let query = UserQuery::new()
1840 .with_username("testuser")
1841 .with_email("test@example.com")
1842 .with_user_type(UserType::Human)
1843 .with_pagination(1, 50);
1844
1845 let params: Vec<_> = query.into();
1846 assert_eq!(params.len(), 5); assert!(params.contains(&("user_name".to_string(), "testuser".to_string())));
1848 assert!(params.contains(&("email_address".to_string(), "test@example.com".to_string())));
1849 assert!(params.contains(&("user_type".to_string(), "HUMAN".to_string())));
1850 assert!(params.contains(&("page".to_string(), "1".to_string())));
1851 assert!(params.contains(&("size".to_string(), "50".to_string())));
1852 }
1853
1854 #[test]
1855 fn test_user_type_serialization() {
1856 assert_eq!(
1857 serde_json::to_string(&UserType::Human).expect("should serialize to json"),
1858 "\"HUMAN\""
1859 );
1860 assert_eq!(
1861 serde_json::to_string(&UserType::ApiService).expect("should serialize to json"),
1862 "\"API\""
1863 );
1864 assert_eq!(
1865 serde_json::to_string(&UserType::Saml).expect("should serialize to json"),
1866 "\"SAML\""
1867 );
1868 assert_eq!(
1869 serde_json::to_string(&UserType::Vosp).expect("should serialize to json"),
1870 "\"VOSP\""
1871 );
1872 }
1873}
1874
1875#[cfg(test)]
1887mod security_tests {
1888 use super::*;
1889 use proptest::prelude::*;
1890
1891 proptest! {
1898 #![proptest_config(ProptestConfig {
1899 cases: if cfg!(miri) { 5 } else { 1000 },
1900 failure_persistence: None,
1901 .. ProptestConfig::default()
1902 })]
1903
1904 #[test]
1911 fn api_credential_debug_redacts_sensitive_data(
1912 api_id in "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
1913 api_key in "[a-f0-9]{64}",
1914 active in any::<bool>(),
1915 ) {
1916 let credential = ApiCredential {
1917 api_id: api_id.clone(),
1918 api_key: Some(api_key.clone()),
1919 expiration_ts: None,
1920 active: Some(active),
1921 created_date: None,
1922 };
1923
1924 let debug_output = format!("{:?}", credential);
1926
1927 assert!(
1929 !debug_output.contains(&api_key),
1930 "API key leaked in debug output! This is a critical security vulnerability. Output: {}",
1931 debug_output
1932 );
1933
1934 assert!(
1936 debug_output.contains("[REDACTED]"),
1937 "Debug output should show [REDACTED] for API key. Output: {}",
1938 debug_output
1939 );
1940
1941 assert!(
1943 debug_output.contains(&api_id),
1944 "API ID should be visible in debug output for identification purposes"
1945 );
1946 }
1947
1948 #[test]
1950 fn api_credential_debug_handles_none_safely(
1951 api_id in "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
1952 ) {
1953 let credential = ApiCredential {
1954 api_id: api_id.clone(),
1955 api_key: None,
1956 expiration_ts: None,
1957 active: Some(true),
1958 created_date: None,
1959 };
1960
1961 let debug_output = format!("{:?}", credential);
1962
1963 assert!(
1965 debug_output.contains("api_key"),
1966 "Debug output should show api_key field even when None"
1967 );
1968 }
1969 }
1970
1971 proptest! {
1981 #![proptest_config(ProptestConfig {
1982 cases: if cfg!(miri) { 5 } else { 1000 },
1983 failure_persistence: None,
1984 .. ProptestConfig::default()
1985 })]
1986
1987 #[test]
1991 fn sanitize_error_prevents_information_disclosure(
1992 raw_error in ".*",
1993 status in 400u16..600u16,
1994 ) {
1995 let context = "test_operation";
1996 let sanitized = IdentityApi::sanitize_error(&raw_error, status, context);
1997
1998 let sensitive_patterns = [
2000 "password",
2001 "token",
2002 "secret",
2003 "key",
2004 "credential",
2005 "database",
2006 "sql",
2007 "stack trace",
2008 "at line",
2009 "error:",
2010 "exception:",
2011 "/usr/",
2012 "/var/",
2013 "C:\\",
2014 ];
2015
2016 for pattern in &sensitive_patterns {
2017 assert!(
2018 !sanitized.to_lowercase().contains(pattern),
2019 "Sanitized error message contains sensitive pattern '{}': {}",
2020 pattern,
2021 sanitized
2022 );
2023 }
2024
2025 if raw_error.len() > 50 || raw_error.contains("Exception") {
2027 assert_ne!(
2028 sanitized, raw_error,
2029 "Raw error should be sanitized, not passed through directly"
2030 );
2031 }
2032
2033 assert!(
2035 sanitized.len() < 200,
2036 "Sanitized error should be concise, got: {}",
2037 sanitized
2038 );
2039 }
2040
2041 #[test]
2043 fn sanitize_error_returns_appropriate_messages(
2044 raw_error in ".*",
2045 ) {
2046 let test_cases = vec![
2048 (400, "invalid"),
2049 (401, "authentication"),
2050 (403, "permission"),
2051 (404, "not found"),
2052 (429, "rate limit"),
2053 (500, "server error"),
2054 ];
2055
2056 for (status, expected_substring) in test_cases {
2057 let sanitized = IdentityApi::sanitize_error(&raw_error, status, "test");
2058 assert!(
2059 sanitized.to_lowercase().contains(expected_substring),
2060 "Status {} should mention '{}', got: {}",
2061 status, expected_substring, sanitized
2062 );
2063 }
2064 }
2065 }
2066
2067 proptest! {
2074 #![proptest_config(ProptestConfig {
2075 cases: if cfg!(miri) { 5 } else { 1000 },
2076 failure_persistence: None,
2077 .. ProptestConfig::default()
2078 })]
2079
2080 #[test]
2085 fn user_query_handles_malicious_input_safely(
2086 username in ".*",
2087 email in ".*",
2088 role_id in ".*",
2089 ) {
2090 let query = UserQuery::new()
2092 .with_username(&username)
2093 .with_email(&email)
2094 .with_role_id(&role_id);
2095
2096 let params = query.to_query_params();
2098
2099 let has_username = params.iter().any(|(k, v)| k == "user_name" && v == &username);
2101 let has_email = params.iter().any(|(k, v)| k == "email_address" && v == &email);
2102 let has_role = params.iter().any(|(k, v)| k == "role_id" && v == &role_id);
2103
2104 assert!(has_username, "Username should be in query params");
2105 assert!(has_email, "Email should be in query params");
2106 assert!(has_role, "Role ID should be in query params");
2107 }
2108
2109 #[test]
2113 fn user_query_params_bounded_memory(
2114 username in ".*",
2115 email in ".*",
2116 ) {
2117 let query = UserQuery::new()
2118 .with_username(&username)
2119 .with_email(&email)
2120 .with_pagination(0, 100);
2121
2122 let params = query.to_query_params();
2123
2124 assert!(
2126 params.len() <= 10,
2127 "Query should not generate excessive parameters: {} params",
2128 params.len()
2129 );
2130
2131 let total_size: usize = params.iter().map(|(k, v)| k.len().saturating_add(v.len())).sum();
2133 let max_expected = username.len().saturating_add(email.len()).saturating_add(200);
2134
2135 assert!(
2136 total_size <= max_expected,
2137 "Query params should not cause excessive memory allocation: {} bytes (max: {})",
2138 total_size, max_expected
2139 );
2140 }
2141 }
2142
2143 proptest! {
2150 #![proptest_config(ProptestConfig {
2151 cases: if cfg!(miri) { 5 } else { 500 },
2152 failure_persistence: None,
2153 .. ProptestConfig::default()
2154 })]
2155
2156 #[test]
2160 fn create_user_request_handles_arbitrary_input(
2161 email in ".*",
2162 first_name in ".*",
2163 last_name in ".*",
2164 username in ".*",
2165 ) {
2166 let request = CreateUserRequest {
2167 email_address: email.clone(),
2168 first_name: first_name.clone(),
2169 last_name: last_name.clone(),
2170 user_name: Some(username.clone()),
2171 user_type: Some(UserType::Human),
2172 send_email_invitation: Some(true),
2173 role_ids: None,
2174 team_ids: None,
2175 permissions: None,
2176 };
2177
2178 let serialized = serde_json::to_string(&request);
2180 assert!(
2181 serialized.is_ok(),
2182 "CreateUserRequest serialization should not panic with arbitrary input"
2183 );
2184
2185 if let Ok(json_str) = serialized {
2187 assert!(!json_str.is_empty(), "Serialized JSON should not be empty");
2188 assert!(json_str.starts_with('{'), "Should serialize to JSON object");
2189 }
2190 }
2191
2192 #[test]
2196 fn create_team_request_handles_arbitrary_input(
2197 team_name in ".*",
2198 team_description in ".*",
2199 ) {
2200 let request = CreateTeamRequest {
2201 team_name: team_name.clone(),
2202 team_description: Some(team_description.clone()),
2203 business_unit_id: None,
2204 user_ids: None,
2205 };
2206
2207 let serialized = serde_json::to_string(&request);
2209 assert!(
2210 serialized.is_ok(),
2211 "CreateTeamRequest serialization should not panic"
2212 );
2213 }
2214 }
2215
2216 proptest! {
2223 #![proptest_config(ProptestConfig {
2224 cases: if cfg!(miri) { 5 } else { 1000 },
2225 failure_persistence: None,
2226 .. ProptestConfig::default()
2227 })]
2228
2229 #[test]
2234 fn pagination_handles_edge_cases(
2235 page in 0u32..1000u32,
2236 size in 1u32..1000u32,
2237 ) {
2238 let query = UserQuery::new().with_pagination(page, size);
2239 let params = query.to_query_params();
2240
2241 let page_param = params.iter().find(|(k, _)| k == "page");
2243 let size_param = params.iter().find(|(k, _)| k == "size");
2244
2245 assert!(page_param.is_some(), "Page parameter should be present");
2246 assert!(size_param.is_some(), "Size parameter should be present");
2247
2248 if let Some((_, page_str)) = page_param {
2250 let parsed: Result<u32, _> = page_str.parse();
2251 assert!(parsed.is_ok(), "Page should be valid u32");
2252 assert_eq!(parsed.expect("Page parsing verified above"), page, "Page value should match");
2253 }
2254
2255 if let Some((_, size_str)) = size_param {
2256 let parsed: Result<u32, _> = size_str.parse();
2257 assert!(parsed.is_ok(), "Size should be valid u32");
2258 assert_eq!(parsed.expect("Size parsing verified above"), size, "Size value should match");
2259 }
2260 }
2261
2262 #[test]
2264 fn page_info_calculations_safe(
2265 number in 0u32..1000u32,
2266 size in 1u32..1000u32,
2267 total_elements in 0u64..1_000_000u64,
2268 total_pages in 0u32..10000u32,
2269 ) {
2270 let page_info = PageInfo {
2271 number: Some(number),
2272 size: Some(size),
2273 total_elements: Some(total_elements),
2274 total_pages: Some(total_pages),
2275 };
2276
2277 let serialized = serde_json::to_string(&page_info);
2279 assert!(serialized.is_ok(), "PageInfo serialization should not panic");
2280
2281 if let (Some(current), Some(total)) = (page_info.number, page_info.total_pages) {
2283 let next_page = current.saturating_add(1);
2285 assert!(
2286 next_page >= current,
2287 "Page increment should not wrap around"
2288 );
2289
2290 let _ = current.saturating_add(1);
2293 let _ = total.saturating_sub(current);
2294 }
2295 }
2296 }
2297
2298 proptest! {
2304 #![proptest_config(ProptestConfig {
2305 cases: if cfg!(miri) { 5 } else { 500 },
2306 failure_persistence: None,
2307 .. ProptestConfig::default()
2308 })]
2309
2310 #[test]
2314 fn identity_error_display_safe(
2315 error_msg in ".*",
2316 ) {
2317 let errors = vec![
2318 IdentityError::UserNotFound,
2319 IdentityError::TeamNotFound,
2320 IdentityError::RoleNotFound,
2321 IdentityError::InvalidInput(error_msg.clone()),
2322 IdentityError::PermissionDenied(error_msg.clone()),
2323 IdentityError::UserAlreadyExists(error_msg.clone()),
2324 IdentityError::TeamAlreadyExists(error_msg.clone()),
2325 ];
2326
2327 for error in errors {
2328 let display_str = format!("{}", error);
2329
2330 assert!(
2332 display_str.len() < 500,
2333 "Error message should be concise: {} chars",
2334 display_str.len()
2335 );
2336
2337 assert!(
2339 !display_str.contains("password"),
2340 "Error should not mention passwords"
2341 );
2342 assert!(
2343 !display_str.contains("token"),
2344 "Error should not mention tokens"
2345 );
2346 }
2347 }
2348 }
2349
2350 proptest! {
2356 #![proptest_config(ProptestConfig {
2357 cases: if cfg!(miri) { 5 } else { 500 },
2358 failure_persistence: None,
2359 .. ProptestConfig::default()
2360 })]
2361
2362 #[test]
2366 fn user_query_conversion_memory_safe(
2367 username in ".*",
2368 email in ".*",
2369 ) {
2370 let query = UserQuery::new()
2371 .with_username(&username)
2372 .with_email(&email);
2373
2374 let params_borrowed: Vec<(String, String)> = Vec::from(&query);
2376 assert!(params_borrowed.len() >= 2);
2377
2378 let params_again = query.to_query_params();
2380 assert_eq!(params_borrowed.len(), params_again.len());
2381
2382 let query2 = UserQuery::new()
2384 .with_username(&username)
2385 .with_email(&email);
2386 let params_moved: Vec<(String, String)> = Vec::from(query2);
2387 assert!(params_moved.len() >= 2);
2388
2389 assert_eq!(params_borrowed.len(), params_moved.len());
2391 }
2392 }
2393
2394 proptest! {
2400 #![proptest_config(ProptestConfig {
2401 cases: if cfg!(miri) { 5 } else { 500 },
2402 failure_persistence: None,
2403 .. ProptestConfig::default()
2404 })]
2405
2406 #[test]
2408 fn user_type_serialization_safe(
2409 user_type_idx in 0usize..4,
2410 ) {
2411 let user_types = [
2412 UserType::Human,
2413 UserType::ApiService,
2414 UserType::Saml,
2415 UserType::Vosp,
2416 ];
2417 let expected_strings = ["\"HUMAN\"", "\"API\"", "\"SAML\"", "\"VOSP\""];
2418
2419 let user_type = user_types.get(user_type_idx).expect("user_type_idx should be in bounds");
2420 let expected = expected_strings.get(user_type_idx).expect("user_type_idx should be in bounds");
2421
2422 let serialized = serde_json::to_string(user_type);
2424 assert!(serialized.is_ok(), "UserType serialization should not fail");
2425
2426 let serialized_str = serialized.expect("UserType serialization verified above");
2427 assert_eq!(serialized_str, *expected, "UserType should serialize to expected format");
2428
2429 let deserialized: Result<UserType, _> = serde_json::from_str(&serialized_str);
2431 assert!(deserialized.is_ok(), "UserType deserialization should not fail");
2432 assert_eq!(&deserialized.expect("UserType deserialization verified above"), user_type, "Roundtrip should preserve value");
2433 }
2434 }
2435}