@@ -496,27 +496,33 @@ pub struct LoginUrlQuery {
496496/// Generate URL to IdP login form. Optional `state` param is included in query
497497/// string if present, and will typically represent the URL to send the user
498498/// back to after successful login.
499- pub fn get_login_url ( state : Option < String > ) -> String {
500- // assume state is not URL encoded, so no risk of double encoding (dropshot
501- // decodes it on the way in)
499+ // TODO this does not know anything about IdPs, and it should. When the user is
500+ // logged out and hits an auth-gated route, if there are multiple IdPs and we
501+ // don't known which one they want to use, we need to send them to a page that
502+ // will allow them to choose among discoverable IdPs. However, there may be ways
503+ // to give ourselves a hint about which one they want, for example, by storing
504+ // that info in a browser cookie when they log in. When their session ends, we
505+ // will not be able to look at the dead session to find the silo or IdP (well,
506+ // maybe we can but we probably shouldn't) but we can look at the cookie and
507+ // default to sending them to the IdP indicated (though if they don't want that
508+ // one we need to make sure they can get to a different one). If there is no
509+ // cookie, we send them to the selector page. In any case, none of this is done
510+ // here yet. We go to /spoof_login no matter what.
511+ fn get_login_url ( state : Option < String > ) -> String {
512+ // assume state is not already URL encoded
502513 let query = match state {
503- Some ( state) if state. is_empty ( ) => None ,
504- Some ( state) => Some (
514+ Some ( state) if !state. is_empty ( ) => {
505515 serde_urlencoded:: to_string ( LoginUrlQuery { state : Some ( state) } )
506- // unwrap is safe because query.state was just deserialized out
507- // of a query param, so we know it's serializable
508- . unwrap ( ) ,
509- ) ,
510- None => None ,
516+ . ok ( ) // in the strange event it's not serializable, no query
517+ }
518+ _ => None ,
511519 } ;
512520 // Once we have IdP integration, this will be a URL for the IdP login page.
513521 // For now we point to our own placeholder login page.
514- let mut url = "/spoof_login" . to_string ( ) ;
515- if let Some ( query) = query {
516- url. push ( '?' ) ;
517- url. push_str ( query. as_str ( ) ) ;
522+ match query {
523+ Some ( query) => format ! ( "/spoof_login?{query}" ) ,
524+ None => "/spoof_login" . to_string ( ) ,
518525 }
519- url
520526}
521527
522528/// Redirect to IdP login URL
@@ -558,6 +564,29 @@ pub async fn session_me(
558564 apictx. external_latencies . instrument_dropshot_handler ( & rqctx, handler) . await
559565}
560566
567+ pub async fn console_index_or_login_redirect (
568+ rqctx : Arc < RequestContext < Arc < ServerContext > > > ,
569+ ) -> Result < Response < Body > , HttpError > {
570+ let opctx = OpContext :: for_external_api ( & rqctx) . await ;
571+
572+ // if authed, serve console index.html with JS bundle in script tag
573+ if let Ok ( opctx) = opctx {
574+ if opctx. authn . actor ( ) . is_some ( ) {
575+ return serve_console_index ( rqctx. context ( ) ) . await ;
576+ }
577+ }
578+
579+ // otherwise redirect to idp
580+
581+ // put the current URI in the query string to redirect back to after login
582+ let uri = rqctx. request . lock ( ) . await . uri ( ) . to_string ( ) ;
583+
584+ Ok ( Response :: builder ( )
585+ . status ( StatusCode :: FOUND )
586+ . header ( http:: header:: LOCATION , get_login_url ( Some ( uri) ) )
587+ . body ( "" . into ( ) ) ?)
588+ }
589+
561590// Dropshot does not have route match ranking and does not allow overlapping
562591// route definitions, so we cannot have a catchall `/*` route for console pages
563592// and then also define, e.g., `/api/blah/blah` and give the latter priority
@@ -575,28 +604,19 @@ pub async fn console_page(
575604 rqctx : Arc < RequestContext < Arc < ServerContext > > > ,
576605 _path_params : Path < RestPathParam > ,
577606) -> Result < Response < Body > , HttpError > {
578- let opctx = OpContext :: for_external_api ( & rqctx) . await ;
579-
580- // if authed, serve HTML page with bundle in script tag
581-
582- // HTML doesn't need to be static -- we'll probably find a reason to do some
583- // minimal templating, e.g., putting a CSRF token in the page
584-
585- // amusingly, at least to start out, I don't think we care about the path
586- // because the real routing is all client-side. we serve the same HTML
587- // regardless, the app starts on the client and renders the right page and
588- // makes the right API requests.
589- if let Ok ( opctx) = opctx {
590- if opctx. authn . actor ( ) . is_some ( ) {
591- return serve_console_index ( rqctx. context ( ) ) . await ;
592- }
593- }
607+ console_index_or_login_redirect ( rqctx) . await
608+ }
594609
595- // otherwise redirect to idp
596- Ok ( Response :: builder ( )
597- . status ( StatusCode :: FOUND )
598- . header ( http:: header:: LOCATION , get_login_url ( None ) )
599- . body ( "" . into ( ) ) ?)
610+ #[ endpoint {
611+ method = GET ,
612+ path = "/settings/{path:.*}" ,
613+ unpublished = true ,
614+ } ]
615+ pub async fn console_settings_page (
616+ rqctx : Arc < RequestContext < Arc < ServerContext > > > ,
617+ _path_params : Path < RestPathParam > ,
618+ ) -> Result < Response < Body > , HttpError > {
619+ console_index_or_login_redirect ( rqctx) . await
600620}
601621
602622/// Fetch a static asset from `<static_dir>/assets`. 404 on virtually all
0 commit comments