feat: complete Phase 0 Enterprise IAM
This commit is contained in:
1 parent
3d2d291006
commit
c2f5ba3f54
202 files changed
+21371
-1360
No files matched your search
@@ -0,0 +1,256 @@
|
||||
use crate::db::models::{
|
||||
ApiToken, Application, AuditLog, Group, Permission, Role, ServiceAccount, Session, Tenant, User,
|
||||
};
|
||||
use crate::db::repository::sqlite::audit::AuditFilter;
|
||||
use crate::db::repository::sqlite::refresh_tokens::RefreshToken;
|
||||
use crate::db::repository::sqlite::users::UserProfile;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait UsersRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<User>, sqlx::Error>;
|
||||
async fn find_by_username(&self, username: &str) -> Result<Option<User>, sqlx::Error>;
|
||||
async fn list(&self, tenant_id: &str) -> Result<Vec<User>, sqlx::Error>;
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
tenant_id: &str,
|
||||
username: &str,
|
||||
password_hash: &str,
|
||||
) -> Result<User, sqlx::Error>;
|
||||
async fn update_status(&self, id: &str, status: i32) -> Result<(), sqlx::Error>;
|
||||
async fn update_password_hash(&self, id: &str, password_hash: &str) -> Result<(), sqlx::Error>;
|
||||
async fn set_last_login(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn username_exists(&self, tenant_id: &str, username: &str) -> Result<bool, sqlx::Error>;
|
||||
async fn count_admins(&self) -> Result<i64, sqlx::Error>;
|
||||
async fn count(&self, tenant_id: &str) -> Result<i64, sqlx::Error>;
|
||||
async fn count_by_status(&self, tenant_id: &str, status: i32) -> Result<i64, sqlx::Error>;
|
||||
async fn get_profile(&self, user_id: &str) -> Result<Option<UserProfile>, sqlx::Error>;
|
||||
async fn upsert_profile(
|
||||
&self,
|
||||
user_id: &str,
|
||||
email: Option<&str>,
|
||||
full_name: Option<&str>,
|
||||
) -> Result<UserProfile, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ServiceAccountsRepository: Send + Sync {
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
tenant_id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<ServiceAccount, sqlx::Error>;
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<ServiceAccount>, sqlx::Error>;
|
||||
async fn list(&self, tenant_id: &str) -> Result<Vec<ServiceAccount>, sqlx::Error>;
|
||||
async fn set_enabled(&self, id: &str, enabled: bool) -> Result<(), sqlx::Error>;
|
||||
async fn delete(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn count(&self, tenant_id: &str) -> Result<i64, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait RolesRepository: Send + Sync {
|
||||
async fn list_all(&self) -> Result<Vec<Role>, sqlx::Error>;
|
||||
async fn find_by_name(&self, name: &str) -> Result<Option<Role>, sqlx::Error>;
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<Role>, sqlx::Error>;
|
||||
async fn list_for_user(&self, user_id: &str) -> Result<Vec<Role>, sqlx::Error>;
|
||||
async fn assign_to_user(&self, user_id: &str, role_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn remove_from_user(&self, user_id: &str, role_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn admin_role_exists(&self) -> Result<bool, sqlx::Error>;
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<Role, sqlx::Error>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<(), sqlx::Error>;
|
||||
async fn delete(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn list_user_ids_for_role(&self, role_id: &str) -> Result<Vec<String>, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait SessionsRepository: Send + Sync {
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
token_hash: &str,
|
||||
ip_address: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
expires_at: &str,
|
||||
) -> Result<Session, sqlx::Error>;
|
||||
async fn find_by_token_hash(&self, token_hash: &str) -> Result<Option<Session>, sqlx::Error>;
|
||||
async fn revoke(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn revoke_all_for_user(&self, user_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn update_last_seen(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn list_active_for_user(&self, user_id: &str) -> Result<Vec<Session>, sqlx::Error>;
|
||||
async fn list_all_active(&self) -> Result<Vec<Session>, sqlx::Error>;
|
||||
async fn count_active(&self) -> Result<i64, sqlx::Error>;
|
||||
async fn cleanup_expired(&self) -> Result<u64, sqlx::Error>;
|
||||
async fn revoke_others(&self, user_id: &str, except_id: &str) -> Result<u64, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ApplicationsRepository: Send + Sync {
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
tenant_id: &str,
|
||||
name: &str,
|
||||
slug: &str,
|
||||
) -> Result<Application, sqlx::Error>;
|
||||
async fn find_by_slug(&self, slug: &str) -> Result<Option<Application>, sqlx::Error>;
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<Application>, sqlx::Error>;
|
||||
async fn list(&self, tenant_id: &str) -> Result<Vec<Application>, sqlx::Error>;
|
||||
async fn set_enabled(&self, id: &str, enabled: bool) -> Result<(), sqlx::Error>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: &str,
|
||||
name: &str,
|
||||
slug: &str,
|
||||
enabled: bool,
|
||||
) -> Result<(), sqlx::Error>;
|
||||
async fn delete(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn count(&self, tenant_id: &str) -> Result<i64, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait RefreshTokensRepository: Send + Sync {
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
token_hash: &str,
|
||||
expires_at: &str,
|
||||
) -> Result<RefreshToken, sqlx::Error>;
|
||||
async fn find_by_hash(&self, token_hash: &str) -> Result<Option<RefreshToken>, sqlx::Error>;
|
||||
async fn revoke(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn revoke_all_for_user(&self, user_id: &str) -> Result<(), sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AuditRepository: Send + Sync {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn insert(
|
||||
&self,
|
||||
id: &str,
|
||||
actor_user_id: Option<&str>,
|
||||
target_user_id: Option<&str>,
|
||||
action: &str,
|
||||
resource_type: &str,
|
||||
resource_id: Option<&str>,
|
||||
severity: &str,
|
||||
ip_address: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
metadata_json: Option<&str>,
|
||||
) -> Result<AuditLog, sqlx::Error>;
|
||||
async fn list_recent(&self, limit: i64) -> Result<Vec<AuditLog>, sqlx::Error>;
|
||||
async fn count(&self) -> Result<i64, sqlx::Error>;
|
||||
async fn list_filtered(&self, filter: &AuditFilter) -> Result<Vec<AuditLog>, sqlx::Error>;
|
||||
async fn count_filtered(&self, filter: &AuditFilter) -> Result<i64, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AuditRepositoryExt: Send + Sync {
|
||||
async fn log(&self, event: crate::audit::AuditEvent<'_>) -> Result<AuditLog, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T: ?Sized + AuditRepository> AuditRepositoryExt for T {
|
||||
async fn log(&self, event: crate::audit::AuditEvent<'_>) -> Result<AuditLog, sqlx::Error> {
|
||||
self.insert(
|
||||
&uuid::Uuid::new_v4().to_string(),
|
||||
event.actor_id,
|
||||
event.target_id,
|
||||
event.action,
|
||||
event.resource_type,
|
||||
event.resource_id,
|
||||
event.severity.as_str(),
|
||||
event.ip,
|
||||
event.ua,
|
||||
event.metadata,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait TokensRepository: Send + Sync {
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
name: &str,
|
||||
token_hash: &str,
|
||||
expires_at: Option<&str>,
|
||||
) -> Result<ApiToken, sqlx::Error>;
|
||||
async fn find_by_hash(&self, token_hash: &str) -> Result<Option<ApiToken>, sqlx::Error>;
|
||||
async fn list_for_user(&self, user_id: &str) -> Result<Vec<ApiToken>, sqlx::Error>;
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<ApiToken>, sqlx::Error>;
|
||||
async fn revoke(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn update_last_used(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait PermissionsRepository: Send + Sync {
|
||||
async fn list_all(&self) -> Result<Vec<Permission>, sqlx::Error>;
|
||||
async fn list_for_role(&self, role_id: &str) -> Result<Vec<Permission>, sqlx::Error>;
|
||||
async fn assign_to_role(&self, role_id: &str, permission_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn remove_from_role(&self, role_id: &str, permission_id: &str)
|
||||
-> Result<(), sqlx::Error>;
|
||||
async fn clear_for_role(&self, role_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn find_by_name(&self, name: &str) -> Result<Option<Permission>, sqlx::Error>;
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<Permission>, sqlx::Error>;
|
||||
async fn list_for_user(&self, user_id: &str) -> Result<Vec<String>, sqlx::Error>;
|
||||
async fn user_has_permission(
|
||||
&self,
|
||||
user_id: &str,
|
||||
permission_name: &str,
|
||||
) -> Result<bool, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait TenantsRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<Tenant>, sqlx::Error>;
|
||||
async fn find_by_slug(&self, slug: &str) -> Result<Option<Tenant>, sqlx::Error>;
|
||||
async fn list(&self) -> Result<Vec<Tenant>, sqlx::Error>;
|
||||
async fn create(&self, id: &str, name: &str, slug: Option<&str>)
|
||||
-> Result<Tenant, sqlx::Error>;
|
||||
async fn update(&self, id: &str, name: &str, slug: Option<&str>) -> Result<(), sqlx::Error>;
|
||||
async fn set_enabled(&self, id: &str, enabled: bool) -> Result<(), sqlx::Error>;
|
||||
async fn delete(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GroupsRepository: Send + Sync {
|
||||
async fn list(&self, tenant_id: &str) -> Result<Vec<Group>, sqlx::Error>;
|
||||
async fn find_by_id(&self, id: &str) -> Result<Option<Group>, sqlx::Error>;
|
||||
async fn create(
|
||||
&self,
|
||||
id: &str,
|
||||
tenant_id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<Group, sqlx::Error>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<(), sqlx::Error>;
|
||||
async fn delete(&self, id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn count_members(&self, group_id: &str) -> Result<i64, sqlx::Error>;
|
||||
async fn list_members(
|
||||
&self,
|
||||
group_id: &str,
|
||||
) -> Result<Vec<crate::db::models::User>, sqlx::Error>;
|
||||
async fn add_member(&self, group_id: &str, user_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn remove_member(&self, group_id: &str, user_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn count(&self, tenant_id: &str) -> Result<i64, sqlx::Error>;
|
||||
}
|
||||
Reference in new issue
Block a user