1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-28 11:02:02 +03:00

Add a admin flag to the compatibility session

Also adds a CLI tool to issue a compatibility token.
This commit is contained in:
Quentin Gliech
2023-06-16 14:30:40 +02:00
parent 08d9b0b886
commit 2a514cf452
13 changed files with 185 additions and 70 deletions

1
Cargo.lock generated
View File

@ -3139,6 +3139,7 @@ dependencies = [
"itertools",
"listenfd",
"mas-config",
"mas-data-model",
"mas-email",
"mas-handlers",
"mas-http",

View File

@ -46,6 +46,7 @@ sentry-tracing = "0.31.3"
sentry-tower = { version = "0.31.3", features = ["http"] }
mas-config = { path = "../config" }
mas-data-model = { path = "../data-model" }
mas-email = { path = "../email" }
mas-handlers = { path = "../handlers", default-features = false }
mas-http = { path = "../http", default-features = false, features = ["axum", "client"] }

View File

@ -15,9 +15,11 @@
use anyhow::Context;
use clap::{Parser, ValueEnum};
use mas_config::{DatabaseConfig, PasswordsConfig, RootConfig};
use mas_data_model::{Device, TokenType};
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
use mas_router::UrlBuilder;
use mas_storage::{
compat::{CompatAccessTokenRepository, CompatSessionRepository},
oauth2::OAuth2ClientRepository,
upstream_oauth2::UpstreamOAuthProviderRepository,
user::{UserEmailRepository, UserPasswordRepository, UserRepository},
@ -182,6 +184,20 @@ enum Subcommand {
#[arg(long)]
client_secret: Option<String>,
},
/// Issue a compatibility token
IssueCompatibilityToken {
/// User for which to issue the token
username: String,
/// Device ID to set in the token. If not specified, a random device ID
/// will be generated.
device_id: Option<String>,
/// Whether that token should be admin
#[arg(long = "yes-i-want-to-grant-synapse-admin-privileges")]
admin: bool,
},
}
impl Options {
@ -375,6 +391,53 @@ impl Options {
Ok(())
}
SC::IssueCompatibilityToken {
username,
admin,
device_id,
} => {
let config: DatabaseConfig = root.load_config()?;
let pool = database_from_config(&config).await?;
let mut repo = PgRepository::from_pool(&pool).await?.boxed();
let user = repo
.user()
.find_by_username(username)
.await?
.context("User not found")?;
let device = if let Some(device_id) = device_id {
device_id.clone().try_into()?
} else {
Device::generate(&mut rng)
};
let compat_session = repo
.compat_session()
.add(&mut rng, &clock, &user, device, *admin)
.await?;
let token = TokenType::CompatAccessToken.generate(&mut rng);
let compat_access_token = repo
.compat_access_token()
.add(&mut rng, &clock, &compat_session, token, None)
.await?;
repo.save().await?;
info!(
%compat_access_token.id,
%compat_session.id,
%compat_session.device,
%user.id,
%user.username,
"Compatibility token issued: {}", compat_access_token.token
);
Ok(())
}
}
}
}

View File

