Skip to content

Commit 423681d

Browse files
authored
Admin API to list and get upstream OAuth links (#4012)
This adds an admin API to list and get upstream OAuth links, similar to the 'external IDs' in Synapse.
2 parents 88e0f76 + a7ae36e commit 423681d

File tree

10 files changed

+1274
-59
lines changed

10 files changed

+1274
-59
lines changed

crates/handlers/src/admin/mod.rs

Lines changed: 68 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use aide::{
88
axum::ApiRouter,
99
openapi::{OAuth2Flow, OAuth2Flows, OpenApi, SecurityScheme, Server, Tag},
10+
transform::TransformOpenApi,
1011
};
1112
use axum::{
1213
extract::{FromRef, FromRequestParts, State},
@@ -37,6 +38,72 @@ mod v1;
3738
use self::call_context::CallContext;
3839
use crate::passwords::PasswordManager;
3940

41+
fn finish(t: TransformOpenApi) -> TransformOpenApi {
42+
t.title("Matrix Authentication Service admin API")
43+
.tag(Tag {
44+
name: "compat-session".to_owned(),
45+
description: Some("Manage compatibility sessions from legacy clients".to_owned()),
46+
..Tag::default()
47+
})
48+
.tag(Tag {
49+
name: "oauth2-session".to_owned(),
50+
description: Some("Manage OAuth2 sessions".to_owned()),
51+
..Tag::default()
52+
})
53+
.tag(Tag {
54+
name: "user".to_owned(),
55+
description: Some("Manage users".to_owned()),
56+
..Tag::default()
57+
})
58+
.tag(Tag {
59+
name: "user-email".to_owned(),
60+
description: Some("Manage emails associated with users".to_owned()),
61+
..Tag::default()
62+
})
63+
.tag(Tag {
64+
name: "user-session".to_owned(),
65+
description: Some("Manage browser sessions of users".to_owned()),
66+
..Tag::default()
67+
})
68+
.tag(Tag {
69+
name: "upstream-oauth-link".to_owned(),
70+
description: Some(
71+
"Manage links between local users and identities from upstream OAuth 2.0 providers"
72+
.to_owned(),
73+
),
74+
..Default::default()
75+
})
76+
.security_scheme(
77+
"oauth2",
78+
SecurityScheme::OAuth2 {
79+
flows: OAuth2Flows {
80+
client_credentials: Some(OAuth2Flow::ClientCredentials {
81+
refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()),
82+
token_url: OAuth2TokenEndpoint::PATH.to_owned(),
83+
scopes: IndexMap::from([(
84+
"urn:mas:admin".to_owned(),
85+
"Grant access to the admin API".to_owned(),
86+
)]),
87+
}),
88+
authorization_code: Some(OAuth2Flow::AuthorizationCode {
89+
authorization_url: OAuth2AuthorizationEndpoint::PATH.to_owned(),
90+
refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()),
91+
token_url: OAuth2TokenEndpoint::PATH.to_owned(),
92+
scopes: IndexMap::from([(
93+
"urn:mas:admin".to_owned(),
94+
"Grant access to the admin API".to_owned(),
95+
)]),
96+
}),
97+
implicit: None,
98+
password: None,
99+
},
100+
description: None,
101+
extensions: IndexMap::default(),
102+
},
103+
)
104+
.security_requirement_scopes("oauth2", ["urn:mas:admin"])
105+
}
106+
40107
pub fn router<S>() -> (OpenApi, Router<S>)
41108
where
42109
S: Clone + Send + Sync + 'static,
@@ -58,65 +125,7 @@ where
58125
let mut api = OpenApi::default();
59126
let router = ApiRouter::<S>::new()
60127
.nest("/api/admin/v1", self::v1::router())
61-
.finish_api_with(&mut api, |t| {
62-
t.title("Matrix Authentication Service admin API")
63-
.tag(Tag {
64-
name: "compat-session".to_owned(),
65-
description: Some(
66-
"Manage compatibility sessions from legacy clients".to_owned(),
67-
),
68-
..Tag::default()
69-
})
70-
.tag(Tag {
71-
name: "oauth2-session".to_owned(),
72-
description: Some("Manage OAuth2 sessions".to_owned()),
73-
..Tag::default()
74-
})
75-
.tag(Tag {
76-
name: "user".to_owned(),
77-
description: Some("Manage users".to_owned()),
78-
..Tag::default()
79-
})
80-
.tag(Tag {
81-
name: "user-email".to_owned(),
82-
description: Some("Manage emails associated with users".to_owned()),
83-
..Tag::default()
84-
})
85-
.tag(Tag {
86-
name: "user-session".to_owned(),
87-
description: Some("Manage browser sessions of users".to_owned()),
88-
..Tag::default()
89-
})
90-
.security_scheme(
91-
"oauth2",
92-
SecurityScheme::OAuth2 {
93-
flows: OAuth2Flows {
94-
client_credentials: Some(OAuth2Flow::ClientCredentials {
95-
refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()),
96-
token_url: OAuth2TokenEndpoint::PATH.to_owned(),
97-
scopes: IndexMap::from([(
98-
"urn:mas:admin".to_owned(),
99-
"Grant access to the admin API".to_owned(),
100-
)]),
101-
}),
102-
authorization_code: Some(OAuth2Flow::AuthorizationCode {
103-
authorization_url: OAuth2AuthorizationEndpoint::PATH.to_owned(),
104-
refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()),
105-
token_url: OAuth2TokenEndpoint::PATH.to_owned(),
106-
scopes: IndexMap::from([(
107-
"urn:mas:admin".to_owned(),
108-
"Grant access to the admin API".to_owned(),
109-
)]),
110-
}),
111-
implicit: None,
112-
password: None,
113-
},
114-
description: None,
115-
extensions: IndexMap::default(),
116-
},
117-
)
118-
.security_requirement_scopes("oauth2", ["urn:mas:admin"])
119-
});
128+
.finish_api_with(&mut api, finish);
120129

