Skip to content

Commit 7c4a9bf

Browse files
committed
Handle the correct conflict
1 parent ae2fe1c commit 7c4a9bf

File tree

2 files changed

+145
-21
lines changed

2 files changed

+145
-21
lines changed

crates/handlers/src/admin/v1/upstream_oauth_links/add.rs

Lines changed: 113 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
use aide::{NoApi, OperationIo, transform::TransformOperation};
77
use axum::{Json, response::IntoResponse};
88
use hyper::StatusCode;
9-
use mas_storage::{BoxRng, upstream_oauth2::UpstreamOAuthLinkFilter};
9+
use mas_storage::BoxRng;
1010
use schemars::JsonSchema;
1111
use serde::Deserialize;
1212
use ulid::Ulid;
1313

1414
use crate::{
1515
admin::{
1616
call_context::CallContext,
17-
model::{Resource, UpstreamOAuthLink, User},
17+
model::{Resource, UpstreamOAuthLink},
1818
response::{ErrorResponse, SingleResponse},
1919
},
2020
impl_from_error_for_route,
@@ -26,8 +26,8 @@ pub enum RouteError {
2626
#[error(transparent)]
2727
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
2828

29-
#[error("User ID {0} already has an upstream link for Upstream Oauth 2.0 Provider ID {1}")]
30-
LinkAlreadyExists(Ulid, Ulid),
29+
#[error("Upstream Oauth 2.0 Provider ID {0} with subject {1} is already linked to a user")]
30+
LinkAlreadyExists(Ulid, String),
3131

3232
#[error("User ID {0} not found")]
3333
UserNotFound(Ulid),
@@ -74,20 +74,25 @@ pub fn doc(operation: TransformOperation) -> TransformOperation {
7474
.id("addUpstreamOAuthLink")
7575
.summary("Add an upstream OAuth 2.0 link")
7676
.tag("upstream-oauth-link")
77+
.response_with::<200, Json<SingleResponse<UpstreamOAuthLink>>, _>(|t| {
78+
let [sample, ..] = UpstreamOAuthLink::samples();
79+
let response = SingleResponse::new_canonical(sample);
80+
t.description("An existing Upstream OAuth 2.0 link was associated to a user")
81+
.example(response)
82+
})
7783
.response_with::<201, Json<SingleResponse<UpstreamOAuthLink>>, _>(|t| {
7884
let [sample, ..] = UpstreamOAuthLink::samples();
7985
let response = SingleResponse::new_canonical(sample);
80-
t.description("Upstream OAuth 2.0 link was created")
86+
t.description("A new Upstream OAuth 2.0 link was created")
8187
.example(response)
8288
})
8389
.response_with::<409, RouteError, _>(|t| {
8490
let [provider_sample, ..] = UpstreamOAuthLink::samples();
85-
let [user_sample, ..] = User::samples();
8691
let response = ErrorResponse::from_error(&RouteError::LinkAlreadyExists(
87-
user_sample.id(),
8892
provider_sample.id(),
93+
String::from("subject1"),
8994
));
90-
t.description("User already has an upstream link for this provider")
95+
t.description("The subject from the provider is already linked to another user")
9196
.example(response)
9297
})
9398
.response_with::<404, RouteError, _>(|t| {
@@ -119,15 +124,28 @@ pub async fn handler(
119124
.await?
120125
.ok_or(RouteError::ProviderNotFound(params.provider_id))?;
121126

122-
let filter = UpstreamOAuthLinkFilter::new()
123-
.for_user(&user)
124-
.for_provider(&provider);
125-
let count = repo.upstream_oauth_link().count(filter).await?;
127+
let maybe_link = repo
128+
.upstream_oauth_link()
129+
.find_by_subject(&provider, &params.subject)
130+
.await?;
131+
if let Some(mut link) = maybe_link {
132+
if link.user_id.is_some() {
133+
return Err(RouteError::LinkAlreadyExists(
134+
link.provider_id,
135+
link.subject,
136+
));
137+
}
138+
139+
repo.upstream_oauth_link()
140+
.associate_to_user(&link, &user)
141+
.await?;
142+
link.user_id = Some(user.id);
126143

127-
if count > 0 {
128-
return Err(RouteError::LinkAlreadyExists(
129-
params.user_id,
130-
params.provider_id,
144+
repo.save().await?;
145+
146+
return Ok((
147+
StatusCode::OK,
148+
Json(SingleResponse::new_canonical(link.into())),
131149
));
132150
}
133151

@@ -224,6 +242,77 @@ mod tests {
224242
"###);
225243
}
226244

245+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
246+
async fn test_association(pool: PgPool) {
247+
setup();
248+
let mut state = TestState::from_pool(pool).await.unwrap();
249+
let token = state.token_with_scope("urn:mas:admin").await;
250+
let mut rng = state.rng();
251+
let mut repo = state.repository().await.unwrap();
252+
253+
let alice = repo
254+
.user()
255+
.add(&mut rng, &state.clock, "alice".to_owned())
256+
.await
257+
.unwrap();
258+
259+
let provider = repo
260+
.upstream_oauth_provider()
261+
.add(
262+
&mut rng,
263+
&state.clock,
264+
test_utils::oidc_provider_params("provider1"),
265+
)
266+
.await
267+
.unwrap();
268+
269+
// Existing unfinished link
270+
repo.upstream_oauth_link()
271+
.add(
272+
&mut rng,
273+
&state.clock,
274+
&provider,
275+
String::from("subject1"),
276+
None,
277+
)
278+
.await
279+
.unwrap();
280+
281+
repo.save().await.unwrap();
282+
283+
let request = Request::post("/api/admin/v1/upstream-oauth-links")
284+
.bearer(&token)
285+
.json(serde_json::json!({
286+
"user_id": alice.id,
287+
"provider_id": provider.id,
288+
"subject": "subject1"
289+
}));
290+
let response = state.request(request).await;
291+
response.assert_status(StatusCode::OK);
292+
let body: serde_json::Value = response.json();
293+
assert_json_snapshot!(body, @r###"
294+
{
295+
"data": {
296+
"type": "upstream-oauth-link",
297+
"id": "01FSHN9AG09NMZYX8MFYH578R9",
298+
"attributes": {
299+
"created_at": "2022-01-16T14:40:00Z",
300+
"provider_id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4",
301+
"subject": "subject1",
302+
"user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
303+
"human_account_name": null
304+
},
305+
"links": {
306+
"self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG09NMZYX8MFYH578R9"
307+
}
308+
},
309+
"links": {
310+
"self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG09NMZYX8MFYH578R9"
311+
}
312+
}
313+
"###);
314+
}
315+
227316
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
228317
async fn test_link_already_exists(pool: PgPool) {
229318
setup();
@@ -238,6 +327,12 @@ mod tests {
238327
.await
239328
.unwrap();
240329

330+
let bob = repo
331+
.user()
332+
.add(&mut rng, &state.clock, "bob".to_owned())
333+
.await
334+
.unwrap();
335+
241336
let provider = repo
242337
.upstream_oauth_provider()
243338
.add(
@@ -270,7 +365,7 @@ mod tests {
270365
let request = Request::post("/api/admin/v1/upstream-oauth-links")
271366
.bearer(&token)
272367
.json(serde_json::json!({
273-
"user_id": alice.id,
368+
"user_id": bob.id,
274369
"provider_id": provider.id,
275370
"subject": "subject1"
276371
}));
@@ -281,7 +376,7 @@ mod tests {
281376
{
282377
"errors": [
283378
{
284-
"title": "User ID 01FSHN9AG0MZAA6S4AF7CTV32E already has an upstream link for Upstream Oauth 2.0 Provider ID 01FSHN9AG0AJ6AC5HQ9X6H4RP4"
379+
"title": "Upstream Oauth 2.0 Provider ID 01FSHN9AG09NMZYX8MFYH578R9 with subject subject1 is already linked to a user"
285380
}
286381
]
287382
}

docs/api/spec.json

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2307,8 +2307,37 @@
23072307
"required": true
23082308
},
23092309
"responses": {
2310+
"200": {
2311+
"description": "An existing Upstream OAuth 2.0 link was associated to a user",
2312+
"content": {
2313+
"application/json": {
2314+
"schema": {
2315+
"$ref": "#/components/schemas/SingleResponse_for_UpstreamOAuthLink"
2316+
},
2317+
"example": {
2318+
"data": {
2319+
"type": "upstream-oauth-link",
2320+
"id": "01040G2081040G2081040G2081",
2321+
"attributes": {
2322+
"created_at": "1970-01-01T00:00:00Z",
2323+
"provider_id": "02081040G2081040G2081040G2",
2324+
"subject": "john-42",
2325+
"user_id": "030C1G60R30C1G60R30C1G60R3",
2326+
"human_account_name": "[email protected]"
2327+
},
2328+
"links": {
2329+
"self": "/api/admin/v1/upstream-oauth-links/01040G2081040G2081040G2081"
2330+
}
2331+
},
2332+
"links": {
2333+
"self": "/api/admin/v1/upstream-oauth-links/01040G2081040G2081040G2081"
2334+
}
2335+
}
2336+
}
2337+
}
2338+
},
23102339
"201": {
2311-
"description": "Upstream OAuth 2.0 link was created",
2340+
"description": "A new Upstream OAuth 2.0 link was created",
23122341
"content": {
23132342
"application/json": {
23142343
"schema": {
@@ -2337,7 +2366,7 @@
23372366
}
23382367
},
23392368
"409": {
2340-
"description": "User already has an upstream link for this provider",
2369+
"description": "The subject from the provider is already linked to another user",
23412370
"content": {
23422371
"application/json": {
23432372
"schema": {
@@ -2346,7 +2375,7 @@
23462375
"example": {
23472376
"errors": [
23482377
{
2349-
"title": "User ID 01040G2081040G2081040G2081 already has an upstream link for Upstream Oauth 2.0 Provider ID 01040G2081040G2081040G2081"
2378+
"title": "Upstream Oauth 2.0 Provider ID 01040G2081040G2081040G2081 with subject subject1 is already linked to a user"
23502379
}
23512380
]
23522381
}

0 commit comments

Comments
 (0)