1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use crate::{VeracodeClient, VeracodeError};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct User {
15 pub user_id: String,
17 pub user_legacy_id: Option<u32>,
19 pub user_name: String,
21 pub email_address: String,
23 pub first_name: String,
25 pub last_name: String,
27 pub user_type: Option<UserType>,
29 pub active: Option<bool>,
31 pub login_enabled: Option<bool>,
33 pub saml_user: Option<bool>,
35 pub roles: Option<Vec<Role>>,
37 pub teams: Option<Vec<Team>>,
39 pub login_status: Option<LoginStatus>,
41 pub created_date: Option<DateTime<Utc>>,
43 pub modified_date: Option<DateTime<Utc>>,
45 pub api_credentials: Option<Vec<ApiCredential>>,
47 #[serde(rename = "_links")]
49 pub links: Option<serde_json::Value>,
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(rename_all = "UPPERCASE")]
55pub enum UserType {
56 Human,
58 #[serde(rename = "API")]
60 ApiService,
61 Saml,
63 #[serde(rename = "VOSP")]
65 Vosp,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Role {
71 pub role_id: String,
73 pub role_legacy_id: Option<i32>,
75 pub role_name: String,
77 pub role_description: Option<String>,
79 pub is_internal: Option<bool>,
81 pub requires_token: Option<bool>,
83 pub assigned_to_proxy_users: Option<bool>,
85 pub team_admin_manageable: Option<bool>,
87 pub jit_assignable: Option<bool>,
89 pub jit_assignable_default: Option<bool>,
91 pub is_api: Option<bool>,
93 pub is_scan_type: Option<bool>,
95 pub ignore_team_restrictions: Option<bool>,
97 pub is_hmac_only: Option<bool>,
99 pub org_id: Option<String>,
101 pub child_roles: Option<Vec<serde_json::Value>>,
103 pub role_disabled: Option<bool>,
105 pub permissions: Option<Vec<Permission>>,
107 #[serde(rename = "_links")]
109 pub links: Option<serde_json::Value>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Permission {
115 pub permission_id: Option<String>,
117 pub permission_name: String,
119 pub description: Option<String>,
121 pub api_only: Option<bool>,
123 pub ui_only: Option<bool>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Team {
130 pub team_id: String,
132 pub team_name: String,
134 pub team_description: Option<String>,
136 pub users: Option<Vec<User>>,
138 pub business_unit: Option<BusinessUnit>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct BusinessUnit {
145 pub bu_id: String,
147 pub bu_name: String,
149 pub bu_description: Option<String>,
151 pub teams: Option<Vec<Team>>,
153}
154
155#[derive(Clone, Serialize, Deserialize)]
157pub struct ApiCredential {
158 pub api_id: String,
160 pub api_key: Option<String>,
162 pub expiration_ts: Option<DateTime<Utc>>,
164 pub active: Option<bool>,
166 pub created_date: Option<DateTime<Utc>>,
168}
169
170impl std::fmt::Debug for ApiCredential {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.debug_struct("ApiCredential")
179 .field("api_id", &self.api_id)
180 .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
181 .field("expiration_ts", &self.expiration_ts)
182 .field("active", &self.active)
183 .field("created_date", &self.created_date)
184 .finish()
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct LoginStatus {
191 pub last_login_date: Option<DateTime<Utc>>,
193 pub never_logged_in: Option<bool>,
195 pub failed_login_attempts: Option<u32>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct CreateUserRequest {
202 pub email_address: String,
204 pub first_name: String,
206 pub last_name: String,
208 pub user_name: Option<String>,
210 pub user_type: Option<UserType>,
212 pub send_email_invitation: Option<bool>,
214 pub role_ids: Option<Vec<String>>,
216 pub team_ids: Option<Vec<String>>,
218 pub permissions: Option<Vec<Permission>>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct UpdateUserRequest {
225 pub email_address: String,
227 pub user_name: String,
229 pub first_name: Option<String>,
231 pub last_name: Option<String>,
233 pub active: Option<bool>,
235 pub role_ids: Vec<String>,
237 pub team_ids: Vec<String>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct CreateTeamRequest {
244 pub team_name: String,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub team_description: Option<String>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub business_unit_id: Option<String>,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub user_ids: Option<Vec<String>>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct UpdateTeamRequest {
260 pub team_name: Option<String>,
262 pub team_description: Option<String>,
264 pub business_unit_id: Option<String>,
266 pub user_ids: Option<Vec<String>>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct CreateApiCredentialRequest {
273 pub user_id: Option<String>,
275 pub expiration_ts: Option<DateTime<Utc>>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct UsersResponse {
282 #[serde(default, skip_serializing_if = "Vec::is_empty")]
284 pub users: Vec<User>,
285 #[serde(rename = "_embedded")]
287 pub embedded: Option<EmbeddedUsers>,
288 pub page: Option<PageInfo>,
290 #[serde(rename = "_links")]
292 pub links: Option<HashMap<String, Link>>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct EmbeddedUsers {
298 pub users: Vec<User>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct TeamsResponse {
305 #[serde(default, skip_serializing_if = "Vec::is_empty")]
307 pub teams: Vec<Team>,
308 #[serde(rename = "_embedded")]
310 pub embedded: Option<EmbeddedTeams>,
311 pub page: Option<PageInfo>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct EmbeddedTeams {
318 pub teams: Vec<Team>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct RolesResponse {
325 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub roles: Vec<Role>,
328 #[serde(rename = "_embedded")]
330 pub embedded: Option<EmbeddedRoles>,
331 pub page: Option<PageInfo>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct EmbeddedRoles {
338 pub roles: Vec<Role>,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct PageInfo {
345 pub number: Option<u32>,
347 pub size: Option<u32>,
349 pub total_elements: Option<u64>,
351 pub total_pages: Option<u32>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct Link {
358 pub href: String,
360}
361
362#[derive(Debug, Clone, Default)]
364pub struct UserQuery {
365 pub user_name: Option<String>,
367 pub email_address: Option<String>,
369 pub role_id: Option<String>,
371 pub user_type: Option<UserType>,
373 pub login_status: Option<String>,
375 pub page: Option<u32>,
377 pub size: Option<u32>,
379}
380
381impl UserQuery {
382 #[must_use]
384 pub fn new() -> Self {
385 Self::default()
386 }
387
388 pub fn with_username(mut self, username: impl Into<String>) -> Self {
390 self.user_name = Some(username.into());
391 self
392 }
393
394 pub fn with_email(mut self, email: impl Into<String>) -> Self {
396 self.email_address = Some(email.into());
397 self
398 }
399
400 pub fn with_role_id(mut self, role_id: impl Into<String>) -> Self {
402 self.role_id = Some(role_id.into());
403 self
404 }
405
406 #[must_use]
408 pub fn with_user_type(mut self, user_type: UserType) -> Self {
409 self.user_type = Some(user_type);
410 self
411 }
412
413 #[must_use]
415 pub fn with_pagination(mut self, page: u32, size: u32) -> Self {
416 self.page = Some(page);
417 self.size = Some(size);
418 self
419 }
420
421 #[must_use]
423 pub fn to_query_params(&self) -> Vec<(String, String)> {
424 Vec::from(self) }
426}
427
428impl From<&UserQuery> for Vec<(String, String)> {
430 fn from(query: &UserQuery) -> Self {
431 let mut params = Vec::new();
432
433 if let Some(ref username) = query.user_name {
434 params.push(("user_name".to_string(), username.clone())); }
436 if let Some(ref email) = query.email_address {
437 params.push(("email_address".to_string(), email.clone()));
438 }
439 if let Some(ref role_id) = query.role_id {
440 params.push(("role_id".to_string(), role_id.clone()));
441 }
442 if let Some(ref user_type) = query.user_type {
443 let type_str = match user_type {
444 UserType::Human => "HUMAN",
445 UserType::ApiService => "API",
446 UserType::Saml => "SAML",
447 UserType::Vosp => "VOSP",
448 };
449 params.push(("user_type".to_string(), type_str.to_string()));
450 }
451 if let Some(ref login_status) = query.login_status {
452 params.push(("login_status".to_string(), login_status.clone()));
453 }
454 if let Some(page) = query.page {
455 params.push(("page".to_string(), page.to_string()));
456 }
457 if let Some(size) = query.size {
458 params.push(("size".to_string(), size.to_string()));
459 }
460
461 params
462 }
463}
464
465impl From<UserQuery> for Vec<(String, String)> {
466 fn from(query: UserQuery) -> Self {
467 let mut params = Vec::new();
468
469 if let Some(username) = query.user_name {
470 params.push(("user_name".to_string(), username)); }
472 if let Some(email) = query.email_address {
473 params.push(("email_address".to_string(), email)); }
475 if let Some(role_id) = query.role_id {
476 params.push(("role_id".to_string(), role_id)); }
478 if let Some(user_type) = query.user_type {
479 let type_str = match user_type {
480 UserType::Human => "HUMAN",
481 UserType::ApiService => "API",
482 UserType::Saml => "SAML",
483 UserType::Vosp => "VOSP",
484 };
485 params.push(("user_type".to_string(), type_str.to_string()));
486 }
487 if let Some(login_status) = query.login_status {
488 params.push(("login_status".to_string(), login_status)); }
490 if let Some(page) = query.page {
491 params.push(("page".to_string(), page.to_string()));
492 }
493 if let Some(size) = query.size {
494 params.push(("size".to_string(), size.to_string()));
495 }
496
497 params
498 }
499}
500
501#[derive(Debug)]
503#[must_use = "Need to handle all error enum types."]
504pub enum IdentityError {
505 Api(VeracodeError),
507 UserNotFound,
509 TeamNotFound,
511 RoleNotFound,
513 InvalidInput(String),
515 PermissionDenied(String),
517 UserAlreadyExists(String),
519 TeamAlreadyExists(String),
521}
522
523impl std::fmt::Display for IdentityError {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 match self {
526 IdentityError::Api(err) => write!(f, "API error: {err}"),
527 IdentityError::UserNotFound => write!(f, "User not found"),
528 IdentityError::TeamNotFound => write!(f, "Team not found"),
529 IdentityError::RoleNotFound => write!(f, "Role not found"),
530 IdentityError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
531 IdentityError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
532 IdentityError::UserAlreadyExists(msg) => write!(f, "User already exists: {msg}"),
533 IdentityError::TeamAlreadyExists(msg) => write!(f, "Team already exists: {msg}"),
534 }
535 }
536}
537
538impl std::error::Error for IdentityError {}
539
540impl From<VeracodeError> for IdentityError {
541 fn from(err: VeracodeError) -> Self {
542 IdentityError::Api(err)
543 }
544}
545
546impl From<reqwest::Error> for IdentityError {
547 fn from(err: reqwest::Error) -> Self {
548 IdentityError::Api(VeracodeError::Http(err))
549 }
550}
551
552impl From<serde_json::Error> for IdentityError {
553 fn from(err: serde_json::Error) -> Self {
554 IdentityError::Api(VeracodeError::Serialization(err))
555 }
556}
557
558pub struct IdentityApi<'a> {
560 client: &'a VeracodeClient,
561}
562
563impl<'a> IdentityApi<'a> {
564 #[must_use]
571 pub fn new(client: &'a VeracodeClient) -> Self {
572 Self { client }
573 }
574
575 pub async fn list_users(&self, query: Option<UserQuery>) -> Result<Vec<User>, IdentityError> {
590 let endpoint = "/api/authn/v2/users";
591 let query_params = query.as_ref().map(Vec::from);
592
593 let response = self.client.get(endpoint, query_params.as_deref()).await?;
594
595 let status = response.status().as_u16();
596 match status {
597 200 => {
598 let response_text = response.text().await?;
599
600 if let Ok(users_response) = serde_json::from_str::<UsersResponse>(&response_text) {
602 let users = if !users_response.users.is_empty() {
603 users_response.users
604 } else if let Some(embedded) = users_response.embedded {
605 embedded.users
606 } else {
607 Vec::new()
608 };
609 return Ok(users);
610 }
611
612 if let Ok(users) = serde_json::from_str::<Vec<User>>(&response_text) {
614 return Ok(users);
615 }
616
617 Err(IdentityError::Api(VeracodeError::InvalidResponse(
618 "Unable to parse users response".to_string(),
619 )))
620 }
621 404 => Err(IdentityError::UserNotFound),
622 _ => {
623 let error_text = response.text().await.unwrap_or_default();
624 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
625 "HTTP {status}: {error_text}"
626 ))))
627 }
628 }
629 }
630
631 pub async fn get_user(&self, user_id: &str) -> Result<User, IdentityError> {
646 let endpoint = format!("/api/authn/v2/users/{user_id}");
647
648 let response = self.client.get(&endpoint, None).await?;
649
650 let status = response.status().as_u16();
651 match status {
652 200 => {
653 let user: User = response.json().await?;
654 Ok(user)
655 }
656 404 => Err(IdentityError::UserNotFound),
657 _ => {
658 let error_text = response.text().await.unwrap_or_default();
659 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
660 "HTTP {status}: {error_text}"
661 ))))
662 }
663 }
664 }
665
666 pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, IdentityError> {
681 let endpoint = "/api/authn/v2/users";
682
683 let mut fixed_request = request.clone();
684
685 if fixed_request.user_name.is_none() {
687 return Err(IdentityError::InvalidInput(
688 "user_name is required".to_string(),
689 ));
690 }
691
692 let has_teams = fixed_request
694 .team_ids
695 .as_ref()
696 .is_some_and(|teams| !teams.is_empty());
697
698 if !has_teams {
699 let has_no_team_restriction_role = if let Some(ref role_ids) = fixed_request.role_ids {
701 let roles = self.list_roles().await?;
703 role_ids.iter().any(|role_id| {
704 roles.iter().any(|r| {
705 &r.role_id == role_id
706 && (r
707 .role_description
708 .as_ref()
709 .is_some_and(|desc| desc == "No Team Restriction API")
710 || r.role_name.to_lowercase() == "noteamrestrictionapi")
711 })
712 })
713 } else {
714 false
715 };
716
717 if !has_no_team_restriction_role {
718 return Err(IdentityError::InvalidInput(
719 "You must select at least one team for this user, or select No Team Restrictions role".to_string()
720 ));
721 }
722 }
723
724 let is_api_user = matches!(fixed_request.user_type, Some(UserType::ApiService));
726 let is_saml_user = matches!(fixed_request.user_type, Some(UserType::Saml));
727
728 if is_api_user && let Some(ref provided_role_ids) = fixed_request.role_ids {
730 let roles = self.list_roles().await?;
731
732 let human_role_descriptions = [
734 "Creator",
735 "Executive",
736 "Mitigation Approver",
737 "Reviewer",
738 "Sandbox User",
739 "Security Lead",
740 "Team Admin",
741 "Workspace Editor",
742 "Analytics Creator",
743 "Delete Scans",
744 "Greenlight IDE User",
745 "Policy Administrator",
746 "Sandbox Administrator",
747 "Security Insights",
748 "Submitter",
749 "Workspace Administrator",
750 ];
751
752 for role_id in provided_role_ids {
753 if let Some(role) = roles.iter().find(|r| &r.role_id == role_id) {
754 if let Some(ref desc) = role.role_description
756 && human_role_descriptions.contains(&desc.as_str())
757 {
758 return Err(IdentityError::InvalidInput(format!(
759 "Role '{}' (description: '{}') is a human-only role and cannot be assigned to API users.",
760 role.role_name, desc
761 )));
762 }
763
764 if role.is_api != Some(true) {
766 return Err(IdentityError::InvalidInput(format!(
767 "Role '{}' (is_api: {}) cannot be assigned to API users. API users can only be assigned API roles.",
768 role.role_name,
769 role.is_api.map_or("None".to_string(), |b| b.to_string())
770 )));
771 }
772 }
773 }
774 }
775
776 if fixed_request
778 .role_ids
779 .as_ref()
780 .is_none_or(|roles| roles.is_empty())
781 {
782 let roles = self.list_roles().await?;
784 let mut default_role_ids = Vec::new();
785
786 if is_api_user {
788 if let Some(api_submit_role) = roles
790 .iter()
791 .find(|r| r.role_name.to_lowercase() == "apisubmitanyscan")
792 {
793 default_role_ids.push(api_submit_role.role_id.clone());
794 }
795
796 if let Some(noteam_role) = roles
798 .iter()
799 .find(|r| r.role_name.to_lowercase() == "noteamrestrictionapi")
800 {
801 default_role_ids.push(noteam_role.role_id.clone());
802 }
803 } else {
804 if let Some(submitter_role) = roles.iter().find(|r| {
806 r.role_description
807 .as_ref()
808 .is_some_and(|desc| desc == "Submitter")
809 }) {
810 default_role_ids.push(submitter_role.role_id.clone());
811 } else if let Some(creator_role) = roles.iter().find(|r| {
812 r.role_description
813 .as_ref()
814 .is_some_and(|desc| desc == "Creator")
815 }) {
816 default_role_ids.push(creator_role.role_id.clone());
817 } else if let Some(reviewer_role) = roles.iter().find(|r| {
818 r.role_description
819 .as_ref()
820 .is_some_and(|desc| desc == "Reviewer")
821 }) {
822 default_role_ids.push(reviewer_role.role_id.clone());
823 }
824
825 if !has_teams
827 && let Some(no_team_role) = roles.iter().find(|r| {
828 r.role_description
829 .as_ref()
830 .is_some_and(|desc| desc == "No Team Restriction API")
831 || r.role_name.to_lowercase() == "noteamrestrictionapi"
832 })
833 {
834 default_role_ids.push(no_team_role.role_id.clone());
835 }
836 }
837
838 if !default_role_ids.is_empty() {
840 fixed_request.role_ids = Some(default_role_ids);
841 }
842 }
843
844 if fixed_request
846 .permissions
847 .as_ref()
848 .is_none_or(|p| p.is_empty())
849 {
850 if is_api_user {
851 let api_user_permission = Permission {
853 permission_id: None,
854 permission_name: "apiUser".to_string(),
855 description: Some("API User".to_string()),
856 api_only: Some(false),
857 ui_only: Some(false),
858 };
859 fixed_request.permissions = Some(vec![api_user_permission]);
860 } else {
861 let human_user_permission = Permission {
863 permission_id: None,
864 permission_name: "humanUser".to_string(),
865 description: Some("Human User".to_string()),
866 api_only: Some(false),
867 ui_only: Some(false),
868 };
869 fixed_request.permissions = Some(vec![human_user_permission]);
870 }
871 }
872
873 let roles_payload = if let Some(ref role_ids) = fixed_request.role_ids {
875 role_ids
876 .iter()
877 .map(|id| serde_json::json!({"role_id": id}))
878 .collect::<Vec<_>>()
879 } else {
880 Vec::new()
881 };
882
883 let teams_payload = if let Some(ref team_ids) = fixed_request.team_ids {
885 team_ids
886 .iter()
887 .map(|id| serde_json::json!({"team_id": id}))
888 .collect::<Vec<_>>()
889 } else {
890 Vec::new()
891 };
892
893 let permissions_payload = if let Some(ref permissions) = fixed_request.permissions {
895 permissions
896 .iter()
897 .map(|p| {
898 serde_json::json!({
899 "permission_name": p.permission_name,
900 "api_only": p.api_only.unwrap_or(false),
901 "ui_only": p.ui_only.unwrap_or(false)
902 })
903 })
904 .collect::<Vec<_>>()
905 } else {
906 Vec::new()
907 };
908
909 let mut payload = serde_json::json!({
911 "email_address": fixed_request.email_address,
912 "first_name": fixed_request.first_name,
913 "last_name": fixed_request.last_name,
914 "apiUser": is_api_user,
915 "samlUser": is_saml_user,
916 "active": true, "send_email_invitation": fixed_request.send_email_invitation.unwrap_or(false)
918 });
919
920 if let Some(ref user_name) = fixed_request.user_name
922 && let Some(obj) = payload.as_object_mut()
923 {
924 obj.insert("user_name".to_string(), serde_json::json!(user_name));
925 }
926
927 if !roles_payload.is_empty()
929 && let Some(obj) = payload.as_object_mut()
930 {
931 obj.insert("roles".to_string(), serde_json::json!(roles_payload));
932 }
933
934 if !teams_payload.is_empty()
936 && let Some(obj) = payload.as_object_mut()
937 {
938 obj.insert("teams".to_string(), serde_json::json!(teams_payload));
939 }
940
941 if !permissions_payload.is_empty()
943 && let Some(obj) = payload.as_object_mut()
944 {
945 obj.insert(
946 "permissions".to_string(),
947 serde_json::json!(permissions_payload),
948 );
949 }
950
951 let response = self.client.post(endpoint, Some(&payload)).await?;
952
953 let status = response.status().as_u16();
954 match status {
955 200 | 201 => {
956 let user: User = response.json().await?;
957 Ok(user)
958 }
959 400 => {
960 let error_text = response.text().await.unwrap_or_default();
961 if error_text.contains("already exists") {
962 Err(IdentityError::UserAlreadyExists(error_text))
963 } else {
964 Err(IdentityError::InvalidInput(error_text))
965 }
966 }
967 403 => {
968 let error_text = response.text().await.unwrap_or_default();
969 Err(IdentityError::PermissionDenied(error_text))
970 }
971 415 => {
972 let error_text = response.text().await.unwrap_or_default();
973 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
974 "HTTP 415 Unsupported Media Type: {error_text}"
975 ))))
976 }
977 _ => {
978 let error_text = response.text().await.unwrap_or_default();
979 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
980 "HTTP {status}: {error_text}"
981 ))))
982 }
983 }
984 }
985
986 pub async fn update_user(
1002 &self,
1003 user_id: &str,
1004 request: UpdateUserRequest,
1005 ) -> Result<User, IdentityError> {
1006 let endpoint = format!("/api/authn/v2/users/{user_id}");
1007
1008 let roles_payload = request
1010 .role_ids
1011 .iter()
1012 .map(|id| serde_json::json!({"role_id": id}))
1013 .collect::<Vec<_>>();
1014
1015 let teams_payload = request
1016 .team_ids
1017 .iter()
1018 .map(|id| serde_json::json!({"team_id": id}))
1019 .collect::<Vec<_>>();
1020
1021 let payload = serde_json::json!({
1022 "email_address": request.email_address,
1023 "user_name": request.user_name,
1024 "first_name": request.first_name,
1025 "last_name": request.last_name,
1026 "active": request.active,
1027 "roles": roles_payload,
1028 "teams": teams_payload
1029 });
1030
1031 let response = self.client.put(&endpoint, Some(&payload)).await?;
1032
1033 let status = response.status().as_u16();
1034 match status {
1035 200 => {
1036 let user: User = response.json().await?;
1037 Ok(user)
1038 }
1039 400 => {
1040 let error_text = response.text().await.unwrap_or_default();
1041 Err(IdentityError::InvalidInput(error_text))
1042 }
1043 403 => {
1044 let error_text = response.text().await.unwrap_or_default();
1045 Err(IdentityError::PermissionDenied(error_text))
1046 }
1047 404 => Err(IdentityError::UserNotFound),
1048 _ => {
1049 let error_text = response.text().await.unwrap_or_default();
1050 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1051 "HTTP {status}: {error_text}"
1052 ))))
1053 }
1054 }
1055 }
1056
1057 pub async fn delete_user(&self, user_id: &str) -> Result<(), IdentityError> {
1072 let endpoint = format!("/api/authn/v2/users/{user_id}");
1073
1074 let response = self.client.delete(&endpoint).await?;
1075
1076 let status = response.status().as_u16();
1077 match status {
1078 200 | 204 => Ok(()),
1079 403 => {
1080 let error_text = response.text().await.unwrap_or_default();
1081 Err(IdentityError::PermissionDenied(error_text))
1082 }
1083 404 => Err(IdentityError::UserNotFound),
1084 _ => {
1085 let error_text = response.text().await.unwrap_or_default();
1086 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1087 "HTTP {status}: {error_text}"
1088 ))))
1089 }
1090 }
1091 }
1092
1093 pub async fn list_roles(&self) -> Result<Vec<Role>, IdentityError> {
1104 let endpoint = "/api/authn/v2/roles";
1105 let mut all_roles = Vec::new();
1106 let mut page: u32 = 0;
1107 let page_size = 500;
1108
1109 loop {
1111 let query_params = vec![
1112 ("page".to_string(), page.to_string()),
1113 ("size".to_string(), page_size.to_string()),
1114 ];
1115
1116 let response = self.client.get(endpoint, Some(&query_params)).await?;
1117 let status = response.status().as_u16();
1118
1119 match status {
1120 200 => {
1121 let response_text = response.text().await?;
1122
1123 if let Ok(roles_response) =
1125 serde_json::from_str::<RolesResponse>(&response_text)
1126 {
1127 let page_roles = if !roles_response.roles.is_empty() {
1128 roles_response.roles
1129 } else if let Some(embedded) = roles_response.embedded {
1130 embedded.roles
1131 } else {
1132 Vec::new()
1133 };
1134
1135 if page_roles.is_empty() {
1136 break; }
1138
1139 all_roles.extend(page_roles);
1140 page = page.saturating_add(1);
1141
1142 if let Some(page_info) = roles_response.page
1144 && let (Some(current_page), Some(total_pages)) =
1145 (page_info.number, page_info.total_pages)
1146 && current_page.saturating_add(1) >= total_pages
1147 {
1148 break;
1149 }
1150
1151 continue;
1152 }
1153
1154 if let Ok(roles) = serde_json::from_str::<Vec<Role>>(&response_text) {
1156 if roles.is_empty() {
1157 break;
1158 }
1159 all_roles.extend(roles);
1160 page = page.saturating_add(1);
1161 continue;
1162 }
1163
1164 if page == 0 {
1166 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1167 "Unable to parse roles response: {response_text}"
1168 ))));
1169 }
1170 break; }
1172 _ => {
1173 let error_text = response.text().await.unwrap_or_default();
1174 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1175 "HTTP {status}: {error_text}"
1176 ))));
1177 }
1178 }
1179 }
1180
1181 Ok(all_roles)
1182 }
1183
1184 pub async fn list_teams(&self) -> Result<Vec<Team>, IdentityError> {
1195 let endpoint = "/api/authn/v2/teams";
1196 let mut all_teams = Vec::new();
1197 let mut page: u32 = 0;
1198 let page_size = 500;
1199
1200 loop {
1202 if page > 100 {
1204 break;
1205 }
1206
1207 let query_params = vec![
1208 ("page".to_string(), page.to_string()),
1209 ("size".to_string(), page_size.to_string()),
1210 ];
1211
1212 let response = self.client.get(endpoint, Some(&query_params)).await?;
1213 let status = response.status().as_u16();
1214
1215 match status {
1216 200 => {
1217 let response_text = response.text().await?;
1218
1219 if let Ok(teams_response) =
1221 serde_json::from_str::<TeamsResponse>(&response_text)
1222 {
1223 let page_teams = if !teams_response.teams.is_empty() {
1224 teams_response.teams
1225 } else if let Some(embedded) = teams_response.embedded {
1226 embedded.teams
1227 } else {
1228 Vec::new()
1229 };
1230
1231 if page_teams.is_empty() {
1232 break; }
1234
1235 all_teams.extend(page_teams);
1236 page = page.saturating_add(1);
1237
1238 if let Some(page_info) = teams_response.page
1240 && let (Some(current_page), Some(total_pages)) =
1241 (page_info.number, page_info.total_pages)
1242 && current_page.saturating_add(1) >= total_pages
1243 {
1244 break; }
1246
1247 continue;
1248 }
1249
1250 if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1252 if teams.is_empty() {
1253 break;
1254 }
1255 all_teams.extend(teams);
1256 page = page.saturating_add(1);
1257 continue;
1258 }
1259
1260 if page == 0 {
1262 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1263 "Unable to parse teams response".to_string(),
1264 )));
1265 }
1266 break;
1268 }
1269 _ => {
1270 let error_text = response.text().await.unwrap_or_default();
1271 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1272 "HTTP {status}: {error_text}"
1273 ))));
1274 }
1275 }
1276 }
1277
1278 Ok(all_teams)
1279 }
1280
1281 pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team, IdentityError> {
1296 let endpoint = "/api/authn/v2/teams";
1297
1298 let response = self.client.post(endpoint, Some(&request)).await?;
1299
1300 let status = response.status().as_u16();
1301 match status {
1302 200 | 201 => {
1303 let team: Team = response.json().await?;
1304 Ok(team)
1305 }
1306 400 => {
1307 let error_text = response.text().await.unwrap_or_default();
1308 if error_text.contains("already exists") {
1309 Err(IdentityError::TeamAlreadyExists(error_text))
1310 } else {
1311 Err(IdentityError::InvalidInput(error_text))
1312 }
1313 }
1314 403 => {
1315 let error_text = response.text().await.unwrap_or_default();
1316 Err(IdentityError::PermissionDenied(error_text))
1317 }
1318 _ => {
1319 let error_text = response.text().await.unwrap_or_default();
1320 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1321 "HTTP {status}: {error_text}"
1322 ))))
1323 }
1324 }
1325 }
1326
1327 pub async fn delete_team(&self, team_id: &str) -> Result<(), IdentityError> {
1342 let endpoint = format!("/api/authn/v2/teams/{team_id}");
1343
1344 let response = self.client.delete(&endpoint).await?;
1345
1346 let status = response.status().as_u16();
1347 match status {
1348 200 | 204 => Ok(()),
1349 403 => {
1350 let error_text = response.text().await.unwrap_or_default();
1351 Err(IdentityError::PermissionDenied(error_text))
1352 }
1353 404 => Err(IdentityError::TeamNotFound),
1354 _ => {
1355 let error_text = response.text().await.unwrap_or_default();
1356 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1357 "HTTP {status}: {error_text}"
1358 ))))
1359 }
1360 }
1361 }
1362
1363 pub async fn get_team_by_name(&self, team_name: &str) -> Result<Option<Team>, IdentityError> {
1382 let endpoint = "/api/authn/v2/teams";
1383
1384 let query_params = vec![
1385 ("page".to_string(), "0".to_string()),
1386 ("size".to_string(), "50".to_string()),
1387 ("team_name".to_string(), team_name.to_string()),
1388 ("ignore_self_teams".to_string(), "false".to_string()),
1389 ("only_manageable".to_string(), "false".to_string()),
1390 ("deleted".to_string(), "false".to_string()),
1391 ];
1392
1393 log::debug!("🔍 Team lookup request - endpoint: {}", endpoint);
1394 log::debug!(
1395 "🔍 Team lookup request - query parameters: [{}]",
1396 query_params
1397 .iter()
1398 .map(|(k, v)| format!("{}={}", k, v))
1399 .collect::<Vec<_>>()
1400 .join(", ")
1401 );
1402 log::debug!(
1403 "🔍 Team lookup request - searching for team: '{}'",
1404 team_name
1405 );
1406
1407 let response = self.client.get(endpoint, Some(&query_params)).await?;
1408 let status = response.status().as_u16();
1409
1410 log::debug!("🔍 Team lookup response - HTTP status: {}", status);
1411
1412 match status {
1413 200 => {
1414 let response_text = response.text().await?;
1415 log::debug!("🔍 Team lookup response - body: {}", response_text);
1416
1417 let process_teams = |teams: Vec<Team>, format_description: &str| -> Option<Team> {
1419 let team_count = teams.len();
1420 log::debug!(
1421 "🔍 Team lookup response - found {} teams total ({})",
1422 team_count,
1423 format_description
1424 );
1425 for (i, team) in teams.iter().enumerate() {
1426 log::debug!(
1427 "🔍 Team lookup response - team {}: '{}' (GUID: {})",
1428 i.saturating_add(1),
1429 team.team_name,
1430 team.team_id
1431 );
1432 }
1433
1434 let found_team = teams
1437 .into_iter()
1438 .find(|team| team.team_name.to_lowercase() == team_name.to_lowercase());
1439 if let Some(ref team) = found_team {
1440 log::debug!(
1441 "🔍 Team lookup result - found case-insensitive match: '{}' (searched for '{}') with GUID: {}",
1442 team.team_name,
1443 team_name,
1444 team.team_id
1445 );
1446 } else {
1447 log::debug!(
1448 "🔍 Team lookup result - no case-insensitive match for '{}' among {} teams",
1449 team_name,
1450 team_count
1451 );
1452 }
1453 found_team
1454 };
1455
1456 if let Ok(teams_response) = serde_json::from_str::<TeamsResponse>(&response_text) {
1458 let teams = if let Some(embedded) = teams_response.embedded {
1459 embedded.teams
1460 } else if !teams_response.teams.is_empty() {
1461 teams_response.teams
1462 } else {
1463 Vec::new()
1464 };
1465 Ok(process_teams(teams, "embedded format"))
1466 } else if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1467 Ok(process_teams(teams, "direct array"))
1469 } else {
1470 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1471 "Unable to parse team response".to_string(),
1472 )))
1473 }
1474 }
1475 404 => {
1476 log::debug!(
1477 "🔍 Team lookup result - HTTP 404: team '{}' not found",
1478 team_name
1479 );
1480 Ok(None) }
1482 403 => {
1483 let error_text = response.text().await.unwrap_or_default();
1484 log::debug!(
1485 "🔍 Team lookup error - HTTP 403: permission denied - {}",
1486 error_text
1487 );
1488 Err(IdentityError::PermissionDenied(error_text))
1489 }
1490 _ => {
1491 let error_text = response.text().await.unwrap_or_default();
1492 log::debug!("🔍 Team lookup error - HTTP {}: {}", status, error_text);
1493 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1494 "HTTP {status}: {error_text}"
1495 ))))
1496 }
1497 }
1498 }
1499
1500 pub async fn get_team_guid_by_name(
1518 &self,
1519 team_name: &str,
1520 ) -> Result<Option<String>, IdentityError> {
1521 match self.get_team_by_name(team_name).await? {
1522 Some(team) => Ok(Some(team.team_id)),
1523 None => Ok(None),
1524 }
1525 }
1526
1527 pub async fn create_api_credentials(
1542 &self,
1543 request: CreateApiCredentialRequest,
1544 ) -> Result<ApiCredential, IdentityError> {
1545 let endpoint = "/api/authn/v2/api_credentials";
1546
1547 let response = self.client.post(endpoint, Some(&request)).await?;
1548
1549 let status = response.status().as_u16();
1550 match status {
1551 200 | 201 => {
1552 let credentials: ApiCredential = response.json().await?;
1553 Ok(credentials)
1554 }
1555 400 => {
1556 let error_text = response.text().await.unwrap_or_default();
1557 Err(IdentityError::InvalidInput(error_text))
1558 }
1559 403 => {
1560 let error_text = response.text().await.unwrap_or_default();
1561 Err(IdentityError::PermissionDenied(error_text))
1562 }
1563 _ => {
1564 let error_text = response.text().await.unwrap_or_default();
1565 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1566 "HTTP {status}: {error_text}"
1567 ))))
1568 }
1569 }
1570 }
1571
1572 pub async fn revoke_api_credentials(&self, api_creds_id: &str) -> Result<(), IdentityError> {
1587 let endpoint = format!("/api/authn/v2/api_credentials/{api_creds_id}");
1588
1589 let response = self.client.delete(&endpoint).await?;
1590
1591 let status = response.status().as_u16();
1592 match status {
1593 200 | 204 => Ok(()), 403 => {
1595 let error_text = response.text().await.unwrap_or_default();
1596 Err(IdentityError::PermissionDenied(error_text))
1597 }
1598 404 => Err(IdentityError::UserNotFound), _ => {
1600 let error_text = response.text().await.unwrap_or_default();
1601 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1602 "HTTP {status}: {error_text}"
1603 ))))
1604 }
1605 }
1606 }
1607}
1608
1609impl<'a> IdentityApi<'a> {
1611 pub async fn find_user_by_email(&self, email: &str) -> Result<Option<User>, IdentityError> {
1626 let query = UserQuery::new().with_email(email);
1627 let users = self.list_users(Some(query)).await?;
1628 Ok(users.into_iter().find(|u| u.email_address == email))
1629 }
1630
1631 pub async fn find_user_by_username(
1646 &self,
1647 username: &str,
1648 ) -> Result<Option<User>, IdentityError> {
1649 let query = UserQuery::new().with_username(username);
1650 let users = self.list_users(Some(query)).await?;
1651 Ok(users.into_iter().find(|u| u.user_name == username))
1652 }
1653
1654 pub async fn create_simple_user(
1673 &self,
1674 email: &str,
1675 username: &str,
1676 first_name: &str,
1677 last_name: &str,
1678 team_ids: Vec<String>,
1679 ) -> Result<User, IdentityError> {
1680 let request = CreateUserRequest {
1681 email_address: email.to_string(),
1682 first_name: first_name.to_string(),
1683 last_name: last_name.to_string(),
1684 user_name: Some(username.to_string()),
1685 user_type: Some(UserType::Human),
1686 send_email_invitation: Some(true),
1687 role_ids: None, team_ids: Some(team_ids),
1689 permissions: None, };
1691
1692 self.create_user(request).await
1693 }
1694
1695 pub async fn create_api_service_account(
1715 &self,
1716 email: &str,
1717 username: &str,
1718 first_name: &str,
1719 last_name: &str,
1720 role_ids: Vec<String>,
1721 team_ids: Option<Vec<String>>,
1722 ) -> Result<User, IdentityError> {
1723 let request = CreateUserRequest {
1724 email_address: email.to_string(),
1725 first_name: first_name.to_string(),
1726 last_name: last_name.to_string(),
1727 user_name: Some(username.to_string()),
1728 user_type: Some(UserType::ApiService), send_email_invitation: Some(false),
1730 role_ids: Some(role_ids),
1731 team_ids, permissions: None, };
1734
1735 self.create_user(request).await
1736 }
1737}
1738
1739#[cfg(test)]
1740#[allow(clippy::expect_used)]
1741mod tests {
1742 use super::*;
1743
1744 #[test]
1745 fn test_user_query_params() {
1746 let query = UserQuery::new()
1747 .with_username("testuser")
1748 .with_email("test@example.com")
1749 .with_user_type(UserType::Human)
1750 .with_pagination(1, 50);
1751
1752 let params: Vec<_> = query.into();
1753 assert_eq!(params.len(), 5); assert!(params.contains(&("user_name".to_string(), "testuser".to_string())));
1755 assert!(params.contains(&("email_address".to_string(), "test@example.com".to_string())));
1756 assert!(params.contains(&("user_type".to_string(), "HUMAN".to_string())));
1757 assert!(params.contains(&("page".to_string(), "1".to_string())));
1758 assert!(params.contains(&("size".to_string(), "50".to_string())));
1759 }
1760
1761 #[test]
1762 fn test_user_type_serialization() {
1763 assert_eq!(
1764 serde_json::to_string(&UserType::Human).expect("should serialize to json"),
1765 "\"HUMAN\""
1766 );
1767 assert_eq!(
1768 serde_json::to_string(&UserType::ApiService).expect("should serialize to json"),
1769 "\"API\""
1770 );
1771 assert_eq!(
1772 serde_json::to_string(&UserType::Saml).expect("should serialize to json"),
1773 "\"SAML\""
1774 );
1775 assert_eq!(
1776 serde_json::to_string(&UserType::Vosp).expect("should serialize to json"),
1777 "\"VOSP\""
1778 );
1779 }
1780}