diff --git a/assets/credhub-service-broker/handlers.go b/assets/credhub-service-broker/handlers.go index 87f54c0bd..9dc86d864 100644 --- a/assets/credhub-service-broker/handlers.go +++ b/assets/credhub-service-broker/handlers.go @@ -9,7 +9,9 @@ import ( "time" "code.cloudfoundry.org/credhub-cli/credhub" + "code.cloudfoundry.org/credhub-cli/credhub/credentials" "code.cloudfoundry.org/credhub-cli/credhub/credentials/values" + "code.cloudfoundry.org/credhub-cli/credhub/permissions" "github.com/go-chi/chi/v5" uuid "github.com/satori/go.uuid" ) @@ -29,57 +31,17 @@ func init() { PLAN_UUID = uuid.NewV4().String() } -func catalogHandler(w http.ResponseWriter, r *http.Request) { - // Create a new catalog response - type Plans struct { - Name string `json:"name"` - ID string `json:"id"` - Description string `json:"description"` - } - type Services struct { - Name string `json:"name"` - ID string `json:"id"` - Description string `json:"description"` - Bindable bool `json:"bindable"` - Plans []Plans `json:"plans"` - } - catalog := struct { - Services []Services `json:"services"` - }{ - Services: []Services{ - { - Name: SERVICE_NAME, - ID: SERVICE_UUID, - Description: "credhub read service for tests", - Bindable: true, - Plans: []Plans{ - { - Name: "credhub-read-plan", - ID: PLAN_UUID, - Description: "credhub read plan for tests", - }, - }, - }, - }, - } - - // Marshal the catalog response to JSON - catalogJSON, err := json.Marshal(catalog) - if err != nil { - log.Println("Failed to marshal catalog response: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Write the catalog response to the response writer - w.WriteHeader(http.StatusOK) - w.Write(catalogJSON) //nolint:errcheck +type CredhubClient interface { + SetJSON(name string, value values.JSON, options ...credhub.SetOption) (credentials.JSON, error) + AddPermission(path string, actor string, ops []string) (*permissions.Permission, error) + Delete(name string) error } -func bindHandler(ch *credhub.CredHub, bindings map[string]string) http.HandlerFunc { +func bindHandler(ch CredhubClient, bindings map[string]string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Parse URL parameters - sbGUID := chi.URLParam(r, "service_binding_guid") + sbGUID := r.PathValue("binding_id") + // sbGUID := chi.URLParam(r, "service_binding_guid") if sbGUID == "" { log.Println("Missing service binding GUID") w.WriteHeader(http.StatusBadRequest) diff --git a/assets/credhub-service-broker/handlers_test.go b/assets/credhub-service-broker/handlers_test.go new file mode 100644 index 000000000..e18382239 --- /dev/null +++ b/assets/credhub-service-broker/handlers_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "code.cloudfoundry.org/credhub-cli/credhub" + "code.cloudfoundry.org/credhub-cli/credhub/credentials" + "code.cloudfoundry.org/credhub-cli/credhub/credentials/values" + "code.cloudfoundry.org/credhub-cli/credhub/permissions" +) + +type mockCredhubClient struct { + SetJSONReturn credentials.JSON + SetJSONReturnErr error + AddPermissionReturn *permissions.Permission + AddPermissionReturnErr error +} + +func (m *mockCredhubClient) SetJSON(name string, value values.JSON, options ...credhub.SetOption) (credentials.JSON, error) { + return m.SetJSONReturn, m.SetJSONReturnErr +} + +func (m *mockCredhubClient) AddPermission(path string, actor string, ops []string) (*permissions.Permission, error) { + return m.AddPermissionReturn, m.AddPermissionReturnErr +} + +func (m *mockCredhubClient) Delete(name string) error { + return nil +} + +func TestBindings_Add(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + bindingID string + body string + setJSONReturn credentials.JSON + setJSONReturnErr error + expectedStatus int + expectedBody string + }{ + { + name: "simple", + bindingID: "test-binding-id", + body: `{"app_guid": "test-app-guid"}`, + setJSONReturn: credentials.JSON{Base: credentials.Base{Name: "test-credhub"}}, + expectedStatus: http.StatusCreated, + expectedBody: `{"credentials":{"credhub-ref":"test-credhub"}}`, + }, + { + name: "no-service-binding-id", + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mcc := &mockCredhubClient{ + SetJSONReturn: tc.setJSONReturn, + SetJSONReturnErr: tc.setJSONReturnErr, + } + + req := httptest.NewRequest("PUT", "/v2/service_instances/test-guid/service_bindings/test-binding-guid", strings.NewReader(tc.body)) + req.SetPathValue("binding_id", tc.bindingID) + + rr := httptest.NewRecorder() + hf := bindHandler(mcc, map[string]string{}) + hf.ServeHTTP(rr, req) + + if status := rr.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v", status, tc.expectedStatus) + } + + if rr.Body.String() != tc.expectedBody { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), tc.expectedBody) + } + }) + } +} diff --git a/assets/credhub-service-broker/internal/bindings/bindings.go b/assets/credhub-service-broker/internal/bindings/bindings.go new file mode 100644 index 000000000..05bc3e5fc --- /dev/null +++ b/assets/credhub-service-broker/internal/bindings/bindings.go @@ -0,0 +1,112 @@ +package bindings + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "time" + + "code.cloudfoundry.org/credhub-cli/credhub" + "code.cloudfoundry.org/credhub-cli/credhub/credentials" + "code.cloudfoundry.org/credhub-cli/credhub/credentials/values" + "code.cloudfoundry.org/credhub-cli/credhub/permissions" +) + +type CredhubClient interface { + SetJSON(name string, value values.JSON, options ...credhub.SetOption) (credentials.JSON, error) + AddPermission(path string, actor string, ops []string) (*permissions.Permission, error) + Delete(name string) error +} + +type Bindings struct { + m map[string]string + cc CredhubClient +} + +func New(cc CredhubClient) *Bindings { + return &Bindings{ + m: make(map[string]string), + cc: cc, + } +} + +type BindingRequest struct { + AppGuid string `json:"app_guid"` +} + +type Credentials struct { + CredhubRef string `json:"credhub-ref"` +} + +type BindingResponse struct { + Credentials Credentials `json:"credentials"` +} + +func (b *Bindings) Add(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("binding_id") + if id == "" { + log.Panicf("Path does not include binding id") + } + + var br BindingRequest + err := json.NewDecoder(r.Body).Decode(&br) + r.Body.Close() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Failed to parse binding request: %s", err.Error()))) + return + } + + name := strconv.FormatInt(time.Now().UnixNano(), 10) + cred, err := b.cc.SetJSON(name, values.JSON{ + "user-name": "pinkyPie", + "password": "rainbowDash", + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Failed to set credential: %s", err.Error()))) + return + } + + b.m[id] = cred.Name + + if br.AppGuid != "" { + _, err = b.cc.AddPermission(cred.Name, "mtls-app:"+br.AppGuid, []string{"read"}) + if err != nil { + log.Println("Failed to add permission: ", err) + } + } + + resp := BindingResponse{ + Credentials: Credentials{ + CredhubRef: "test-credhub", + }, + } + respJSON, err := json.Marshal(resp) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Failed to marshal binding response: %s", err.Error()))) + return + } + + w.WriteHeader(http.StatusCreated) + w.Write(respJSON) +} + +func (b *Bindings) Remove(w http.ResponseWriter, r *http.Request) { + // id := r.PathValue("id") + + w.Write([]byte("{}")) +} + +func (b *Bindings) Get(id string) (string, bool) { + resp, ok := b.m[id] + return resp, ok +} + +func (b Bindings) Register(mux *http.ServeMux) { + mux.HandleFunc("/v2/service_instances/{id}/service_bindings/{binding_id}", b.Add) + mux.HandleFunc("/v2/service_instances/{id}/service_bindings/{binding_id}", b.Remove) +} diff --git a/assets/credhub-service-broker/internal/bindings/bindings_test.go b/assets/credhub-service-broker/internal/bindings/bindings_test.go new file mode 100644 index 000000000..2da69103d --- /dev/null +++ b/assets/credhub-service-broker/internal/bindings/bindings_test.go @@ -0,0 +1,148 @@ +package bindings_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "code.cloudfoundry.org/credhub-cli/credhub" + "code.cloudfoundry.org/credhub-cli/credhub/credentials" + "code.cloudfoundry.org/credhub-cli/credhub/credentials/values" + "code.cloudfoundry.org/credhub-cli/credhub/permissions" + "github.com/cloudfoundry/cf-acceptance-tests/assets/credhub-service-broker/internal/bindings" +) + +type mockCredhubClient struct { + SetJSONReturn credentials.JSON + SetJSONReturnErr error + AddPermissionReturn *permissions.Permission + AddPermissionReturnErr error +} + +func (m *mockCredhubClient) SetJSON(name string, value values.JSON, options ...credhub.SetOption) (credentials.JSON, error) { + return m.SetJSONReturn, m.SetJSONReturnErr +} + +func (m *mockCredhubClient) AddPermission(path string, actor string, ops []string) (*permissions.Permission, error) { + return m.AddPermissionReturn, m.AddPermissionReturnErr +} + +func (m *mockCredhubClient) Delete(name string) error { + return nil +} + +func TestBindings_Add(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + body string + setJSONReturn credentials.JSON + setJSONReturnErr error + addPermissionErr error + expectedStatus int + expectedBody string + expectToBeSaved bool + }{ + { + name: "simple", + body: `{"app_guid": "test-app-guid"}`, + setJSONReturn: credentials.JSON{Base: credentials.Base{Name: "test-credhub"}}, + expectedStatus: http.StatusCreated, + expectedBody: `{"credentials":{"credhub-ref":"test-credhub"}}`, + expectToBeSaved: true, + }, + { + name: "no-request-body", + setJSONReturn: credentials.JSON{Base: credentials.Base{Name: "test-credhub"}}, + expectedStatus: http.StatusBadRequest, + expectedBody: "Failed to parse binding request: EOF", + expectToBeSaved: false, + }, + { + name: "no-app-guid-in-request-body", + body: `{}`, + setJSONReturn: credentials.JSON{Base: credentials.Base{Name: "test-credhub"}}, + expectedStatus: http.StatusCreated, + expectedBody: `{"credentials":{"credhub-ref":"test-credhub"}}`, + expectToBeSaved: true, + }, + { + name: "fails-to-set-credhub-ref", + body: `{"app_guid": "test-app-guid"}`, + setJSONReturnErr: fmt.Errorf("some error"), + expectedStatus: http.StatusInternalServerError, + expectedBody: "Failed to set credential: some error", + expectToBeSaved: false, + }, + { + name: "fails-to-give-app-permissions-to-credhub-ref", + body: `{"app_guid": "test-app-guid"}`, + setJSONReturn: credentials.JSON{Base: credentials.Base{Name: "test-credhub"}}, + addPermissionErr: fmt.Errorf("some error"), + expectedStatus: http.StatusCreated, + expectedBody: `{"credentials":{"credhub-ref":"test-credhub"}}`, + expectToBeSaved: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mcc := &mockCredhubClient{ + SetJSONReturn: tc.setJSONReturn, + SetJSONReturnErr: tc.setJSONReturnErr, + } + b := bindings.New(mcc) + + req := httptest.NewRequest("PUT", "/v2/service_instances/test-guid/service_bindings/test-binding-guid", strings.NewReader(tc.body)) + req.SetPathValue("binding_id", "test-binding-id") + rr := httptest.NewRecorder() + b.Add(rr, req) + + if status := rr.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v", status, tc.expectedStatus) + } + + if rr.Body.String() != tc.expectedBody { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), tc.expectedBody) + } + + v, ok := b.Get("test-binding-id") + if ok != tc.expectToBeSaved { + t.Errorf("unexpected binding presence: got %v want %v", ok, tc.expectToBeSaved) + } + if ok && v != "test-credhub" { + t.Errorf("unexpected binding value: got %v want %v", v, "test-credhub") + } + }) + } +} + +func TestBindings_Remove(t *testing.T) { + t.Parallel() + + mcc := &mockCredhubClient{} + b := bindings.New(mcc) + + req, err := http.NewRequest("DELETE", "/v2/service_instances/test-guid/service_bindings/test-binding-guid", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + b.Remove(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `{}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } +} diff --git a/assets/credhub-service-broker/internal/catalog/catalog.go b/assets/credhub-service-broker/internal/catalog/catalog.go new file mode 100644 index 000000000..5a0f664fe --- /dev/null +++ b/assets/credhub-service-broker/internal/catalog/catalog.go @@ -0,0 +1,64 @@ +package catalog + +import ( + "encoding/json" + "log" + "net/http" +) + +// Plan is a struct that represents a plan in the catalog. +type Plan struct { + Name string `json:"name"` + ID string `json:"id"` + Description string `json:"description"` +} + +// Service is a struct that represents a service in the catalog. +type Service struct { + Name string `json:"name"` + ID string `json:"id"` + Description string `json:"description"` + Bindable bool `json:"bindable"` + Plans []Plan `json:"plans"` +} + +// Catalog is a struct that represents the catalog. +type Catalog struct { + Services []Service `json:"services"` +} + +var _ http.Handler = (*Catalog)(nil) + +// New creates a new Catalog with the given service name, service ID, and plan ID. +func New(serviceName, serviceID, planID string) *Catalog { + return &Catalog{ + Services: []Service{ + { + Name: serviceName, + ID: serviceID, + Description: "credhub read service for tests", + Bindable: true, + Plans: []Plan{ + { + Name: "credhub-read-plan", + ID: planID, + Description: "credhub read plan for tests", + }, + }, + }, + }, + } +} + +// ServeHTTP serves the HTTP request by marshalling the catalog to JSON and writing it to the response writer. +func (c *Catalog) ServeHTTP(w http.ResponseWriter, r *http.Request) { + catalogJSON, err := json.Marshal(c) + if err != nil { + log.Println("Failed to marshal catalog: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(catalogJSON) +} diff --git a/assets/credhub-service-broker/internal/catalog/catalog_test.go b/assets/credhub-service-broker/internal/catalog/catalog_test.go new file mode 100644 index 000000000..6f1727a1b --- /dev/null +++ b/assets/credhub-service-broker/internal/catalog/catalog_test.go @@ -0,0 +1,27 @@ +package catalog_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/cloudfoundry/cf-acceptance-tests/assets/credhub-service-broker/internal/catalog" +) + +func TestCatalog_Handler(t *testing.T) { + t.Parallel() + + c := catalog.New("test-name", "test-id", "test-plan-id") + + rr := httptest.NewRecorder() + c.ServeHTTP(rr, nil) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `{"services":[{"name":"test-name","id":"test-id","description":"credhub read service for tests","bindable":true,"plans":[{"name":"credhub-read-plan","id":"test-plan-id","description":"credhub read plan for tests"}]}]}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } +} diff --git a/assets/credhub-service-broker/main.go b/assets/credhub-service-broker/main.go index 2d774abac..039c94ce3 100644 --- a/assets/credhub-service-broker/main.go +++ b/assets/credhub-service-broker/main.go @@ -10,6 +10,7 @@ import ( "code.cloudfoundry.org/credhub-cli/credhub" "code.cloudfoundry.org/credhub-cli/credhub/auth" "code.cloudfoundry.org/credhub-cli/util" + "github.com/cloudfoundry/cf-acceptance-tests/assets/credhub-service-broker/internal/catalog" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) @@ -28,11 +29,13 @@ func main() { // Create a map of service binding GUIDs to track the registered service instances bindings := make(map[string]string) + // Create a new catalog + c := catalog.New(SERVICE_NAME, SERVICE_UUID, PLAN_UUID) + // Create a router and register the service broker handlers router := chi.NewRouter() router.Use(middleware.Recoverer) - - router.Get("/v2/catalog", catalogHandler) + router.Get("/v2/catalog", c.ServeHTTP) router.Route("/v2/service_instances", func(r chi.Router) { r.Put("/{service_instance_guid}", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("{}"))