@ -80,6 +80,12 @@ impl TryFrom<String> for Device {
}
}
impl std::fmt::Display for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.id)
}
}
#[cfg(test)]
mod test {
use oauth2_types::scope::OPENID;

View File

@ -68,6 +68,7 @@ pub struct CompatSession {
pub user_id: Ulid,
pub device: Device,
pub created_at: DateTime<Utc>,
pub is_synapse_admin: bool,
}
impl std::ops::Deref for CompatSession {

View File

@ -399,7 +399,7 @@ async fn user_password_login(
let session = repo
.compat_session()
.add(&mut rng, clock, &user, device)
.add(&mut rng, clock, &user, device, false)
.await?;
Ok((session, user))
@ -726,7 +726,7 @@ mod tests {
// Complete the flow by fulfilling it with a session
let compat_session = repo
.compat_session()
.add(&mut state.rng(), &state.clock, user, device.clone())
.add(&mut state.rng(), &state.clock, user, device.clone(), false)
.await
.unwrap();

View File

@ -200,7 +200,7 @@ pub async fn post(
let compat_session = repo
.compat_session()
.add(&mut rng, &clock, &session.user, device)
.add(&mut rng, &clock, &session.user, device, false)
.await?;
repo.compat_sso_login()

View File

@ -120,6 +120,7 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse {
};
const API_SCOPE: ScopeToken = ScopeToken::from_static("urn:matrix:org.matrix.msc2967.client:api:*");
const SYNAPSE_ADMIN_SCOPE: ScopeToken = ScopeToken::from_static("urn:synapse:admin:*");
#[tracing::instrument(
name = "handlers.oauth2.introspection.post",
@ -267,8 +268,13 @@ pub(crate) async fn post(
// XXX: is that the right error to bubble up?
.ok_or(RouteError::UnknownToken)?;
// Grant the synapse admin scope if the session has the admin flag set.
let synapse_admin = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
let device_scope = session.device.to_scope_token();
let scope = [API_SCOPE, device_scope].into_iter().collect();
let scope = [API_SCOPE, device_scope]
.into_iter()
.chain(synapse_admin)
.collect();
IntrospectionResponse {
active: true,
@ -308,8 +314,13 @@ pub(crate) async fn post(
// XXX: is that the right error to bubble up?
.ok_or(RouteError::UnknownToken)?;
// Grant the synapse admin scope if the session has the admin flag set.
let synapse_admin = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
let device_scope = session.device.to_scope_token();
let scope = [API_SCOPE, device_scope].into_iter().collect();
let scope = [API_SCOPE, device_scope]
.into_iter()
.chain(synapse_admin)
.collect();
IntrospectionResponse {
active: true,

View File

@ -0,0 +1,16 @@
-- Copyright 2023 The Matrix.org Foundation C.I.C.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
ALTER TABLE compat_sessions
ADD COLUMN is_synapse_admin BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -14,6 +14,56 @@
},
"query": "\n UPDATE oauth2_authorization_grants\n SET fulfilled_at = $2\n , oauth2_session_id = $3\n WHERE oauth2_authorization_grant_id = $1\n "
},
"0469c1d3ad11fd96febacad33302709c870ead848d6920cdfdb18912d543488e": {
"describe": {
"columns": [
{
"name": "compat_session_id",
"ordinal": 0,
"type_info": "Uuid"
},
{
"name": "device_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "user_id",
"ordinal": 2,
"type_info": "Uuid"
},
{
"name": "created_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "finished_at",
"ordinal": 4,
"type_info": "Timestamptz"
},
{
"name": "is_synapse_admin",
"ordinal": 5,
"type_info": "Bool"
}
],
"nullable": [
false,
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Uuid"
]
}
},
"query": "\n SELECT compat_session_id\n , device_id\n , user_id\n , created_at\n , finished_at\n , is_synapse_admin\n FROM compat_sessions\n WHERE compat_session_id = $1\n "
},
"08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35": {
"describe": {
"columns": [
@ -112,21 +162,6 @@
},
"query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n created_at\n FROM upstream_oauth_providers\n "
},
"18c3e56c72ef26bd42653c379767ffdd97bb06cb1686dfbf4099f3ad3d7b22c8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Text",
"Timestamptz"
]
}
},
"query": "\n INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at)\n VALUES ($1, $2, $3, $4)\n "
},
"1a8701f5672de052bb766933f60b93249acc7237b996e8b93cd61b9f69c902ff": {
"describe": {
"columns": [],
@ -1913,6 +1948,22 @@
},
"query": "\n UPDATE oauth2_authorization_grants\n SET exchanged_at = $2\n WHERE oauth2_authorization_grant_id = $1\n "
},
"cff3ac0fff62ffdc5640fce08c2ffabc1d89202561b736c5d03b501dfcd8d886": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Text",
"Timestamptz",
"Bool"
]
}
},
"query": "\n INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at, is_synapse_admin)\n VALUES ($1, $2, $3, $4, $5)\n "
},
"d0b403e9c843ef19fa5ad60bec32ebf14a1ba0d01681c3836366d3f55e7851f4": {
"describe": {
"columns": [],
@ -2388,50 +2439,6 @@
},
"query": "\n SELECT oauth2_session_id\n , user_session_id\n , oauth2_client_id\n , scope\n , created_at\n , finished_at\n FROM oauth2_sessions\n\n WHERE oauth2_session_id = $1\n "
},
"f3ee06958d827b152c57328caa0a6030c372cb99cdb60e4b75a28afeb5096f45": {
"describe": {
"columns": [
{
"name": "compat_session_id",
"ordinal": 0,
"type_info": "Uuid"
},
{
"name": "device_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "user_id",
"ordinal": 2,
"type_info": "Uuid"
},
{
"name": "created_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "finished_at",
"ordinal": 4,
"type_info": "Timestamptz"
}
],
"nullable": [
false,
false,
false,
false,
true
],
"parameters": {
"Left": [
"Uuid"
]
}
},
"query": "\n SELECT compat_session_id\n , device_id\n , user_id\n , created_at\n , finished_at\n FROM compat_sessions\n WHERE compat_session_id = $1\n "
},
"f5edcd4c306ca8179cdf9d4aab59fbba971b54611c91345849920954dd8089b3": {
"describe": {
"columns": [],

View File

@ -62,7 +62,7 @@ mod tests {
let device_str = device.as_str().to_owned();
let session = repo
.compat_session()
.add(&mut rng, &clock, &user, device)
.add(&mut rng, &clock, &user, device, false)
.await
.unwrap();
assert_eq!(session.user_id, user.id);
@ -118,7 +118,7 @@ mod tests {
let device = Device::generate(&mut rng);
let session = repo
.compat_session()
.add(&mut rng, &clock, &user, device)
.add(&mut rng, &clock, &user, device, false)
.await
.unwrap();
@ -238,7 +238,7 @@ mod tests {
let device = Device::generate(&mut rng);
let session = repo
.compat_session()
.add(&mut rng, &clock, &user, device)
.add(&mut rng, &clock, &user, device, false)
.await
.unwrap();
@ -391,7 +391,7 @@ mod tests {
let device = Device::generate(&mut rng);
let session = repo
.compat_session()
.add(&mut rng, &clock, &user, device)
.add(&mut rng, &clock, &user, device, false)
.await
.unwrap();

View File

@ -42,6 +42,7 @@ struct CompatSessionLookup {
user_id: Uuid,
created_at: DateTime<Utc>,
finished_at: Option<DateTime<Utc>>,
is_synapse_admin: bool,
}
impl TryFrom<CompatSessionLookup> for CompatSession {
@ -67,6 +68,7 @@ impl TryFrom<CompatSessionLookup> for CompatSession {
user_id: value.user_id.into(),
device,
created_at: value.created_at,
is_synapse_admin: value.is_synapse_admin,
};
Ok(session)
@ -95,6 +97,7 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
, user_id
, created_at
, finished_at
, is_synapse_admin
FROM compat_sessions
WHERE compat_session_id = $1
"#,
@ -128,6 +131,7 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
clock: &dyn Clock,
user: &User,
device: Device,
is_synapse_admin: bool,
) -> Result<CompatSession, Self::Error> {
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
@ -135,13 +139,14 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
sqlx::query!(
r#"
INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at)
VALUES ($1, $2, $3, $4)
INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at, is_synapse_admin)
VALUES ($1, $2, $3, $4, $5)
"#,
Uuid::from(id),
Uuid::from(user.id),
device.as_str(),
created_at,
is_synapse_admin,
)
.traced()
.execute(&mut *self.conn)
@ -153,6 +158,7 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
user_id: user.id,
device,
created_at,
is_synapse_admin,
})
}

View File

@ -49,6 +49,7 @@ pub trait CompatSessionRepository: Send + Sync {
/// * `clock`: The clock used to generate timestamps
/// * `user`: The user to create the compat session for
/// * `device`: The device ID of this session
/// * `is_synapse_admin`: Whether the session is a synapse admin session
///
/// # Errors
///
@ -59,6 +60,7 @@ pub trait CompatSessionRepository: Send + Sync {
clock: &dyn Clock,
user: &User,
device: Device,
is_synapse_admin: bool,
) -> Result<CompatSession, Self::Error>;
/// End a compat session
@ -89,6 +91,7 @@ repository_impl!(CompatSessionRepository:
clock: &dyn Clock,
user: &User,
device: Device,
is_synapse_admin: bool,
) -> Result<CompatSession, Self::Error>;
async fn finish(