Skip to main content

mas_policy/
model.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8//! Input and output types for policy evaluation.
9//!
10//! This is useful to generate JSON schemas for each input type, which can then
11//! be type-checked by Open Policy Agent.
12
13use std::net::IpAddr;
14
15use mas_data_model::{Client, User};
16use oauth2_types::{registration::VerifiedClientMetadata, scope::Scope};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20/// Violation variants identified by a well-known policy code (under the `code`
21/// key).
22#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema, PartialEq, Eq)]
23#[serde(tag = "code", rename_all = "kebab-case")]
24pub enum ViolationVariant {
25    /// The username is too short.
26    UsernameTooShort,
27
28    /// The username is too long.
29    UsernameTooLong,
30
31    /// The username contains invalid characters.
32    UsernameInvalidChars,
33
34    /// The username contains only numeric characters.
35    UsernameAllNumeric,
36
37    /// The username is banned.
38    UsernameBanned,
39
40    /// The username is not allowed.
41    UsernameNotAllowed,
42
43    /// The email domain is not allowed.
44    EmailDomainNotAllowed,
45
46    /// The email domain is banned.
47    EmailDomainBanned,
48
49    /// The email address is not allowed.
50    EmailNotAllowed,
51
52    /// The email address is banned.
53    EmailBanned,
54
55    /// An admin scope was requested but is not allowed.
56    AdminScopeNotAllowed,
57
58    /// The client is not allowed by the policy.
59    ClientNotAllowed,
60
61    /// The user has reached their session limit.
62    TooManySessions {
63        /// How many devices need to be removed to make room for the new session
64        need_to_remove: u32,
65    },
66}
67
68impl ViolationVariant {
69    /// Returns the code as a string
70    #[must_use]
71    pub fn as_str(&self) -> &'static str {
72        match self {
73            Self::UsernameTooShort => "username-too-short",
74            Self::UsernameTooLong => "username-too-long",
75            Self::UsernameInvalidChars => "username-invalid-chars",
76            Self::UsernameAllNumeric => "username-all-numeric",
77            Self::UsernameBanned => "username-banned",
78            Self::UsernameNotAllowed => "username-not-allowed",
79            Self::EmailDomainNotAllowed => "email-domain-not-allowed",
80            Self::EmailDomainBanned => "email-domain-banned",
81            Self::EmailNotAllowed => "email-not-allowed",
82            Self::EmailBanned => "email-banned",
83            Self::AdminScopeNotAllowed => "admin-scope-not-allowed",
84            Self::ClientNotAllowed => "client-not-allowed",
85            Self::TooManySessions { .. } => "too-many-sessions",
86        }
87    }
88}
89
90/// A single violation of a policy.
91#[derive(Serialize, Deserialize, Debug, JsonSchema)]
92pub struct Violation {
93    pub msg: String,
94    pub redirect_uri: Option<String>,
95    pub field: Option<String>,
96
97    // We flatten as policies expect `code` as another top-level field.
98    //
99    // This also means all of the extra fields from the variant will be splatted at this
100    // level which is fine (arbitrary).
101    #[serde(flatten)]
102    pub variant: Option<ViolationVariant>,
103}
104
105/// The result of a policy evaluation.
106#[derive(Deserialize, Debug)]
107pub struct EvaluationResult {
108    #[serde(rename = "result")]
109    pub violations: Vec<Violation>,
110}
111
112impl std::fmt::Display for EvaluationResult {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        let mut first = true;
115        for violation in &self.violations {
116            if first {
117                first = false;
118            } else {
119                write!(f, ", ")?;
120            }
121            write!(f, "{}", violation.msg)?;
122        }
123        Ok(())
124    }
125}
126
127impl EvaluationResult {
128    /// Returns true if the policy evaluation was successful.
129    #[must_use]
130    pub fn valid(&self) -> bool {
131        self.violations.is_empty()
132    }
133}
134
135/// Identity of the requester
136#[derive(Serialize, Debug, Default, JsonSchema)]
137#[serde(rename_all = "snake_case")]
138pub struct Requester {
139    /// IP address of the entity making the request
140    pub ip_address: Option<IpAddr>,
141
142    /// User agent of the entity making the request
143    pub user_agent: Option<String>,
144}
145
146#[derive(Serialize, Debug, JsonSchema)]
147pub enum RegistrationMethod {
148    #[serde(rename = "password")]
149    Password,
150
151    #[serde(rename = "upstream-oauth2")]
152    UpstreamOAuth2,
153}
154
155/// Input for the user registration policy.
156#[derive(Serialize, Debug, JsonSchema)]
157#[serde(tag = "registration_method")]
158pub struct RegisterInput<'a> {
159    pub registration_method: RegistrationMethod,
160
161    pub username: &'a str,
162
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub email: Option<&'a str>,
165
166    pub requester: Requester,
167}
168
169/// Input for the client registration policy.
170#[derive(Serialize, Debug, JsonSchema)]
171#[serde(rename_all = "snake_case")]
172pub struct ClientRegistrationInput<'a> {
173    #[schemars(with = "std::collections::HashMap<String, serde_json::Value>")]
174    pub client_metadata: &'a VerifiedClientMetadata,
175    pub requester: Requester,
176}
177
178#[derive(Serialize, Debug, JsonSchema)]
179#[serde(rename_all = "snake_case")]
180pub enum GrantType {
181    AuthorizationCode,
182    ClientCredentials,
183    #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
184    DeviceCode,
185}
186
187/// Input for the authorization grant policy.
188#[derive(Serialize, Debug, JsonSchema)]
189#[serde(rename_all = "snake_case")]
190pub struct AuthorizationGrantInput<'a> {
191    #[schemars(with = "Option<std::collections::HashMap<String, serde_json::Value>>")]
192    pub user: Option<&'a User>,
193
194    /// How many sessions the user has.
195    /// Not populated if it's not a user logging in.
196    pub session_counts: Option<SessionCounts>,
197
198    #[schemars(with = "std::collections::HashMap<String, serde_json::Value>")]
199    pub client: &'a Client,
200
201    #[schemars(with = "String")]
202    pub scope: &'a Scope,
203
204    pub grant_type: GrantType,
205
206    pub requester: Requester,
207}
208
209/// Input for the compatibility login policy.
210#[derive(Serialize, Debug, JsonSchema)]
211#[serde(rename_all = "snake_case")]
212pub struct CompatLoginInput<'a> {
213    #[schemars(with = "std::collections::HashMap<String, serde_json::Value>")]
214    pub user: &'a User,
215
216    /// How many sessions the user has.
217    pub session_counts: SessionCounts,
218
219    /// Whether a session will be replaced by this login
220    pub session_replaced: bool,
221
222    /// What type of login is being performed.
223    /// This also determines whether the login is interactive.
224    pub login: CompatLogin,
225
226    pub requester: Requester,
227}
228
229#[derive(Serialize, Debug, JsonSchema)]
230#[serde(tag = "type")]
231pub enum CompatLogin {
232    /// Used as the interactive part of SSO login.
233    #[serde(rename = "m.login.sso")]
234    Sso { redirect_uri: String },
235
236    /// Used as the final (non-interactive) stage of SSO login.
237    #[serde(rename = "m.login.token")]
238    Token,
239
240    /// Non-interactive password-over-the-API login.
241    #[serde(rename = "m.login.password")]
242    Password,
243}
244
245/// Information about how many sessions the user has
246#[derive(Serialize, Debug, JsonSchema)]
247pub struct SessionCounts {
248    pub total: u64,
249
250    pub oauth2: u64,
251    pub compat: u64,
252    pub personal: u64,
253}
254
255/// Input for the email add policy.
256#[derive(Serialize, Debug, JsonSchema)]
257#[serde(rename_all = "snake_case")]
258pub struct EmailInput<'a> {
259    pub email: &'a str,
260
261    pub requester: Requester,
262}