mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-10-22 21:54:07 +00:00
Admin API: allow searching in upstream OAuth links on 'human account name' (#5869)
This commit is contained in:
6 files changed
+174
-2
No files matched your search
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -42,6 +43,11 @@ pub struct FilterParams {
|
||||
/// Retrieve the items with the given subject
|
||||
#[serde(rename = "filter[subject]")]
|
||||
subject: Option<String>,
|
||||
|
||||
/// Retrieve the items with a human account name matching the given
|
||||
/// substring (case-insensitive)
|
||||
#[serde(rename = "filter[human-account-name]")]
|
||||
human_account_name: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FilterParams {
|
||||
@@ -63,6 +69,11 @@ impl std::fmt::Display for FilterParams {
|
||||
sep = '&';
|
||||
}
|
||||
|
||||
if let Some(human_account_name) = &self.human_account_name {
|
||||
write!(f, "{sep}filter[human-account-name]={human_account_name}")?;
|
||||
sep = '&';
|
||||
}
|
||||
|
||||
let _ = sep;
|
||||
Ok(())
|
||||
}
|
||||
@@ -187,6 +198,12 @@ pub async fn handler(
|
||||
filter
|
||||
};
|
||||
|
||||
let filter = if let Some(name) = ¶ms.human_account_name {
|
||||
filter.matching_human_account_name(name)
|
||||
} else {
|
||||
filter
|
||||
};
|
||||
|
||||
let response = match include_count {
|
||||
IncludeCount::True => {
|
||||
let page = repo
|
||||
@@ -561,6 +578,68 @@ mod tests {
|
||||
}
|
||||
"#);
|
||||
|
||||
// Filter by human account name (case-insensitive substring)
|
||||
let request =
|
||||
Request::get("/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE%40")
|
||||
.bearer(&token)
|
||||
.empty();
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let body: serde_json::Value = response.json();
|
||||
assert_json_snapshot!(body, @r#"
|
||||
{
|
||||
"meta": {
|
||||
"count": 2
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"type": "upstream-oauth-link",
|
||||
"id": "01FSHN9AG0AQZQP8DX40GD59PW",
|
||||
"attributes": {
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
"provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
|
||||
"subject": "subject1",
|
||||
"user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
|
||||
"human_account_name": "alice@acme"
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
|
||||
},
|
||||
"meta": {
|
||||
"page": {
|
||||
"cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "upstream-oauth-link",
|
||||
"id": "01FSHN9AG0QHEHKX2JNQ2A2D07",
|
||||
"attributes": {
|
||||
"created_at": "2022-01-16T14:40:00Z",
|
||||
"provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z",
|
||||
"subject": "subject2",
|
||||
"user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
|
||||
"human_account_name": "alice@example"
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07"
|
||||
},
|
||||
"meta": {
|
||||
"page": {
|
||||
"cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE@&page[first]=10",
|
||||
"first": "/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE@&page[first]=10",
|
||||
"last": "/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE@&page[last]=10"
|
||||
}
|
||||
}
|
||||
"#);
|
||||
|
||||
// Test count=false
|
||||
let request = Request::get("/api/admin/v1/upstream-oauth-links?count=false")
|
||||
.bearer(&token)
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2026 Element Creations Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
-- Please see LICENSE files in the repository root for full details.
|
||||
|
||||
-- This adds an index on the human_account_name field for ILIKE '%search%'
|
||||
-- operations, enabling fuzzy searches of upstream OAuth link account names
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS upstream_oauth_links_human_account_name_trgm_idx
|
||||
ON upstream_oauth_links USING gin(human_account_name gin_trgm_ops);
|
||||
@@ -15,7 +15,9 @@ use mas_storage::{
|
||||
};
|
||||
use opentelemetry_semantic_conventions::trace::DB_QUERY_TEXT;
|
||||
use rand::RngCore;
|
||||
use sea_query::{Expr, ExprTrait, PostgresQueryBuilder, Query, enum_def};
|
||||
use sea_query::{
|
||||
Expr, ExprTrait, PostgresQueryBuilder, Query, enum_def, extension::postgres::PgExpr as _,
|
||||
};
|
||||
use sea_query_sqlx::SqlxBinder;
|
||||
use sqlx::PgConnection;
|
||||
use tracing::Instrument;
|
||||
@@ -114,6 +116,13 @@ impl Filter for UpstreamOAuthLinkFilter<'_> {
|
||||
.add_option(self.subject().map(|subject| {
|
||||
Expr::col((UpstreamOAuthLinks::Table, UpstreamOAuthLinks::Subject)).eq(subject)
|
||||
}))
|
||||
.add_option(self.human_account_name().map(|human_account_name| {
|
||||
Expr::col((
|
||||
UpstreamOAuthLinks::Table,
|
||||
UpstreamOAuthLinks::HumanAccountName,
|
||||
))
|
||||
.ilike(format!("%{human_account_name}%"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,13 @@ mod tests {
|
||||
// Create a link
|
||||
let link = repo
|
||||
.upstream_oauth_link()
|
||||
.add(&mut rng, &clock, &provider, "a-subject".to_owned(), None)
|
||||
.add(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&provider,
|
||||
"a-subject".to_owned(),
|
||||
Some("alice@example.com".to_owned()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -220,6 +226,40 @@ mod tests {
|
||||
|
||||
assert_eq!(repo.upstream_oauth_link().count(filter).await.unwrap(), 1);
|
||||
|
||||
// Filtering on a case-insensitive partial human_account_name should match
|
||||
let matching_filter = UpstreamOAuthLinkFilter::new().matching_human_account_name("LICE");
|
||||
let links = repo
|
||||
.upstream_oauth_link()
|
||||
.list(matching_filter, Pagination::first(10))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(links.edges.len(), 1);
|
||||
assert_eq!(links.edges[0].node.id, link.id);
|
||||
assert_eq!(
|
||||
repo.upstream_oauth_link()
|
||||
.count(matching_filter)
|
||||
.await
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
|
||||
// A non-matching human_account_name should return nothing
|
||||
let non_matching_filter =
|
||||
UpstreamOAuthLinkFilter::new().matching_human_account_name("nope");
|
||||
let links = repo
|
||||
.upstream_oauth_link()
|
||||
.list(non_matching_filter, Pagination::first(10))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(links.edges.len(), 0);
|
||||
assert_eq!(
|
||||
repo.upstream_oauth_link()
|
||||
.count(non_matching_filter)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
|
||||
// There should be exactly one enabled provider
|
||||
assert_eq!(
|
||||
repo.upstream_oauth_provider()
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct UpstreamOAuthLinkFilter<'a> {
|
||||
provider: Option<&'a UpstreamOAuthProvider>,
|
||||
provider_enabled: Option<bool>,
|
||||
subject: Option<&'a str>,
|
||||
human_account_name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> UpstreamOAuthLinkFilter<'a> {
|
||||
@@ -91,6 +92,22 @@ impl<'a> UpstreamOAuthLinkFilter<'a> {
|
||||
pub const fn subject(&self) -> Option<&str> {
|
||||
self.subject
|
||||
}
|
||||
|
||||
/// Only return links whose `human_account_name` matches the given substring
|
||||
/// (case-insensitive)
|
||||
#[must_use]
|
||||
pub fn matching_human_account_name(mut self, human_account_name: &'a str) -> Self {
|
||||
self.human_account_name = Some(human_account_name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the human account name filter
|
||||
///
|
||||
/// Returns [`None`] if no filter was set
|
||||
#[must_use]
|
||||
pub fn human_account_name(&self) -> Option<&'a str> {
|
||||
self.human_account_name
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`UpstreamOAuthLinkRepository`] helps interacting with
|
||||
|
||||
@@ -4325,6 +4325,16 @@
|
||||
"type": "string"
|
||||
},
|
||||
"style": "form"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "filter[human-account-name]",
|
||||
"description": "Retrieve the items with a human account name matching the given\nsubstring (case-insensitive)",
|
||||
"schema": {
|
||||
"description": "Retrieve the items with a human account name matching the given\nsubstring (case-insensitive)",
|
||||
"type": "string"
|
||||
},
|
||||
"style": "form"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -7452,6 +7462,13 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"filter[human-account-name]": {
|
||||
"description": "Retrieve the items with a human account name matching the given\nsubstring (case-insensitive)",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user