yggdrasil/models/
mcp_token.rs1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
21#[serde(rename_all = "lowercase")]
22pub enum TokenScope {
23 Read,
24 Write,
25 Admin,
26}
27
28impl TokenScope {
29 pub fn as_str(self) -> &'static str {
31 match self {
32 TokenScope::Read => "read",
33 TokenScope::Write => "write",
34 TokenScope::Admin => "admin",
35 }
36 }
37
38 pub fn from_db(s: &str) -> Option<Self> {
40 match s {
41 "read" => Some(TokenScope::Read),
42 "write" => Some(TokenScope::Write),
43 "admin" => Some(TokenScope::Admin),
44 _ => None,
45 }
46 }
47
48 fn rank(self) -> u8 {
50 match self {
51 TokenScope::Read => 1,
52 TokenScope::Write => 2,
53 TokenScope::Admin => 3,
54 }
55 }
56
57 pub fn grants(self, required: TokenScope) -> bool {
59 self.rank() >= required.rank()
60 }
61}
62
63impl PartialOrd for TokenScope {
64 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
65 Some(self.cmp(other))
66 }
67}
68
69impl Ord for TokenScope {
70 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
71 self.rank().cmp(&other.rank())
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct McpToken {
78 pub id: String,
79 pub user_id: i32,
80 pub name: String,
81 pub scope: TokenScope,
82 #[serde(skip)]
84 pub token_enc: String,
85 #[serde(skip)]
87 pub token_hash: String,
88 pub created_at: chrono::DateTime<chrono::Utc>,
89 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
90 pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
91 pub revoked_at: Option<chrono::DateTime<chrono::Utc>>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub struct McpTokenSummary {
97 pub id: String,
98 pub name: String,
99 pub scope: TokenScope,
100 pub created_at: chrono::DateTime<chrono::Utc>,
101 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
102 pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
103 pub revoked_at: Option<chrono::DateTime<chrono::Utc>>,
104}
105
106impl From<McpToken> for McpTokenSummary {
107 fn from(t: McpToken) -> Self {
108 Self {
109 id: t.id,
110 name: t.name,
111 scope: t.scope,
112 created_at: t.created_at,
113 expires_at: t.expires_at,
114 last_used_at: t.last_used_at,
115 revoked_at: t.revoked_at,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122pub struct CreateTokenResponse {
123 #[serde(flatten)]
124 pub summary: McpTokenSummary,
125 pub plaintext: String,
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn scope_ranking_grants_chain() {
135 assert!(TokenScope::Read.grants(TokenScope::Read));
137 assert!(!TokenScope::Read.grants(TokenScope::Write));
138 assert!(!TokenScope::Read.grants(TokenScope::Admin));
139 assert!(TokenScope::Write.grants(TokenScope::Read));
141 assert!(TokenScope::Write.grants(TokenScope::Write));
142 assert!(!TokenScope::Write.grants(TokenScope::Admin));
143 assert!(TokenScope::Admin.grants(TokenScope::Read));
145 assert!(TokenScope::Admin.grants(TokenScope::Write));
146 assert!(TokenScope::Admin.grants(TokenScope::Admin));
147 }
148
149 #[test]
150 fn scope_ord_total_order() {
151 assert!(TokenScope::Read < TokenScope::Write);
152 assert!(TokenScope::Write < TokenScope::Admin);
153 assert!(TokenScope::Admin >= TokenScope::Read);
154 let mut v = [TokenScope::Admin, TokenScope::Read, TokenScope::Write];
156 v.sort();
157 assert_eq!(v, [TokenScope::Read, TokenScope::Write, TokenScope::Admin]);
158 }
159
160 #[test]
161 fn scope_db_roundtrip() {
162 for s in [TokenScope::Read, TokenScope::Write, TokenScope::Admin] {
163 assert_eq!(TokenScope::from_db(s.as_str()), Some(s));
164 }
165 assert_eq!(TokenScope::from_db("root"), None);
166 assert_eq!(TokenScope::from_db(""), None);
167 }
168
169 #[test]
170 fn scope_serde_lowercase() {
171 let s = serde_json::to_string(&TokenScope::Admin).unwrap();
172 assert_eq!(s, "\"admin\"");
173 let v: TokenScope = serde_json::from_str("\"read\"").unwrap();
174 assert_eq!(v, TokenScope::Read);
175 }
176}