mas_handlers/admin/
model.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use std::net::IpAddr;
8
9use chrono::{DateTime, Utc};
10use mas_data_model::Device;
11use schemars::JsonSchema;
12use serde::Serialize;
13use ulid::Ulid;
14use url::Url;
15
16/// A resource, with a type and an ID
17pub trait Resource {
18    /// The type of the resource
19    const KIND: &'static str;
20
21    /// The canonical path prefix for this kind of resource
22    const PATH: &'static str;
23
24    /// The ID of the resource
25    fn id(&self) -> Ulid;
26
27    /// The canonical path for this resource
28    ///
29    /// This is the concatenation of the canonical path prefix and the ID
30    fn path(&self) -> String {
31        format!("{}/{}", Self::PATH, self.id())
32    }
33}
34
35/// A user
36#[derive(Serialize, JsonSchema)]
37pub struct User {
38    #[serde(skip)]
39    id: Ulid,
40
41    /// The username (localpart) of the user
42    username: String,
43
44    /// When the user was created
45    created_at: DateTime<Utc>,
46
47    /// When the user was locked. If null, the user is not locked.
48    locked_at: Option<DateTime<Utc>>,
49
50    /// When the user was deactivated. If null, the user is not deactivated.
51    deactivated_at: Option<DateTime<Utc>>,
52
53    /// Whether the user can request admin privileges.
54    admin: bool,
55
56    /// Whether the user was a guest before migrating to MAS,
57    legacy_guest: bool,
58}
59
60impl User {
61    /// Samples of users with different properties for examples in the schema
62    pub fn samples() -> [Self; 3] {
63        [
64            Self {
65                id: Ulid::from_bytes([0x01; 16]),
66                username: "alice".to_owned(),
67                created_at: DateTime::default(),
68                locked_at: None,
69                deactivated_at: None,
70                admin: false,
71                legacy_guest: false,
72            },
73            Self {
74                id: Ulid::from_bytes([0x02; 16]),
75                username: "bob".to_owned(),
76                created_at: DateTime::default(),
77                locked_at: None,
78                deactivated_at: None,
79                admin: true,
80                legacy_guest: false,
81            },
82            Self {
83                id: Ulid::from_bytes([0x03; 16]),
84                username: "charlie".to_owned(),
85                created_at: DateTime::default(),
86                locked_at: Some(DateTime::default()),
87                deactivated_at: None,
88                admin: false,
89                legacy_guest: true,
90            },
91        ]
92    }
93}
94
95impl From<mas_data_model::User> for User {
96    fn from(user: mas_data_model::User) -> Self {
97        Self {
98            id: user.id,
99            username: user.username,
100            created_at: user.created_at,
101            locked_at: user.locked_at,
102            deactivated_at: user.deactivated_at,
103            admin: user.can_request_admin,
104            legacy_guest: user.is_guest,
105        }
106    }
107}
108
109impl Resource for User {
110    const KIND: &'static str = "user";
111    const PATH: &'static str = "/api/admin/v1/users";
112
113    fn id(&self) -> Ulid {
114        self.id
115    }
116}
117
118/// An email address for a user
119#[derive(Serialize, JsonSchema)]
120pub struct UserEmail {
121    #[serde(skip)]
122    id: Ulid,
123
124    /// When the object was created
125    created_at: DateTime<Utc>,
126
127    /// The ID of the user who owns this email address
128    #[schemars(with = "super::schema::Ulid")]
129    user_id: Ulid,
130
131    /// The email address
132    email: String,
133}
134
135impl Resource for UserEmail {
136    const KIND: &'static str = "user-email";
137    const PATH: &'static str = "/api/admin/v1/user-emails";
138
139    fn id(&self) -> Ulid {
140        self.id
141    }
142}
143
144impl From<mas_data_model::UserEmail> for UserEmail {
145    fn from(value: mas_data_model::UserEmail) -> Self {
146        Self {
147            id: value.id,
148            created_at: value.created_at,
149            user_id: value.user_id,
150            email: value.email,
151        }
152    }
153}
154
155impl UserEmail {
156    pub fn samples() -> [Self; 1] {
157        [Self {
158            id: Ulid::from_bytes([0x01; 16]),
159            created_at: DateTime::default(),
160            user_id: Ulid::from_bytes([0x02; 16]),
161            email: "alice@example.com".to_owned(),
162        }]
163    }
164}
165
166/// A compatibility session for legacy clients
167#[derive(Serialize, JsonSchema)]
168pub struct CompatSession {
169    #[serde(skip)]
170    pub id: Ulid,
171
172    /// The ID of the user that owns this session
173    #[schemars(with = "super::schema::Ulid")]
174    pub user_id: Ulid,
175
176    /// The Matrix device ID of this session
177    #[schemars(with = "super::schema::Device")]
178    pub device_id: Option<Device>,
179
180    /// The ID of the user session that started this session, if any
181    #[schemars(with = "super::schema::Ulid")]
182    pub user_session_id: Option<Ulid>,
183
184    /// The redirect URI used to login in the client, if it was an SSO login
185    pub redirect_uri: Option<Url>,
186
187    /// The time this session was created
188    pub created_at: DateTime<Utc>,
189
190    /// The user agent string that started this session, if any
191    pub user_agent: Option<String>,
192
193    /// The time this session was last active
194    pub last_active_at: Option<DateTime<Utc>>,
195
196    /// The last IP address recorded for this session
197    pub last_active_ip: Option<std::net::IpAddr>,
198
199    /// The time this session was finished
200    pub finished_at: Option<DateTime<Utc>>,
201
202    /// The user-provided name, if any
203    pub human_name: Option<String>,
204}
205
206impl
207    From<(
208        mas_data_model::CompatSession,
209        Option<mas_data_model::CompatSsoLogin>,
210    )> for CompatSession
211{
212    fn from(
213        (session, sso_login): (
214            mas_data_model::CompatSession,
215            Option<mas_data_model::CompatSsoLogin>,
216        ),
217    ) -> Self {
218        let finished_at = session.finished_at();
219        Self {
220            id: session.id,
221            user_id: session.user_id,
222            device_id: session.device,
223            user_session_id: session.user_session_id,
224            redirect_uri: sso_login.map(|sso| sso.redirect_uri),
225            created_at: session.created_at,
226            user_agent: session.user_agent,
227            last_active_at: session.last_active_at,
228            last_active_ip: session.last_active_ip,
229            finished_at,
230            human_name: session.human_name,
231        }
232    }
233}
234
235impl Resource for CompatSession {
236    const KIND: &'static str = "compat-session";
237    const PATH: &'static str = "/api/admin/v1/compat-sessions";
238
239    fn id(&self) -> Ulid {
240        self.id
241    }
242}
243
244impl CompatSession {
245    pub fn samples() -> [Self; 3] {
246        [
247            Self {
248                id: Ulid::from_bytes([0x01; 16]),
249                user_id: Ulid::from_bytes([0x01; 16]),
250                device_id: Some("AABBCCDDEE".to_owned().into()),
251                user_session_id: Some(Ulid::from_bytes([0x11; 16])),
252                redirect_uri: Some("https://example.com/redirect".parse().unwrap()),
253                created_at: DateTime::default(),
254                user_agent: Some("Mozilla/5.0".to_owned()),
255                last_active_at: Some(DateTime::default()),
256                last_active_ip: Some([1, 2, 3, 4].into()),
257                finished_at: None,
258                human_name: Some("Laptop".to_owned()),
259            },
260            Self {
261                id: Ulid::from_bytes([0x02; 16]),
262                user_id: Ulid::from_bytes([0x01; 16]),
263                device_id: Some("FFGGHHIIJJ".to_owned().into()),
264                user_session_id: Some(Ulid::from_bytes([0x12; 16])),
265                redirect_uri: None,
266                created_at: DateTime::default(),
267                user_agent: Some("Mozilla/5.0".to_owned()),
268                last_active_at: Some(DateTime::default()),
269                last_active_ip: Some([1, 2, 3, 4].into()),
270                finished_at: Some(DateTime::default()),
271                human_name: None,
272            },
273            Self {
274                id: Ulid::from_bytes([0x03; 16]),
275                user_id: Ulid::from_bytes([0x01; 16]),
276                device_id: None,
277                user_session_id: None,
278                redirect_uri: None,
279                created_at: DateTime::default(),
280                user_agent: None,
281                last_active_at: None,
282                last_active_ip: None,
283                finished_at: None,
284                human_name: None,
285            },
286        ]
287    }
288}
289
290/// A OAuth 2.0 session
291#[derive(Serialize, JsonSchema)]
292pub struct OAuth2Session {
293    #[serde(skip)]
294    id: Ulid,
295
296    /// When the object was created
297    created_at: DateTime<Utc>,
298
299    /// When the session was finished
300    finished_at: Option<DateTime<Utc>>,
301
302    /// The ID of the user who owns the session
303    #[schemars(with = "Option<super::schema::Ulid>")]
304    user_id: Option<Ulid>,
305
306    /// The ID of the browser session which started this session
307    #[schemars(with = "Option<super::schema::Ulid>")]
308    user_session_id: Option<Ulid>,
309
310    /// The ID of the client which requested this session
311    #[schemars(with = "super::schema::Ulid")]
312    client_id: Ulid,
313
314    /// The scope granted for this session
315    scope: String,
316
317    /// The user agent string of the client which started this session
318    user_agent: Option<String>,
319
320    /// The last time the session was active
321    last_active_at: Option<DateTime<Utc>>,
322
323    /// The last IP address used by the session
324    last_active_ip: Option<IpAddr>,
325
326    /// The user-provided name, if any
327    human_name: Option<String>,
328}
329
330impl From<mas_data_model::Session> for OAuth2Session {
331    fn from(session: mas_data_model::Session) -> Self {
332        Self {
333            id: session.id,
334            created_at: session.created_at,
335            finished_at: session.finished_at(),
336            user_id: session.user_id,
337            user_session_id: session.user_session_id,
338            client_id: session.client_id,
339            scope: session.scope.to_string(),
340            user_agent: session.user_agent,
341            last_active_at: session.last_active_at,
342            last_active_ip: session.last_active_ip,
343            human_name: session.human_name,
344        }
345    }
346}
347
348impl OAuth2Session {
349    /// Samples of OAuth 2.0 sessions
350    pub fn samples() -> [Self; 3] {
351        [
352            Self {
353                id: Ulid::from_bytes([0x01; 16]),
354                created_at: DateTime::default(),
355                finished_at: None,
356                user_id: Some(Ulid::from_bytes([0x02; 16])),
357                user_session_id: Some(Ulid::from_bytes([0x03; 16])),
358                client_id: Ulid::from_bytes([0x04; 16]),
359                scope: "openid".to_owned(),
360                user_agent: Some("Mozilla/5.0".to_owned()),
361                last_active_at: Some(DateTime::default()),
362                last_active_ip: Some("127.0.0.1".parse().unwrap()),
363                human_name: Some("Laptop".to_owned()),
364            },
365            Self {
366                id: Ulid::from_bytes([0x02; 16]),
367                created_at: DateTime::default(),
368                finished_at: None,
369                user_id: None,
370                user_session_id: None,
371                client_id: Ulid::from_bytes([0x05; 16]),
372                scope: "urn:mas:admin".to_owned(),
373                user_agent: None,
374                last_active_at: None,
375                last_active_ip: None,
376                human_name: None,
377            },
378            Self {
379                id: Ulid::from_bytes([0x03; 16]),
380                created_at: DateTime::default(),
381                finished_at: Some(DateTime::default()),
382                user_id: Some(Ulid::from_bytes([0x04; 16])),
383                user_session_id: Some(Ulid::from_bytes([0x05; 16])),
384                client_id: Ulid::from_bytes([0x06; 16]),
385                scope: "urn:matrix:client:api:*".to_owned(),
386                user_agent: Some("Mozilla/5.0".to_owned()),
387                last_active_at: Some(DateTime::default()),
388                last_active_ip: Some("127.0.0.1".parse().unwrap()),
389                human_name: None,
390            },
391        ]
392    }
393}
394
395impl Resource for OAuth2Session {
396    const KIND: &'static str = "oauth2-session";
397    const PATH: &'static str = "/api/admin/v1/oauth2-sessions";
398
399    fn id(&self) -> Ulid {
400        self.id
401    }
402}
403
404/// The browser (cookie) session for a user
405#[derive(Serialize, JsonSchema)]
406pub struct UserSession {
407    #[serde(skip)]
408    id: Ulid,
409
410    /// When the object was created
411    created_at: DateTime<Utc>,
412
413    /// When the session was finished
414    finished_at: Option<DateTime<Utc>>,
415
416    /// The ID of the user who owns the session
417    #[schemars(with = "super::schema::Ulid")]
418    user_id: Ulid,
419
420    /// The user agent string of the client which started this session
421    user_agent: Option<String>,
422
423    /// The last time the session was active
424    last_active_at: Option<DateTime<Utc>>,
425
426    /// The last IP address used by the session
427    last_active_ip: Option<IpAddr>,
428}
429
430impl From<mas_data_model::BrowserSession> for UserSession {
431    fn from(value: mas_data_model::BrowserSession) -> Self {
432        Self {
433            id: value.id,
434            created_at: value.created_at,
435            finished_at: value.finished_at,
436            user_id: value.user.id,
437            user_agent: value.user_agent,
438            last_active_at: value.last_active_at,
439            last_active_ip: value.last_active_ip,
440        }
441    }
442}
443
444impl UserSession {
445    /// Samples of user sessions
446    pub fn samples() -> [Self; 3] {
447        [
448            Self {
449                id: Ulid::from_bytes([0x01; 16]),
450                created_at: DateTime::default(),
451                finished_at: None,
452                user_id: Ulid::from_bytes([0x02; 16]),
453                user_agent: Some("Mozilla/5.0".to_owned()),
454                last_active_at: Some(DateTime::default()),
455                last_active_ip: Some("127.0.0.1".parse().unwrap()),
456            },
457            Self {
458                id: Ulid::from_bytes([0x02; 16]),
459                created_at: DateTime::default(),
460                finished_at: None,
461                user_id: Ulid::from_bytes([0x03; 16]),
462                user_agent: None,
463                last_active_at: None,
464                last_active_ip: None,
465            },
466            Self {
467                id: Ulid::from_bytes([0x03; 16]),
468                created_at: DateTime::default(),
469                finished_at: Some(DateTime::default()),
470                user_id: Ulid::from_bytes([0x04; 16]),
471                user_agent: Some("Mozilla/5.0".to_owned()),
472                last_active_at: Some(DateTime::default()),
473                last_active_ip: Some("127.0.0.1".parse().unwrap()),
474            },
475        ]
476    }
477}
478
479impl Resource for UserSession {
480    const KIND: &'static str = "user-session";
481    const PATH: &'static str = "/api/admin/v1/user-sessions";
482
483    fn id(&self) -> Ulid {
484        self.id
485    }
486}
487
488/// An upstream OAuth 2.0 link
489#[derive(Serialize, JsonSchema)]
490pub struct UpstreamOAuthLink {
491    #[serde(skip)]
492    id: Ulid,
493
494    /// When the object was created
495    created_at: DateTime<Utc>,
496
497    /// The ID of the provider
498    #[schemars(with = "super::schema::Ulid")]
499    provider_id: Ulid,
500
501    /// The subject of the upstream account, unique per provider
502    subject: String,
503
504    /// The ID of the user who owns this link, if any
505    #[schemars(with = "Option<super::schema::Ulid>")]
506    user_id: Option<Ulid>,
507
508    /// A human-readable name of the upstream account
509    human_account_name: Option<String>,
510}
511
512impl Resource for UpstreamOAuthLink {
513    const KIND: &'static str = "upstream-oauth-link";
514    const PATH: &'static str = "/api/admin/v1/upstream-oauth-links";
515
516    fn id(&self) -> Ulid {
517        self.id
518    }
519}
520
521impl From<mas_data_model::UpstreamOAuthLink> for UpstreamOAuthLink {
522    fn from(value: mas_data_model::UpstreamOAuthLink) -> Self {
523        Self {
524            id: value.id,
525            created_at: value.created_at,
526            provider_id: value.provider_id,
527            subject: value.subject,
528            user_id: value.user_id,
529            human_account_name: value.human_account_name,
530        }
531    }
532}
533
534impl UpstreamOAuthLink {
535    /// Samples of upstream OAuth 2.0 links
536    pub fn samples() -> [Self; 3] {
537        [
538            Self {
539                id: Ulid::from_bytes([0x01; 16]),
540                created_at: DateTime::default(),
541                provider_id: Ulid::from_bytes([0x02; 16]),
542                subject: "john-42".to_owned(),
543                user_id: Some(Ulid::from_bytes([0x03; 16])),
544                human_account_name: Some("john.doe@example.com".to_owned()),
545            },
546            Self {
547                id: Ulid::from_bytes([0x02; 16]),
548                created_at: DateTime::default(),
549                provider_id: Ulid::from_bytes([0x03; 16]),
550                subject: "jane-123".to_owned(),
551                user_id: None,
552                human_account_name: None,
553            },
554            Self {
555                id: Ulid::from_bytes([0x03; 16]),
556                created_at: DateTime::default(),
557                provider_id: Ulid::from_bytes([0x04; 16]),
558                subject: "bob@social.example.com".to_owned(),
559                user_id: Some(Ulid::from_bytes([0x05; 16])),
560                human_account_name: Some("bob".to_owned()),
561            },
562        ]
563    }
564}
565
566/// The policy data
567#[derive(Serialize, JsonSchema)]
568pub struct PolicyData {
569    #[serde(skip)]
570    id: Ulid,
571
572    /// The creation date of the policy data
573    created_at: DateTime<Utc>,
574
575    /// The policy data content
576    data: serde_json::Value,
577}
578
579impl From<mas_data_model::PolicyData> for PolicyData {
580    fn from(policy_data: mas_data_model::PolicyData) -> Self {
581        Self {
582            id: policy_data.id,
583            created_at: policy_data.created_at,
584            data: policy_data.data,
585        }
586    }
587}
588
589impl Resource for PolicyData {
590    const KIND: &'static str = "policy-data";
591    const PATH: &'static str = "/api/admin/v1/policy-data";
592
593    fn id(&self) -> Ulid {
594        self.id
595    }
596}
597
598impl PolicyData {
599    /// Samples of policy data
600    pub fn samples() -> [Self; 1] {
601        [Self {
602            id: Ulid::from_bytes([0x01; 16]),
603            created_at: DateTime::default(),
604            data: serde_json::json!({
605                "hello": "world",
606                "foo": 42,
607                "bar": true
608            }),
609        }]
610    }
611}
612
613/// A registration token
614#[derive(Serialize, JsonSchema)]
615pub struct UserRegistrationToken {
616    #[serde(skip)]
617    id: Ulid,
618
619    /// The token string
620    token: String,
621
622    /// Whether the token is valid
623    valid: bool,
624
625    /// Maximum number of times this token can be used
626    usage_limit: Option<u32>,
627
628    /// Number of times this token has been used
629    times_used: u32,
630
631    /// When the token was created
632    created_at: DateTime<Utc>,
633
634    /// When the token was last used. If null, the token has never been used.
635    last_used_at: Option<DateTime<Utc>>,
636
637    /// When the token expires. If null, the token never expires.
638    expires_at: Option<DateTime<Utc>>,
639
640    /// When the token was revoked. If null, the token is not revoked.
641    revoked_at: Option<DateTime<Utc>>,
642}
643
644impl UserRegistrationToken {
645    pub fn new(token: mas_data_model::UserRegistrationToken, now: DateTime<Utc>) -> Self {
646        Self {
647            id: token.id,
648            valid: token.is_valid(now),
649            token: token.token,
650            usage_limit: token.usage_limit,
651            times_used: token.times_used,
652            created_at: token.created_at,
653            last_used_at: token.last_used_at,
654            expires_at: token.expires_at,
655            revoked_at: token.revoked_at,
656        }
657    }
658}
659
660impl Resource for UserRegistrationToken {
661    const KIND: &'static str = "user-registration_token";
662    const PATH: &'static str = "/api/admin/v1/user-registration-tokens";
663
664    fn id(&self) -> Ulid {
665        self.id
666    }
667}
668
669impl UserRegistrationToken {
670    /// Samples of registration tokens
671    pub fn samples() -> [Self; 2] {
672        [
673            Self {
674                id: Ulid::from_bytes([0x01; 16]),
675                token: "abc123def456".to_owned(),
676                valid: true,
677                usage_limit: Some(10),
678                times_used: 5,
679                created_at: DateTime::default(),
680                last_used_at: Some(DateTime::default()),
681                expires_at: Some(DateTime::default() + chrono::Duration::days(30)),
682                revoked_at: None,
683            },
684            Self {
685                id: Ulid::from_bytes([0x02; 16]),
686                token: "xyz789abc012".to_owned(),
687                valid: false,
688                usage_limit: None,
689                times_used: 0,
690                created_at: DateTime::default(),
691                last_used_at: None,
692                expires_at: None,
693                revoked_at: Some(DateTime::default()),
694            },
695        ]
696    }
697}