@@ -22,7 +22,6 @@ import (
2222 "encoding/json"
2323 "errors"
2424 "fmt"
25- htmltemplate "html/template"
2625 "net/http"
2726 "net/url"
2827 "strings"
@@ -42,15 +41,10 @@ import (
4241 "github.com/kube-bind/kube-bind/backend/kubernetes/resources"
4342 "github.com/kube-bind/kube-bind/backend/session"
4443 "github.com/kube-bind/kube-bind/backend/spaserver"
45- "github.com/kube-bind/kube-bind/backend/template"
4644 bindversion "github.com/kube-bind/kube-bind/pkg/version"
4745 kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
4846)
4947
50- var (
51- resourcesTemplate = htmltemplate .Must (htmltemplate .New ("resource" ).Parse (mustRead (template .Files .ReadFile , "resources.gohtml" )))
52- )
53-
5448// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
5549var noCacheHeaders = map [string ]string {
5650 "Expires" : time .Unix (0 , 0 ).Format (time .RFC1123 ),
@@ -105,20 +99,23 @@ func NewHandler(
10599func (h * handler ) AddRoutes (mux * mux.Router ) {
106100 // API routes - Server contains double routes for when backend is multi-cluster aware or single cluster.
107101 // When called multi-cluster aware route in single cluster mode, it will ignore cluster parameter.
108- mux .HandleFunc ("/api/clusters/{cluster}/exports" , h .handleServiceExport ).Methods ("GET" )
109- mux .HandleFunc ("/api/exports" , h .handleServiceExport ).Methods ("GET" )
102+ mux .HandleFunc ("/api/clusters/{cluster}/exports" , h .handleServiceExport ).Methods (http .MethodGet )
103+ mux .HandleFunc ("/api/exports" , h .handleServiceExport ).Methods (http .MethodGet )
104+
105+ mux .HandleFunc ("/api/clusters/{cluster}/resources" , h .handleResources ).Methods (http .MethodGet )
106+ mux .HandleFunc ("/api/resources" , h .handleResources ).Methods (http .MethodGet )
110107
111- mux .HandleFunc ("/api/clusters/{cluster}/resources " , h .handleResources ).Methods ("GET" )
112- mux .HandleFunc ("/api/resources " , h .handleResources ).Methods ("GET" )
108+ mux .HandleFunc ("/api/clusters/{cluster}/bind " , h .handleBind ).Methods (http . MethodGet )
109+ mux .HandleFunc ("/api/bind " , h .handleBind ).Methods (http . MethodGet )
113110
114- mux .HandleFunc ("/api/clusters/{cluster}/bind" , h .handleBind ).Methods ("GET" )
115- mux .HandleFunc ("/api/bind" , h .handleBind ).Methods ("GET" )
111+ mux .HandleFunc ("/api/clusters/{cluster}/bind" , h .handleBindPost ).Methods (http . MethodPost )
112+ mux .HandleFunc ("/api/bind" , h .handleBindPost ).Methods (http . MethodPost )
116113
117- mux .HandleFunc ("/api/clusters/{cluster}/authorize" , h .handleAuthorize ).Methods ("GET" )
118- mux .HandleFunc ("/api/authorize" , h .handleAuthorize ).Methods ("GET" )
114+ mux .HandleFunc ("/api/clusters/{cluster}/authorize" , h .handleAuthorize ).Methods (http . MethodGet )
115+ mux .HandleFunc ("/api/authorize" , h .handleAuthorize ).Methods (http . MethodGet )
119116
120- mux .HandleFunc ("/api/callback" , h .handleCallback ).Methods ("GET" )
121- mux .HandleFunc ("/api/healthz" , h .handleHealthz ).Methods ("GET" )
117+ mux .HandleFunc ("/api/callback" , h .handleCallback ).Methods (http . MethodGet )
118+ mux .HandleFunc ("/api/healthz" , h .handleHealthz ).Methods (http . MethodGet )
122119
123120 if strings .HasPrefix (h .frontend , "http://" ) {
124121 spaserver , err := spaserver .NewSPAReverseProxyServer (h .frontend )
@@ -523,12 +520,149 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
523520 http .Redirect (w , r , parsedAuthURL .String (), http .StatusFound )
524521}
525522
526- func mustRead (f func (name string ) ([]byte , error ), name string ) string {
527- bs , err := f (name )
523+ func (h * handler ) handleBindPost (w http.ResponseWriter , r * http.Request ) {
524+ logger := getLogger (r )
525+ providerCluster := mux .Vars (r )["cluster" ]
526+
527+ prepareNoCache (w )
528+
529+ // Get session ID from query parameter
530+ sessionID := r .URL .Query ().Get ("s" )
531+ if sessionID == "" {
532+ logger .Error (errors .New ("missing session parameter" ), "failed to get session from query" )
533+ http .Error (w , "missing session parameter 's'" , http .StatusBadRequest )
534+ return
535+ }
536+
537+ // Get session cookie
538+ ck , err := r .Cookie (sessionID )
539+ if err != nil {
540+ logger .Error (err , "failed to get session cookie" )
541+ http .Error (w , "no session cookie found" , http .StatusBadRequest )
542+ return
543+ }
544+
545+ // Decode session state
546+ state := session.State {}
547+ s := securecookie .New (h .cookieSigningKey , h .cookieEncryptionKey )
548+ if err := s .Decode (sessionID , ck .Value , & state ); err != nil {
549+ logger .Error (err , "failed to decode session cookie" )
550+ http .Error (w , "internal error" , http .StatusInternalServerError )
551+ return
552+ }
553+
554+ // Parse JSON request body
555+ var bindRequest kubebindv1alpha2.BindableResourcesRequest
556+ if err := json .NewDecoder (r .Body ).Decode (& bindRequest ); err != nil {
557+ logger .Error (err , "failed to parse JSON request body" )
558+ http .Error (w , "invalid JSON request body" , http .StatusBadRequest )
559+ return
560+ }
561+
562+ logger .V (1 ).Info ("received bind request" , "resources" , len (bindRequest .Resources ), "permissionClaims" , len (bindRequest .PermissionClaims ))
563+
564+ // Validate request
565+ if len (bindRequest .Resources ) == 0 {
566+ logger .Error (errors .New ("no resources specified" ), "validation failed" )
567+ http .Error (w , "at least one resource must be specified" , http .StatusBadRequest )
568+ return
569+ }
570+
571+ // Handle kubeconfig setup
572+ kfg , err := h .kubeManager .HandleResources (r .Context (), state .Token .Subject + "#" + state .ClusterID , providerCluster )
573+ if err != nil {
574+ logger .Error (err , "failed to handle resources" )
575+ http .Error (w , "internal error" , http .StatusInternalServerError )
576+ return
577+ }
578+
579+ // Create API service export requests for each resource
580+ var requests []runtime.RawExtension
581+ for _ , bindableResource := range bindRequest .Resources {
582+ exportRequest := kubebindv1alpha2.APIServiceExportRequestResponse {
583+ TypeMeta : metav1.TypeMeta {
584+ APIVersion : kubebindv1alpha2 .SchemeGroupVersion .String (),
585+ Kind : "APIServiceExportRequest" ,
586+ },
587+ ObjectMeta : kubebindv1alpha2.NameObjectMeta {
588+ Name : bindableResource .Resource + "." + bindableResource .Group ,
589+ },
590+ Spec : kubebindv1alpha2.APIServiceExportRequestSpec {
591+ Resources : []kubebindv1alpha2.APIServiceExportRequestResource {
592+ {
593+ GroupResource : kubebindv1alpha2.GroupResource {
594+ Group : bindableResource .Group ,
595+ Resource : bindableResource .Resource ,
596+ },
597+ Versions : []string {bindableResource .APIVersion },
598+ },
599+ },
600+ },
601+ }
602+
603+ requestBytes , err := json .Marshal (& exportRequest )
604+ if err != nil {
605+ logger .Error (err , "failed to marshal export request" , "resource" , bindableResource .Resource )
606+ http .Error (w , "internal error" , http .StatusInternalServerError )
607+ return
608+ }
609+
610+ requests = append (requests , runtime.RawExtension {Raw : requestBytes })
611+ }
612+
613+ // Create binding response
614+ response := kubebindv1alpha2.BindingResponse {
615+ TypeMeta : metav1.TypeMeta {
616+ APIVersion : kubebindv1alpha2 .SchemeGroupVersion .String (),
617+ Kind : "BindingResponse" ,
618+ },
619+ Authentication : kubebindv1alpha2.BindingResponseAuthentication {
620+ OAuth2CodeGrant : & kubebindv1alpha2.BindingResponseAuthenticationOAuth2CodeGrant {
621+ SessionID : state .SessionID ,
622+ ID : state .Token .Issuer + "/" + state .Token .Subject ,
623+ },
624+ },
625+ Kubeconfig : kfg ,
626+ Requests : requests ,
627+ }
628+
629+ payload , err := json .Marshal (& response )
528630 if err != nil {
529- panic (err )
631+ logger .Error (err , "failed to marshal binding response" )
632+ http .Error (w , "internal error" , http .StatusInternalServerError )
633+ return
634+ }
635+
636+ encoded := base64 .URLEncoding .EncodeToString (payload )
637+
638+ parsedAuthURL , err := url .Parse (state .RedirectURL )
639+ if err != nil {
640+ logger .Error (err , "failed to parse redirect URL" )
641+ http .Error (w , "internal error" , http .StatusInternalServerError )
642+ return
643+ }
644+
645+ values := parsedAuthURL .Query ()
646+ values .Add ("response" , encoded )
647+
648+ parsedAuthURL .RawQuery = values .Encode ()
649+
650+ logger .V (1 ).Info ("redirecting to auth callback after POST bind" ,
651+ "url" , state .RedirectURL + "?response=<redacted>" ,
652+ "resourceCount" , len (bindRequest .Resources ))
653+
654+ // For POST requests, we can either redirect or return JSON response
655+ // Since this is called from frontend JavaScript, return the redirect URL as JSON
656+ w .Header ().Set ("Content-Type" , "application/json" )
657+ redirectResponse := map [string ]string {
658+ "redirectURL" : parsedAuthURL .String (),
659+ }
660+
661+ if err := json .NewEncoder (w ).Encode (redirectResponse ); err != nil {
662+ logger .Error (err , "failed to encode redirect response" )
663+ http .Error (w , "internal error" , http .StatusInternalServerError )
664+ return
530665 }
531- return string (bs )
532666}
533667
534668func (h * handler ) getBackendDynamicResource (ctx context.Context , cluster string ) (kubebindv1alpha2.ExportedSchemas , error ) {
0 commit comments