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 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 UpdateTeamRequest {
252 pub team_name: Option<String>,
254 pub team_description: Option<String>,
256 pub business_unit_id: Option<String>,
258 pub user_ids: Option<Vec<String>>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct CreateApiCredentialRequest {
265 pub user_id: Option<String>,
267 pub expiration_ts: Option<DateTime<Utc>>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct UsersResponse {
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
276 pub users: Vec<User>,
277 #[serde(rename = "_embedded")]
279 pub embedded: Option<EmbeddedUsers>,
280 pub page: Option<PageInfo>,
282 #[serde(rename = "_links")]
284 pub links: Option<HashMap<String, Link>>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct EmbeddedUsers {
290 pub users: Vec<User>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct TeamsResponse {
297 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub teams: Vec<Team>,
300 #[serde(rename = "_embedded")]
302 pub embedded: Option<EmbeddedTeams>,
303 pub page: Option<PageInfo>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct EmbeddedTeams {
310 pub teams: Vec<Team>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct RolesResponse {
317 #[serde(default, skip_serializing_if = "Vec::is_empty")]
319 pub roles: Vec<Role>,
320 #[serde(rename = "_embedded")]
322 pub embedded: Option<EmbeddedRoles>,
323 pub page: Option<PageInfo>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct EmbeddedRoles {
330 pub roles: Vec<Role>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct PageInfo {
337 pub number: Option<u32>,
339 pub size: Option<u32>,
341 pub total_elements: Option<u64>,
343 pub total_pages: Option<u32>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct Link {
350 pub href: String,
352}
353
354#[derive(Debug, Clone, Default)]
356pub struct UserQuery {
357 pub user_name: Option<String>,
359 pub email_address: Option<String>,
361 pub role_id: Option<String>,
363 pub user_type: Option<UserType>,
365 pub login_status: Option<String>,
367 pub page: Option<u32>,
369 pub size: Option<u32>,
371}
372
373impl UserQuery {
374 pub fn new() -> Self {
376 Self::default()
377 }
378
379 pub fn with_username(mut self, username: impl Into<String>) -> Self {
381 self.user_name = Some(username.into());
382 self
383 }
384
385 pub fn with_email(mut self, email: impl Into<String>) -> Self {
387 self.email_address = Some(email.into());
388 self
389 }
390
391 pub fn with_role_id(mut self, role_id: impl Into<String>) -> Self {
393 self.role_id = Some(role_id.into());
394 self
395 }
396
397 pub fn with_user_type(mut self, user_type: UserType) -> Self {
399 self.user_type = Some(user_type);
400 self
401 }
402
403 pub fn with_pagination(mut self, page: u32, size: u32) -> Self {
405 self.page = Some(page);
406 self.size = Some(size);
407 self
408 }
409
410 pub fn to_query_params(&self) -> Vec<(String, String)> {
412 let mut params = Vec::new();
413
414 if let Some(ref username) = self.user_name {
415 params.push(("user_name".to_string(), username.clone()));
416 }
417 if let Some(ref email) = self.email_address {
418 params.push(("email_address".to_string(), email.clone()));
419 }
420 if let Some(ref role_id) = self.role_id {
421 params.push(("role_id".to_string(), role_id.clone()));
422 }
423 if let Some(ref user_type) = self.user_type {
424 let type_str = match user_type {
425 UserType::Human => "HUMAN",
426 UserType::ApiService => "API",
427 UserType::Saml => "SAML",
428 UserType::Vosp => "VOSP",
429 };
430 params.push(("user_type".to_string(), type_str.to_string()));
431 }
432 if let Some(ref login_status) = self.login_status {
433 params.push(("login_status".to_string(), login_status.clone()));
434 }
435 if let Some(page) = self.page {
436 params.push(("page".to_string(), page.to_string()));
437 }
438 if let Some(size) = self.size {
439 params.push(("size".to_string(), size.to_string()));
440 }
441
442 params
443 }
444}
445
446#[derive(Debug)]
448pub enum IdentityError {
449 Api(VeracodeError),
451 UserNotFound,
453 TeamNotFound,
455 RoleNotFound,
457 InvalidInput(String),
459 PermissionDenied(String),
461 UserAlreadyExists(String),
463 TeamAlreadyExists(String),
465}
466
467impl std::fmt::Display for IdentityError {
468 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469 match self {
470 IdentityError::Api(err) => write!(f, "API error: {err}"),
471 IdentityError::UserNotFound => write!(f, "User not found"),
472 IdentityError::TeamNotFound => write!(f, "Team not found"),
473 IdentityError::RoleNotFound => write!(f, "Role not found"),
474 IdentityError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
475 IdentityError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
476 IdentityError::UserAlreadyExists(msg) => write!(f, "User already exists: {msg}"),
477 IdentityError::TeamAlreadyExists(msg) => write!(f, "Team already exists: {msg}"),
478 }
479 }
480}
481
482impl std::error::Error for IdentityError {}
483
484impl From<VeracodeError> for IdentityError {
485 fn from(err: VeracodeError) -> Self {
486 IdentityError::Api(err)
487 }
488}
489
490impl From<reqwest::Error> for IdentityError {
491 fn from(err: reqwest::Error) -> Self {
492 IdentityError::Api(VeracodeError::Http(err))
493 }
494}
495
496impl From<serde_json::Error> for IdentityError {
497 fn from(err: serde_json::Error) -> Self {
498 IdentityError::Api(VeracodeError::Serialization(err))
499 }
500}
501
502pub struct IdentityApi<'a> {
504 client: &'a VeracodeClient,
505}
506
507impl<'a> IdentityApi<'a> {
508 pub fn new(client: &'a VeracodeClient) -> Self {
510 Self { client }
511 }
512
513 pub async fn list_users(&self, query: Option<UserQuery>) -> Result<Vec<User>, IdentityError> {
523 let endpoint = "/api/authn/v2/users";
524 let query_params = query.as_ref().map(|q| q.to_query_params());
525
526 let response = self.client.get(endpoint, query_params.as_deref()).await?;
527
528 let status = response.status().as_u16();
529 match status {
530 200 => {
531 let response_text = response.text().await?;
532
533 if let Ok(users_response) = serde_json::from_str::<UsersResponse>(&response_text) {
535 let users = if !users_response.users.is_empty() {
536 users_response.users
537 } else if let Some(embedded) = users_response.embedded {
538 embedded.users
539 } else {
540 Vec::new()
541 };
542 return Ok(users);
543 }
544
545 if let Ok(users) = serde_json::from_str::<Vec<User>>(&response_text) {
547 return Ok(users);
548 }
549
550 Err(IdentityError::Api(VeracodeError::InvalidResponse(
551 "Unable to parse users response".to_string(),
552 )))
553 }
554 404 => Err(IdentityError::UserNotFound),
555 _ => {
556 let error_text = response.text().await.unwrap_or_default();
557 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
558 "HTTP {status}: {error_text}"
559 ))))
560 }
561 }
562 }
563
564 pub async fn get_user(&self, user_id: &str) -> Result<User, IdentityError> {
574 let endpoint = format!("/api/authn/v2/users/{user_id}");
575
576 let response = self.client.get(&endpoint, None).await?;
577
578 let status = response.status().as_u16();
579 match status {
580 200 => {
581 let user: User = response.json().await?;
582 Ok(user)
583 }
584 404 => Err(IdentityError::UserNotFound),
585 _ => {
586 let error_text = response.text().await.unwrap_or_default();
587 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
588 "HTTP {status}: {error_text}"
589 ))))
590 }
591 }
592 }
593
594 pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, IdentityError> {
604 let endpoint = "/api/authn/v2/users";
605
606 let mut fixed_request = request.clone();
607
608 if fixed_request.user_name.is_none() {
610 return Err(IdentityError::InvalidInput(
611 "user_name is required".to_string(),
612 ));
613 }
614
615 let has_teams = fixed_request.team_ids.is_some()
617 && !fixed_request.team_ids.as_ref().unwrap().is_empty();
618
619 if !has_teams {
620 let has_no_team_restriction_role = if let Some(ref role_ids) = fixed_request.role_ids {
622 let roles = self.list_roles().await?;
624 role_ids.iter().any(|role_id| {
625 roles.iter().any(|r| {
626 &r.role_id == role_id
627 && (r
628 .role_description
629 .as_ref()
630 .is_some_and(|desc| desc == "No Team Restriction API")
631 || r.role_name.to_lowercase() == "noteamrestrictionapi")
632 })
633 })
634 } else {
635 false
636 };
637
638 if !has_no_team_restriction_role {
639 return Err(IdentityError::InvalidInput(
640 "You must select at least one team for this user, or select No Team Restrictions role".to_string()
641 ));
642 }
643 }
644
645 let is_api_user = matches!(fixed_request.user_type, Some(UserType::ApiService));
647 let is_saml_user = matches!(fixed_request.user_type, Some(UserType::Saml));
648
649 if is_api_user && fixed_request.role_ids.is_some() {
651 let roles = self.list_roles().await?;
652 let provided_role_ids = fixed_request.role_ids.as_ref().unwrap();
653
654 let human_role_descriptions = [
656 "Creator",
657 "Executive",
658 "Mitigation Approver",
659 "Reviewer",
660 "Sandbox User",
661 "Security Lead",
662 "Team Admin",
663 "Workspace Editor",
664 "Analytics Creator",
665 "Delete Scans",
666 "Greenlight IDE User",
667 "Policy Administrator",
668 "Sandbox Administrator",
669 "Security Insights",
670 "Submitter",
671 "Workspace Administrator",
672 ];
673
674 for role_id in provided_role_ids {
675 if let Some(role) = roles.iter().find(|r| &r.role_id == role_id) {
676 if let Some(ref desc) = role.role_description {
678 if human_role_descriptions.contains(&desc.as_str()) {
679 return Err(IdentityError::InvalidInput(format!(
680 "Role '{}' (description: '{}') is a human-only role and cannot be assigned to API users.",
681 role.role_name, desc
682 )));
683 }
684 }
685
686 if role.is_api != Some(true) {
688 return Err(IdentityError::InvalidInput(format!(
689 "Role '{}' (is_api: {:?}) cannot be assigned to API users. API users can only be assigned API roles.",
690 role.role_name, role.is_api
691 )));
692 }
693 }
694 }
695 }
696
697 if fixed_request.role_ids.is_none() || fixed_request.role_ids.as_ref().unwrap().is_empty() {
699 let roles = self.list_roles().await?;
701 let mut default_role_ids = Vec::new();
702
703 if is_api_user {
705 if let Some(api_submit_role) = roles
707 .iter()
708 .find(|r| r.role_name.to_lowercase() == "apisubmitanyscan")
709 {
710 default_role_ids.push(api_submit_role.role_id.clone());
711 }
712
713 if let Some(noteam_role) = roles
715 .iter()
716 .find(|r| r.role_name.to_lowercase() == "noteamrestrictionapi")
717 {
718 default_role_ids.push(noteam_role.role_id.clone());
719 }
720 } else {
721 if let Some(submitter_role) = roles.iter().find(|r| {
723 r.role_description
724 .as_ref()
725 .is_some_and(|desc| desc == "Submitter")
726 }) {
727 default_role_ids.push(submitter_role.role_id.clone());
728 } else if let Some(creator_role) = roles.iter().find(|r| {
729 r.role_description
730 .as_ref()
731 .is_some_and(|desc| desc == "Creator")
732 }) {
733 default_role_ids.push(creator_role.role_id.clone());
734 } else if let Some(reviewer_role) = roles.iter().find(|r| {
735 r.role_description
736 .as_ref()
737 .is_some_and(|desc| desc == "Reviewer")
738 }) {
739 default_role_ids.push(reviewer_role.role_id.clone());
740 }
741
742 if !has_teams {
744 if let Some(no_team_role) = roles.iter().find(|r| {
745 r.role_description
746 .as_ref()
747 .is_some_and(|desc| desc == "No Team Restriction API")
748 || r.role_name.to_lowercase() == "noteamrestrictionapi"
749 }) {
750 default_role_ids.push(no_team_role.role_id.clone());
751 }
752 }
753 }
754
755 if !default_role_ids.is_empty() {
757 fixed_request.role_ids = Some(default_role_ids);
758 }
759 }
760
761 if fixed_request.permissions.is_none()
763 || fixed_request.permissions.as_ref().unwrap().is_empty()
764 {
765 if is_api_user {
766 let api_user_permission = Permission {
768 permission_id: None,
769 permission_name: "apiUser".to_string(),
770 description: Some("API User".to_string()),
771 api_only: Some(false),
772 ui_only: Some(false),
773 };
774 fixed_request.permissions = Some(vec![api_user_permission]);
775 } else {
776 let human_user_permission = Permission {
778 permission_id: None,
779 permission_name: "humanUser".to_string(),
780 description: Some("Human User".to_string()),
781 api_only: Some(false),
782 ui_only: Some(false),
783 };
784 fixed_request.permissions = Some(vec![human_user_permission]);
785 }
786 }
787
788 let roles_payload = if let Some(ref role_ids) = fixed_request.role_ids {
790 role_ids
791 .iter()
792 .map(|id| serde_json::json!({"role_id": id}))
793 .collect::<Vec<_>>()
794 } else {
795 Vec::new()
796 };
797
798 let teams_payload = if let Some(ref team_ids) = fixed_request.team_ids {
800 team_ids
801 .iter()
802 .map(|id| serde_json::json!({"team_id": id}))
803 .collect::<Vec<_>>()
804 } else {
805 Vec::new()
806 };
807
808 let permissions_payload = if let Some(ref permissions) = fixed_request.permissions {
810 permissions
811 .iter()
812 .map(|p| {
813 serde_json::json!({
814 "permission_name": p.permission_name,
815 "api_only": p.api_only.unwrap_or(false),
816 "ui_only": p.ui_only.unwrap_or(false)
817 })
818 })
819 .collect::<Vec<_>>()
820 } else {
821 Vec::new()
822 };
823
824 let mut payload = serde_json::json!({
826 "email_address": fixed_request.email_address,
827 "first_name": fixed_request.first_name,
828 "last_name": fixed_request.last_name,
829 "apiUser": is_api_user,
830 "samlUser": is_saml_user,
831 "active": true, "send_email_invitation": fixed_request.send_email_invitation.unwrap_or(false)
833 });
834
835 if let Some(ref user_name) = fixed_request.user_name {
837 payload["user_name"] = serde_json::json!(user_name);
838 }
839
840 if !roles_payload.is_empty() {
842 payload["roles"] = serde_json::json!(roles_payload);
843 }
844
845 if !teams_payload.is_empty() {
847 payload["teams"] = serde_json::json!(teams_payload);
848 }
849
850 if !permissions_payload.is_empty() {
852 payload["permissions"] = serde_json::json!(permissions_payload);
853 }
854
855 let response = self.client.post(endpoint, Some(&payload)).await?;
856
857 let status = response.status().as_u16();
858 match status {
859 200 | 201 => {
860 let user: User = response.json().await?;
861 Ok(user)
862 }
863 400 => {
864 let error_text = response.text().await.unwrap_or_default();
865 if error_text.contains("already exists") {
866 Err(IdentityError::UserAlreadyExists(error_text))
867 } else {
868 Err(IdentityError::InvalidInput(error_text))
869 }
870 }
871 403 => {
872 let error_text = response.text().await.unwrap_or_default();
873 Err(IdentityError::PermissionDenied(error_text))
874 }
875 415 => {
876 let error_text = response.text().await.unwrap_or_default();
877 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
878 "HTTP 415 Unsupported Media Type: {error_text}"
879 ))))
880 }
881 _ => {
882 let error_text = response.text().await.unwrap_or_default();
883 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
884 "HTTP {status}: {error_text}"
885 ))))
886 }
887 }
888 }
889
890 pub async fn update_user(
901 &self,
902 user_id: &str,
903 request: UpdateUserRequest,
904 ) -> Result<User, IdentityError> {
905 let endpoint = format!("/api/authn/v2/users/{user_id}");
906
907 let roles_payload = request
909 .role_ids
910 .iter()
911 .map(|id| serde_json::json!({"role_id": id}))
912 .collect::<Vec<_>>();
913
914 let teams_payload = request
915 .team_ids
916 .iter()
917 .map(|id| serde_json::json!({"team_id": id}))
918 .collect::<Vec<_>>();
919
920 let payload = serde_json::json!({
921 "email_address": request.email_address,
922 "user_name": request.user_name,
923 "first_name": request.first_name,
924 "last_name": request.last_name,
925 "active": request.active,
926 "roles": roles_payload,
927 "teams": teams_payload
928 });
929
930 let response = self.client.put(&endpoint, Some(&payload)).await?;
931
932 let status = response.status().as_u16();
933 match status {
934 200 => {
935 let user: User = response.json().await?;
936 Ok(user)
937 }
938 400 => {
939 let error_text = response.text().await.unwrap_or_default();
940 Err(IdentityError::InvalidInput(error_text))
941 }
942 403 => {
943 let error_text = response.text().await.unwrap_or_default();
944 Err(IdentityError::PermissionDenied(error_text))
945 }
946 404 => Err(IdentityError::UserNotFound),
947 _ => {
948 let error_text = response.text().await.unwrap_or_default();
949 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
950 "HTTP {status}: {error_text}"
951 ))))
952 }
953 }
954 }
955
956 pub async fn delete_user(&self, user_id: &str) -> Result<(), IdentityError> {
966 let endpoint = format!("/api/authn/v2/users/{user_id}");
967
968 let response = self.client.delete(&endpoint).await?;
969
970 let status = response.status().as_u16();
971 match status {
972 200 | 204 => Ok(()),
973 403 => {
974 let error_text = response.text().await.unwrap_or_default();
975 Err(IdentityError::PermissionDenied(error_text))
976 }
977 404 => Err(IdentityError::UserNotFound),
978 _ => {
979 let error_text = response.text().await.unwrap_or_default();
980 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
981 "HTTP {status}: {error_text}"
982 ))))
983 }
984 }
985 }
986
987 pub async fn list_roles(&self) -> Result<Vec<Role>, IdentityError> {
993 let endpoint = "/api/authn/v2/roles";
994 let mut all_roles = Vec::new();
995 let mut page = 0;
996 let page_size = 500;
997
998 loop {
1000 let query_params = vec![
1001 ("page".to_string(), page.to_string()),
1002 ("size".to_string(), page_size.to_string()),
1003 ];
1004
1005 let response = self.client.get(endpoint, Some(&query_params)).await?;
1006 let status = response.status().as_u16();
1007
1008 match status {
1009 200 => {
1010 let response_text = response.text().await?;
1011
1012 if let Ok(roles_response) =
1014 serde_json::from_str::<RolesResponse>(&response_text)
1015 {
1016 let page_roles = if !roles_response.roles.is_empty() {
1017 roles_response.roles
1018 } else if let Some(embedded) = roles_response.embedded {
1019 embedded.roles
1020 } else {
1021 Vec::new()
1022 };
1023
1024 if page_roles.is_empty() {
1025 break; }
1027
1028 all_roles.extend(page_roles);
1029 page += 1;
1030
1031 if let Some(page_info) = roles_response.page {
1033 if let (Some(current_page), Some(total_pages)) =
1034 (page_info.number, page_info.total_pages)
1035 {
1036 if current_page + 1 >= total_pages {
1037 break;
1038 }
1039 }
1040 }
1041
1042 continue;
1043 }
1044
1045 if let Ok(roles) = serde_json::from_str::<Vec<Role>>(&response_text) {
1047 if roles.is_empty() {
1048 break;
1049 }
1050 all_roles.extend(roles);
1051 page += 1;
1052 continue;
1053 }
1054
1055 if page == 0 {
1057 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1058 "Unable to parse roles response: {response_text}"
1059 ))));
1060 } else {
1061 break; }
1063 }
1064 _ => {
1065 let error_text = response.text().await.unwrap_or_default();
1066 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1067 "HTTP {status}: {error_text}"
1068 ))));
1069 }
1070 }
1071 }
1072
1073 Ok(all_roles)
1074 }
1075
1076 pub async fn list_teams(&self) -> Result<Vec<Team>, IdentityError> {
1082 let endpoint = "/api/authn/v2/teams";
1083 let mut all_teams = Vec::new();
1084 let mut page = 0;
1085 let page_size = 500;
1086
1087 loop {
1089 if page > 100 {
1091 break;
1092 }
1093
1094 let query_params = vec![
1095 ("page".to_string(), page.to_string()),
1096 ("size".to_string(), page_size.to_string()),
1097 ];
1098
1099 let response = self.client.get(endpoint, Some(&query_params)).await?;
1100 let status = response.status().as_u16();
1101
1102 match status {
1103 200 => {
1104 let response_text = response.text().await?;
1105
1106 if let Ok(teams_response) =
1108 serde_json::from_str::<TeamsResponse>(&response_text)
1109 {
1110 let page_teams = if !teams_response.teams.is_empty() {
1111 teams_response.teams
1112 } else if let Some(embedded) = teams_response.embedded {
1113 embedded.teams
1114 } else {
1115 Vec::new()
1116 };
1117
1118 if page_teams.is_empty() {
1119 break; }
1121
1122 all_teams.extend(page_teams);
1123 page += 1;
1124
1125 if let Some(page_info) = teams_response.page {
1127 if let (Some(current_page), Some(total_pages)) =
1128 (page_info.number, page_info.total_pages)
1129 {
1130 if current_page + 1 >= total_pages {
1131 break; }
1133 }
1134 }
1135
1136 continue;
1137 }
1138
1139 if let Ok(teams) = serde_json::from_str::<Vec<Team>>(&response_text) {
1141 if teams.is_empty() {
1142 break;
1143 }
1144 all_teams.extend(teams);
1145 page += 1;
1146 continue;
1147 }
1148
1149 if page == 0 {
1151 return Err(IdentityError::Api(VeracodeError::InvalidResponse(
1152 "Unable to parse teams response".to_string(),
1153 )));
1154 } else {
1155 break;
1157 }
1158 }
1159 _ => {
1160 let error_text = response.text().await.unwrap_or_default();
1161 return Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1162 "HTTP {status}: {error_text}"
1163 ))));
1164 }
1165 }
1166 }
1167
1168 Ok(all_teams)
1169 }
1170
1171 pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team, IdentityError> {
1181 let endpoint = "/api/authn/v2/teams";
1182
1183 let response = self.client.post(endpoint, Some(&request)).await?;
1184
1185 let status = response.status().as_u16();
1186 match status {
1187 200 | 201 => {
1188 let team: Team = response.json().await?;
1189 Ok(team)
1190 }
1191 400 => {
1192 let error_text = response.text().await.unwrap_or_default();
1193 if error_text.contains("already exists") {
1194 Err(IdentityError::TeamAlreadyExists(error_text))
1195 } else {
1196 Err(IdentityError::InvalidInput(error_text))
1197 }
1198 }
1199 403 => {
1200 let error_text = response.text().await.unwrap_or_default();
1201 Err(IdentityError::PermissionDenied(error_text))
1202 }
1203 _ => {
1204 let error_text = response.text().await.unwrap_or_default();
1205 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1206 "HTTP {status}: {error_text}"
1207 ))))
1208 }
1209 }
1210 }
1211
1212 pub async fn delete_team(&self, team_id: &str) -> Result<(), IdentityError> {
1222 let endpoint = format!("/api/authn/v2/teams/{team_id}");
1223
1224 let response = self.client.delete(&endpoint).await?;
1225
1226 let status = response.status().as_u16();
1227 match status {
1228 200 | 204 => Ok(()),
1229 403 => {
1230 let error_text = response.text().await.unwrap_or_default();
1231 Err(IdentityError::PermissionDenied(error_text))
1232 }
1233 404 => Err(IdentityError::TeamNotFound),
1234 _ => {
1235 let error_text = response.text().await.unwrap_or_default();
1236 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1237 "HTTP {status}: {error_text}"
1238 ))))
1239 }
1240 }
1241 }
1242
1243 pub async fn create_api_credentials(
1253 &self,
1254 request: CreateApiCredentialRequest,
1255 ) -> Result<ApiCredential, IdentityError> {
1256 let endpoint = "/api/authn/v2/api_credentials";
1257
1258 let response = self.client.post(endpoint, Some(&request)).await?;
1259
1260 let status = response.status().as_u16();
1261 match status {
1262 200 | 201 => {
1263 let credentials: ApiCredential = response.json().await?;
1264 Ok(credentials)
1265 }
1266 400 => {
1267 let error_text = response.text().await.unwrap_or_default();
1268 Err(IdentityError::InvalidInput(error_text))
1269 }
1270 403 => {
1271 let error_text = response.text().await.unwrap_or_default();
1272 Err(IdentityError::PermissionDenied(error_text))
1273 }
1274 _ => {
1275 let error_text = response.text().await.unwrap_or_default();
1276 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1277 "HTTP {status}: {error_text}"
1278 ))))
1279 }
1280 }
1281 }
1282
1283 pub async fn revoke_api_credentials(&self, api_creds_id: &str) -> Result<(), IdentityError> {
1293 let endpoint = format!("/api/authn/v2/api_credentials/{api_creds_id}");
1294
1295 let response = self.client.delete(&endpoint).await?;
1296
1297 let status = response.status().as_u16();
1298 match status {
1299 200 | 204 => Ok(()), 403 => {
1301 let error_text = response.text().await.unwrap_or_default();
1302 Err(IdentityError::PermissionDenied(error_text))
1303 }
1304 404 => Err(IdentityError::UserNotFound), _ => {
1306 let error_text = response.text().await.unwrap_or_default();
1307 Err(IdentityError::Api(VeracodeError::InvalidResponse(format!(
1308 "HTTP {status}: {error_text}"
1309 ))))
1310 }
1311 }
1312 }
1313}
1314
1315impl<'a> IdentityApi<'a> {
1317 pub async fn find_user_by_email(&self, email: &str) -> Result<Option<User>, IdentityError> {
1327 let query = UserQuery::new().with_email(email);
1328 let users = self.list_users(Some(query)).await?;
1329 Ok(users.into_iter().find(|u| u.email_address == email))
1330 }
1331
1332 pub async fn find_user_by_username(
1342 &self,
1343 username: &str,
1344 ) -> Result<Option<User>, IdentityError> {
1345 let query = UserQuery::new().with_username(username);
1346 let users = self.list_users(Some(query)).await?;
1347 Ok(users.into_iter().find(|u| u.user_name == username))
1348 }
1349
1350 pub async fn create_simple_user(
1364 &self,
1365 email: &str,
1366 username: &str,
1367 first_name: &str,
1368 last_name: &str,
1369 team_ids: Vec<String>,
1370 ) -> Result<User, IdentityError> {
1371 let request = CreateUserRequest {
1372 email_address: email.to_string(),
1373 first_name: first_name.to_string(),
1374 last_name: last_name.to_string(),
1375 user_name: Some(username.to_string()),
1376 user_type: Some(UserType::Human),
1377 send_email_invitation: Some(true),
1378 role_ids: None, team_ids: Some(team_ids),
1380 permissions: None, };
1382
1383 self.create_user(request).await
1384 }
1385
1386 pub async fn create_api_service_account(
1401 &self,
1402 email: &str,
1403 username: &str,
1404 first_name: &str,
1405 last_name: &str,
1406 role_ids: Vec<String>,
1407 team_ids: Option<Vec<String>>,
1408 ) -> Result<User, IdentityError> {
1409 let request = CreateUserRequest {
1410 email_address: email.to_string(),
1411 first_name: first_name.to_string(),
1412 last_name: last_name.to_string(),
1413 user_name: Some(username.to_string()),
1414 user_type: Some(UserType::ApiService), send_email_invitation: Some(false),
1416 role_ids: Some(role_ids),
1417 team_ids, permissions: None, };
1420
1421 self.create_user(request).await
1422 }
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427 use super::*;
1428
1429 #[test]
1430 fn test_user_query_params() {
1431 let query = UserQuery::new()
1432 .with_username("testuser")
1433 .with_email("test@example.com")
1434 .with_user_type(UserType::Human)
1435 .with_pagination(1, 50);
1436
1437 let params = query.to_query_params();
1438 assert_eq!(params.len(), 5); assert!(params.contains(&("user_name".to_string(), "testuser".to_string())));
1440 assert!(params.contains(&("email_address".to_string(), "test@example.com".to_string())));
1441 assert!(params.contains(&("user_type".to_string(), "HUMAN".to_string())));
1442 assert!(params.contains(&("page".to_string(), "1".to_string())));
1443 assert!(params.contains(&("size".to_string(), "50".to_string())));
1444 }
1445
1446 #[test]
1447 fn test_user_type_serialization() {
1448 assert_eq!(
1449 serde_json::to_string(&UserType::Human).unwrap(),
1450 "\"HUMAN\""
1451 );
1452 assert_eq!(
1453 serde_json::to_string(&UserType::ApiService).unwrap(),
1454 "\"API\""
1455 );
1456 assert_eq!(serde_json::to_string(&UserType::Saml).unwrap(), "\"SAML\"");
1457 assert_eq!(serde_json::to_string(&UserType::Vosp).unwrap(), "\"VOSP\"");
1458 }
1459}