1use crate::{RelationTuple, Subject};
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct TokenClaims {
57 pub sub: String,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub iss: Option<String>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub aud: Option<Vec<String>>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub exp: Option<i64>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub iat: Option<i64>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub scope: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub roles: Option<Vec<String>>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub groups: Option<Vec<String>>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub org_id: Option<String>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub tenant_id: Option<String>,
95
96 #[serde(flatten)]
98 pub custom: HashMap<String, serde_json::Value>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ScopeMapping {
104 pub scope: String,
106
107 pub namespace: String,
109
110 pub object_id: String,
114
115 pub relation: String,
117}
118
119impl ScopeMapping {
120 pub fn new(
122 scope: impl Into<String>,
123 namespace: impl Into<String>,
124 object_id: impl Into<String>,
125 relation: impl Into<String>,
126 ) -> Self {
127 Self {
128 scope: scope.into(),
129 namespace: namespace.into(),
130 object_id: object_id.into(),
131 relation: relation.into(),
132 }
133 }
134
135 pub fn resolve_object_id(&self, claims: &TokenClaims) -> Vec<String> {
137 if self.object_id == "*" {
138 vec![]
141 } else if self.object_id.contains('{') {
142 let mut result = self.object_id.clone();
144
145 if let Some(ref org_id) = claims.org_id {
147 result = result.replace("{org_id}", org_id);
148 }
149
150 if let Some(ref tenant_id) = claims.tenant_id {
152 result = result.replace("{tenant_id}", tenant_id);
153 }
154
155 result = result.replace("{sub}", &claims.sub);
157
158 vec![result]
159 } else {
160 vec![self.object_id.clone()]
162 }
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct RoleMapping {
169 pub role: String,
171
172 pub namespace: String,
174
175 pub object_id: String,
177
178 pub relation: String,
180}
181
182pub struct OAuth2Mapper {
184 role_mappings: Vec<RoleMapping>,
186}
187
188impl OAuth2Mapper {
189 pub fn new() -> Self {
191 Self {
192 role_mappings: Vec::new(),
193 }
194 }
195
196 pub fn add_role_mapping(&mut self, mapping: RoleMapping) {
198 self.role_mappings.push(mapping);
199 }
200
201 pub fn claims_to_tuples(
203 &self,
204 claims: &TokenClaims,
205 scope_mappings: &[ScopeMapping],
206 ) -> Result<Vec<RelationTuple>, String> {
207 let mut tuples = Vec::new();
208
209 let scopes: Vec<&str> = claims
211 .scope
212 .as_ref()
213 .map(|s| s.split_whitespace().collect())
214 .unwrap_or_default();
215
216 let subject = if claims.sub.starts_with("user:") {
218 Subject::User(claims.sub.clone())
219 } else {
220 Subject::User(format!("user:{}", claims.sub))
221 };
222
223 for scope in scopes {
225 for mapping in scope_mappings {
226 if mapping.scope == scope {
227 let object_ids = mapping.resolve_object_id(claims);
228
229 for object_id in object_ids {
230 tuples.push(RelationTuple::new(
231 mapping.namespace.clone(),
232 mapping.relation.clone(),
233 object_id,
234 subject.clone(),
235 ));
236 }
237 }
238 }
239 }
240
241 if let Some(ref roles) = claims.roles {
243 for role in roles {
244 for mapping in &self.role_mappings {
245 if &mapping.role == role {
246 let object_id = if mapping.object_id.contains('{') {
247 let mut resolved = mapping.object_id.clone();
249 if let Some(ref org_id) = claims.org_id {
250 resolved = resolved.replace("{org_id}", org_id);
251 }
252 resolved
253 } else {
254 mapping.object_id.clone()
255 };
256
257 tuples.push(RelationTuple::new(
258 mapping.namespace.clone(),
259 mapping.relation.clone(),
260 object_id,
261 subject.clone(),
262 ));
263 }
264 }
265 }
266 }
267
268 if let Some(ref groups) = claims.groups {
270 for group in groups {
271 tuples.push(RelationTuple::new(
273 "group",
274 "member",
275 group.clone(),
276 subject.clone(),
277 ));
278 }
279 }
280
281 Ok(tuples)
282 }
283
284 pub fn extract_org_membership(&self, claims: &TokenClaims) -> Option<RelationTuple> {
286 claims.org_id.as_ref().map(|org_id| {
287 let subject = if claims.sub.starts_with("user:") {
288 Subject::User(claims.sub.clone())
289 } else {
290 Subject::User(format!("user:{}", claims.sub))
291 };
292
293 RelationTuple::new("organization", "member", org_id.clone(), subject)
294 })
295 }
296
297 pub fn jwt_to_tuples(
299 &self,
300 jwt_payload: &str,
301 scope_mappings: &[ScopeMapping],
302 ) -> Result<Vec<RelationTuple>, String> {
303 let claims: TokenClaims = serde_json::from_str(jwt_payload)
304 .map_err(|e| format!("Failed to parse JWT claims: {}", e))?;
305
306 self.claims_to_tuples(&claims, scope_mappings)
307 }
308}
309
310impl Default for OAuth2Mapper {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_scope_mapping() {
322 let mapping = ScopeMapping::new("read:documents", "document", "doc123", "viewer");
323
324 assert_eq!(mapping.scope, "read:documents");
325 assert_eq!(mapping.namespace, "document");
326 assert_eq!(mapping.object_id, "doc123");
327 assert_eq!(mapping.relation, "viewer");
328 }
329
330 #[test]
331 fn test_template_resolution() {
332 let mapping = ScopeMapping::new("read:org_documents", "document", "org_{org_id}", "viewer");
333
334 let claims = TokenClaims {
335 sub: "alice".to_string(),
336 org_id: Some("acme".to_string()),
337 ..Default::default()
338 };
339
340 let resolved = mapping.resolve_object_id(&claims);
341 assert_eq!(resolved, vec!["org_acme"]);
342 }
343
344 #[test]
345 fn test_claims_to_tuples() {
346 let mapper = OAuth2Mapper::new();
347
348 let claims = TokenClaims {
349 sub: "alice".to_string(),
350 scope: Some("read:documents write:documents".to_string()),
351 ..Default::default()
352 };
353
354 let mappings = vec![
355 ScopeMapping::new("read:documents", "document", "doc123", "viewer"),
356 ScopeMapping::new("write:documents", "document", "doc123", "editor"),
357 ];
358
359 let tuples = mapper.claims_to_tuples(&claims, &mappings).unwrap();
360
361 assert_eq!(tuples.len(), 2);
362 assert_eq!(tuples[0].namespace, "document");
363 assert_eq!(tuples[0].relation, "viewer");
364 assert_eq!(tuples[1].relation, "editor");
365 }
366
367 #[test]
368 fn test_role_mapping() {
369 let mut mapper = OAuth2Mapper::new();
370
371 mapper.add_role_mapping(RoleMapping {
372 role: "admin".to_string(),
373 namespace: "organization".to_string(),
374 object_id: "{org_id}".to_string(),
375 relation: "owner".to_string(),
376 });
377
378 let claims = TokenClaims {
379 sub: "alice".to_string(),
380 roles: Some(vec!["admin".to_string()]),
381 org_id: Some("acme".to_string()),
382 ..Default::default()
383 };
384
385 let tuples = mapper.claims_to_tuples(&claims, &[]).unwrap();
386
387 assert_eq!(tuples.len(), 1);
388 assert_eq!(tuples[0].namespace, "organization");
389 assert_eq!(tuples[0].object_id, "acme");
390 assert_eq!(tuples[0].relation, "owner");
391 }
392
393 #[test]
394 fn test_group_mapping() {
395 let mapper = OAuth2Mapper::new();
396
397 let claims = TokenClaims {
398 sub: "alice".to_string(),
399 groups: Some(vec!["engineering".to_string(), "admins".to_string()]),
400 ..Default::default()
401 };
402
403 let tuples = mapper.claims_to_tuples(&claims, &[]).unwrap();
404
405 assert_eq!(tuples.len(), 2);
406 assert_eq!(tuples[0].namespace, "group");
407 assert_eq!(tuples[0].relation, "member");
408 assert_eq!(tuples[0].object_id, "engineering");
409 }
410
411 #[test]
412 fn test_org_membership() {
413 let mapper = OAuth2Mapper::new();
414
415 let claims = TokenClaims {
416 sub: "alice".to_string(),
417 org_id: Some("acme".to_string()),
418 ..Default::default()
419 };
420
421 let tuple = mapper.extract_org_membership(&claims).unwrap();
422
423 assert_eq!(tuple.namespace, "organization");
424 assert_eq!(tuple.relation, "member");
425 assert_eq!(tuple.object_id, "acme");
426 }
427}