systemprompt-models 0.14.3

Foundation data models for systemprompt.io AI governance infrastructure. Shared DTOs, config, and domain types consumed by every layer of the MCP governance pipeline.
Documentation
//! Authenticated-principal and OAuth request types.
//!
//! [`AuthenticatedUser`] is the resolved principal carried through a request;
//! [`AuthError`] is the crate's authentication/OAuth error enum. [`PkceMethod`]
//! and [`ResponseType`] model the OAuth authorization-request parameters.

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use systemprompt_identifiers::ClientId;
use uuid::Uuid;

use super::enums::UserType;
use super::permission::Permission;

pub const BEARER_PREFIX: &str = "Bearer ";

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthenticatedUser {
    pub id: Uuid,
    pub username: String,
    pub email: String,
    pub permissions: Vec<Permission>,
    #[serde(default)]
    pub roles: Vec<String>,
    /// Opaque ABAC attribute bag forwarded into `JwtClaims.attributes` and
    /// onward to `AuthzRequest.attributes`. Tenant-defined, namespaced
    /// keys; core never interprets values.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub attributes: BTreeMap<String, serde_json::Value>,
}

impl AuthenticatedUser {
    pub const fn new(
        id: Uuid,
        username: String,
        email: String,
        permissions: Vec<Permission>,
    ) -> Self {
        Self {
            id,
            username,
            email,
            permissions,
            roles: Vec::new(),
            attributes: BTreeMap::new(),
        }
    }

    pub const fn new_with_roles(
        id: Uuid,
        username: String,
        email: String,
        permissions: Vec<Permission>,
        roles: Vec<String>,
    ) -> Self {
        Self {
            id,
            username,
            email,
            permissions,
            roles,
            attributes: BTreeMap::new(),
        }
    }

    #[must_use]
    pub fn with_attributes(mut self, attributes: BTreeMap<String, serde_json::Value>) -> Self {
        self.attributes = attributes;
        self
    }

    #[must_use]
    pub const fn attributes(&self) -> &BTreeMap<String, serde_json::Value> {
        &self.attributes
    }

    pub fn has_permission(&self, permission: Permission) -> bool {
        self.permissions.contains(&permission)
            || self.permissions.iter().any(|p| p.implies(&permission))
    }

    pub fn is_admin(&self) -> bool {
        self.has_permission(Permission::Admin)
    }

    pub fn permissions(&self) -> &[Permission] {
        &self.permissions
    }

    pub fn has_role(&self, role: &str) -> bool {
        self.roles.iter().any(|r| r == role)
    }

    pub fn roles(&self) -> &[String] {
        &self.roles
    }

    pub fn user_type(&self) -> UserType {
        UserType::from_permissions(&self.permissions)
    }
}

#[derive(Debug, thiserror::Error)]
pub enum AuthError {
    #[error("Invalid token format")]
    InvalidTokenFormat,

    #[error("Token expired")]
    TokenExpired,

    #[error("Token signature invalid")]
    InvalidSignature,

    #[error("User not found")]
    UserNotFound,

    #[error("Insufficient permissions")]
    InsufficientPermissions,

    #[error("Authentication failed: {message}")]
    AuthenticationFailed { message: String },

    #[error("Invalid OAuth request: {reason}")]
    InvalidRequest { reason: String },

    #[error("CSRF token (state) is required")]
    MissingState,

    #[error("Redirect URI is required and must be registered")]
    InvalidRedirectUri,

    #[error("PKCE code_challenge is required")]
    MissingCodeChallenge,

    #[error("PKCE method '{method}' not allowed (must be S256)")]
    WeakPkceMethod { method: String },

    #[error("Client ID {client_id} not found")]
    ClientNotFound { client_id: ClientId },

    #[error("Scope '{scope}' is invalid")]
    InvalidScope { scope: String },

    #[error("Token revocation requires authenticated user")]
    UnauthenticatedRevocation,

    #[error("WebAuthn RP ID could not be determined")]
    InvalidRpId,

    #[error("Client registration validation failed: {reason}")]
    RegistrationFailed { reason: String },

    #[error("Internal error: {0}")]
    Internal(String),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PkceMethod {
    S256,
}

impl std::str::FromStr for PkceMethod {
    type Err = AuthError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "S256" => Ok(Self::S256),
            "plain" => Err(AuthError::WeakPkceMethod {
                method: s.to_owned(),
            }),
            _ => Err(AuthError::InvalidRequest {
                reason: format!("Unknown PKCE method: {s}"),
            }),
        }
    }
}

impl std::fmt::Display for PkceMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::S256 => write!(f, "S256"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponseType {
    Code,
    Token,
}

impl std::str::FromStr for ResponseType {
    type Err = AuthError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "code" => Ok(Self::Code),
            "token" => Ok(Self::Token),
            _ => Err(AuthError::InvalidRequest {
                reason: format!("Unknown response type: {s}"),
            }),
        }
    }
}

impl std::fmt::Display for ResponseType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Code => write!(f, "code"),
            Self::Token => write!(f, "token"),
        }
    }
}