}
return allIds
}
+
+func (sd ServiceAPIDescription) GetAefProfileById(id *string) *AefProfile {
+ if sd.AefProfiles != nil {
+ for _, aefProfile := range *sd.AefProfiles {
+ if aefProfile.AefId == *id {
+ return &aefProfile
+ }
+ }
+ }
+ return nil
+}
--- /dev/null
+// -
+//
+// ========================LICENSE_START=================================
+// O-RAN-SC
+// %%
+// Copyright (C) 2023: Nordix Foundation
+// %%
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================LICENSE_END===================================
+package securityapi
+
+import (
+ "fmt"
+ "strings"
+
+ "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
+)
+
+var securityMethods []publishserviceapi.SecurityMethod
+
+func (newContext *ServiceSecurity) PrepareNewSecurityContext(services []publishserviceapi.ServiceAPIDescription) error {
+ securityMethods = []publishserviceapi.SecurityMethod{}
+ for i, securityInfo := range newContext.SecurityInfo {
+
+ if securityInfo.InterfaceDetails != nil {
+ addSecurityMethodsFromInterfaceDetails(securityInfo.InterfaceDetails.SecurityMethods, &securityInfo.PrefSecurityMethods)
+
+ } else {
+ checkNil := securityInfo.ApiId != nil && securityInfo.AefId != nil
+ if checkNil {
+ service := getServiceByApiId(&services, securityInfo.ApiId)
+ afpProfile := service.GetAefProfileById(securityInfo.AefId)
+
+ addSecurityMethodsFromAefProfile(afpProfile)
+ }
+ }
+
+ if isSecuryMethodsEmpty() {
+ return fmt.Errorf("not found compatible security method")
+ }
+ newContext.SecurityInfo[i].SelSecurityMethod = &securityMethods[0]
+ }
+ return nil
+}
+
+func isSecuryMethodsEmpty() bool {
+ return len(securityMethods) <= 0
+}
+
+func addSecurityMethodsFromInterfaceDetails(methodsFromInterface *[]publishserviceapi.SecurityMethod, prefMethods *[]publishserviceapi.SecurityMethod) {
+
+ if methodsFromInterface != nil {
+ securityMethods = append(securityMethods, *methodsFromInterface...)
+ }
+ if prefMethods != nil {
+ securityMethods = append(securityMethods, *prefMethods...)
+ }
+}
+
+func addSecurityMethodsFromAefProfile(afpProfile *publishserviceapi.AefProfile) {
+ if afpProfile.SecurityMethods != nil {
+ securityMethods = append(securityMethods, *afpProfile.SecurityMethods...)
+ }
+}
+
+func getServiceByApiId(services *[]publishserviceapi.ServiceAPIDescription, apiId *string) *publishserviceapi.ServiceAPIDescription {
+
+ for _, service := range *services {
+ if apiId != nil && strings.Compare(*service.ApiId, *apiId) == 0 {
+ return &service
+ }
+ }
+ return nil
+}
--- /dev/null
+// -
+// ========================LICENSE_START=================================
+// O-RAN-SC
+// %%
+// Copyright (C) 2023: Nordix Foundation
+// %%
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================LICENSE_END===================================
+//
+
+package securityapi
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "oransc.org/nonrtric/capifcore/internal/common29122"
+ publishapi "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
+)
+
+func TestPrepareNewSecurityContext(t *testing.T) {
+ apiId := "app-management"
+ aefId := "aefId"
+ description := "Description"
+ services := []publishapi.ServiceAPIDescription{
+ {
+ AefProfiles: &[]publishapi.AefProfile{
+ {
+ AefId: aefId,
+ Versions: []publishapi.Version{
+ {
+ Resources: &[]publishapi.Resource{
+ {
+ CommType: "REQUEST_RESPONSE",
+ },
+ },
+ },
+ },
+ SecurityMethods: &[]publishapi.SecurityMethod{
+ publishapi.SecurityMethodPKI,
+ },
+ },
+ },
+ ApiId: &apiId,
+ Description: &description,
+ },
+ }
+
+ servSecurityUnderTest := ServiceSecurity{
+ NotificationDestination: common29122.Uri("http://golang.cafe/"),
+ SecurityInfo: []SecurityInformation{
+ {
+ PrefSecurityMethods: []publishapi.SecurityMethod{
+ publishapi.SecurityMethodOAUTH,
+ },
+ },
+ },
+ }
+
+ err := servSecurityUnderTest.PrepareNewSecurityContext(services)
+
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "not found ")
+ assert.Contains(t, err.Error(), "security method")
+
+ servSecurityUnderTest.SecurityInfo = []SecurityInformation{
+ {
+ ApiId: &apiId,
+ AefId: &aefId,
+ PrefSecurityMethods: []publishapi.SecurityMethod{
+ publishapi.SecurityMethodOAUTH,
+ },
+ },
+ }
+
+ servSecurityUnderTest.PrepareNewSecurityContext(services)
+ assert.Equal(t, publishapi.SecurityMethodPKI, *servSecurityUnderTest.SecurityInfo[0].SelSecurityMethod)
+
+ servSecurityUnderTest.SecurityInfo = []SecurityInformation{
+ {
+ ApiId: &apiId,
+ PrefSecurityMethods: []publishapi.SecurityMethod{
+ publishapi.SecurityMethodOAUTH,
+ },
+ InterfaceDetails: &publishapi.InterfaceDescription{
+ SecurityMethods: &[]publishapi.SecurityMethod{
+ publishapi.SecurityMethodPSK,
+ },
+ },
+ },
+ }
+
+ servSecurityUnderTest.PrepareNewSecurityContext(services)
+ assert.Equal(t, publishapi.SecurityMethodPSK, *servSecurityUnderTest.SecurityInfo[0].SelSecurityMethod)
+
+}
package securityapi
import (
+ "errors"
+ "fmt"
+ "net/url"
"strings"
)
return true, AccessTokenErr{}
}
+func (ss ServiceSecurity) Validate() error {
+
+ if len(strings.TrimSpace(string(ss.NotificationDestination))) == 0 {
+ return errors.New("ServiceSecurity missing required notificationDestination")
+ }
+
+ if _, err := url.ParseRequestURI(string(ss.NotificationDestination)); err != nil {
+ return fmt.Errorf("ServiceSecurity has invalid notificationDestination, err=%s", err)
+ }
+
+ if len(ss.SecurityInfo) == 0 {
+ return errors.New("ServiceSecurity missing required SecurityInfo")
+ }
+ for _, securityInfo := range ss.SecurityInfo {
+ securityInfo.Validate()
+ }
+ return nil
+}
+
+func (si SecurityInformation) Validate() error {
+ if len(si.PrefSecurityMethods) == 0 {
+ return errors.New("SecurityInformation missing required PrefSecurityMethods")
+ }
+ return nil
+}
+
func createAccessTokenError(err AccessTokenErrError, message string) AccessTokenErr {
return AccessTokenErr{
Error: err,
"testing"
"github.com/stretchr/testify/assert"
+ "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
)
func TestValidateClientIdNotPresent(t *testing.T) {
valid, err = accessTokenUnderTest.Validate()
assert.Equal(t, true, valid)
}
+
+func TestValidateServiceSecurity(t *testing.T) {
+ serviceSecurityUnderTest := ServiceSecurity{}
+
+ err := serviceSecurityUnderTest.Validate()
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "missing")
+ assert.Contains(t, err.Error(), "notificationDestination")
+
+ serviceSecurityUnderTest.NotificationDestination = "invalid dest"
+ err = serviceSecurityUnderTest.Validate()
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "invalid")
+ assert.Contains(t, err.Error(), "notificationDestination")
+ }
+
+ serviceSecurityUnderTest.NotificationDestination = "http://golang.cafe/"
+ err = serviceSecurityUnderTest.Validate()
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "missing")
+ assert.Contains(t, err.Error(), "SecurityInfo")
+
+ serviceSecurityUnderTest.SecurityInfo = []SecurityInformation{
+ {
+ PrefSecurityMethods: []publishserviceapi.SecurityMethod{
+ publishserviceapi.SecurityMethodOAUTH,
+ },
+ },
+ }
+ err = serviceSecurityUnderTest.Validate()
+ assert.Nil(t, err)
+}
+
+func TestValidatePrefSecurityMethodsNotPresent(t *testing.T) {
+ securityInfoUnderTest := SecurityInformation{}
+ err := securityInfoUnderTest.Validate()
+
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "missing")
+ assert.Contains(t, err.Error(), "PrefSecurityMethods")
+
+ securityInfoUnderTest.PrefSecurityMethods = []publishserviceapi.SecurityMethod{
+ publishserviceapi.SecurityMethodOAUTH,
+ }
+ err = securityInfoUnderTest.Validate()
+ assert.Nil(t, err)
+}
package security
import (
+ "fmt"
"net/http"
+ "path"
"strings"
+ "sync"
"github.com/labstack/echo/v4"
publishRegister publishservice.PublishRegister
invokerRegister invokermanagement.InvokerRegister
keycloak keycloak.AccessManagement
+ trustedInvokers map[string]securityapi.ServiceSecurity
+ lock sync.Mutex
}
func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, km keycloak.AccessManagement) *Security {
publishRegister: publishRegister,
invokerRegister: invokerRegister,
keycloak: km,
+ trustedInvokers: make(map[string]securityapi.ServiceSecurity),
}
}
}
func (s *Security) PutTrustedInvokersApiInvokerId(ctx echo.Context, apiInvokerId string) error {
- return ctx.NoContent(http.StatusNotImplemented)
+ errMsg := "Unable to update security context due to %s."
+
+ if !s.invokerRegister.IsInvokerRegistered(apiInvokerId) {
+ return sendCoreError(ctx, http.StatusBadRequest, "Unable to update security context due to Invoker not registered")
+ }
+ serviceSecurity, err := getServiceSecurityFromRequest(ctx)
+ if err != nil {
+ return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
+ }
+
+ if err := serviceSecurity.Validate(); err != nil {
+ return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
+ }
+
+ err = s.prepareNewSecurityContext(&serviceSecurity, apiInvokerId)
+ if err != nil {
+ return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err))
+ }
+
+ uri := ctx.Request().Host + ctx.Request().URL.String()
+ ctx.Response().Header().Set(echo.HeaderLocation, ctx.Scheme()+`://`+path.Join(uri, apiInvokerId))
+
+ err = ctx.JSON(http.StatusCreated, s.trustedInvokers[apiInvokerId])
+ if err != nil {
+ // Something really bad happened, tell Echo that our handler failed
+ return err
+ }
+
+ return nil
+}
+
+func getServiceSecurityFromRequest(ctx echo.Context) (securityapi.ServiceSecurity, error) {
+ var serviceSecurity securityapi.ServiceSecurity
+ err := ctx.Bind(&serviceSecurity)
+ if err != nil {
+ return securityapi.ServiceSecurity{}, fmt.Errorf("invalid format for service security")
+ }
+ return serviceSecurity, nil
+}
+
+func (s *Security) prepareNewSecurityContext(newContext *securityapi.ServiceSecurity, apiInvokerId string) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ err := newContext.PrepareNewSecurityContext(s.publishRegister.GetAllPublishedServices())
+ if err != nil {
+ return err
+ }
+
+ s.trustedInvokers[apiInvokerId] = *newContext
+ return nil
}
func (s *Security) PostTrustedInvokersApiInvokerIdDelete(ctx echo.Context, apiInvokerId string) error {
}
return ctx.JSON(code, accessTokenErr)
}
+
+// This function wraps sending of an error in the Error format, and
+// handling the failure to marshal that.
+func sendCoreError(ctx echo.Context, code int, message string) error {
+ pd := common29122.ProblemDetails{
+ Cause: &message,
+ Status: &code,
+ }
+ err := ctx.JSON(code, pd)
+ return err
+}
"os"
"testing"
+ "oransc.org/nonrtric/capifcore/internal/common29122"
"oransc.org/nonrtric/capifcore/internal/keycloak"
+ "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
"oransc.org/nonrtric/capifcore/internal/securityapi"
"oransc.org/nonrtric/capifcore/internal/invokermanagement"
accessMgmMock.AssertCalled(t, "GetToken", clientId, clientSecret, "3gpp#"+aefId+":"+path, "invokerrealm")
}
+func TestPutTrustedInvokerSuccessfully(t *testing.T) {
+ invokerRegisterMock := invokermocks.InvokerRegister{}
+ invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true)
+ aefId := "aefId"
+ aefProfile := getAefProfile(aefId)
+ aefProfile.SecurityMethods = &[]publishserviceapi.SecurityMethod{
+ publishserviceapi.SecurityMethodPKI,
+ }
+ aefProfiles := []publishserviceapi.AefProfile{
+ aefProfile,
+ }
+ apiId := "apiId"
+ publishedServices := []publishserviceapi.ServiceAPIDescription{
+ {
+ ApiId: &apiId,
+ AefProfiles: &aefProfiles,
+ },
+ }
+ publishRegisterMock := publishmocks.PublishRegister{}
+ publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices)
+
+ requestHandler := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil)
+
+ invokerId := "invokerId"
+ serviceSecurityUnderTest := getServiceSecurity(aefId, apiId)
+ serviceSecurityUnderTest.SecurityInfo[0].ApiId = &apiId
+
+ result := testutil.NewRequest().Put("/trustedInvokers/"+invokerId).WithJsonBody(serviceSecurityUnderTest).Go(t, requestHandler)
+
+ assert.Equal(t, http.StatusCreated, result.Code())
+ var resultResponse securityapi.ServiceSecurity
+ err := result.UnmarshalBodyToObject(&resultResponse)
+ assert.NoError(t, err, "error unmarshaling response")
+ assert.NotEmpty(t, resultResponse.NotificationDestination)
+
+ for _, security := range resultResponse.SecurityInfo {
+ assert.Equal(t, *security.ApiId, apiId)
+ assert.Equal(t, *security.SelSecurityMethod, publishserviceapi.SecurityMethodPKI)
+ }
+ invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId)
+
+}
+
+func TestPutTrustedInkoverNotRegistered(t *testing.T) {
+ invokerRegisterMock := invokermocks.InvokerRegister{}
+ invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(false)
+
+ requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil)
+
+ invokerId := "invokerId"
+ serviceSecurityUnderTest := getServiceSecurity("aefId", "apiId")
+
+ result := testutil.NewRequest().Put("/trustedInvokers/"+invokerId).WithJsonBody(serviceSecurityUnderTest).Go(t, requestHandler)
+
+ badRequest := http.StatusBadRequest
+ assert.Equal(t, badRequest, result.Code())
+ var problemDetails common29122.ProblemDetails
+ err := result.UnmarshalBodyToObject(&problemDetails)
+ assert.NoError(t, err, "error unmarshaling response")
+ assert.Equal(t, &badRequest, problemDetails.Status)
+ assert.Contains(t, *problemDetails.Cause, "Invoker not registered")
+ invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId)
+}
+
+func TestPutTrustedInkoverInvalidInputServiceSecurity(t *testing.T) {
+ invokerRegisterMock := invokermocks.InvokerRegister{}
+ invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true)
+
+ requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil)
+
+ invokerId := "invokerId"
+ notificationUrl := "url"
+ serviceSecurityUnderTest := getServiceSecurity("aefId", "apiId")
+ serviceSecurityUnderTest.NotificationDestination = common29122.Uri(notificationUrl)
+
+ result := testutil.NewRequest().Put("/trustedInvokers/"+invokerId).WithJsonBody(serviceSecurityUnderTest).Go(t, requestHandler)
+
+ badRequest := http.StatusBadRequest
+ assert.Equal(t, badRequest, result.Code())
+ var problemDetails common29122.ProblemDetails
+ err := result.UnmarshalBodyToObject(&problemDetails)
+ assert.NoError(t, err, "error unmarshaling response")
+ assert.Equal(t, &badRequest, problemDetails.Status)
+ assert.Contains(t, *problemDetails.Cause, "ServiceSecurity has invalid notificationDestination")
+ invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId)
+}
+
+func TestPutTrustedInvokerInterfaceDetailsNotNil(t *testing.T) {
+ invokerRegisterMock := invokermocks.InvokerRegister{}
+ invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true)
+ aefId := "aefId"
+ aefProfile := getAefProfile(aefId)
+ aefProfile.SecurityMethods = &[]publishserviceapi.SecurityMethod{
+ publishserviceapi.SecurityMethodPKI,
+ }
+ aefProfiles := []publishserviceapi.AefProfile{
+ aefProfile,
+ }
+ apiId := "apiId"
+ publishedServices := []publishserviceapi.ServiceAPIDescription{
+ {
+ ApiId: &apiId,
+ AefProfiles: &aefProfiles,
+ },
+ }
+ publishRegisterMock := publishmocks.PublishRegister{}
+ publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices)
+
+ requestHandler := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil)
+
+ invokerId := "invokerId"
+ serviceSecurityUnderTest := getServiceSecurity(aefId, apiId)
+ serviceSecurityUnderTest.SecurityInfo[0] = securityapi.SecurityInformation{
+ ApiId: &apiId,
+ PrefSecurityMethods: []publishserviceapi.SecurityMethod{
+ publishserviceapi.SecurityMethodOAUTH,
+ },
+ InterfaceDetails: &publishserviceapi.InterfaceDescription{
+ SecurityMethods: &[]publishserviceapi.SecurityMethod{
+ publishserviceapi.SecurityMethodPSK,
+ },
+ },
+ }
+
+ result := testutil.NewRequest().Put("/trustedInvokers/"+invokerId).WithJsonBody(serviceSecurityUnderTest).Go(t, requestHandler)
+
+ assert.Equal(t, http.StatusCreated, result.Code())
+ var resultResponse securityapi.ServiceSecurity
+ err := result.UnmarshalBodyToObject(&resultResponse)
+ assert.NoError(t, err, "error unmarshaling response")
+ assert.NotEmpty(t, resultResponse.NotificationDestination)
+
+ for _, security := range resultResponse.SecurityInfo {
+ assert.Equal(t, apiId, *security.ApiId)
+ assert.Equal(t, publishserviceapi.SecurityMethodPSK, *security.SelSecurityMethod)
+ }
+ invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId)
+
+}
+
+func TestPutTrustedInvokerNotFoundSecurityMethod(t *testing.T) {
+ invokerRegisterMock := invokermocks.InvokerRegister{}
+ invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true)
+
+ aefProfiles := []publishserviceapi.AefProfile{
+ getAefProfile("aefId"),
+ }
+ apiId := "apiId"
+ publishedServices := []publishserviceapi.ServiceAPIDescription{
+ {
+ ApiId: &apiId,
+ AefProfiles: &aefProfiles,
+ },
+ }
+ publishRegisterMock := publishmocks.PublishRegister{}
+ publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices)
+
+ requestHandler := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil)
+
+ invokerId := "invokerId"
+ serviceSecurityUnderTest := getServiceSecurity("aefId", "apiId")
+
+ result := testutil.NewRequest().Put("/trustedInvokers/"+invokerId).WithJsonBody(serviceSecurityUnderTest).Go(t, requestHandler)
+
+ badRequest := http.StatusBadRequest
+ assert.Equal(t, badRequest, result.Code())
+ var problemDetails common29122.ProblemDetails
+ err := result.UnmarshalBodyToObject(&problemDetails)
+ assert.NoError(t, err, "error unmarshaling response")
+ assert.Equal(t, &badRequest, problemDetails.Status)
+ assert.Contains(t, *problemDetails.Cause, "not found")
+ assert.Contains(t, *problemDetails.Cause, "security method")
+ invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId)
+}
+
func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, keycloakMgm keycloak.AccessManagement) *echo.Echo {
swagger, err := securityapi.GetSwagger()
if err != nil {
securityapi.RegisterHandlers(e, s)
return e
}
+
+func getServiceSecurity(aefId string, apiId string) securityapi.ServiceSecurity {
+ return securityapi.ServiceSecurity{
+ NotificationDestination: common29122.Uri("http://golang.cafe/"),
+ SecurityInfo: []securityapi.SecurityInformation{
+ {
+ AefId: &aefId,
+ ApiId: &apiId,
+ PrefSecurityMethods: []publishserviceapi.SecurityMethod{
+ publishserviceapi.SecurityMethodOAUTH,
+ },
+ },
+ },
+ }
+}
+
+func getAefProfile(aefId string) publishserviceapi.AefProfile {
+ return publishserviceapi.AefProfile{
+ AefId: aefId,
+ Versions: []publishserviceapi.Version{
+ {
+ Resources: &[]publishserviceapi.Resource{
+ {
+ CommType: "REQUEST_RESPONSE",
+ },
+ },
+ },
+ },
+ }
+}