Skip to content

Commit ae11127

Browse files
committed
Add post method handler into backend
1 parent 2b1f62f commit ae11127

4 files changed

Lines changed: 286 additions & 43 deletions

File tree

backend/http/handler.go

Lines changed: 154 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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
5549
var noCacheHeaders = map[string]string{
5650
"Expires": time.Unix(0, 0).Format(time.RFC1123),
@@ -105,20 +99,23 @@ func NewHandler(
10599
func (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

534668
func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) (kubebindv1alpha2.ExportedSchemas, error) {

sdk/apis/kubebind/v1alpha2/bindingresponse_types.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type BindableResourcesResponse struct {
8585
Resources []BindableResource `json:"resources"`
8686
}
8787

88+
// BindableResource describes a resource that the user can select to bind to.
8889
type BindableResource struct {
8990
// name is the name of the resource.
9091
//
@@ -128,9 +129,30 @@ type BindableResource struct {
128129
Resource string `json:"resource"`
129130

130131
// sessionID is a session ID that the consumer must pass back to the service provider
131-
// during the binding step.
132+
// during the binding step. If multiple backends are aggregated, this can be used to
133+
// to authenticate the user to the correct backend.
132134
//
133135
// +required
134136
// +kubebuilder:validation:Required
135137
SessionID string `json:"sessionID"`
136138
}
139+
140+
// BindableResourcesRequest is sent by the consumer to the service provider
141+
// to indicate which resources the user wants to bind to. It is sent after
142+
// authentication and resource selection on the service provider website.
143+
type BindableResourcesRequest struct {
144+
metav1.TypeMeta `json:",inline"`
145+
146+
// resources is a list of resources that the user can select from.
147+
//
148+
// +required
149+
// +kubebuilder:validation:Required
150+
// +kubebuilder:validation:MinItems=1
151+
Resources []BindableResource `json:"resources"`
152+
153+
// PermissionClaims are additional permissions that the user wants to have.
154+
//
155+
// +optional
156+
// +kubebuilder:validation:Optional
157+
PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"`
158+
}

web/src/services/auth.ts

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ export interface SessionInfo {
66
isAuthenticated: boolean
77
}
88

9+
export interface BindableResource {
10+
name: string
11+
kind: string
12+
scope: string
13+
apiVersion: string
14+
group: string
15+
resource: string
16+
sessionID: string
17+
}
18+
19+
export interface PermissionClaim {
20+
// Add permission claim properties as needed
21+
[key: string]: any
22+
}
23+
24+
export interface BindableResourcesRequest {
25+
apiVersion: string
26+
kind: string
27+
resources: BindableResource[]
28+
permissionClaims?: PermissionClaim[]
29+
}
30+
931
class AuthService {
1032
private sessionInfo: SessionInfo | null = null
1133

@@ -95,17 +117,14 @@ class AuthService {
95117
}
96118
}
97119

98-
async bindResource(group: string, resource: string, version: string, clusterId: string = ''): Promise<void> {
120+
async bindResource(group: string, resource: string, version: string, clusterId: string = ''): Promise<any> {
99121
try {
100122
const sessionCookie = this.getSessionCookie()
101123
if (!sessionCookie) {
102124
throw new Error('No session found')
103125
}
104126

105-
// Use cluster-aware endpoint if clusterId is provided
106-
const bindPath = clusterId ? `/api/clusters/${clusterId}/bind` : '/api/bind'
107-
const bindUrl = `${bindPath}?group=${group}&resource=${resource}&version=${version}&s=${sessionCookie}`
108-
window.location.href = bindUrl
127+
return this.bindResourceWithSession(group, resource, version, clusterId, sessionCookie)
109128
} catch (error) {
110129
console.error('Failed to bind resource:', error)
111130
throw error
@@ -140,14 +159,51 @@ class AuthService {
140159
}
141160
}
142161

143-
async bindResourceWithSession(group: string, resource: string, version: string, clusterId: string = '', sessionId: string): Promise<void> {
162+
async bindResourceWithSession(group: string, resource: string, version: string, clusterId: string = '', sessionId: string, scope: string = 'Namespaced', kind: string = '', name: string = ''): Promise<any> {
144163
try {
164+
console.log('🔗 Binding resource with POST request')
165+
console.log('📋 Resource details:', { group, resource, version, clusterId, sessionId })
166+
145167
// Use cluster-aware endpoint if clusterId is provided
146168
const bindPath = clusterId ? `/api/clusters/${clusterId}/bind` : '/api/bind'
147-
const bindUrl = `${bindPath}?group=${group}&resource=${resource}&version=${version}&s=${sessionId}`
148-
window.location.href = bindUrl
149-
} catch (error) {
150-
console.error('Failed to bind resource with session:', error)
169+
const bindUrl = `${bindPath}?s=${sessionId}`
170+
171+
console.log('🌐 POST request to:', bindUrl)
172+
173+
// Create the BindableResourcesRequest payload
174+
const requestPayload: BindableResourcesRequest = {
175+
apiVersion: 'kubebind.io/v1alpha2',
176+
kind: 'BindableResourcesRequest',
177+
resources: [{
178+
name: name || `${resource}.${group || 'core'}`,
179+
kind: kind || resource,
180+
scope: scope,
181+
apiVersion: version,
182+
group: group || '',
183+
resource: resource,
184+
sessionID: sessionId
185+
}],
186+
permissionClaims: []
187+
}
188+
189+
console.log('📦 Request payload:', requestPayload)
190+
191+
const response = await axios.post(bindUrl, requestPayload, {
192+
headers: {
193+
'Content-Type': 'application/json'
194+
}
195+
})
196+
197+
console.log('✅ Bind response status:', response.status)
198+
console.log('📦 Bind response data:', response.data)
199+
200+
return response.data
201+
} catch (error: any) {
202+
console.error('❌ Failed to bind resource with session:', error)
203+
if (error.response) {
204+
console.error('📄 Error Response Status:', error.response.status)
205+
console.error('📄 Error Response Data:', error.response.data)
206+
}
151207
throw error
152208
}
153209
}

0 commit comments

Comments
 (0)