Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions cmd/server/internal/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const InvalidRequestMethod = "invalid request method"
const AuthorizationCheckFailed = "authorization check failed"
const BearerPrefix = "Bearer "
const BasicPrefix = "Basic "
const ContentType = "Content-Type"

const (
CallbackSucceeded = "SUCCEEDED"
Expand All @@ -77,6 +78,7 @@ const (
Step = "step"
TenantProvisioning = "Tenant Provisioning"
TenantDeprovisioning = "Tenant Deprovisioning"
GetDependencies = "Get Dependencies"
)

type RequestInfo struct {
Expand Down Expand Up @@ -157,6 +159,12 @@ type tenantInfo struct {
tenantSubDomain string
}

type GetDependenciesAuthError struct{}

func (err *GetDependenciesAuthError) Error() string {
return "Not authorized"
}

func (s *SubscriptionHandler) CreateTenant(reqInfo *RequestInfo) *Result {
util.LogInfo("Create Tenant triggered", TenantProvisioning, "CreateTenant", nil)
var created, updated = false, false
Expand Down Expand Up @@ -1125,6 +1133,137 @@ func getSubscriptionDomain(payload map[string]any) string {
return ""
}

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]any

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
}

// Subscription Dependency check
if isServiceRelevantForDependencies(service, 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 isServiceRelevantForDependencies(serviceInfo v1alpha1.ServiceInfo, credentials map[string]any) bool {
if serviceInfo.GetSubscriptionDependency() == v1alpha1.SubscriptionDependencyAlways {
return true
}

if serviceInfo.GetSubscriptionDependency() == v1alpha1.SubscriptionDependencyAuto {
return serviceInfo.Class == "destination" || serviceInfo.Class == "connectivity" || (serviceInfo.Class == "auditlog" && credentials["plan"] == "oauth2") || (checkSaasRegistryAppNameInCredentials(credentials) && checkXsAppNameInUaaCredentials(credentials)) || checkSaasRegistryEnabled(credentials)
}

return false
}

func (s *SubscriptionHandler) getDependencies(req *http.Request) ([]byte, error) {
var dependenciesArray []map[string]interface{}

// Read the cap application by using the provider subaccount id & app-name passed in the URI
// URI format - /dependencies/providersubaccountId/app-name?tenantId=
providersubaccountId := req.PathValue("providersubaccountId")
appName := req.PathValue("appName")
if providersubaccountId == "" || appName == "" {
err := errors.New("wrong get dependencies request uri - providersubaccountId or appName not found")
util.LogError(err, "Wrong get dependencies request URI - providersubaccountId or appName not found", GetDependencies, "InvalidURI", nil, "uri", req.RequestURI)
return nil, err
}

util.LogInfo("Get dependencies endpoint called", GetDependencies, "GetDependencies", nil, "providersubaccountId", providersubaccountId, "btpAppName", appName)

ca, err := s.checkCAPApp("", providersubaccountId, appName)
if err != nil {
util.LogError(err, "CAP Application resource not found", GetDependencies, nil, nil, "providersubaccountId", providersubaccountId, "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, "providersubaccountId", providersubaccountId, "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, "providersubaccountId", providersubaccountId, "btpAppName", appName)
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, "providersubaccountId", providersubaccountId, "btpAppName", appName)
return nil, nil
}

dependencies, err := json.Marshal(dependenciesArray)
if err != nil {
util.LogError(err, "Json marshal of dependencies failed", GetDependencies, ca, nil, "providersubaccountId", providersubaccountId, "btpAppName", appName)
return nil, err
}

util.LogInfo("Dependencies returned", GetDependencies, ca, nil, "providersubaccountId", providersubaccountId, "btpAppName", appName, "dependencies", string(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(ContentType, "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{}}
}
Expand Down
145 changes: 145 additions & 0 deletions cmd/server/internal/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,65 @@
"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"
}`),
},
Expand Down Expand Up @@ -1545,3 +1604,89 @@
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, nil, client)

Check failure on line 1658 in cmd/server/internal/handler_test.go

View workflow job for this annotation

GitHub Actions / Build and Test

cannot use client (variable of type *http.Client) as "k8s.io/apimachinery/pkg/runtime".Object value in argument to setup: *http.Client does not implement "k8s.io/apimachinery/pkg/runtime".Object (missing method DeepCopyObject)

Check failure on line 1658 in cmd/server/internal/handler_test.go

View workflow job for this annotation

GitHub Actions / Build and Test

cannot use ca (variable of type *"github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1".CAPApplication) as *http.Client value in argument to setup

res := httptest.NewRecorder()
var req *http.Request
if testData.invalidURI == true {
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.SetPathValue("globalAccountId", globalAccountId)
req.SetPathValue("appName", appName)
}

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)
}
}
})
}
}
2 changes: 2 additions & 0 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func main() {

http.HandleFunc("/provision/", util.InstrumentHttpHandler(subHandler.HandleSaaSRequest, subsctiptionHandlerMetricPrefix, subscriptionHandlerDesc))
http.HandleFunc("/sms/provision/", util.InstrumentHttpHandler(subHandler.HandleSMSRequest, subsctiptionHandlerMetricPrefix+"_sms", subscriptionHandlerDesc))
// TODO: instrument this route too
http.HandleFunc("/dependencies/{providerSubaccountId}/{appName}/", subHandler.HandleGetDependenciesRequest)

// Initialize/start metrics server
util.InitMetricsServer()
Expand Down
6 changes: 6 additions & 0 deletions crds/sme.sap.com_capapplications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ spec:
type: string
secret:
type: string
subscriptionDependency:
enum:
- Auto
- Always
- Never
type: string
required:
- class
- name
Expand Down
14 changes: 14 additions & 0 deletions pkg/apis/sme.sap.com/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,22 @@ type ServiceInfo struct {
Secret string `json:"secret"`
// Type of service
Class string `json:"class"`
// SubscriptionDependency may be used to specify whether this service should be part of getDependencies call from subscripton service (e.g. saas-registry)
SubscriptionDependency *SubscriptionDependency `json:"subscriptionDependency,omitempty"`
}

// +kubebuilder:validation:Enum=Auto;Always;Never
type SubscriptionDependency string

const (
// Automatically determine if the service needs to be returned in getDependencies call
SubscriptionDependencyAuto SubscriptionDependency = "Auto"
// Always return the service in getDependencies call
SubscriptionDependencyAlways SubscriptionDependency = "Always"
// Never return the service in getDependencies call
SubscriptionDependencyNever SubscriptionDependency = "Never"
)

// Custom resource status
type GenericStatus struct {
// Observed generation of the resource where this status was identified
Expand Down
7 changes: 7 additions & 0 deletions pkg/apis/sme.sap.com/v1alpha1/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,10 @@ func (cdom *ClusterDomain) GetStatusReadyConditionMessage() string {
}
return ""
}

func (serviceInfo ServiceInfo) GetSubscriptionDependency() SubscriptionDependency {
if serviceInfo.SubscriptionDependency == nil {
return SubscriptionDependencyAuto
}
return *serviceInfo.SubscriptionDependency
}
Loading
Loading