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 {
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#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct LoginStatus {
186 pub last_login_date: Option<DateTime<Utc>>,
188 pub never_logged_in: Option<bool>,
190 pub failed_login_attempts: Option<u32>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct CreateUserRequest {
197 pub email_address: String,
199 pub first_name: String,
201 pub last_name: String,
203 pub user_name: Option<String>,
205 pub user_type: Option<UserType>,
207 pub send_email_invitation: Option<bool>,
209 pub role_ids: Option<Vec<String>>,
211 pub team_ids: Option<Vec<String>>,
213 pub permissions: Option<Vec<Permission>>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct UpdateUserRequest {
220 pub email_address: String,
222 pub user_name: String,
224 pub first_name: Option<String>,
226 pub last_name: Option<String>,
228 pub active: Option<bool>,
230 pub role_ids: Vec<String>,
232 pub team_ids: Vec<String>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct CreateTeamRequest {
239 pub team_name: String,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub team_description: Option<String>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub business_unit_id: Option<String>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub user_ids: Option<Vec<String>>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct UpdateTeamRequest {
255 pub team_name: Option<String>,
257 pub team_description: Option<String>,
259 pub business_unit_id: Option<String>,
261 pub user_ids: Option<Vec<String>>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct CreateApiCredentialRequest {
268 pub user_id: Option<String>,
270 pub expiration_ts: Option<DateTime<Utc>>,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct UsersResponse {
277 #[serde(default, skip_serializing_if = "Vec::is_empty")]
279 pub users: Vec<User>,
280 #[serde(rename = "_embedded")]
282 pub embedded: Option<EmbeddedUsers>,
283 pub page: Option<PageInfo>,
285 #[serde(rename = "_links")]
287 pub links: Option<HashMap<String, Link>>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct EmbeddedUsers {
293 pub users: Vec<User>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct TeamsResponse {
300 #[serde(default, skip_serializing_if = "Vec::is_empty")]
302 pub teams: Vec<Team>,
303 #[serde(rename = "_embedded")]
305 pub embedded: Option<EmbeddedTeams>,
306 pub page: Option<PageInfo>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct EmbeddedTeams {
313 pub teams: Vec<Team>,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct RolesResponse {
320 #[serde(default, skip_serializing_if = "Vec::is_empty")]
322 pub roles: Vec<Role>,
323 #[serde(rename = "_embedded")]
325 pub embedded: Option<EmbeddedRoles>,
326 pub page: Option<PageInfo>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct EmbeddedRoles {
333 pub roles: Vec<Role>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct PageInfo {
340 pub number: Option<u32>,
342 pub size: Option<u32>,
344 pub total_elements: Option<u64>,
346 pub total_pages: Option<u32>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct Link {
353 pub href: String,
355}
356
357#[derive(Debug, Clone, Default)]
359pub struct UserQuery {
360 pub user_name: Option<String>,
362 pub email_address: Option<String>,
364 pub role_id: Option<String>,
366 pub user_type: Option<UserType>,
368 pub login_status: Option<String>,
370 pub page: Option<u32>,
372 pub size: Option<u32>,
374}
375
376impl UserQuery {
377 #[must_use]
379 pub fn new() -> Self {
380 Self::default()
381 }
382
383 pub fn with_username(mut self, username: impl Into<String>) -> Self {
385 self.user_name = Some(username.into());
386 self
387 }
388
389 pub fn with_email(mut self, email: impl Into<String>) -> Self {
391 self.email_address = Some(email.into());
392 self
393 }
394
395 pub fn with_role_id(mut self, role_id: impl Into<String>) -> Self {
397 self.role_id = Some(role_id.into());
398 self
399 }
400
401 #[must_use]
403 pub fn with_user_type(mut self, user_type: UserType) -> Self {
404 self.user_type = Some(user_type);
405 self
406 }
407
408 #[must_use]
410 pub fn with_pagination(mut self, page: u32, size: u32) -> Self {
411 self.page = Some(page);
412 self.size = Some(size);
413 self
414 }
415
416 #[must_use]
418 pub fn to_query_params(&self) -> Vec<(String, String)> {
419 Vec::from(self) }
421}
422
423impl From<&UserQuery> for Vec<(String, String)> {
425 fn from(query: &UserQuery) -> Self {
426 let mut params = Vec::new();
427
428 if let Some(ref username) = query.user_name {
429 params.push(("user_name".to_string(), username.clone())); }
431 if let Some(ref email) = query.email_address {
432 params.push(("email_address".to_string(), email.clone()));
433 }
434 if let Some(ref role_id) = query.role_id {
435 params.push(("role_id".to_string(), role_id.clone()));
436 }
437 if let Some(ref user_type) = query.user_type {
438 let type_str = match user_type {
439 UserType::Human => "HUMAN",
440 UserType::ApiService => "API",
441 UserType::Saml => "SAML",
442 UserType::Vosp => "VOSP",
443 };
444 params.push(("user_type".to_string(), type_str.to_string()));
445 }
446 if let Some(ref login_status) = query.login_status {
447 params.push(("login_status".to_string(), login_status.clone()));
448 }
449 if let Some(page) = query.page {
450 params.push(("page".to_string(), page.to_string()));
451 }
452 if let Some(size) = query.size {
453 params.push(("size".to_string(), size.to_string()));
454 }
455
456 params
457 }
458}
459
460impl From<UserQuery> for Vec<(String, String)> {
461 fn from(query: UserQuery) -> Self {
462 let mut params = Vec::new();
463
464 if let Some(username) = query.user_name {
465 params.push(("user_name".to_string(), username)); }
467 if let Some(email) = query.email_address {
468 params.push(("email_address".to_string(), email)); }
470 if let Some(role_id) = query.role_id {
471 params.push(("role_id".to_string(), role_id)); }
473 if let Some(user_type) = query.user_type {
474 let type_str = match user_type {
475 UserType::Human => "HUMAN",
476 UserType::ApiService => "API",
477 UserType::Saml => "SAML",
478 UserType::Vosp => "VOSP",
479 };
480 params.push(("user_type".to_string(), type_str.to_string()));
481 }
482 if let Some(login_status) = query.login_status {
483 params.push(("login_status".to_string(), login_status)); }
485 if let Some(page) = query.page {
486 params.push(("page".to_string(), page.to_string()));
487 }
488 if let Some(size) = query.size {
489 params.push(("size".to_string(), size.to_string()));
490 }
491
492 params
493 }
494}
495
496#[derive(Debug)]
498pub enum IdentityError {
499 Api(VeracodeError),
501 UserNotFound,
503 TeamNotFound,
505 RoleNotFound,
507 InvalidInput(String),
509 PermissionDenied(String),
511 UserAlreadyExists(String),
513 TeamAlreadyExists(String),
515}
516
517impl std::fmt::Display for IdentityError {
518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519 match self {
520 IdentityError::Api(err) => write!(f, "API error: {err}"),
521 IdentityError::UserNotFound => write!(f, "User not found"),
522 IdentityError::TeamNotFound => write!(f, "Team not found"),
523 IdentityError::RoleNotFound => write!(f, "Role not found"),
524 IdentityError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
525 IdentityError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
526 IdentityError::UserAlreadyExists(msg) => write!(f, "User already exists: {msg}"),
527 IdentityError::TeamAlreadyExists(msg) => write!(f, "Team already exists: {msg}"),
528 }
529 }
530}
531
532impl std::error::Error for IdentityError {}
533
534impl From<VeracodeError> for IdentityError {
535 fn from(err: VeracodeError) -> Self {
536 IdentityError::Api(err)
537 }
538}
539
540impl From<reqwest::Error> for IdentityError {
541 fn from(err: reqwest::Error) -> Self {
542 IdentityError::Api(VeracodeError::Http(err))
543 }
544}
545
546impl From<serde_json::Error> for IdentityError {
547 fn from(err: serde_json::Error) -> Self {
548 IdentityError::Api(VeracodeError::Serialization(err))
549 }
550}
551
552pub struct IdentityApi<'a> {
554 client: &'a VeracodeClient,
555}
556
557impl<'a> IdentityApi<'a> {
558 #[must_use]
560 pub fn new(client: &'a VeracodeClient) -> Self {
561 Self { client }
562 }
563
564 pub async fn list_users(&self, query: Option<UserQuery>) -> Result<Vec<User>, IdentityError> {
574 let endpoint = "/api/authn/v2/users";
575 let query_params = query.as_ref().map(Vec::from);
576
577 let response = self.client.get(endpoint, query_params.as_deref()).await?;
578
579 let status = response.status().as_u16();
580 match status {
581 200 => {
582 let response_text = response.text().await?;
583
584 if let Ok(users_response) = serde_json::from_str::<UsersResponse>(&response_text) {
586 let users = if !users_response.users.is_empty() {
587 users_response.users
588 } else if let Some(embedded) = users_response.embedded {
589 embedded.users
590 } else {
591 Vec::new()
592 };
593 return Ok(users);
594 }
595
596 if let Ok(users) = serde_json::from_str::<Vec<User>>(&response_text) {
598 return Ok(users);
599 }
600
601 Err(IdentityError::Api(VeracodeError::InvalidResponse(
602 "Unable to parse users response".to_string(),
603 )))
604 }
605 404 => Err(IdentityError::UserNotFound),
606 _ => {
607 let error_text = response.text().await.unwrap_or_default();
608 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
609 "HTTP {status}: {error_text}"
610 ))))
611 }
612 }
613 }
614
615 pub async fn get_user(&self, user_id: &str) -> Result<User, IdentityError> {
625 let endpoint = format!("/api/authn/v2/users/{user_id}");
626
627 let response = self.client.get(&endpoint, None).await?;
628
629 let status = response.status().as_u16();
630 match status {
631 200 => {
632 let user: User = response.json().await?;
633 Ok(user)
634 }
635 404 => Err(IdentityError::UserNotFound),
636 _ => {
637 let error_text = response.text().await.unwrap_or_default();
638 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
639 "HTTP {status}: {error_text}"
640 ))))
641 }
642 }
643 }
644
645 pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, IdentityError> {
655 let endpoint = "/api/authn/v2/users";
656
657 let mut fixed_request = request.clone();
658
659 if fixed_request.user_name.is_none() {
661 return Err(IdentityError::InvalidInput(
662 "user_name is required".to_string(),
663 ));
664 }
665
666 let has_teams = fixed_request
668 .team_ids
669 .as_ref()
670 .is_some_and(|teams| !teams.is_empty());
671
672 if !has_teams {
673 let has_no_team_restriction_role = if let Some(ref role_ids) = fixed_request.role_ids {
675 let roles = self.list_roles().await?;
677 role_ids.iter().any(|role_id| {
678 roles.iter().any(|r| {
679 &r.role_id == role_id
680 && (r
681 .role_description
682 .as_ref()
683 .is_some_and(|desc| desc == "No Team Restriction API")
684 || r.role_name.to_lowercase() == "noteamrestrictionapi")
685 })
686 })
687 } else {
688 false
689 };
690
691 if !has_no_team_restriction_role {
692 return Err(IdentityError::InvalidInput(
693 "You must select at least one team for this user, or select No Team Restrictions role".to_string()
694 ));
695 }
696 }
697
698 let is_api_user = matches!(fixed_request.user_type, Some(UserType::ApiService));
700 let is_saml_user = matches!(fixed_request.user_type, Some(UserType::Saml));
701
702 if is_api_user && fixed_request.role_ids.is_some() {
704 let roles = self.list_roles().await?;
705 let provided_role_ids = fixed_request
706 .role_ids
707 .as_ref()
708 .expect("role_ids was checked to be Some");
709
710 let human_role_descriptions = [
712 "Creator",
713 "Executive",
714 "Mitigation Approver",
715 "Reviewer",
716 "Sandbox User",
717 "Security Lead",
718 "Team Admin",
719 "Workspace Editor",
720 "Analytics Creator",
721 "Delete Scans",
722 "Greenlight IDE User",
723 "Policy Administrator",
724 "Sandbox Administrator",
725 "Security Insights",
726 "Submitter",
727 "Workspace Administrator",
728 ];
729
730 for role_id in provided_role_ids {
731 if let Some(role) = roles.iter().find(|r| &r.role_id == role_id) {
732 if let Some(ref desc) = role.role_description
734 && human_role_descriptions.contains(&desc.as_str())
735 {
736 return Err(IdentityError::InvalidInput(format!(
737 "Role '{}' (description: '{}') is a human-only role and cannot be assigned to API users.",
738 role.role_name, desc
739 )));
740 }
741
742 if role.is_api != Some(true) {
744 return Err(IdentityError::InvalidInput(format!(
745 "Role '{}' (is_api: {:?}) cannot be assigned to API users. API users can only be assigned API roles.",
746 role.role_name, role.is_api
747 )));
748 }
749 }
750 }
751 }
752
753 if fixed_request
755 .role_ids
756 .as_ref()
757 .is_none_or(|roles| roles.is_empty())
758 {
759 let roles = self.list_roles().await?;
761 let mut default_role_ids = Vec::new();
762
763 if is_api_user {
765 if let Some(api_submit_role) = roles
767 .iter()
768 .find(|r| r.role_name.to_lowercase() == "apisubmitanyscan")
769 {
770 default_role_ids.push(api_submit_role.role_id.clone());
771 }
772
773 if let Some(noteam_role) = roles
775 .iter()
776 .find(|r| r.role_name.to_lowercase() == "noteamrestrictionapi")
777 {
778 default_role_ids.push(noteam_role.role_id.clone());
779 }
780 } else {
781 if let Some(submitter_role) = roles.iter().find(|r| {
783 r.role_description
784 .as_ref()
785 .is_some_and(|desc| desc == "Submitter")
786 }) {
787 default_role_ids.push(submitter_role.role_id.clone());
788 } else if let Some(creator_role) = roles.iter().find(|r| {
789 r.role_description
790 .as_ref()
791 .is_some_and(|desc| desc == "Creator")
792 }) {
793 default_role_ids.push(creator_role.role_id.clone());
794 } else if let Some(reviewer_role) = roles.iter().find(|r| {
795 r.role_description
796 .as_ref()
797 .is_some_and(|desc| desc == "Reviewer")
798 }) {
799 default_role_ids.push(reviewer_role.role_id.clone());
800 }
801
802 if !has_teams
804 && let Some(no_team_role) = roles.iter().find(|r| {
805 r.role_description
806 .as_ref()
807 .is_some_and(|desc| desc == "No Team Restriction API")
808 || r.role_name.to_lowercase() == "noteamrestrictionapi"
809 })
810 {
811 default_role_ids.push(no_team_role.role_id.clone());
812 }
813 }
814
815 if !default_role_ids.is_empty() {
817 fixed_request.role_ids = Some(default_role_ids);
818 }
819 }
820
821 if fixed_request.permissions.is_none()
823 || fixed_request
824 .permissions
825 .as_ref()
826 .expect("permissions was checked to be Some")
827 .is_empty()
828 {
829 if is_api_user {
830 let api_user_permission = Permission {
832 permission_id: None,
833 permission_name: "apiUser".to_string(),
834 description: Some("API User".to_string()),
835 api_only: Some(false),
836 ui_only: Some(false),
837 };
838 fixed_request.permissions = Some(vec![api_user_permission]);
839 } else {
840 let human_user_permission = Permission {
842 permission_id: None,
843 permission_name: "humanUser".to_string(),
844 description: Some("Human User".to_string()),
845 api_only: Some(false),
846 ui_only: Some(false),
847 };
848 fixed_request.permissions = Some(vec![human_user_permission]);
849 }
850 }
851
852 let roles_payload = if let Some(ref role_ids) = fixed_request.role_ids {
854 role_ids
855 .iter()
856 .map(|id| serde_json::json!({"role_id": id}))
857 .collect::<Vec<_>>()
858 } else {
859 Vec::new()
860 };
861
862 let teams_payload = if let Some(ref team_ids) = fixed_request.team_ids {
864 team_ids
865 .iter()
866 .map(|id| serde_json::json!({"team_id": id}))
867 .collect::<Vec<_>>()
868 } else {
869 Vec::new()
870 };
871
872 let permissions_payload = if let Some(ref permissions) = fixed_request.permissions {
874 permissions
875 .iter()
876 .map(|p| {
877 serde_json::json!({
878 "permission_name": p.permission_name,
879 "api_only": p.api_only.unwrap_or(false),
880 "ui_only": p.ui_only.unwrap_or(false)
881 })
882 })
883 .collect::<Vec<_>>()
884 } else {
885 Vec::new()
886 };
887
888 let mut payload = serde_json::json!({
890 "email_address": fixed_request.email_address,
891 "first_name": fixed_request.first_name,
892 "last_name": fixed_request.last_name,
893 "apiUser": is_api_user,
894 "samlUser": is_saml_user,
895 "active": true, "send_email_invitation": fixed_request.send_email_invitation.unwrap_or(false)
897 });
898
899 if let Some(ref user_name) = fixed_request.user_name {
901 payload["user_name"] = serde_json::json!(user_name);
902 }
903
904 if !roles_payload.is_empty() {
906 payload["roles"] = serde_json::json!(roles_payload);
907 }
908
909 if !teams_payload.is_empty() {
911 payload["teams"] = serde_json::json!(teams_payload);
912 }
913
914 if !permissions_payload.is_empty() {
916 payload["permissions"] = serde_json::json!(permissions_payload);
917 }
918
919 let response = self.client.post(endpoint, Some(&payload)).await?;
920
921 let status = response.status().as_u16();
922 match status {
923 200 | 201 => {
924 let user: User = response.json().await?;
925 Ok(user)
926 }
927 400 => {
928 let error_text = response.text().await.unwrap_or_default();
929 if error_text.contains("already exists") {
930 Err(IdentityError::UserAlreadyExists(error_text))
931 } else {
932 Err(IdentityError::InvalidInput(error_text))
933 }
934 }
935 403 => {
936 let error_text = response.text().await.unwrap_or_default();
937 Err(IdentityError::PermissionDenied(error_text))
938 }
939 415 => {
940 let error_text = response.text().await.unwrap_or_default();
941 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
942 "HTTP 415 Unsupported Media Type: {error_text}"
943 ))))
944 }
945 _ => {
946 let error_text = response.text().await.unwrap_or_default();
947 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
948 "HTTP {status}: {error_text}"
949 ))))
950 }
951 }
952 }
953
954 pub async fn update_user(
965 &self,
966 user_id: &str,
967 request: UpdateUserRequest,
968 ) -> Result<User, IdentityError> {
969 let endpoint = format!("/api/authn/v2/users/{user_id}");
970
971 let roles_payload = request
973 .role_ids
974 .iter()
975 .map(|id| serde_json::json!({"role_id": id}))
976 .collect::<Vec<_>>();
977
978 let teams_payload = request
979 .team_ids
980 .iter()
981 .map(|id| serde_json::json!({"team_id": id}))
982 .collect::<Vec<_>>();
983
984 let payload = serde_json::json!({
985 "email_address": request.email_address,
986 "user_name": request.user_name,
987 "first_name": request.first_name,
988 "last_name": request.last_name,
989 "active": request.active,
990 "roles": roles_payload,
991 "teams": teams_payload
992 });
993
994 let response = self.client.put(&endpoint, Some(&payload)).await?;
995
996 let status = response.status().as_u16();
997 match status {
998 200 => {
999 let user: User = response.json().await?;
1000 Ok(user)
1001 }
1002 400 => {
1003 let error_text = response.text().await.unwrap_or_default();
1004 Err(IdentityError::InvalidInput(error_text))
1005 }
1006 403 => {
1007 let error_text = response.text().await.unwrap_or_default();
1008 Err(IdentityError::PermissionDenied(error_text))
1009 }
1010 404 => Err(IdentityError::UserNotFound),
1011 _ => {
1012 let error_text = response.text().await.unwrap_or_default();
1013 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1014 "HTTP {status}: {error_text}"
1015 ))))
1016 }
1017 }
1018 }
1019
1020 pub async fn delete_user(&self, user_id: &str) -> Result<(), IdentityError> {
1030 let endpoint = format!("/api/authn/v2/users/{user_id}");
1031
1032 let response = self.client.delete(&endpoint).await?;
1033
1034 let status = response.status().as_u16();
1035 match status {
1036 200 | 204 => Ok(()),
1037 403 => {
1038 let error_text = response.text().await.unwrap_or_default();
1039 Err(IdentityError::PermissionDenied(error_text))
1040 }
1041 404 => Err(IdentityError::UserNotFound),
1042 _ => {
1043 let error_text = response.text().await.unwrap_or_default();
1044 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1045 "HTTP {status}: {error_text}"
1046 ))))
1047 }
1048 }
1049 }
1050
1051 pub async fn list_roles(&self) -> Result<Vec<Role>, IdentityError> {
1057 let endpoint = "/api/authn/v2/roles";
1058 let mut all_roles = Vec::new();
1059 let mut page = 0;
1060 let page_size = 500;
1061
1062 loop {
1064 let query_params = vec![
1065 ("page".to_string(), page.to_string()),
1066 ("size".to_string(), page_size.to_string()),
1067 ];
1068
1069 let response = self.client.get(endpoint, Some(&query_params)).await?;
1070 let status = response.status().as_u16();
1071
1072 match status {
1073 200 => {
1074 let response_text = response.text().await?;
1075
1076 if let Ok(roles_response) =
1078 serde_json::from_str::<RolesResponse>(&response_text)
1079 {
1080 let page_roles = if !roles_response.roles.is_empty() {
1081 roles_response.roles
1082 } else if let Some(embedded) = roles_response.embedded {
1083 embedded.roles
1084 } else {
1085 Vec::new()
1086 };
1087
1088 if page_roles.is_empty() {
1089 break; }
1091
1092 all_roles.extend(page_roles);
1093 page += 1;
1094
1095 if let Some(page_info) = roles_response.page
1097 && let (Some(current_page), Some(total_pages)) =
1098 (page_info.number, page_info.total_pages)
1099 && current_page + 1 >= total_pages
1100 {
1101 break;
1102 }
1103
1104 continue;
1105 }
1106
1107 if let Ok(roles) = serde_json::from_str::<Vec<Role>>(&response_text) {
1109 if roles.is_empty() {
1110 break;
1111 }
1112 all_roles.extend(roles);
1113 page += 1;
1114 continue;
1115 }
1116
1117 if page == 0 {
1119 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1120 "Unable to parse roles response: {response_text}"
1121 ))));
1122 }
1123 break; }
1125 _ => {
1126 let error_text = response.text().await.unwrap_or_default();
1127 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1128 "HTTP {status}: {error_text}"
1129 ))));
1130 }
1131 }
1132 }
1133
1134 Ok(all_roles)
1135 }
1136
1137 pub async fn list_teams(&self) -> Result<Vec<Team>, IdentityError> {
1143 let endpoint = "/api/authn/v2/teams";
1144 let mut all_teams = Vec::new();
1145 let mut page = 0;
1146 let page_size = 500;
1147
1148 loop {
1150 if page > 100 {
1152 break;
1153 }
1154
1155 let query_params = vec![
1156 ("page".to_string(), page.to_string()),
1157 ("size".to_string(), page_size.to_string()),
1158 ];
1159
1160 let response = self.client.get(endpoint, Some(&query_params)).await?;
1161 let status = response.status().as_u16();
1162
1163 match status {
1164 200 => {
1165 let response_text = response.text().await?;
1166
1167 if let Ok(teams_response) =
1169 serde_json::from_str::<TeamsResponse>(&response_text)
1170 {
1171 let page_teams = if !teams_response.teams.is_empty() {
1172 teams_response.teams
1173 } else if let Some(embedded) = teams_response.embedded {
1174 embedded.teams
1175 } else {
1176 Vec::new()
1177 };
1178
1179 if page_teams.is_empty() {
1180 break; }
1182
1183 all_teams.extend(page_teams);
1184 page += 1;
1185
1186 if let Some(page_info) = teams_response.page
1188 && let (Some(current_page), Some(total_pages)) =
1189 (page_info.number, page_info.total_pages)
1190 && current_page + 1 >= total_pages
1191 {
1192 break; }
1194
1195 continue;
1196 }
1197
1198 if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1200 if teams.is_empty() {
1201 break;
1202 }
1203 all_teams.extend(teams);
1204 page += 1;
1205 continue;
1206 }
1207
1208 if page == 0 {
1210 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1211 "Unable to parse teams response".to_string(),
1212 )));
1213 }
1214 break;
1216 }
1217 _ => {
1218 let error_text = response.text().await.unwrap_or_default();
1219 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1220 "HTTP {status}: {error_text}"
1221 ))));
1222 }
1223 }
1224 }
1225
1226 Ok(all_teams)
1227 }
1228
1229 pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team, IdentityError> {
1239 let endpoint = "/api/authn/v2/teams";
1240
1241 let response = self.client.post(endpoint, Some(&request)).await?;
1242
1243 let status = response.status().as_u16();
1244 match status {
1245 200 | 201 => {
1246 let team: Team = response.json().await?;
1247 Ok(team)
1248 }
1249 400 => {
1250 let error_text = response.text().await.unwrap_or_default();
1251 if error_text.contains("already exists") {
1252 Err(IdentityError::TeamAlreadyExists(error_text))
1253 } else {
1254 Err(IdentityError::InvalidInput(error_text))
1255 }
1256 }
1257 403 => {
1258 let error_text = response.text().await.unwrap_or_default();
1259 Err(IdentityError::PermissionDenied(error_text))
1260 }
1261 _ => {
1262 let error_text = response.text().await.unwrap_or_default();
1263 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1264 "HTTP {status}: {error_text}"
1265 ))))
1266 }
1267 }
1268 }
1269
1270 pub async fn delete_team(&self, team_id: &str) -> Result<(), IdentityError> {
1280 let endpoint = format!("/api/authn/v2/teams/{team_id}");
1281
1282 let response = self.client.delete(&endpoint).await?;
1283
1284 let status = response.status().as_u16();
1285 match status {
1286 200 | 204 => Ok(()),
1287 403 => {
1288 let error_text = response.text().await.unwrap_or_default();
1289 Err(IdentityError::PermissionDenied(error_text))
1290 }
1291 404 => Err(IdentityError::TeamNotFound),
1292 _ => {
1293 let error_text = response.text().await.unwrap_or_default();
1294 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1295 "HTTP {status}: {error_text}"
1296 ))))
1297 }
1298 }
1299 }
1300
1301 pub async fn get_team_by_name(&self, team_name: &str) -> Result<Option<Team>, IdentityError> {
1315 let endpoint = "/api/authn/v2/teams";
1316
1317 let query_params = vec![
1318 ("page".to_string(), "0".to_string()),
1319 ("size".to_string(), "50".to_string()),
1320 ("team_name".to_string(), team_name.to_string()),
1321 ("ignore_self_teams".to_string(), "false".to_string()),
1322 ("only_manageable".to_string(), "false".to_string()),
1323 ("deleted".to_string(), "false".to_string()),
1324 ];
1325
1326 log::debug!("🔍 Team lookup request - endpoint: {}", endpoint);
1327 log::debug!(
1328 "🔍 Team lookup request - query parameters: {:?}",
1329 query_params
1330 );
1331 log::debug!(
1332 "🔍 Team lookup request - searching for team: '{}'",
1333 team_name
1334 );
1335
1336 let response = self.client.get(endpoint, Some(&query_params)).await?;
1337 let status = response.status().as_u16();
1338
1339 log::debug!("🔍 Team lookup response - HTTP status: {}", status);
1340
1341 match status {
1342 200 => {
1343 let response_text = response.text().await?;
1344 log::debug!("🔍 Team lookup response - body: {}", response_text);
1345
1346 let process_teams = |teams: Vec<Team>, format_description: &str| -> Option<Team> {
1348 let team_count = teams.len();
1349 log::debug!(
1350 "🔍 Team lookup response - found {} teams total ({})",
1351 team_count,
1352 format_description
1353 );
1354 for (i, team) in teams.iter().enumerate() {
1355 log::debug!(
1356 "🔍 Team lookup response - team {}: '{}' (GUID: {})",
1357 i + 1,
1358 team.team_name,
1359 team.team_id
1360 );
1361 }
1362
1363 let found_team = teams
1366 .into_iter()
1367 .find(|team| team.team_name.to_lowercase() == team_name.to_lowercase());
1368 if let Some(ref team) = found_team {
1369 log::debug!(
1370 "🔍 Team lookup result - found case-insensitive match: '{}' (searched for '{}') with GUID: {}",
1371 team.team_name,
1372 team_name,
1373 team.team_id
1374 );
1375 } else {
1376 log::debug!(
1377 "🔍 Team lookup result - no case-insensitive match for '{}' among {} teams",
1378 team_name,
1379 team_count
1380 );
1381 }
1382 found_team
1383 };
1384
1385 if let Ok(teams_response) = serde_json::from_str::<TeamsResponse>(&response_text) {
1387 let teams = if let Some(embedded) = teams_response.embedded {
1388 embedded.teams
1389 } else if !teams_response.teams.is_empty() {
1390 teams_response.teams
1391 } else {
1392 Vec::new()
1393 };
1394 Ok(process_teams(teams, "embedded format"))
1395 } else if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1396 Ok(process_teams(teams, "direct array"))
1398 } else {
1399 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1400 "Unable to parse team response".to_string(),
1401 )))
1402 }
1403 }
1404 404 => {
1405 log::debug!(
1406 "🔍 Team lookup result - HTTP 404: team '{}' not found",
1407 team_name
1408 );
1409 Ok(None) }
1411 403 => {
1412 let error_text = response.text().await.unwrap_or_default();
1413 log::debug!(
1414 "🔍 Team lookup error - HTTP 403: permission denied - {}",
1415 error_text
1416 );
1417 Err(IdentityError::PermissionDenied(error_text))
1418 }
1419 _ => {
1420 let error_text = response.text().await.unwrap_or_default();
1421 log::debug!("🔍 Team lookup error - HTTP {}: {}", status, error_text);
1422 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1423 "HTTP {status}: {error_text}"
1424 ))))
1425 }
1426 }
1427 }
1428
1429 pub async fn get_team_guid_by_name(
1442 &self,
1443 team_name: &str,
1444 ) -> Result<Option<String>, IdentityError> {
1445 match self.get_team_by_name(team_name).await? {
1446 Some(team) => Ok(Some(team.team_id)),
1447 None => Ok(None),
1448 }
1449 }
1450
1451 pub async fn create_api_credentials(
1461 &self,
1462 request: CreateApiCredentialRequest,
1463 ) -> Result<ApiCredential, IdentityError> {
1464 let endpoint = "/api/authn/v2/api_credentials";
1465
1466 let response = self.client.post(endpoint, Some(&request)).await?;
1467
1468 let status = response.status().as_u16();
1469 match status {
1470 200 | 201 => {
1471 let credentials: ApiCredential = response.json().await?;
1472 Ok(credentials)
1473 }
1474 400 => {
1475 let error_text = response.text().await.unwrap_or_default();
1476 Err(IdentityError::InvalidInput(error_text))
1477 }
1478 403 => {
1479 let error_text = response.text().await.unwrap_or_default();
1480 Err(IdentityError::PermissionDenied(error_text))
1481 }
1482 _ => {
1483 let error_text = response.text().await.unwrap_or_default();
1484 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1485 "HTTP {status}: {error_text}"
1486 ))))
1487 }
1488 }
1489 }
1490
1491 pub async fn revoke_api_credentials(&self, api_creds_id: &str) -> Result<(), IdentityError> {
1501 let endpoint = format!("/api/authn/v2/api_credentials/{api_creds_id}");
1502
1503 let response = self.client.delete(&endpoint).await?;
1504
1505 let status = response.status().as_u16();
1506 match status {
1507 200 | 204 => Ok(()), 403 => {
1509 let error_text = response.text().await.unwrap_or_default();
1510 Err(IdentityError::PermissionDenied(error_text))
1511 }
1512 404 => Err(IdentityError::UserNotFound), _ => {
1514 let error_text = response.text().await.unwrap_or_default();
1515 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1516 "HTTP {status}: {error_text}"
1517 ))))
1518 }
1519 }
1520 }
1521}
1522
1523impl<'a> IdentityApi<'a> {
1525 pub async fn find_user_by_email(&self, email: &str) -> Result<Option<User>, IdentityError> {
1535 let query = UserQuery::new().with_email(email);
1536 let users = self.list_users(Some(query)).await?;
1537 Ok(users.into_iter().find(|u| u.email_address == email))
1538 }
1539
1540 pub async fn find_user_by_username(
1550 &self,
1551 username: &str,
1552 ) -> Result<Option<User>, IdentityError> {
1553 let query = UserQuery::new().with_username(username);
1554 let users = self.list_users(Some(query)).await?;
1555 Ok(users.into_iter().find(|u| u.user_name == username))
1556 }
1557
1558 pub async fn create_simple_user(
1572 &self,
1573 email: &str,
1574 username: &str,
1575 first_name: &str,
1576 last_name: &str,
1577 team_ids: Vec<String>,
1578 ) -> Result<User, IdentityError> {
1579 let request = CreateUserRequest {
1580 email_address: email.to_string(),
1581 first_name: first_name.to_string(),
1582 last_name: last_name.to_string(),
1583 user_name: Some(username.to_string()),
1584 user_type: Some(UserType::Human),
1585 send_email_invitation: Some(true),
1586 role_ids: None, team_ids: Some(team_ids),
1588 permissions: None, };
1590
1591 self.create_user(request).await
1592 }
1593
1594 pub async fn create_api_service_account(
1609 &self,
1610 email: &str,
1611 username: &str,
1612 first_name: &str,
1613 last_name: &str,
1614 role_ids: Vec<String>,
1615 team_ids: Option<Vec<String>>,
1616 ) -> Result<User, IdentityError> {
1617 let request = CreateUserRequest {
1618 email_address: email.to_string(),
1619 first_name: first_name.to_string(),
1620 last_name: last_name.to_string(),
1621 user_name: Some(username.to_string()),
1622 user_type: Some(UserType::ApiService), send_email_invitation: Some(false),
1624 role_ids: Some(role_ids),
1625 team_ids, permissions: None, };
1628
1629 self.create_user(request).await
1630 }
1631}
1632
1633#[cfg(test)]
1634mod tests {
1635 use super::*;
1636
1637 #[test]
1638 fn test_user_query_params() {
1639 let query = UserQuery::new()
1640 .with_username("testuser")
1641 .with_email("test@example.com")
1642 .with_user_type(UserType::Human)
1643 .with_pagination(1, 50);
1644
1645 let params: Vec<_> = query.into();
1646 assert_eq!(params.len(), 5); assert!(params.contains(&("user_name".to_string(), "testuser".to_string())));
1648 assert!(params.contains(&("email_address".to_string(), "test@example.com".to_string())));
1649 assert!(params.contains(&("user_type".to_string(), "HUMAN".to_string())));
1650 assert!(params.contains(&("page".to_string(), "1".to_string())));
1651 assert!(params.contains(&("size".to_string(), "50".to_string())));
1652 }
1653
1654 #[test]
1655 fn test_user_type_serialization() {
1656 assert_eq!(
1657 serde_json::to_string(&UserType::Human).unwrap(),
1658 "\"HUMAN\""
1659 );
1660 assert_eq!(
1661 serde_json::to_string(&UserType::ApiService).unwrap(),
1662 "\"API\""
1663 );
1664 assert_eq!(serde_json::to_string(&UserType::Saml).unwrap(), "\"SAML\"");
1665 assert_eq!(serde_json::to_string(&UserType::Vosp).unwrap(), "\"VOSP\"");
1666 }
1667}