1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use chrono::{DateTime, Utc};
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(Debug, 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
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct LoginStatus {
173 pub last_login_date: Option<DateTime<Utc>>,
175 pub never_logged_in: Option<bool>,
177 pub failed_login_attempts: Option<u32>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct CreateUserRequest {
184 pub email_address: String,
186 pub first_name: String,
188 pub last_name: String,
190 pub user_name: Option<String>,
192 pub user_type: Option<UserType>,
194 pub send_email_invitation: Option<bool>,
196 pub role_ids: Option<Vec<String>>,
198 pub team_ids: Option<Vec<String>>,
200 pub permissions: Option<Vec<Permission>>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct UpdateUserRequest {
207 pub email_address: String,
209 pub user_name: String,
211 pub first_name: Option<String>,
213 pub last_name: Option<String>,
215 pub active: Option<bool>,
217 pub role_ids: Vec<String>,
219 pub team_ids: Vec<String>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct CreateTeamRequest {
226 pub team_name: String,
228 pub team_description: Option<String>,
230 pub business_unit_id: Option<String>,
232 pub user_ids: Option<Vec<String>>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct UpdateTeamRequest {
239 pub team_name: Option<String>,
241 pub team_description: Option<String>,
243 pub business_unit_id: Option<String>,
245 pub user_ids: Option<Vec<String>>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct CreateApiCredentialRequest {
252 pub user_id: Option<String>,
254 pub expiration_ts: Option<DateTime<Utc>>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct UsersResponse {
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
263 pub users: Vec<User>,
264 #[serde(rename = "_embedded")]
266 pub embedded: Option<EmbeddedUsers>,
267 pub page: Option<PageInfo>,
269 #[serde(rename = "_links")]
271 pub links: Option<HashMap<String, Link>>,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct EmbeddedUsers {
277 pub users: Vec<User>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct TeamsResponse {
284 #[serde(default, skip_serializing_if = "Vec::is_empty")]
286 pub teams: Vec<Team>,
287 #[serde(rename = "_embedded")]
289 pub embedded: Option<EmbeddedTeams>,
290 pub page: Option<PageInfo>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct EmbeddedTeams {
297 pub teams: Vec<Team>,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct RolesResponse {
304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
306 pub roles: Vec<Role>,
307 #[serde(rename = "_embedded")]
309 pub embedded: Option<EmbeddedRoles>,
310 pub page: Option<PageInfo>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct EmbeddedRoles {
317 pub roles: Vec<Role>,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct PageInfo {
324 pub number: Option<u32>,
326 pub size: Option<u32>,
328 pub total_elements: Option<u64>,
330 pub total_pages: Option<u32>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct Link {
337 pub href: String,
339}
340
341#[derive(Debug, Clone, Default)]
343pub struct UserQuery {
344 pub user_name: Option<String>,
346 pub email_address: Option<String>,
348 pub role_id: Option<String>,
350 pub user_type: Option<UserType>,
352 pub login_status: Option<String>,
354 pub page: Option<u32>,
356 pub size: Option<u32>,
358}
359
360impl UserQuery {
361 pub fn new() -> Self {
363 Self::default()
364 }
365
366 pub fn with_username(mut self, username: impl Into<String>) -> Self {
368 self.user_name = Some(username.into());
369 self
370 }
371
372 pub fn with_email(mut self, email: impl Into<String>) -> Self {
374 self.email_address = Some(email.into());
375 self
376 }
377
378 pub fn with_role_id(mut self, role_id: impl Into<String>) -> Self {
380 self.role_id = Some(role_id.into());
381 self
382 }
383
384 pub fn with_user_type(mut self, user_type: UserType) -> Self {
386 self.user_type = Some(user_type);
387 self
388 }
389
390 pub fn with_pagination(mut self, page: u32, size: u32) -> Self {
392 self.page = Some(page);
393 self.size = Some(size);
394 self
395 }
396
397 pub fn to_query_params(&self) -> Vec<(String, String)> {
399 let mut params = Vec::new();
400
401 if let Some(ref username) = self.user_name {
402 params.push(("user_name".to_string(), username.clone()));
403 }
404 if let Some(ref email) = self.email_address {
405 params.push(("email_address".to_string(), email.clone()));
406 }
407 if let Some(ref role_id) = self.role_id {
408 params.push(("role_id".to_string(), role_id.clone()));
409 }
410 if let Some(ref user_type) = self.user_type {
411 let type_str = match user_type {
412 UserType::Human => "HUMAN",
413 UserType::ApiService => "API",
414 UserType::Saml => "SAML",
415 UserType::Vosp => "VOSP",
416 };
417 params.push(("user_type".to_string(), type_str.to_string()));
418 }
419 if let Some(ref login_status) = self.login_status {
420 params.push(("login_status".to_string(), login_status.clone()));
421 }
422 if let Some(page) = self.page {
423 params.push(("page".to_string(), page.to_string()));
424 }
425 if let Some(size) = self.size {
426 params.push(("size".to_string(), size.to_string()));
427 }
428
429 params
430 }
431}
432
433#[derive(Debug)]
435pub enum IdentityError {
436 Api(VeracodeError),
438 UserNotFound,
440 TeamNotFound,
442 RoleNotFound,
444 InvalidInput(String),
446 PermissionDenied(String),
448 UserAlreadyExists(String),
450 TeamAlreadyExists(String),
452}
453
454impl std::fmt::Display for IdentityError {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 match self {
457 IdentityError::Api(err) => write!(f, "API error: {err}"),
458 IdentityError::UserNotFound => write!(f, "User not found"),
459 IdentityError::TeamNotFound => write!(f, "Team not found"),
460 IdentityError::RoleNotFound => write!(f, "Role not found"),
461 IdentityError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
462 IdentityError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
463 IdentityError::UserAlreadyExists(msg) => write!(f, "User already exists: {msg}"),
464 IdentityError::TeamAlreadyExists(msg) => write!(f, "Team already exists: {msg}"),
465 }
466 }
467}
468
469impl std::error::Error for IdentityError {}
470
471impl From<VeracodeError> for IdentityError {
472 fn from(err: VeracodeError) -> Self {
473 IdentityError::Api(err)
474 }
475}
476
477impl From<reqwest::Error> for IdentityError {
478 fn from(err: reqwest::Error) -> Self {
479 IdentityError::Api(VeracodeError::Http(err))
480 }
481}
482
483impl From<serde_json::Error> for IdentityError {
484 fn from(err: serde_json::Error) -> Self {
485 IdentityError::Api(VeracodeError::Serialization(err))
486 }
487}
488
489pub struct IdentityApi<'a> {
491 client: &'a VeracodeClient,
492}
493
494impl<'a> IdentityApi<'a> {
495 pub fn new(client: &'a VeracodeClient) -> Self {
497 Self { client }
498 }
499
500 pub async fn list_users(&self, query: Option<UserQuery>) -> Result<Vec<User>, IdentityError> {
510 let endpoint = "/api/authn/v2/users";
511 let query_params = query.as_ref().map(|q| q.to_query_params());
512
513 let response = self.client.get(endpoint, query_params.as_deref()).await?;
514
515 let status = response.status().as_u16();
516 match status {
517 200 => {
518 let response_text = response.text().await?;
519
520
521 if let Ok(users_response) = serde_json::from_str::<UsersResponse>(&response_text) {
523 let users = if !users_response.users.is_empty() {
524 users_response.users
525 } else if let Some(embedded) = users_response.embedded {
526 embedded.users
527 } else {
528 Vec::new()
529 };
530 return Ok(users);
531 }
532
533 if let Ok(users) = serde_json::from_str::<Vec<User>>(&response_text) {
535 return Ok(users);
536 }
537
538 Err(IdentityError::Api(VeracodeError::InvalidResponse(
539 "Unable to parse users response".to_string()
540 )))
541 }
542 404 => Err(IdentityError::UserNotFound),
543 _ => {
544 let error_text = response.text().await.unwrap_or_default();
545 Err(IdentityError::Api(VeracodeError::InvalidResponse(
546 format!("HTTP {status}: {error_text}")
547 )))
548 }
549 }
550 }
551
552 pub async fn get_user(&self, user_id: &str) -> Result<User, IdentityError> {
562 let endpoint = format!("/api/authn/v2/users/{user_id}");
563
564 let response = self.client.get(&endpoint, None).await?;
565
566 let status = response.status().as_u16();
567 match status {
568 200 => {
569 let user: User = response.json().await?;
570 Ok(user)
571 }
572 404 => Err(IdentityError::UserNotFound),
573 _ => {
574 let error_text = response.text().await.unwrap_or_default();
575 Err(IdentityError::Api(VeracodeError::InvalidResponse(
576 format!("HTTP {status}: {error_text}")
577 )))
578 }
579 }
580 }
581
582 pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, IdentityError> {
592 let endpoint = "/api/authn/v2/users";
593
594 let mut fixed_request = request.clone();
595
596 if fixed_request.user_name.is_none() {
598 return Err(IdentityError::InvalidInput("user_name is required".to_string()));
599 }
600
601 let has_teams = fixed_request.team_ids.is_some() && !fixed_request.team_ids.as_ref().unwrap().is_empty();
603
604 if !has_teams {
605 let has_no_team_restriction_role = if let Some(ref role_ids) = fixed_request.role_ids {
607 let roles = self.list_roles().await?;
609 role_ids.iter().any(|role_id| {
610 roles.iter().any(|r| &r.role_id == role_id &&
611 (r.role_description.as_ref().is_some_and(|desc| desc == "No Team Restriction API") ||
612 r.role_name.to_lowercase() == "noteamrestrictionapi"))
613 })
614 } else {
615 false
616 };
617
618 if !has_no_team_restriction_role {
619 return Err(IdentityError::InvalidInput(
620 "You must select at least one team for this user, or select No Team Restrictions role".to_string()
621 ));
622 }
623 }
624
625 let is_api_user = matches!(fixed_request.user_type, Some(UserType::ApiService));
627 let is_saml_user = matches!(fixed_request.user_type, Some(UserType::Saml));
628
629 if is_api_user && fixed_request.role_ids.is_some() {
631 let roles = self.list_roles().await?;
632 let provided_role_ids = fixed_request.role_ids.as_ref().unwrap();
633
634 let human_role_descriptions = [
636 "Creator", "Executive", "Mitigation Approver", "Reviewer", "Sandbox User",
637 "Security Lead", "Team Admin", "Workspace Editor", "Analytics Creator",
638 "Delete Scans", "Greenlight IDE User", "Policy Administrator",
639 "Sandbox Administrator", "Security Insights", "Submitter", "Workspace Administrator"
640 ];
641
642 for role_id in provided_role_ids {
643 if let Some(role) = roles.iter().find(|r| &r.role_id == role_id) {
644 if let Some(ref desc) = role.role_description {
646 if human_role_descriptions.contains(&desc.as_str()) {
647 return Err(IdentityError::InvalidInput(
648 format!("Role '{}' (description: '{}') is a human-only role and cannot be assigned to API users.",
649 role.role_name, desc)
650 ));
651 }
652 }
653
654 if role.is_api != Some(true) {
656 return Err(IdentityError::InvalidInput(
657 format!("Role '{}' (is_api: {:?}) cannot be assigned to API users. API users can only be assigned API roles.",
658 role.role_name, role.is_api)
659 ));
660 }
661 }
662 }
663 }
664
665 if fixed_request.role_ids.is_none() || fixed_request.role_ids.as_ref().unwrap().is_empty() {
667 let roles = self.list_roles().await?;
669 let mut default_role_ids = Vec::new();
670
671 if is_api_user {
673 if let Some(api_submit_role) = roles.iter().find(|r|
675 r.role_name.to_lowercase() == "apisubmitanyscan"
676 ) {
677 default_role_ids.push(api_submit_role.role_id.clone());
678 }
679
680 if let Some(noteam_role) = roles.iter().find(|r|
682 r.role_name.to_lowercase() == "noteamrestrictionapi"
683 ) {
684 default_role_ids.push(noteam_role.role_id.clone());
685 }
686 } else {
687 if let Some(submitter_role) = roles.iter().find(|r|
689 r.role_description.as_ref().is_some_and(|desc| desc == "Submitter")
690 ) {
691 default_role_ids.push(submitter_role.role_id.clone());
692 } else if let Some(creator_role) = roles.iter().find(|r|
693 r.role_description.as_ref().is_some_and(|desc| desc == "Creator")
694 ) {
695 default_role_ids.push(creator_role.role_id.clone());
696 } else if let Some(reviewer_role) = roles.iter().find(|r|
697 r.role_description.as_ref().is_some_and(|desc| desc == "Reviewer")
698 ) {
699 default_role_ids.push(reviewer_role.role_id.clone());
700 }
701
702 if !has_teams {
704 if let Some(no_team_role) = roles.iter().find(|r|
705 r.role_description.as_ref().is_some_and(|desc| desc == "No Team Restriction API") ||
706 r.role_name.to_lowercase() == "noteamrestrictionapi"
707 ) {
708 default_role_ids.push(no_team_role.role_id.clone());
709 }
710 }
711 }
712
713 if !default_role_ids.is_empty() {
715 fixed_request.role_ids = Some(default_role_ids);
716 }
717 }
718
719 if fixed_request.permissions.is_none() || fixed_request.permissions.as_ref().unwrap().is_empty() {
721 if is_api_user {
722 let api_user_permission = Permission {
724 permission_id: None,
725 permission_name: "apiUser".to_string(),
726 description: Some("API User".to_string()),
727 api_only: Some(false),
728 ui_only: Some(false),
729 };
730 fixed_request.permissions = Some(vec![api_user_permission]);
731 } else {
732 let human_user_permission = Permission {
734 permission_id: None,
735 permission_name: "humanUser".to_string(),
736 description: Some("Human User".to_string()),
737 api_only: Some(false),
738 ui_only: Some(false),
739 };
740 fixed_request.permissions = Some(vec![human_user_permission]);
741 }
742 }
743
744 let roles_payload = if let Some(ref role_ids) = fixed_request.role_ids {
746 role_ids.iter().map(|id| serde_json::json!({"role_id": id})).collect::<Vec<_>>()
747 } else {
748 Vec::new()
749 };
750
751 let teams_payload = if let Some(ref team_ids) = fixed_request.team_ids {
753 team_ids.iter().map(|id| serde_json::json!({"team_id": id})).collect::<Vec<_>>()
754 } else {
755 Vec::new()
756 };
757
758 let permissions_payload = if let Some(ref permissions) = fixed_request.permissions {
760 permissions.iter().map(|p| serde_json::json!({
761 "permission_name": p.permission_name,
762 "api_only": p.api_only.unwrap_or(false),
763 "ui_only": p.ui_only.unwrap_or(false)
764 })).collect::<Vec<_>>()
765 } else {
766 Vec::new()
767 };
768
769 let mut payload = serde_json::json!({
771 "email_address": fixed_request.email_address,
772 "first_name": fixed_request.first_name,
773 "last_name": fixed_request.last_name,
774 "apiUser": is_api_user,
775 "samlUser": is_saml_user,
776 "active": true, "send_email_invitation": fixed_request.send_email_invitation.unwrap_or(false)
778 });
779
780 if let Some(ref user_name) = fixed_request.user_name {
782 payload["user_name"] = serde_json::json!(user_name);
783 }
784
785 if !roles_payload.is_empty() {
787 payload["roles"] = serde_json::json!(roles_payload);
788 }
789
790 if !teams_payload.is_empty() {
792 payload["teams"] = serde_json::json!(teams_payload);
793 }
794
795 if !permissions_payload.is_empty() {
797 payload["permissions"] = serde_json::json!(permissions_payload);
798 }
799
800
801 let response = self.client.post(endpoint, Some(&payload)).await?;
802
803 let status = response.status().as_u16();
804 match status {
805 200 | 201 => {
806 let user: User = response.json().await?;
807 Ok(user)
808 }
809 400 => {
810 let error_text = response.text().await.unwrap_or_default();
811 if error_text.contains("already exists") {
812 Err(IdentityError::UserAlreadyExists(error_text))
813 } else {
814 Err(IdentityError::InvalidInput(error_text))
815 }
816 }
817 403 => {
818 let error_text = response.text().await.unwrap_or_default();
819 Err(IdentityError::PermissionDenied(error_text))
820 }
821 415 => {
822 let error_text = response.text().await.unwrap_or_default();
823 Err(IdentityError::Api(VeracodeError::InvalidResponse(
824 format!("HTTP 415 Unsupported Media Type: {error_text}")
825 )))
826 }
827 _ => {
828 let error_text = response.text().await.unwrap_or_default();
829 Err(IdentityError::Api(VeracodeError::InvalidResponse(
830 format!("HTTP {status}: {error_text}")
831 )))
832 }
833 }
834 }
835
836 pub async fn update_user(&self, user_id: &str, request: UpdateUserRequest) -> Result<User, IdentityError> {
847 let endpoint = format!("/api/authn/v2/users/{user_id}");
848
849 let roles_payload = request.role_ids.iter().map(|id| serde_json::json!({"role_id": id})).collect::<Vec<_>>();
851
852 let teams_payload = request.team_ids.iter().map(|id| serde_json::json!({"team_id": id})).collect::<Vec<_>>();
853
854 let payload = serde_json::json!({
855 "email_address": request.email_address,
856 "user_name": request.user_name,
857 "first_name": request.first_name,
858 "last_name": request.last_name,
859 "active": request.active,
860 "roles": roles_payload,
861 "teams": teams_payload
862 });
863
864 let response = self.client.put(&endpoint, Some(&payload)).await?;
865
866 let status = response.status().as_u16();
867 match status {
868 200 => {
869 let user: User = response.json().await?;
870 Ok(user)
871 }
872 400 => {
873 let error_text = response.text().await.unwrap_or_default();
874 Err(IdentityError::InvalidInput(error_text))
875 }
876 403 => {
877 let error_text = response.text().await.unwrap_or_default();
878 Err(IdentityError::PermissionDenied(error_text))
879 }
880 404 => Err(IdentityError::UserNotFound),
881 _ => {
882 let error_text = response.text().await.unwrap_or_default();
883 Err(IdentityError::Api(VeracodeError::InvalidResponse(
884 format!("HTTP {status}: {error_text}")
885 )))
886 }
887 }
888 }
889
890 pub async fn delete_user(&self, user_id: &str) -> Result<(), IdentityError> {
900 let endpoint = format!("/api/authn/v2/users/{user_id}");
901
902 let response = self.client.delete(&endpoint).await?;
903
904 let status = response.status().as_u16();
905 match status {
906 200 | 204 => Ok(()),
907 403 => {
908 let error_text = response.text().await.unwrap_or_default();
909 Err(IdentityError::PermissionDenied(error_text))
910 }
911 404 => Err(IdentityError::UserNotFound),
912 _ => {
913 let error_text = response.text().await.unwrap_or_default();
914 Err(IdentityError::Api(VeracodeError::InvalidResponse(
915 format!("HTTP {status}: {error_text}")
916 )))
917 }
918 }
919 }
920
921 pub async fn list_roles(&self) -> Result<Vec<Role>, IdentityError> {
927 let endpoint = "/api/authn/v2/roles";
928 let mut all_roles = Vec::new();
929 let mut page = 0;
930 let page_size = 500;
931
932 loop {
934 let query_params = vec![
935 ("page".to_string(), page.to_string()),
936 ("size".to_string(), page_size.to_string()),
937 ];
938
939 let response = self.client.get(endpoint, Some(&query_params)).await?;
940 let status = response.status().as_u16();
941
942 match status {
943 200 => {
944 let response_text = response.text().await?;
945
946 if let Ok(roles_response) = serde_json::from_str::<RolesResponse>(&response_text) {
948 let page_roles = if !roles_response.roles.is_empty() {
949 roles_response.roles
950 } else if let Some(embedded) = roles_response.embedded {
951 embedded.roles
952 } else {
953 Vec::new()
954 };
955
956 if page_roles.is_empty() {
957 break; }
959
960 all_roles.extend(page_roles);
961 page += 1;
962
963 if let Some(page_info) = roles_response.page {
965 if let (Some(current_page), Some(total_pages)) = (page_info.number, page_info.total_pages) {
966 if current_page + 1 >= total_pages {
967 break;
968 }
969 }
970 }
971
972 continue;
973 }
974
975 if let Ok(roles) = serde_json::from_str::<Vec<Role>>(&response_text) {
977 if roles.is_empty() {
978 break;
979 }
980 all_roles.extend(roles);
981 page += 1;
982 continue;
983 }
984
985 if page == 0 {
987 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
988 format!("Unable to parse roles response: {response_text}")
989 )));
990 } else {
991 break; }
993 }
994 _ => {
995 let error_text = response.text().await.unwrap_or_default();
996 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
997 format!("HTTP {status}: {error_text}")
998 )));
999 }
1000 }
1001 }
1002
1003 Ok(all_roles)
1004 }
1005
1006 pub async fn list_teams(&self) -> Result<Vec<Team>, IdentityError> {
1012 let endpoint = "/api/authn/v2/teams";
1013 let mut all_teams = Vec::new();
1014 let mut page = 0;
1015 let page_size = 500;
1016
1017 loop {
1019 if page > 100 {
1021 break;
1022 }
1023
1024 let query_params = vec![
1025 ("page".to_string(), page.to_string()),
1026 ("size".to_string(), page_size.to_string()),
1027 ];
1028
1029 let response = self.client.get(endpoint, Some(&query_params)).await?;
1030 let status = response.status().as_u16();
1031
1032 match status {
1033 200 => {
1034 let response_text = response.text().await?;
1035
1036 if let Ok(teams_response) = serde_json::from_str::<TeamsResponse>(&response_text) {
1038 let page_teams = if !teams_response.teams.is_empty() {
1039 teams_response.teams
1040 } else if let Some(embedded) = teams_response.embedded {
1041 embedded.teams
1042 } else {
1043 Vec::new()
1044 };
1045
1046 if page_teams.is_empty() {
1047 break; }
1049
1050 all_teams.extend(page_teams);
1051 page += 1;
1052
1053 if let Some(page_info) = teams_response.page {
1055 if let (Some(current_page), Some(total_pages)) = (page_info.number, page_info.total_pages) {
1056 if current_page + 1 >= total_pages {
1057 break; }
1059 }
1060 }
1061
1062 continue;
1063 }
1064
1065 if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1067 if teams.is_empty() {
1068 break;
1069 }
1070 all_teams.extend(teams);
1071 page += 1;
1072 continue;
1073 }
1074
1075 if page == 0 {
1077 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1078 "Unable to parse teams response".to_string()
1079 )));
1080 } else {
1081 break;
1083 }
1084 }
1085 _ => {
1086 let error_text = response.text().await.unwrap_or_default();
1087 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1088 format!("HTTP {status}: {error_text}")
1089 )));
1090 }
1091 }
1092 }
1093
1094 Ok(all_teams)
1095 }
1096
1097 pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team, IdentityError> {
1107 let endpoint = "/api/authn/v2/teams";
1108
1109 let response = self.client.post(endpoint, Some(&request)).await?;
1110
1111 let status = response.status().as_u16();
1112 match status {
1113 200 | 201 => {
1114 let team: Team = response.json().await?;
1115 Ok(team)
1116 }
1117 400 => {
1118 let error_text = response.text().await.unwrap_or_default();
1119 if error_text.contains("already exists") {
1120 Err(IdentityError::TeamAlreadyExists(error_text))
1121 } else {
1122 Err(IdentityError::InvalidInput(error_text))
1123 }
1124 }
1125 403 => {
1126 let error_text = response.text().await.unwrap_or_default();
1127 Err(IdentityError::PermissionDenied(error_text))
1128 }
1129 _ => {
1130 let error_text = response.text().await.unwrap_or_default();
1131 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1132 format!("HTTP {status}: {error_text}")
1133 )))
1134 }
1135 }
1136 }
1137
1138 pub async fn delete_team(&self, team_id: &str) -> Result<(), IdentityError> {
1148 let endpoint = format!("/api/authn/v2/teams/{team_id}");
1149
1150 let response = self.client.delete(&endpoint).await?;
1151
1152 let status = response.status().as_u16();
1153 match status {
1154 200 | 204 => Ok(()),
1155 403 => {
1156 let error_text = response.text().await.unwrap_or_default();
1157 Err(IdentityError::PermissionDenied(error_text))
1158 }
1159 404 => Err(IdentityError::TeamNotFound),
1160 _ => {
1161 let error_text = response.text().await.unwrap_or_default();
1162 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1163 format!("HTTP {status}: {error_text}")
1164 )))
1165 }
1166 }
1167 }
1168
1169 pub async fn create_api_credentials(&self, request: CreateApiCredentialRequest) -> Result<ApiCredential, IdentityError> {
1179 let endpoint = "/api/authn/v2/api_credentials";
1180
1181 let response = self.client.post(endpoint, Some(&request)).await?;
1182
1183 let status = response.status().as_u16();
1184 match status {
1185 200 | 201 => {
1186 let credentials: ApiCredential = response.json().await?;
1187 Ok(credentials)
1188 }
1189 400 => {
1190 let error_text = response.text().await.unwrap_or_default();
1191 Err(IdentityError::InvalidInput(error_text))
1192 }
1193 403 => {
1194 let error_text = response.text().await.unwrap_or_default();
1195 Err(IdentityError::PermissionDenied(error_text))
1196 }
1197 _ => {
1198 let error_text = response.text().await.unwrap_or_default();
1199 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1200 format!("HTTP {status}: {error_text}")
1201 )))
1202 }
1203 }
1204 }
1205
1206 pub async fn revoke_api_credentials(&self, api_creds_id: &str) -> Result<(), IdentityError> {
1216 let endpoint = format!("/api/authn/v2/api_credentials/{api_creds_id}");
1217
1218 let response = self.client.delete(&endpoint).await?;
1219
1220 let status = response.status().as_u16();
1221 match status {
1222 200 | 204 => Ok(()), 403 => {
1224 let error_text = response.text().await.unwrap_or_default();
1225 Err(IdentityError::PermissionDenied(error_text))
1226 }
1227 404 => Err(IdentityError::UserNotFound), _ => {
1229 let error_text = response.text().await.unwrap_or_default();
1230 Err(IdentityError::Api(VeracodeError::InvalidResponse(
1231 format!("HTTP {status}: {error_text}")
1232 )))
1233 }
1234 }
1235 }
1236}
1237
1238impl<'a> IdentityApi<'a> {
1240 pub async fn find_user_by_email(&self, email: &str) -> Result<Option<User>, IdentityError> {
1250 let query = UserQuery::new().with_email(email);
1251 let users = self.list_users(Some(query)).await?;
1252 Ok(users.into_iter().find(|u| u.email_address == email))
1253 }
1254
1255 pub async fn find_user_by_username(&self, username: &str) -> Result<Option<User>, IdentityError> {
1265 let query = UserQuery::new().with_username(username);
1266 let users = self.list_users(Some(query)).await?;
1267 Ok(users.into_iter().find(|u| u.user_name == username))
1268 }
1269
1270 pub async fn create_simple_user(&self, email: &str, username: &str, first_name: &str, last_name: &str, team_ids: Vec<String>) -> Result<User, IdentityError> {
1284 let request = CreateUserRequest {
1285 email_address: email.to_string(),
1286 first_name: first_name.to_string(),
1287 last_name: last_name.to_string(),
1288 user_name: Some(username.to_string()),
1289 user_type: Some(UserType::Human),
1290 send_email_invitation: Some(true),
1291 role_ids: None, team_ids: Some(team_ids),
1293 permissions: None, };
1295
1296 self.create_user(request).await
1297 }
1298
1299 pub async fn create_api_service_account(
1314 &self,
1315 email: &str,
1316 username: &str,
1317 first_name: &str,
1318 last_name: &str,
1319 role_ids: Vec<String>,
1320 team_ids: Option<Vec<String>>,
1321 ) -> Result<User, IdentityError> {
1322 let request = CreateUserRequest {
1323 email_address: email.to_string(),
1324 first_name: first_name.to_string(),
1325 last_name: last_name.to_string(),
1326 user_name: Some(username.to_string()),
1327 user_type: Some(UserType::ApiService), send_email_invitation: Some(false),
1329 role_ids: Some(role_ids),
1330 team_ids, permissions: None, };
1333
1334 self.create_user(request).await
1335 }
1336}
1337
1338
1339#[cfg(test)]
1340mod tests {
1341 use super::*;
1342
1343 #[test]
1344 fn test_user_query_params() {
1345 let query = UserQuery::new()
1346 .with_username("testuser")
1347 .with_email("test@example.com")
1348 .with_user_type(UserType::Human)
1349 .with_pagination(1, 50);
1350
1351 let params = query.to_query_params();
1352 assert_eq!(params.len(), 5); assert!(params.contains(&("user_name".to_string(), "testuser".to_string())));
1354 assert!(params.contains(&("email_address".to_string(), "test@example.com".to_string())));
1355 assert!(params.contains(&("user_type".to_string(), "HUMAN".to_string())));
1356 assert!(params.contains(&("page".to_string(), "1".to_string())));
1357 assert!(params.contains(&("size".to_string(), "50".to_string())));
1358 }
1359
1360 #[test]
1361 fn test_user_type_serialization() {
1362 assert_eq!(serde_json::to_string(&UserType::Human).unwrap(), "\"HUMAN\"");
1363 assert_eq!(serde_json::to_string(&UserType::ApiService).unwrap(), "\"API\"");
1364 assert_eq!(serde_json::to_string(&UserType::Saml).unwrap(), "\"SAML\"");
1365 assert_eq!(serde_json::to_string(&UserType::Vosp).unwrap(), "\"VOSP\"");
1366 }
1367}