Skip to content

Commit 38278fa

Browse files
committed
Admin API: add endpoint to get an Upstream OAuth Provider by ID
1 parent 3b9fa84 commit 38278fa

File tree

4 files changed

+286
-1
lines changed

4 files changed

+286
-1
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,11 @@ where
195195
self::upstream_oauth_providers::list_doc,
196196
),
197197
)
198+
.api_route(
199+
"/upstream-oauth-providers/{id}",
200+
get_with(
201+
self::upstream_oauth_providers::get,
202+
self::upstream_oauth_providers::get_doc,
203+
),
204+
)
198205
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright 2025 New Vector Ltd.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
4+
// Please see LICENSE files in the repository root for full details.
5+
6+
use aide::{OperationIo, transform::TransformOperation};
7+
use axum::{Json, response::IntoResponse};
8+
use hyper::StatusCode;
9+
use mas_axum_utils::record_error;
10+
use mas_storage::{RepositoryAccess, upstream_oauth2::UpstreamOAuthProviderRepository};
11+
12+
use crate::{
13+
admin::{
14+
call_context::CallContext,
15+
model::UpstreamOAuthProvider,
16+
params::UlidPathParam,
17+
response::{ErrorResponse, SingleResponse},
18+
},
19+
impl_from_error_for_route,
20+
};
21+
22+
#[derive(Debug, thiserror::Error, OperationIo)]
23+
#[aide(output_with = "Json<ErrorResponse>")]
24+
pub enum RouteError {
25+
#[error(transparent)]
26+
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
27+
28+
#[error("Provider not found")]
29+
NotFound,
30+
}
31+
32+
impl_from_error_for_route!(mas_storage::RepositoryError);
33+
34+
impl IntoResponse for RouteError {
35+
fn into_response(self) -> axum::response::Response {
36+
let error = ErrorResponse::from_error(&self);
37+
let sentry_event_id = record_error!(self, Self::Internal(_));
38+
let status = match self {
39+
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
40+
Self::NotFound => StatusCode::NOT_FOUND,
41+
};
42+
43+
(status, sentry_event_id, Json(error)).into_response()
44+
}
45+
}
46+
47+
pub fn doc(operation: TransformOperation) -> TransformOperation {
48+
operation
49+
.id("getUpstreamOAuthProvider")
50+
.summary("Get upstream OAuth provider")
51+
.tag("upstream-oauth-provider")
52+
.response_with::<200, Json<SingleResponse<UpstreamOAuthProvider>>, _>(|t| {
53+
let [sample, ..] = UpstreamOAuthProvider::samples();
54+
t.description("The upstream OAuth provider")
55+
.example(SingleResponse::new_canonical(sample))
56+
})
57+
.response_with::<404, Json<ErrorResponse>, _>(|t| t.description("Provider not found"))
58+
}
59+
60+
#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_providers.get", skip_all)]
61+
pub async fn handler(
62+
CallContext { mut repo, .. }: CallContext,
63+
id: UlidPathParam,
64+
) -> Result<Json<SingleResponse<UpstreamOAuthProvider>>, RouteError> {
65+
let provider = repo
66+
.upstream_oauth_provider()
67+
.lookup(*id)
68+
.await?
69+
.ok_or(RouteError::NotFound)?;
70+
71+
Ok(Json(SingleResponse::new_canonical(
72+
UpstreamOAuthProvider::from(provider),
73+
)))
74+
}
75+
76+
#[cfg(test)]
77+
mod tests {
78+
use hyper::{Request, StatusCode};
79+
use mas_data_model::{
80+
UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
81+
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderOnBackchannelLogout,
82+
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod,
83+
};
84+
use mas_iana::jose::JsonWebSignatureAlg;
85+
use mas_storage::{
86+
RepositoryAccess,
87+
upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository},
88+
};
89+
use oauth2_types::scope::{OPENID, Scope};
90+
use sqlx::PgPool;
91+
use ulid::Ulid;
92+
93+
use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
94+
95+
async fn create_test_provider(state: &mut TestState) -> UpstreamOAuthProvider {
96+
let mut repo = state.repository().await.unwrap();
97+
98+
let params = UpstreamOAuthProviderParams {
99+
issuer: Some("https://accounts.google.com".to_owned()),
100+
human_name: Some("Google".to_owned()),
101+
brand_name: Some("google".to_owned()),
102+
discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
103+
pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
104+
jwks_uri_override: None,
105+
authorization_endpoint_override: None,
106+
token_endpoint_override: None,
107+
userinfo_endpoint_override: None,
108+
fetch_userinfo: true,
109+
userinfo_signed_response_alg: None,
110+
client_id: "google-client-id".to_owned(),
111+
encrypted_client_secret: Some("encrypted-secret".to_owned()),
112+
token_endpoint_signing_alg: None,
113+
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost,
114+
id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
115+
response_mode: None,
116+
scope: Scope::from_iter([OPENID]),
117+
claims_imports: UpstreamOAuthProviderClaimsImports::default(),
118+
additional_authorization_parameters: vec![],
119+
forward_login_hint: false,
120+
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
121+
ui_order: 0,
122+
};
123+
124+
let provider = repo
125+
.upstream_oauth_provider()
126+
.add(&mut state.rng(), &state.clock, params)
127+
.await
128+
.unwrap();
129+
130+
Box::new(repo).save().await.unwrap();
131+
132+
provider
133+
}
134+
135+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
136+
async fn test_get_provider(pool: PgPool) {
137+
setup();
138+
let mut state = TestState::from_pool(pool).await.unwrap();
139+
let admin_token = state.token_with_scope("urn:mas:admin").await;
140+
let provider = create_test_provider(&mut state).await;
141+
142+
let request = Request::get(format!(
143+
"/api/admin/v1/upstream-oauth-providers/{}",
144+
provider.id
145+
))
146+
.bearer(&admin_token)
147+
.empty();
148+
149+
let response = state.request(request).await;
150+
response.assert_status(StatusCode::OK);
151+
let body: serde_json::Value = response.json::<serde_json::Value>();
152+
153+
assert_eq!(body["data"]["type"], "upstream-oauth-provider");
154+
assert_eq!(body["data"]["id"], provider.id.to_string());
155+
assert_eq!(body["data"]["attributes"]["human_name"], "Google");
156+
157+
insta::assert_json_snapshot!(body, @r###"
158+
{
159+
"data": {
160+
"type": "upstream-oauth-provider",
161+
"id": "01FSHN9AG0MZAA6S4AF7CTV32E",
162+
"attributes": {
163+
"issuer": "https://accounts.google.com",
164+
"human_name": "Google",
165+
"brand_name": "google",
166+
"created_at": "2022-01-16T14:40:00Z",
167+
"disabled_at": null
168+
},
169+
"links": {
170+
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
171+
}
172+
},
173+
"links": {
174+
"self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E"
175+
}
176+
}
177+
"###);
178+
}
179+
180+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
181+
async fn test_not_found(pool: PgPool) {
182+
setup();
183+
let mut state = TestState::from_pool(pool).await.unwrap();
184+
let admin_token = state.token_with_scope("urn:mas:admin").await;
185+
186+
let provider_id = Ulid::nil();
187+
let request = Request::get(format!(
188+
"/api/admin/v1/upstream-oauth-providers/{provider_id}"
189+
))
190+
.bearer(&admin_token)
191+
.empty();
192+
193+
let response = state.request(request).await;
194+
response.assert_status(StatusCode::NOT_FOUND);
195+
}
196+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
44
// Please see LICENSE files in the repository root for full details.
55

6+
mod get;
67
mod list;
78

8-
pub use self::list::{doc as list_doc, handler as list};
9+
pub use self::{
10+
get::{doc as get_doc, handler as get},
11+
list::{doc as list_doc, handler as list},
12+
};

docs/api/spec.json

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3552,6 +3552,68 @@
35523552
}
35533553
}
35543554
}
3555+
},
3556+
"/api/admin/v1/upstream-oauth-providers/{id}": {
3557+
"get": {
3558+
"tags": [
3559+
"upstream-oauth-provider"
3560+
],
3561+
"summary": "Get upstream OAuth provider",
3562+
"operationId": "getUpstreamOAuthProvider",
3563+
"parameters": [
3564+
{
3565+
"in": "path",
3566+
"name": "id",
3567+
"required": true,
3568+
"schema": {
3569+
"title": "The ID of the resource",
3570+
"$ref": "#/components/schemas/ULID"
3571+
},
3572+
"style": "simple"
3573+
}
3574+
],
3575+
"responses": {
3576+
"200": {
3577+
"description": "The upstream OAuth provider",
3578+
"content": {
3579+
"application/json": {
3580+
"schema": {
3581+
"$ref": "#/components/schemas/SingleResponse_for_UpstreamOAuthProvider"
3582+
},
3583+
"example": {
3584+
"data": {
3585+
"type": "upstream-oauth-provider",
3586+
"id": "01040G2081040G2081040G2081",
3587+
"attributes": {
3588+
"issuer": "https://accounts.google.com",
3589+
"human_name": "Google",
3590+
"brand_name": "google",
3591+
"created_at": "1970-01-01T00:00:00Z",
3592+
"disabled_at": null
3593+
},
3594+
"links": {
3595+
"self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081"
3596+
}
3597+
},
3598+
"links": {
3599+
"self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081"
3600+
}
3601+
}
3602+
}
3603+
}
3604+
},
3605+
"404": {
3606+
"description": "Provider not found",
3607+
"content": {
3608+
"application/json": {
3609+
"schema": {
3610+
"$ref": "#/components/schemas/ErrorResponse"
3611+
}
3612+
}
3613+
}
3614+
}
3615+
}
3616+
}
35553617
}
35563618
},
35573619
"components": {
@@ -5243,6 +5305,22 @@
52435305
"nullable": true
52445306
}
52455307
}
5308+
},
5309+
"SingleResponse_for_UpstreamOAuthProvider": {
5310+
"description": "A top-level response with a single resource",
5311+
"type": "object",
5312+
"required": [
5313+
"data",
5314+
"links"
5315+
],
5316+
"properties": {
5317+
"data": {
5318+
"$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthProvider"
5319+
},
5320+
"links": {
5321+
"$ref": "#/components/schemas/SelfLinks"
5322+
}
5323+
}
52465324
}
52475325
}
52485326
},

0 commit comments

Comments
 (0)