use super::AdminConfig;
use super::access::has_permission;
use better_auth_core::wire::{SessionView, UserView};
use better_auth_core::{
AuthContext, AuthError, AuthRequest, AuthSchema, HttpMethod, NativeRequest,
};
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum AdminPermissionError {
#[error("AdminPlugin is not registered")]
PluginNotRegistered,
#[error("a permission check requires at least one action for each resource")]
EmptyPermissions,
#[error("user id or role is required")]
MissingSubject,
#[error("user not found")]
UserNotFound,
#[error("the request is not authenticated")]
Unauthenticated,
#[error("the authenticated user lacks the requested permissions")]
Forbidden,
#[error("the permission check could not complete: {0}")]
Internal(#[source] AuthError),
}
#[derive(Debug)]
pub struct AuthorizedSession {
user: UserView,
session: SessionView,
}
impl AuthorizedSession {
pub fn user(&self) -> &UserView {
&self.user
}
pub fn session(&self) -> &SessionView {
&self.session
}
pub fn into_parts(self) -> (UserView, SessionView) {
(self.user, self.session)
}
}
pub struct AdminPermissionApi<'a, S: AuthSchema> {
config: &'a AdminConfig,
context: &'a AuthContext<S>,
}
impl<'a, S: AuthSchema> AdminPermissionApi<'a, S> {
pub fn from_context(context: &'a AuthContext<S>) -> Result<Self, AdminPermissionError> {
let config = context
.extensions
.get::<AdminConfig>()
.ok_or(AdminPermissionError::PluginNotRegistered)?;
Ok(Self { config, context })
}
pub fn role_has_permission(
&self,
role: &str,
permissions: &HashMap<String, Vec<String>>,
) -> Result<bool, AdminPermissionError> {
require_permissions(permissions)?;
if role.is_empty() {
return Err(AdminPermissionError::MissingSubject);
}
Ok(has_permission(None, Some(role), self.config, permissions))
}
pub async fn user_has_permission(
&self,
user_id: &str,
permissions: &HashMap<String, Vec<String>>,
) -> Result<bool, AdminPermissionError> {
require_permissions(permissions)?;
if user_id.is_empty() {
return Err(AdminPermissionError::MissingSubject);
}
let user = self
.context
.with_native_context(NativeRequest::default(), |context| async move {
context.database.get_user_by_id(user_id).await
})
.await
.map_err(AdminPermissionError::Internal)?
.ok_or(AdminPermissionError::UserNotFound)?;
Ok(has_permission(
user.id.as_str(),
user.role.as_deref(),
self.config,
permissions,
))
}
pub async fn authorize_request(
&self,
headers: &HashMap<String, String>,
permissions: &HashMap<String, Vec<String>>,
) -> Result<AuthorizedSession, AdminPermissionError> {
require_permissions(permissions)?;
let (user, session) = self
.context
.with_native_context(
NativeRequest {
request: None,
headers: Some(headers),
},
|context| async move {
let request = AuthRequest::new(HttpMethod::Post, "/admin/has-permission")
.with_optional_headers(Some(headers.clone()));
let mut hook_context =
better_auth_core::RequestHookContext::from_request(&request)?;
hook_context.body = better_auth_core::FieldValue::from_json(
serde_json::json!({ "permissions": permissions }),
)?;
better_auth_core::with_request_hook_context_value(hook_context, async {
context.require_authoritative_session(&request).await
})
.await
},
)
.await
.map_err(session_error)?;
if has_permission(
user.id.as_str(),
user.role.as_deref(),
self.config,
permissions,
) {
Ok(AuthorizedSession { user, session })
} else {
Err(AdminPermissionError::Forbidden)
}
}
}
fn require_permissions(
permissions: &HashMap<String, Vec<String>>,
) -> Result<(), AdminPermissionError> {
if permissions.is_empty() || permissions.values().any(Vec::is_empty) {
Err(AdminPermissionError::EmptyPermissions)
} else {
Ok(())
}
}
fn session_error(error: AuthError) -> AdminPermissionError {
match error {
AuthError::SessionNotFound | AuthError::UserNotFound => {
AdminPermissionError::Unauthenticated
}
error if error.status_code() == 401 => AdminPermissionError::Unauthenticated,
error => AdminPermissionError::Internal(error),
}
}