66use aide:: { NoApi , OperationIo , transform:: TransformOperation } ;
77use axum:: { Json , response:: IntoResponse } ;
88use hyper:: StatusCode ;
9- use mas_storage:: { BoxRng , upstream_oauth2 :: UpstreamOAuthLinkFilter } ;
9+ use mas_storage:: BoxRng ;
1010use schemars:: JsonSchema ;
1111use serde:: Deserialize ;
1212use ulid:: Ulid ;
1313
1414use 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 }
0 commit comments