kirino 0.3.0

A customizable, zero-trust authentication framework.
Documentation
use anyhow::{anyhow, Result};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use uuid::Uuid;

use crate::{
    auth::{
        credential::basic::JwtManager,
        passport::static_password::{hash_password, verify_password},
    },
    models::identity::Identity,
    rbac::{
        dynamic::arbiter::AuthorizationArbiter,
        engine::RbacEngine,
        shared::Shared,
        store::{
            memory::InMemoryAssignmentStore,
            registry::{SimpleRole, StaticPermissionRegistry, StaticRoleRegistry},
        },
        subject::StringSubject,
        traits::{AssignmentStore, Permission, Role},
    },
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KirinoPermission {
    AgentRead,
    AgentWrite,
    AgentExecute,
    ConfigRead,
    ConfigWrite,
    KnowledgeRead,
    KnowledgeWrite,
    ContainerRead,
    ContainerWrite,
    SystemRead,
    SystemWrite,
    DeployRead,
    DeployExecute,
}

impl KirinoPermission {
    pub fn all() -> HashSet<Self> {
        use KirinoPermission::*;
        [
            AgentRead,
            AgentWrite,
            AgentExecute,
            ConfigRead,
            ConfigWrite,
            KnowledgeRead,
            KnowledgeWrite,
            ContainerRead,
            ContainerWrite,
            SystemRead,
            SystemWrite,
            DeployRead,
            DeployExecute,
        ]
        .into_iter()
        .collect()
    }
}

impl Permission for KirinoPermission {
    fn name(&self) -> &str {
        match self {
            KirinoPermission::AgentRead => "agent_read",
            KirinoPermission::AgentWrite => "agent_write",
            KirinoPermission::AgentExecute => "agent_execute",
            KirinoPermission::ConfigRead => "config_read",
            KirinoPermission::ConfigWrite => "config_write",
            KirinoPermission::KnowledgeRead => "knowledge_read",
            KirinoPermission::KnowledgeWrite => "knowledge_write",
            KirinoPermission::ContainerRead => "container_read",
            KirinoPermission::ContainerWrite => "container_write",
            KirinoPermission::SystemRead => "system_read",
            KirinoPermission::SystemWrite => "system_write",
            KirinoPermission::DeployRead => "deploy_read",
            KirinoPermission::DeployExecute => "deploy_execute",
        }
    }

    fn domain(&self) -> &str {
        match self {
            KirinoPermission::AgentRead
            | KirinoPermission::AgentWrite
            | KirinoPermission::AgentExecute => "agent",
            KirinoPermission::ConfigRead | KirinoPermission::ConfigWrite => "config",
            KirinoPermission::KnowledgeRead | KirinoPermission::KnowledgeWrite => "knowledge",
            KirinoPermission::ContainerRead | KirinoPermission::ContainerWrite => "container",
            KirinoPermission::SystemRead | KirinoPermission::SystemWrite => "system",
            KirinoPermission::DeployRead | KirinoPermission::DeployExecute => "deploy",
        }
    }
}

#[derive(Debug, Clone)]
pub struct UserRecord {
    pub id: Uuid,
    pub username: String,
    pub password_hash: String,
    pub display_name: Option<String>,
    pub is_active: bool,
    pub identity: Identity,
    pub created_at: chrono::DateTime<Utc>,
    pub updated_at: chrono::DateTime<Utc>,
}

#[derive(Debug, Clone)]
pub struct LoginResult {
    pub token: String,
    pub user_id: String,
    pub username: String,
    pub display_name: Option<String>,
    pub roles: Vec<String>,
}

pub struct AuthService<DB, P, R, A>
where
    P: Permission,
    R: Role<P>,
    A: AssignmentStore<StringSubject, P>,
{
    db: DB,
    jwt: JwtManager,
    engine: Shared<RbacEngine<StringSubject, P, R, A>>,
    arbiter: Option<Shared<AuthorizationArbiter>>,
    first_user_role: String,
    default_role: String,
}

impl<DB, P, R, A> AuthService<DB, P, R, A>
where
    DB: UserDatabase,
    P: Permission,
    R: Role<P>,
    A: AssignmentStore<StringSubject, P>,
{
    pub fn new(
        db: DB,
        jwt_secret: &str,
        jwt_expiration_hours: i64,
        engine: Shared<RbacEngine<StringSubject, P, R, A>>,
        first_user_role: &str,
        default_role: &str,
    ) -> Self {
        Self {
            db,
            jwt: JwtManager::new(jwt_secret, jwt_expiration_hours),
            engine,
            arbiter: None,
            first_user_role: first_user_role.to_string(),
            default_role: default_role.to_string(),
        }
    }

    pub fn with_arbiter(mut self, arbiter: AuthorizationArbiter) -> Self {
        self.arbiter = Some(Shared::new(arbiter));
        self
    }

    pub fn arbiter(&self) -> Option<Shared<AuthorizationArbiter>> {
        self.arbiter.clone()
    }

    pub fn jwt_manager(&self) -> &JwtManager {
        &self.jwt
    }

    pub fn engine(&self) -> Shared<RbacEngine<StringSubject, P, R, A>> {
        self.engine.clone()
    }

    pub async fn register(
        &self,
        username: &str,
        password: &str,
        display_name: Option<&str>,
    ) -> Result<UserRecord> {
        if username.trim().is_empty() {
            return Err(anyhow!("username must not be empty"));
        }
        if password.len() < 6 {
            return Err(anyhow!("password must be at least 6 characters"));
        }

        if self.db.find_by_username(username).await?.is_some() {
            return Err(anyhow!("username already exists"));
        }

        let password_hash = hash_password(password)?;
        let user_id = Uuid::now_v7();
        let now = Utc::now();
        let identity = Identity::Basic { id: user_id };

        let user = UserRecord {
            id: user_id,
            username: username.to_string(),
            password_hash,
            display_name: display_name.map(|s| s.to_string()),
            is_active: true,
            identity,
            created_at: now,
            updated_at: now,
        };

        self.db.create_user(&user).await?;

        let is_first = self.db.count_users().await? <= 1;
        let role_name = if is_first {
            &self.first_user_role
        } else {
            &self.default_role
        };

        let subject = StringSubject::new(user_id.to_string());
        self.engine
            .assignment_store()
            .assign_role(&subject, role_name)
            .await?;

        Ok(user)
    }

    pub async fn login(&self, username: &str, password: &str) -> Result<LoginResult> {
        let user = self
            .db
            .find_by_username(username)
            .await?
            .ok_or_else(|| anyhow!("invalid credentials"))?;

        if !user.is_active {
            return Err(anyhow!("account disabled"));
        }

        if !verify_password(password, &user.password_hash)? {
            return Err(anyhow!("invalid credentials"));
        }

        let user_id = user.id.to_string();
        let subject = StringSubject::new(&user_id);
        let roles = self
            .engine
            .assignment_store()
            .roles_of(&subject)
            .await
            .unwrap_or_default();

        let token = self.jwt.issue(&user_id, &user.username, roles.clone())?;

        Ok(LoginResult {
            token,
            user_id,
            username: user.username.clone(),
            display_name: user.display_name.clone(),
            roles,
        })
    }

    pub async fn verify_token(
        &self,
        token: &str,
    ) -> Result<crate::auth::credential::basic::Claims> {
        self.jwt.verify(token)
    }

    pub async fn check_permission(&self, user_id: &str, permission: &P) -> bool {
        let subject = StringSubject::new(user_id);
        self.engine.check(&subject, permission).await
    }

    pub async fn check_static_and_dynamic(
        &self,
        user_id: &str,
        permission: &P,
        action_request: &crate::rbac::dynamic::metrics::ActionRequest,
    ) -> bool {
        let subject = StringSubject::new(user_id);
        if !self.engine.check(&subject, permission).await {
            return false;
        }
        if let Some(ref arbiter) = self.arbiter {
            let verdict = arbiter.authorize(action_request).await;
            return verdict.allowed;
        }
        true
    }

    pub async fn change_password(
        &self,
        user_id: &str,
        old_password: &str,
        new_password: &str,
    ) -> Result<()> {
        let uid = Uuid::parse_str(user_id).map_err(|_| anyhow!("invalid user_id"))?;

        let user = self
            .db
            .find_by_id(&uid)
            .await?
            .ok_or_else(|| anyhow!("user not found"))?;

        if !verify_password(old_password, &user.password_hash)? {
            return Err(anyhow!("old password is incorrect"));
        }

        if new_password.len() < 6 {
            return Err(anyhow!("new password must be at least 6 characters"));
        }

        let new_hash = hash_password(new_password)?;
        self.db.update_password(&uid, &new_hash).await
    }

    pub async fn list_users(&self) -> Result<Vec<UserRecord>> {
        self.db.list_users().await
    }

    pub async fn delete_user(&self, user_id: &str) -> Result<bool> {
        let uid = Uuid::parse_str(user_id).map_err(|_| anyhow!("invalid user_id"))?;
        self.db.delete_user(&uid).await
    }
}

type DefaultEngine = RbacEngine<
    StringSubject,
    KirinoPermission,
    SimpleRole<KirinoPermission>,
    InMemoryAssignmentStore<StringSubject, KirinoPermission>,
>;

pub fn build_default_engine() -> Shared<DefaultEngine> {
    let mut role_reg = StaticRoleRegistry::new();
    role_reg.register(SimpleRole::new("admin", KirinoPermission::all()));
    role_reg.register(SimpleRole::new(
        "operator",
        [
            KirinoPermission::AgentRead,
            KirinoPermission::AgentWrite,
            KirinoPermission::AgentExecute,
            KirinoPermission::ConfigRead,
            KirinoPermission::ConfigWrite,
            KirinoPermission::KnowledgeRead,
            KirinoPermission::KnowledgeWrite,
            KirinoPermission::ContainerRead,
            KirinoPermission::ContainerWrite,
            KirinoPermission::DeployRead,
            KirinoPermission::DeployExecute,
            KirinoPermission::SystemRead,
        ]
        .into_iter()
        .collect(),
    ));
    role_reg.register(SimpleRole::new(
        "viewer",
        [
            KirinoPermission::AgentRead,
            KirinoPermission::ConfigRead,
            KirinoPermission::KnowledgeRead,
            KirinoPermission::ContainerRead,
            KirinoPermission::SystemRead,
            KirinoPermission::DeployRead,
        ]
        .into_iter()
        .collect(),
    ));
    role_reg.register(SimpleRole::new(
        "agent",
        [
            KirinoPermission::AgentRead,
            KirinoPermission::AgentExecute,
            KirinoPermission::KnowledgeRead,
        ]
        .into_iter()
        .collect(),
    ));

    let perm_reg = StaticPermissionRegistry::new(KirinoPermission::all());
    let store = InMemoryAssignmentStore::new();

    Shared::new(RbacEngine::new(role_reg, perm_reg, store))
}

#[async_trait::async_trait]
pub trait UserDatabase: Send + Sync + Clone + 'static {
    async fn create_user(&self, user: &UserRecord) -> Result<()>;
    async fn find_by_username(&self, username: &str) -> Result<Option<UserRecord>>;
    async fn find_by_id(&self, id: &Uuid) -> Result<Option<UserRecord>>;
    async fn update_password(&self, id: &Uuid, new_hash: &str) -> Result<()>;
    async fn delete_user(&self, id: &Uuid) -> Result<bool>;
    async fn list_users(&self) -> Result<Vec<UserRecord>>;
    async fn count_users(&self) -> Result<u64>;
}