use anyhow::{Context, Result}; use common::proto::komp_ac::auth::{ AddRoleRequest, AssignUserRoleRequest, AuthResponse, AuthorizationSnapshot, ChangePasswordRequest, GetAuthorizationRequest, GrantPermissionRequest, GrantableObject, ListGrantableObjectsRequest, ListRolePermissionsRequest, ListRolesRequest, ListUsersRequest, LoginRequest, LoginResponse, LogoutRequest, PasswordOperationResponse, RegisterRequest, RemoveRoleRequest, ResetUserPasswordRequest, RevokePermissionRequest, RevokeUserSessionsRequest, Role, RolePermissions, UserSummary, auth_service_client::AuthServiceClient, }; use tonic::transport::Channel; use tonic::Request; use crate::transport::{DEFAULT_GRPC_ENDPOINT, authenticated_request, connect_channel}; pub fn validate_password_change(password: &str, confirmation: &str) -> Result<()> { if password.is_empty() { anyhow::bail!("Password is required"); } if password != confirmation { anyhow::bail!("Passwords do not match"); } if password.len() < 8 { anyhow::bail!("Password must be at least 8 characters"); } Ok(()) } pub fn validate_optional_password(password: Option<&str>, confirmation: Option<&str>) -> Result<()> { let password = password.unwrap_or_default(); let confirmation = confirmation.unwrap_or_default(); if password.is_empty() && confirmation.is_empty() { return Ok(()); } validate_password_change(password, confirmation) } #[derive(Clone)] pub struct AuthClient { client: AuthServiceClient, } impl AuthClient { pub async fn new() -> Result { let endpoint = std::env::var("GRPC_ENDPOINT") .unwrap_or_else(|_| DEFAULT_GRPC_ENDPOINT.to_string()); Self::connect(&endpoint).await } pub async fn connect(endpoint: &str) -> Result { Self::with_channel(connect_channel(endpoint).await?).await } pub async fn with_channel(channel: Channel) -> Result { Ok(Self { client: AuthServiceClient::new(channel), }) } pub async fn login(&mut self, identifier: String, password: String) -> Result { Ok(self .client .login(Request::new(LoginRequest { identifier, password, })) .await? .into_inner()) } pub async fn register( &mut self, username: String, email: String, password: Option, password_confirmation: Option, timezone: String, phone_country: String, ) -> Result { validate_optional_password(password.as_deref(), password_confirmation.as_deref())?; Ok(self .client .register(Request::new(RegisterRequest { username, email, password: password.unwrap_or_default(), password_confirmation: password_confirmation.unwrap_or_default(), timezone, phone_country, })) .await? .into_inner()) } pub async fn change_password( &mut self, token: &str, current_password: String, new_password: String, new_password_confirmation: String, ) -> Result { validate_password_change(&new_password, &new_password_confirmation)?; let request = authenticated_request( Some(token), ChangePasswordRequest { current_password, new_password, new_password_confirmation, }, )?; Ok(self.client.change_password(request).await?.into_inner()) } pub async fn logout(&mut self, token: &str) -> Result<()> { self.client .logout(authenticated_request(Some(token), LogoutRequest {})?) .await?; Ok(()) } pub async fn get_authorization(&mut self, token: &str) -> Result { Ok(self .client .get_authorization(authenticated_request(Some(token), GetAuthorizationRequest {})?) .await? .into_inner()) } pub async fn list_roles(&mut self, token: &str) -> Result> { Ok(self .client .list_roles(authenticated_request(Some(token), ListRolesRequest {})?) .await? .into_inner() .roles) } pub async fn add_role(&mut self, token: &str, name: String, parent: String) -> Result<()> { self.client .add_role(authenticated_request( Some(token), AddRoleRequest { name, parent }, )?) .await?; Ok(()) } pub async fn remove_role(&mut self, token: &str, name: String) -> Result<()> { self.client .remove_role(authenticated_request(Some(token), RemoveRoleRequest { name })?) .await?; Ok(()) } pub async fn grant_permission( &mut self, token: &str, role: String, object: String, action: String, ) -> Result<()> { self.client .grant_permission(authenticated_request( Some(token), GrantPermissionRequest { role, object, action, }, )?) .await?; Ok(()) } pub async fn revoke_permission( &mut self, token: &str, role: String, object: String, action: String, ) -> Result<()> { self.client .revoke_permission(authenticated_request( Some(token), RevokePermissionRequest { role, object, action, }, )?) .await?; Ok(()) } pub async fn try_revoke_permission( &mut self, token: &str, role: String, object: String, action: String, ) -> Result { let request = authenticated_request( Some(token), RevokePermissionRequest { role, object, action, }, )?; match self.client.revoke_permission(request).await { Ok(_) => Ok(true), Err(status) if status.code() == tonic::Code::NotFound => Ok(false), Err(status) => Err(status.into()), } } pub async fn list_role_permissions( &mut self, token: &str, role: String, ) -> Result { Ok(self .client .list_role_permissions(authenticated_request( Some(token), ListRolePermissionsRequest { role }, )?) .await? .into_inner()) } pub async fn list_grantable_objects( &mut self, token: &str, target_role: String, ) -> Result> { Ok(self .client .list_grantable_objects(authenticated_request( Some(token), ListGrantableObjectsRequest { target_role }, )?) .await? .into_inner() .objects) } pub async fn assign_user_role( &mut self, token: &str, username: String, role: String, ) -> Result<()> { self.client .assign_user_role(authenticated_request( Some(token), AssignUserRoleRequest { username, role }, )?) .await?; Ok(()) } pub async fn list_users(&mut self, token: &str) -> Result> { Ok(self .client .list_users(authenticated_request(Some(token), ListUsersRequest {})?) .await? .into_inner() .users) } pub async fn revoke_user_sessions(&mut self, token: &str, username: String) -> Result<()> { self.client .revoke_user_sessions(authenticated_request( Some(token), RevokeUserSessionsRequest { username }, )?) .await?; Ok(()) } pub async fn reset_user_password( &mut self, token: &str, username: String, new_password: String, new_password_confirmation: String, ) -> Result { validate_password_change(&new_password, &new_password_confirmation)?; Ok(self .client .reset_user_password(authenticated_request( Some(token), ResetUserPasswordRequest { username, new_password, new_password_confirmation, }, )?) .await .context("Failed to reset user password")? .into_inner()) } }