From 2b56a9d1d24d0e416452a78467106717dd21ba0a Mon Sep 17 00:00:00 2001 From: anirudhprasad-sap <126493692+anirudhprasad-sap@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:59:11 +0000 Subject: [PATCH 01/11] getDependencies endpoint implementation --- cmd/server/internal/handler.go | 137 +++++++++++++++++++++++++++++++++ cmd/server/server.go | 1 + 2 files changed, 138 insertions(+) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 64f4f6e..80a9152 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -70,6 +70,7 @@ const ( Step = "step" TenantProvisioning = "Tenant Provisioning" TenantDeprovisioning = "Tenant Deprovisioning" + GetDependencies = "Get Dependencies" ) type Result struct { @@ -93,6 +94,12 @@ type OAuthResponse struct { AccessToken string `json:"access_token"` } +type GetDependenciesAuthError struct{} + +func (err *GetDependenciesAuthError) Error() string { + return "Not authorized" +} + func (s *SubscriptionHandler) CreateTenant(req *http.Request) *Result { util.LogInfo("Create Tenant triggered", TenantProvisioning, "CreateTenant", nil) var created = false @@ -604,6 +611,136 @@ func (s *SubscriptionHandler) HandleRequest(w http.ResponseWriter, req *http.Req w.Write(res) } +func checkXsAppNameInUaaCredentials(credential map[string]interface{}) bool { + return credential["uaa"] != nil && credential["uaa"].(map[string]interface{})["xsappname"] != nil && credential["uaa"].(map[string]interface{})["xsappname"].(string) != "" +} + +func checkXsAppNameInCredentials(credential map[string]interface{}) bool { + return credential["xsappname"] != nil && credential["xsappname"].(string) != "" +} + +func checkSaasRegistryAppNameInCredentials(credential map[string]interface{}) bool { + return credential["saasregistryappname"] != nil && credential["saasregistryappname"].(string) != "" +} + +func checkSaasRegistryEnabled(credential map[string]interface{}) bool { + return credential["saasregistryenabled"] != nil && credential["saasregistryenabled"].(bool) +} + +func (s *SubscriptionHandler) getServiceDependencies(capApp *v1alpha1.CAPApplication, service v1alpha1.ServiceInfo) map[string]interface{} { + var credentials map[string]interface{} + + serviceSecretCred, err := s.KubeClienset.CoreV1().Secrets(capApp.Namespace).Get(context.TODO(), service.Secret, metav1.GetOptions{}) + if err != nil { + util.LogError(err, "Service secret read failed", GetDependencies, capApp, nil, "secretName", service.Secret) + return nil + } + + if err = json.Unmarshal(serviceSecretCred.Data["credentials"], &credentials); err != nil { + util.LogError(err, "Could not read xsuaa secret with key credentials", GetDependencies, capApp, nil, "secretName", service.Secret) + return nil + } + + if service.Class == "destination" || service.Class == "connectivity" { + if checkXsAppNameInCredentials(credentials) { + return map[string]interface{}{ + "appId": credentials["xsappname"].(string), + "appName": service.Class, + } + } else if checkXsAppNameInUaaCredentials(credentials) { + return map[string]interface{}{ + "appId": credentials["uaa"].(map[string]interface{})["xsappname"].(string), + "appName": service.Class, + } + } + } else if checkSaasRegistryAppNameInCredentials(credentials) && checkXsAppNameInUaaCredentials(credentials) { + return map[string]interface{}{ + "appId": credentials["uaa"].(map[string]interface{})["xsappname"].(string), + "appName": credentials["saasregistryappname"].(string), + } + } else if checkSaasRegistryEnabled(credentials) { + if checkXsAppNameInCredentials(credentials) { + return map[string]interface{}{ + "xsappname": credentials["xsappname"].(string), + } + } else if checkXsAppNameInUaaCredentials(credentials) { + return map[string]interface{}{ + "xsappname": credentials["uaa"].(map[string]interface{})["xsappname"].(string), + } + } + } + return nil +} + +func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) { + var dependenciesArray []map[string]interface{} + + // Read the cap application by assuming sme.sap.com/btp-app-identifier is passed in the URL in + // the format dependencies/global-account-id/app-name?tenantId= + uriWithOutParam := strings.Split(req.RequestURI, "?")[0] + btpAppIdentifier := strings.Split(uriWithOutParam, "/") + + util.LogInfo("Get dependencies endpoint called", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + + ca, err := s.checkCAPApp(btpAppIdentifier[len(btpAppIdentifier)-2], btpAppIdentifier[len(btpAppIdentifier)-1]) + if err != nil { + util.LogError(err, "Get dependencies call failed", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + return nil, err + } + + // fetch SaaS Registry and XSUAA information + saasData, uaaData := s.getServiceDetails(ca, GetDependencies) + if saasData == nil || uaaData == nil { + util.LogError(err, "Cannot read saas registry and xsuaa information", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + return nil, err + } + + // validate token + if err = s.checkAuthorization(req.Header.Get("Authorization"), saasData, uaaData, GetDependencies); err != nil { + util.LogError(err, "Check authorization failed", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + return nil, &GetDependenciesAuthError{} + } + + for _, service := range ca.Spec.BTP.Services { + if serviceDependency := s.getServiceDependencies(ca, service); serviceDependency != nil { + dependenciesArray = append(dependenciesArray, serviceDependency) + } + } + + if len(dependenciesArray) == 0 { + util.LogInfo("No dependencies found", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + return nil, nil + } + + dependencies, err := json.Marshal(dependenciesArray) + if err != nil { + util.LogError(err, "Json marshal of dependencies failed", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + return nil, err + } + + util.LogInfo("Dependencies returned", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1], "dependencies", dependencies) + + return dependencies, nil +} + +func (s *SubscriptionHandler) HandleGetDependenciesRequest(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + dependencies, err := s.getDependencies(req) + if err != nil { + if _, ok := err.(*GetDependenciesAuthError); ok { + w.WriteHeader(http.StatusUnauthorized) + } else { + w.WriteHeader(http.StatusBadRequest) + } + } else { + w.Header().Set("Content-Type", "application/json") + w.Write(dependencies) + } + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} func NewSubscriptionHandler(clientset versioned.Interface, kubeClienset kubernetes.Interface) *SubscriptionHandler { return &SubscriptionHandler{Clientset: clientset, KubeClienset: kubeClienset, httpClientGenerator: &httpClientGeneratorImpl{}} } diff --git a/cmd/server/server.go b/cmd/server/server.go index 4bd94d8..898cc2f 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -22,6 +22,7 @@ func main() { klog.SetLogger(util.GetLogger()) subHandler := getSubscriptionHandler() http.HandleFunc("/provision/", subHandler.HandleRequest) + http.HandleFunc("/callback/v1.0/dependencies/", subHandler.HandleGetDependenciesRequest) // Default port port := "4000" From 368a24d17c72ec60c534edfff9bb9faf4787d57d Mon Sep 17 00:00:00 2001 From: anirudhprasad-sap <126493692+anirudhprasad-sap@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:52:58 +0000 Subject: [PATCH 02/11] Added auditlog to getdependencies --- cmd/server/internal/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 80a9152..9815fd6 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -641,7 +641,7 @@ func (s *SubscriptionHandler) getServiceDependencies(capApp *v1alpha1.CAPApplica return nil } - if service.Class == "destination" || service.Class == "connectivity" { + if service.Class == "destination" || service.Class == "connectivity" || (service.Class == "auditlog" && credentials["plan"] == "oauth2") { if checkXsAppNameInCredentials(credentials) { return map[string]interface{}{ "appId": credentials["xsappname"].(string), From 109abf0c50f372c04d7390f1789e2acd79c17002 Mon Sep 17 00:00:00 2001 From: i325261 Date: Fri, 30 Aug 2024 14:21:59 +0200 Subject: [PATCH 03/11] Minor fixes + unit tests --- cmd/server/internal/handler.go | 12 ++- cmd/server/internal/handler_test.go | 142 ++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 3 deletions(-) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 9815fd6..9c7986b 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -680,11 +680,17 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) uriWithOutParam := strings.Split(req.RequestURI, "?")[0] btpAppIdentifier := strings.Split(uriWithOutParam, "/") - util.LogInfo("Get dependencies endpoint called", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + if len(btpAppIdentifier) < 6 { + err := errors.New("Wrong get dependencies request URI") + util.LogError(err, "Wrong get dependencies request URI", GetDependencies, "InvalidURI", nil, "uri", req.RequestURI) + return nil, err + } + + util.LogInfo("Get dependencies endpoint called", GetDependencies, "GetDependencies", nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) ca, err := s.checkCAPApp(btpAppIdentifier[len(btpAppIdentifier)-2], btpAppIdentifier[len(btpAppIdentifier)-1]) if err != nil { - util.LogError(err, "Get dependencies call failed", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogError(err, "CAP Application resource not found", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) return nil, err } @@ -697,7 +703,7 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) // validate token if err = s.checkAuthorization(req.Header.Get("Authorization"), saasData, uaaData, GetDependencies); err != nil { - util.LogError(err, "Check authorization failed", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogError(err, "Authorization check failed", GetDependencies, "checkAuthorization", nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) return nil, &GetDependenciesAuthError{} } diff --git a/cmd/server/internal/handler_test.go b/cmd/server/internal/handler_test.go index 92c9123..0299cff 100644 --- a/cmd/server/internal/handler_test.go +++ b/cmd/server/internal/handler_test.go @@ -112,6 +112,65 @@ func createSecrets() []runtime.Object { "uaadomain": "auth.service.local", "sburl": "internal.auth.service.local", "url": "https://app-domain.auth.service.local", + "saasregistryenabled": true, + "uaa": {"xsappname": "appname!b15" }, + "credential-type": "instance-secret" + }`), + }, + }, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-dest-sec", + Namespace: v1.NamespaceDefault, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "credentials": []byte(`{ + "saas_registry_url": "https://sm.service.local", + "clientid": "clientid", + "clientsecret": "clientsecret", + "uaadomain": "auth.service.local", + "sburl": "internal.auth.service.local", + "url": "https://app-domain.auth.service.local", + "saasregistryenabled": true, + "uaa": {"xsappname": "appname!b15" }, + "credential-type": "instance-secret" + }`), + }, + }, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-html-rt-sec", + Namespace: v1.NamespaceDefault, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "credentials": []byte(`{ + "saas_registry_url": "https://sm.service.local", + "clientid": "clientid", + "clientsecret": "clientsecret", + "uaadomain": "auth.service.local", + "sburl": "internal.auth.service.local", + "url": "https://app-domain.auth.service.local", + "saasregistryappname": "saasregistryappname", + "uaa": {"xsappname": "appname!b15" }, + "credential-type": "instance-secret" + }`), + }, + }, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sm-sec", + Namespace: v1.NamespaceDefault, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "credentials": []byte(`{ + "saas_registry_url": "https://sm.service.local", + "clientid": "clientid", + "clientsecret": "clientsecret", + "uaadomain": "auth.service.local", + "sburl": "internal.auth.service.local", + "url": "https://app-domain.auth.service.local", + "saasregistryenabled": true, + "xsappname": "appname!b15", "credential-type": "instance-secret" }`), }, @@ -704,3 +763,86 @@ func TestMultiXSUAA(t *testing.T) { func execTestsWithBLI(t *testing.T, name string, backlogItems []string, test func(t *testing.T)) { t.Run(name+", BLIs: "+strings.Join(backlogItems, ", "), test) } + +func TestGetDependencies(t *testing.T) { + tests := []struct { + name string + method string + invalidToken bool + invalidURI bool + expectedStatusCode int + expectedResponse []map[string]string + }{ + { + name: "Invalid get dependency request - wrong method", + method: http.MethodPut, + expectedStatusCode: http.StatusMethodNotAllowed, + expectedResponse: nil, + }, + { + name: "Not authorized request", + method: http.MethodGet, + invalidToken: true, + expectedStatusCode: http.StatusUnauthorized, + expectedResponse: nil, + }, + { + name: "Invalid URI", + method: http.MethodGet, + invalidURI: true, + expectedStatusCode: http.StatusBadRequest, + expectedResponse: nil, + }, + { + name: "Valid get dependency request", + method: http.MethodGet, + expectedStatusCode: http.StatusOK, + expectedResponse: []map[string]string{ + {"xsappname": "appname!b15"}, + {"xsappname": "appname!b15"}, + {"appId": "appname!b15", "appName": "destination"}, + {"appId": "appname!b15", "appName": "saasregistryappname"}, + }, + }, + } + + for _, testData := range tests { + t.Run(testData.name, func(t *testing.T) { + ca := createCA() + + client, tokenString, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14") + if err != nil { + t.Fatal(err.Error()) + } + subHandler := setup(ca, nil, client) + + res := httptest.NewRecorder() + var req *http.Request + if testData.invalidURI == true { + req = httptest.NewRequest(testData.method, "/callback/dependencies/"+globalAccountId+"/"+appName, nil) + } else { + req = httptest.NewRequest(testData.method, "/callback/v1.0/dependencies/"+globalAccountId+"/"+appName, nil) + } + + if testData.invalidToken == true { + tokenString = "abc" //invalid token + } + + req.Header.Set("Authorization", "Bearer "+tokenString) + subHandler.HandleGetDependenciesRequest(res, req) + + if res.Code != testData.expectedStatusCode { + t.Errorf("Expected status '%d', received '%d'", testData.expectedStatusCode, res.Code) + } + + // Get the relevant response + if res.Code == http.StatusOK { + resBodyStr := res.Body.String() + expectedResponseByte, _ := json.Marshal(testData.expectedResponse) + if resBodyStr != string(expectedResponseByte) { + t.Error("Unexpected error in expected response: ", res.Body) + } + } + }) + } +} From e0fb060793874808949a71b704205a29a838c206 Mon Sep 17 00:00:00 2001 From: i325261 Date: Fri, 30 Aug 2024 14:30:18 +0200 Subject: [PATCH 04/11] added pattern to BTPAppName on CA CRD --- crds/sme.sap.com_capapplications.yaml | 1 + pkg/apis/sme.sap.com/v1alpha1/types.go | 1 + 2 files changed, 2 insertions(+) diff --git a/crds/sme.sap.com_capapplications.yaml b/crds/sme.sap.com_capapplications.yaml index f762f2c..8c06cc8 100644 --- a/crds/sme.sap.com_capapplications.yaml +++ b/crds/sme.sap.com_capapplications.yaml @@ -56,6 +56,7 @@ spec: - services type: object btpAppName: + pattern: ^[a-zA-Z0-9_-]+$ type: string domains: properties: diff --git a/pkg/apis/sme.sap.com/v1alpha1/types.go b/pkg/apis/sme.sap.com/v1alpha1/types.go index 219c094..1f2c648 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/types.go +++ b/pkg/apis/sme.sap.com/v1alpha1/types.go @@ -81,6 +81,7 @@ type CAPApplicationSpec struct { // SAP BTP Global Account Identifier where services are entitles for the current application GlobalAccountId string `json:"globalAccountId"` // Short name for the application (similar to BTP XSAPPNAME) + // +kubebuilder:validation:Pattern=^[a-zA-Z0-9_-]+$ BTPAppName string `json:"btpAppName"` // Provider subaccount where application services are created Provider BTPTenantIdentification `json:"provider"` From cb034326c7778751de860f8ac766a4e5e9e82e9d Mon Sep 17 00:00:00 2001 From: i325261 Date: Fri, 30 Aug 2024 14:31:50 +0200 Subject: [PATCH 05/11] Use ContentType constant --- cmd/server/internal/handler.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 9c7986b..57ae729 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -56,6 +56,7 @@ const InvalidRequestMethod = "invalid request method" const AuthorizationCheckFailed = "authorization check failed" const BearerPrefix = "Bearer " const BasicPrefix = "Basic " +const ContentType = "Content-Type" const ( CallbackSucceeded = "SUCCEEDED" @@ -518,7 +519,7 @@ func prepareTokenRequest(ctx context.Context, saasData *util.SaasRegistryCredent if err != nil { return nil, err } - tokenReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + tokenReq.Header.Set(ContentType, "application/x-www-form-urlencoded") if saasData.CredentialType != "x509" { tokenReq.Header.Set("Authorization", BasicPrefix+base64.StdEncoding.EncodeToString([]byte(saasData.ClientId+":"+saasData.ClientSecret))) } @@ -566,7 +567,7 @@ func (s *SubscriptionHandler) handleAsyncCallback(ctx context.Context, saasData AdditionalOutput: additionalOutput, }) callbackReq, _ := http.NewRequestWithContext(ctx, http.MethodPut, saasData.SaasManagerUrl+asyncCallbackPath, bytes.NewBuffer(payload)) - callbackReq.Header.Set("Content-Type", "application/json") + callbackReq.Header.Set(ContentType, "application/json") callbackReq.Header.Set("Authorization", BearerPrefix+oAuthType.AccessToken) client := s.httpClientGenerator.NewHTTPClient() @@ -740,7 +741,7 @@ func (s *SubscriptionHandler) HandleGetDependenciesRequest(w http.ResponseWriter w.WriteHeader(http.StatusBadRequest) } } else { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(ContentType, "application/json") w.Write(dependencies) } default: From e3b4711c5207fcda19a7d86ca90f5763ad0c88c5 Mon Sep 17 00:00:00 2001 From: i325261 Date: Tue, 3 Sep 2024 15:28:12 +0200 Subject: [PATCH 06/11] Added pattern to btpAppName Dependency uri changed minor fixes --- cmd/server/internal/handler.go | 11 ++++++----- cmd/server/internal/handler_test.go | 2 +- cmd/server/server.go | 2 +- crds/sme.sap.com_capapplications.yaml | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 57ae729..7f43831 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -17,6 +17,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" "sync" "time" @@ -679,14 +680,14 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) // Read the cap application by assuming sme.sap.com/btp-app-identifier is passed in the URL in // the format dependencies/global-account-id/app-name?tenantId= uriWithOutParam := strings.Split(req.RequestURI, "?")[0] - btpAppIdentifier := strings.Split(uriWithOutParam, "/") - - if len(btpAppIdentifier) < 6 { - err := errors.New("Wrong get dependencies request URI") + re := regexp.MustCompile(`^/dependencies/.*\/.*`) + if !re.MatchString(uriWithOutParam) { + err := errors.New("wrong get dependencies request uri") util.LogError(err, "Wrong get dependencies request URI", GetDependencies, "InvalidURI", nil, "uri", req.RequestURI) return nil, err } + btpAppIdentifier := strings.Split(uriWithOutParam, "/") util.LogInfo("Get dependencies endpoint called", GetDependencies, "GetDependencies", nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) ca, err := s.checkCAPApp(btpAppIdentifier[len(btpAppIdentifier)-2], btpAppIdentifier[len(btpAppIdentifier)-1]) @@ -725,7 +726,7 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) return nil, err } - util.LogInfo("Dependencies returned", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1], "dependencies", dependencies) + util.LogInfo("Dependencies returned", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1], "dependencies", string(dependencies)) return dependencies, nil } diff --git a/cmd/server/internal/handler_test.go b/cmd/server/internal/handler_test.go index 0299cff..b089a28 100644 --- a/cmd/server/internal/handler_test.go +++ b/cmd/server/internal/handler_test.go @@ -821,7 +821,7 @@ func TestGetDependencies(t *testing.T) { if testData.invalidURI == true { req = httptest.NewRequest(testData.method, "/callback/dependencies/"+globalAccountId+"/"+appName, nil) } else { - req = httptest.NewRequest(testData.method, "/callback/v1.0/dependencies/"+globalAccountId+"/"+appName, nil) + req = httptest.NewRequest(testData.method, "/dependencies/"+globalAccountId+"/"+appName, nil) } if testData.invalidToken == true { diff --git a/cmd/server/server.go b/cmd/server/server.go index 898cc2f..5ce9170 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -22,7 +22,7 @@ func main() { klog.SetLogger(util.GetLogger()) subHandler := getSubscriptionHandler() http.HandleFunc("/provision/", subHandler.HandleRequest) - http.HandleFunc("/callback/v1.0/dependencies/", subHandler.HandleGetDependenciesRequest) + http.HandleFunc("/dependencies/", subHandler.HandleGetDependenciesRequest) // Default port port := "4000" diff --git a/crds/sme.sap.com_capapplications.yaml b/crds/sme.sap.com_capapplications.yaml index 8c06cc8..846710f 100644 --- a/crds/sme.sap.com_capapplications.yaml +++ b/crds/sme.sap.com_capapplications.yaml @@ -56,7 +56,7 @@ spec: - services type: object btpAppName: - pattern: ^[a-zA-Z0-9_-]+$ + pattern: ^[a-z0-9_-]+$ type: string domains: properties: From e12e852ea3ccb50704b4083c42849ccffac8f8ca Mon Sep 17 00:00:00 2001 From: anirudhprasad-sap <126493692+anirudhprasad-sap@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:32:08 +0200 Subject: [PATCH 07/11] Update types.go - btpAppName pattern fix --- pkg/apis/sme.sap.com/v1alpha1/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/sme.sap.com/v1alpha1/types.go b/pkg/apis/sme.sap.com/v1alpha1/types.go index 4cd0be5..7210d78 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/types.go +++ b/pkg/apis/sme.sap.com/v1alpha1/types.go @@ -83,7 +83,7 @@ type CAPApplicationSpec struct { // SAP BTP Global Account Identifier where services are entitles for the current application GlobalAccountId string `json:"globalAccountId"` // Short name for the application (similar to BTP XSAPPNAME) - // +kubebuilder:validation:Pattern=^[a-zA-Z0-9_-]+$ + // +kubebuilder:validation:Pattern=^[a-z0-9_-]+$ BTPAppName string `json:"btpAppName"` // Provider subaccount where application services are created Provider BTPTenantIdentification `json:"provider"` From 6946f3a97dddcd06fd0863e9162bc8203e83be03 Mon Sep 17 00:00:00 2001 From: i325261 Date: Tue, 3 Sep 2024 15:41:11 +0200 Subject: [PATCH 08/11] test fix --- cmd/server/internal/handler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/server/internal/handler_test.go b/cmd/server/internal/handler_test.go index 9652703..0dbd403 100644 --- a/cmd/server/internal/handler_test.go +++ b/cmd/server/internal/handler_test.go @@ -835,7 +835,7 @@ func TestGetDependencies(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - subHandler := setup(ca, nil, client) + subHandler := setup(ca, nil, nil, client) res := httptest.NewRecorder() var req *http.Request From c82a3efbbb75a15df84b90c25fd919333e25f2be Mon Sep 17 00:00:00 2001 From: i325261 Date: Fri, 6 Sep 2024 11:44:35 +0200 Subject: [PATCH 09/11] getDependencies documentation update --- .../concepts/operator-components/subscription-server.md | 8 ++++---- website/content/en/docs/usage/prerequisites.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/website/content/en/docs/concepts/operator-components/subscription-server.md b/website/content/en/docs/concepts/operator-components/subscription-server.md index 91ab402..1ddd7b3 100644 --- a/website/content/en/docs/concepts/operator-components/subscription-server.md +++ b/website/content/en/docs/concepts/operator-components/subscription-server.md @@ -7,13 +7,13 @@ description: > Integration with SAP Software-as-a-Service Provisioning service (SaaS) --- -The Subscription Server handles HTTP requests from the [SAP Software-as-a-Service Provisioning service](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/2cd8913a50bc4d3e8172f84bb4bfba20.html) for tenant subscription operations on SAP Cloud Application Programming Model applications that have been installed in the cluster. +The Subscription Server handles HTTP requests from the [SAP Software-as-a-Service Provisioning service](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/2cd8913a50bc4d3e8172f84bb4bfba20.html) for tenant subscription operations on SAP Cloud Application Programming Model applications that have been installed in the cluster. -During the creation of a `saas-registry` service instance (in the provider subaccount), [callback URLs are configured](../../../usage/prerequisites/#saas-provisioning-service), which point to the subscription server routes. +During the creation of a `saas-registry` service instance (in the provider subaccount), [callback URLs are configured](../../../usage/prerequisites/#saas-provisioning-service), which point to the subscription server routes. Additionally, the `getDependecies` URLs can also be configured to point to the subscription server routes. -When a consumer tenant subscribes to an application managed by the operator, a subscription callback is received by the subscription server, which then generates the `CAPTenant` custom resource object. +When a consumer tenant subscribes to an application managed by the operator, a subscription callback is received by the subscription server, which then generates the `CAPTenant` custom resource object. -The subscription server returns an `Accepted` (202) response code and starts a routine/thread, which keeps polling for the tenant status until the changes to the `CAPTenant` are then independently reconciled by the controller. +The subscription server returns an `Accepted` (202) response code and starts a routine/thread, which keeps polling for the tenant status until the changes to the `CAPTenant` are then independently reconciled by the controller. Once the tenant provisioning process has completed (or has failed), the tracking routine will return the appropriate status to the SaaS Registry via an asynchronous callback (by obtaining the necessary authorization token). diff --git a/website/content/en/docs/usage/prerequisites.md b/website/content/en/docs/usage/prerequisites.md index bd25e7b..012ed41 100644 --- a/website/content/en/docs/usage/prerequisites.md +++ b/website/content/en/docs/usage/prerequisites.md @@ -90,7 +90,7 @@ parameters: appName: appUrls: callbackTimeoutMillis: 300000 # <-- used to fail subscription process when no response is received - getDependencies: https://..cluster-x.my-project.shoot.url.k8s.example.com/callback/v1.0/dependencies # <-- handled by the application + getDependencies: https:///dependencies// # the /getDependencies route is forwarded directly to CAP Operator (Subscription Server) and must be specified as such onSubscription: https:///provision/tenants/{tenantId} # <-- the /provision route is forwarded directly to CAP Operator (Subscription Server) and must be specified as such onSubscriptionAsync: true onUnSubscriptionAsync: true From 85585a1b0d6d6bb5c27793a0a1b21c79df3c5e39 Mon Sep 17 00:00:00 2001 From: i325261 Date: Thu, 26 Sep 2024 15:14:56 +0200 Subject: [PATCH 10/11] comments update --- cmd/server/internal/handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 25b8086..7e09107 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -717,8 +717,8 @@ func (s *SubscriptionHandler) getServiceDependencies(capApp *v1alpha1.CAPApplica func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) { var dependenciesArray []map[string]interface{} - // Read the cap application by assuming sme.sap.com/btp-app-identifier is passed in the URL in - // the format dependencies/global-account-id/app-name?tenantId= + // Read the cap application by using the global-account-id & app-name passed in the URI + // URI format - /dependencies/global-account-id/app-name?tenantId= uriWithOutParam := strings.Split(req.RequestURI, "?")[0] re := regexp.MustCompile(`^/dependencies/.*\/.*`) if !re.MatchString(uriWithOutParam) { From ff64b7dd1b3865f342b65b498bf2189bead16050 Mon Sep 17 00:00:00 2001 From: i325261 Date: Fri, 27 Sep 2024 09:20:28 +0200 Subject: [PATCH 11/11] Use req.PathValue --- cmd/server/internal/handler.go | 28 +++++++++++++--------------- cmd/server/internal/handler_test.go | 7 +++++-- cmd/server/server.go | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index 7e09107..d61ecb8 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -17,7 +17,6 @@ import ( "fmt" "net/http" "net/url" - "regexp" "strings" "sync" "time" @@ -719,33 +718,32 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) // Read the cap application by using the global-account-id & app-name passed in the URI // URI format - /dependencies/global-account-id/app-name?tenantId= - uriWithOutParam := strings.Split(req.RequestURI, "?")[0] - re := regexp.MustCompile(`^/dependencies/.*\/.*`) - if !re.MatchString(uriWithOutParam) { - err := errors.New("wrong get dependencies request uri") - util.LogError(err, "Wrong get dependencies request URI", GetDependencies, "InvalidURI", nil, "uri", req.RequestURI) + globalAccountId := req.PathValue("globalAccountId") + appName := req.PathValue("appName") + if globalAccountId == "" || appName == "" { + err := errors.New("wrong get dependencies request uri - globalAccountId or appName not found") + util.LogError(err, "Wrong get dependencies request URI - globalAccountId or appName not found", GetDependencies, "InvalidURI", nil, "uri", req.RequestURI) return nil, err } - btpAppIdentifier := strings.Split(uriWithOutParam, "/") - util.LogInfo("Get dependencies endpoint called", GetDependencies, "GetDependencies", nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogInfo("Get dependencies endpoint called", GetDependencies, "GetDependencies", nil, "globalAccountId", globalAccountId, "btpAppName", appName) - ca, err := s.checkCAPApp(btpAppIdentifier[len(btpAppIdentifier)-2], btpAppIdentifier[len(btpAppIdentifier)-1]) + ca, err := s.checkCAPApp(globalAccountId, appName) if err != nil { - util.LogError(err, "CAP Application resource not found", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogError(err, "CAP Application resource not found", GetDependencies, nil, nil, "globalAccountId", globalAccountId, "btpAppName", appName) return nil, err } // fetch SaaS Registry and XSUAA information saasData, uaaData := s.getServiceDetails(ca, GetDependencies) if saasData == nil || uaaData == nil { - util.LogError(err, "Cannot read saas registry and xsuaa information", GetDependencies, nil, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogError(err, "Cannot read saas registry and xsuaa information", GetDependencies, nil, nil, "globalAccountId", globalAccountId, "btpAppName", appName) return nil, err } // validate token if err = s.checkAuthorization(req.Header.Get("Authorization"), saasData, uaaData, GetDependencies); err != nil { - util.LogError(err, "Authorization check failed", GetDependencies, "checkAuthorization", nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogError(err, "Authorization check failed", GetDependencies, "checkAuthorization", nil, "globalAccountId", globalAccountId, "btpAppName", appName) return nil, &GetDependenciesAuthError{} } @@ -756,17 +754,17 @@ func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) } if len(dependenciesArray) == 0 { - util.LogInfo("No dependencies found", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogInfo("No dependencies found", GetDependencies, ca, nil, "globalAccountId", globalAccountId, "btpAppName", appName) return nil, nil } dependencies, err := json.Marshal(dependenciesArray) if err != nil { - util.LogError(err, "Json marshal of dependencies failed", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1]) + util.LogError(err, "Json marshal of dependencies failed", GetDependencies, ca, nil, "globalAccountId", globalAccountId, "btpAppName", appName) return nil, err } - util.LogInfo("Dependencies returned", GetDependencies, ca, nil, "globalAccountId", btpAppIdentifier[len(btpAppIdentifier)-2], "btpAppName", btpAppIdentifier[len(btpAppIdentifier)-1], "dependencies", string(dependencies)) + util.LogInfo("Dependencies returned", GetDependencies, ca, nil, "globalAccountId", globalAccountId, "btpAppName", appName, "dependencies", string(dependencies)) return dependencies, nil } diff --git a/cmd/server/internal/handler_test.go b/cmd/server/internal/handler_test.go index 0dbd403..5a66ebc 100644 --- a/cmd/server/internal/handler_test.go +++ b/cmd/server/internal/handler_test.go @@ -840,9 +840,12 @@ func TestGetDependencies(t *testing.T) { res := httptest.NewRecorder() var req *http.Request if testData.invalidURI == true { - req = httptest.NewRequest(testData.method, "/callback/dependencies/"+globalAccountId+"/"+appName, nil) + req = httptest.NewRequest(testData.method, "/callback/dependencies/globalAccountId/{appName}", nil) + req.SetPathValue("appName", appName) } else { - req = httptest.NewRequest(testData.method, "/dependencies/"+globalAccountId+"/"+appName, nil) + req = httptest.NewRequest(testData.method, "/dependencies/{globalAccountId}/{appName}", nil) + req.SetPathValue("globalAccountId", globalAccountId) + req.SetPathValue("appName", appName) } if testData.invalidToken == true { diff --git a/cmd/server/server.go b/cmd/server/server.go index 5ce9170..602dbbe 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -22,7 +22,7 @@ func main() { klog.SetLogger(util.GetLogger()) subHandler := getSubscriptionHandler() http.HandleFunc("/provision/", subHandler.HandleRequest) - http.HandleFunc("/dependencies/", subHandler.HandleGetDependenciesRequest) + http.HandleFunc("/dependencies/{globalAccountId}/{appName}", subHandler.HandleGetDependenciesRequest) // Default port port := "4000"