Files
komp_ac/komp-app/src/auth.rs
2026-08-30 07:49:19 +02:00

305 lines
8.9 KiB
Rust

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<Channel>,
}
impl AuthClient {
pub async fn new() -> Result<Self> {
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> {
Self::with_channel(connect_channel(endpoint).await?).await
}
pub async fn with_channel(channel: Channel) -> Result<Self> {
Ok(Self {
client: AuthServiceClient::new(channel),
})
}
pub async fn login(&mut self, identifier: String, password: String) -> Result<LoginResponse> {
Ok(self
.client
.login(Request::new(LoginRequest {
identifier,
password,
}))
.await?
.into_inner())
}
pub async fn register(
&mut self,
username: String,
email: String,
password: Option<String>,
password_confirmation: Option<String>,
timezone: String,
phone_country: String,
) -> Result<AuthResponse> {
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<PasswordOperationResponse> {
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<AuthorizationSnapshot> {
Ok(self
.client
.get_authorization(authenticated_request(Some(token), GetAuthorizationRequest {})?)
.await?
.into_inner())
}
pub async fn list_roles(&mut self, token: &str) -> Result<Vec<Role>> {
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<bool> {
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<RolePermissions> {
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<Vec<GrantableObject>> {
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<Vec<UserSummary>> {
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<PasswordOperationResponse> {
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())
}
}