121130
let router = router
122131
// Serve the OpenAPI spec as JSON

crates/handlers/src/admin/model.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,81 @@ impl Resource for UserSession {
456456
self.id
457457
}
458458
}
459+
460+
/// An upstream OAuth 2.0 link
461+
#[derive(Serialize, JsonSchema)]
462+
pub struct UpstreamOAuthLink {
463+
#[serde(skip)]
464+
id: Ulid,
465+
466+
/// When the object was created
467+
created_at: DateTime<Utc>,
468+
469+
/// The ID of the provider
470+
#[schemars(with = "super::schema::Ulid")]
471+
provider_id: Ulid,
472+
473+
/// The subject of the upstream account, unique per provider
474+
subject: String,
475+
476+
/// The ID of the user who owns this link, if any
477+
#[schemars(with = "Option<super::schema::Ulid>")]
478+
user_id: Option<Ulid>,
479+
480+
/// A human-readable name of the upstream account
481+
human_account_name: Option<String>,
482+
}
483+
484+
impl Resource for UpstreamOAuthLink {
485+
const KIND: &'static str = "upstream-oauth-link";
486+
const PATH: &'static str = "/api/admin/v1/upstream-oauth-links";
487+
488+
fn id(&self) -> Ulid {
489+
self.id
490+
}
491+
}
492+
493+
impl From<mas_data_model::UpstreamOAuthLink> for UpstreamOAuthLink {
494+
fn from(value: mas_data_model::UpstreamOAuthLink) -> Self {
495+
Self {
496+
id: value.id,
497+
created_at: value.created_at,
498+
provider_id: value.provider_id,
499+
subject: value.subject,
500+
user_id: value.user_id,
501+
human_account_name: value.human_account_name,
502+
}
503+
}
504+
}
505+
506+
impl UpstreamOAuthLink {
507+
/// Samples of upstream OAuth 2.0 links
508+
pub fn samples() -> [Self; 3] {
509+
[
510+
Self {
511+
id: Ulid::from_bytes([0x01; 16]),
512+
created_at: DateTime::default(),
513+
provider_id: Ulid::from_bytes([0x02; 16]),
514+
subject: "john-42".to_owned(),
515+
user_id: Some(Ulid::from_bytes([0x03; 16])),
516+
human_account_name: Some("[email protected]".to_owned()),
517+
},
518+
Self {
519+
id: Ulid::from_bytes([0x02; 16]),
520+
created_at: DateTime::default(),
521+
provider_id: Ulid::from_bytes([0x03; 16]),
522+
subject: "jane-123".to_owned(),
523+
user_id: None,
524+
human_account_name: None,
525+
},
526+
Self {
527+
id: Ulid::from_bytes([0x03; 16]),
528+
created_at: DateTime::default(),
529+
provider_id: Ulid::from_bytes([0x04; 16]),
530+
subject: "[email protected]".to_owned(),
531+
user_id: Some(Ulid::from_bytes([0x05; 16])),
532+
human_account_name: Some("bob".to_owned()),
533+
},
534+
]
535+
}
536+
}

crates/handlers/src/admin/v1/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::passwords::PasswordManager;
1717

1818
mod compat_sessions;
1919
mod oauth2_sessions;
20+
mod upstream_oauth_links;
2021
mod user_emails;
2122
mod user_sessions;
2223
mod users;
@@ -95,4 +96,18 @@ where
9596
"/user-sessions/{id}",
9697
get_with(self::user_sessions::get, self::user_sessions::get_doc),
9798
)
99+
.api_route(
100+
"/upstream-oauth-links",
101+
get_with(
102+
self::upstream_oauth_links::list,
103+
self::upstream_oauth_links::list_doc,
104+
),
105+
)
106+
.api_route(
107+
"/upstream-oauth-links/{id}",
108+
get_with(
109+
self::upstream_oauth_links::get,
110+
self::upstream_oauth_links::get_doc,
111+
),
112+
)
98113
}

0 commit comments

Comments
 (0)