From 65f718afb57c1aab02fa48d34582f133ff65d5af Mon Sep 17 00:00:00 2001 From: shikha0203 Date: Tue, 7 Feb 2023 14:54:29 +0000 Subject: [PATCH 01/16] Added typeupdate and typeaccess Issue-ID: NONRTRIC-814 Signed-off-by: shikha0203 Change-Id: I4d74454a88b5d1bb3d98375510211ddb69476e3d --- .../invokermanagement/invokermanagement.go | 12 ----- .../internal/publishservice/publishservice.go | 53 ++-------------------- .../internal/publishservice/publishservice_test.go | 2 - capifcore/internal/publishserviceapi/typeaccess.go | 30 ++++++++++++ capifcore/internal/publishserviceapi/typeupdate.go | 26 +++++++++++ 5 files changed, 59 insertions(+), 64 deletions(-) create mode 100644 capifcore/internal/publishserviceapi/typeaccess.go create mode 100644 capifcore/internal/publishserviceapi/typeupdate.go diff --git a/capifcore/internal/invokermanagement/invokermanagement.go b/capifcore/internal/invokermanagement/invokermanagement.go index c6f2db3..1fbe2f0 100644 --- a/capifcore/internal/invokermanagement/invokermanagement.go +++ b/capifcore/internal/invokermanagement/invokermanagement.go @@ -21,14 +21,12 @@ package invokermanagement import ( - "errors" "fmt" "net/http" "path" "sync" "oransc.org/nonrtric/capifcore/internal/eventsapi" - publishapi "oransc.org/nonrtric/capifcore/internal/publishserviceapi" "oransc.org/nonrtric/capifcore/internal/common29122" invokerapi "oransc.org/nonrtric/capifcore/internal/invokermanagementapi" @@ -214,20 +212,10 @@ func (im *InvokerManager) validateInvoker(invoker invokerapi.APIInvokerEnrolment if err := invoker.Validate(); err != nil { return err } - if !im.areAPIsPublished(invoker.ApiList) { - return errors.New("some APIs needed by invoker are not registered") - } return nil } -func (im *InvokerManager) areAPIsPublished(apis *invokerapi.APIList) bool { - if apis == nil { - return true - } - return im.publishRegister.AreAPIsPublished((*[]publishapi.ServiceAPIDescription)(apis)) -} - func (im *InvokerManager) sendEvent(invokerId string, eventType eventsapi.CAPIFEvent) { invokerIds := []string{invokerId} event := eventsapi.EventNotification{ diff --git a/capifcore/internal/publishservice/publishservice.go b/capifcore/internal/publishservice/publishservice.go index ee3efef..bf79899 100644 --- a/capifcore/internal/publishservice/publishservice.go +++ b/capifcore/internal/publishservice/publishservice.go @@ -42,9 +42,6 @@ import ( //go:generate mockery --name PublishRegister type PublishRegister interface { - // Checks if the provided APIs are published. - // Returns true if all provided APIs have been published, false otherwise. - AreAPIsPublished(serviceDescriptions *[]publishapi.ServiceAPIDescription) bool // Checks if the provided API is published. // Returns true if the provided API has been published, false otherwise. IsAPIPublished(aefId, path string) bool @@ -71,15 +68,6 @@ func NewPublishService(serviceRegister providermanagement.ServiceRegister, hm he } } -func (ps *PublishService) AreAPIsPublished(serviceDescriptions *[]publishapi.ServiceAPIDescription) bool { - - if serviceDescriptions != nil { - registeredApis := ps.getAllAefIds() - return checkNewDescriptions(*serviceDescriptions, registeredApis) - } - return true -} - func (ps *PublishService) getAllAefIds() []string { ps.lock.Lock() defer ps.lock.Unlock() @@ -87,46 +75,12 @@ func (ps *PublishService) getAllAefIds() []string { allIds := []string{} for _, descriptions := range ps.publishedServices { for _, description := range descriptions { - allIds = append(allIds, getIdsFromDescription(description)...) + allIds = append(allIds, description.GetAefIds()...) } } return allIds } -func getIdsFromDescription(description publishapi.ServiceAPIDescription) []string { - allIds := []string{} - if description.AefProfiles != nil { - for _, aefProfile := range *description.AefProfiles { - allIds = append(allIds, aefProfile.AefId) - } - } - return allIds -} - -func checkNewDescriptions(newDescriptions []publishapi.ServiceAPIDescription, registeredAefIds []string) bool { - registered := true - for _, newApi := range newDescriptions { - if !checkProfiles(newApi.AefProfiles, registeredAefIds) { - registered = false - break - } - } - return registered -} - -func checkProfiles(newProfiles *[]publishapi.AefProfile, registeredAefIds []string) bool { - allRegistered := true - if newProfiles != nil { - for _, profile := range *newProfiles { - if !slices.Contains(registeredAefIds, profile.AefId) { - allRegistered = false - break - } - } - } - return allRegistered -} - func (ps *PublishService) IsAPIPublished(aefId, path string) bool { return slices.Contains(ps.getAllAefIds(), aefId) } @@ -181,8 +135,7 @@ func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) e } } - newId := "api_id_" + newServiceAPIDescription.ApiName - newServiceAPIDescription.ApiId = &newId + newServiceAPIDescription.PrepareNewService() shouldReturn, returnValue := ps.installHelmChart(newServiceAPIDescription, ctx) if shouldReturn { @@ -307,11 +260,11 @@ func (ps *PublishService) PutApfIdServiceApisServiceApiId(ctx echo.Context, apfI if err != nil { return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err)) } - ps.updateDescription(pos, apfId, &updatedServiceDescription, &publishedService) err = ps.checkProfilesRegistered(apfId, *updatedServiceDescription.AefProfiles) if err != nil { return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err)) } + ps.updateDescription(pos, apfId, &updatedServiceDescription, &publishedService) publishedService.AefProfiles = updatedServiceDescription.AefProfiles ps.publishedServices[apfId][pos] = publishedService err = ctx.JSON(http.StatusOK, publishedService) diff --git a/capifcore/internal/publishservice/publishservice_test.go b/capifcore/internal/publishservice/publishservice_test.go index 3c19204..b69b956 100644 --- a/capifcore/internal/publishservice/publishservice_test.go +++ b/capifcore/internal/publishservice/publishservice_test.go @@ -80,8 +80,6 @@ func TestPublishUnpublishService(t *testing.T) { assert.Equal(t, newApiId, *resultService.ApiId) assert.Equal(t, "http://example.com/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation)) newServiceDescription.ApiId = &newApiId - wantedAPILIst := []publishapi.ServiceAPIDescription{newServiceDescription} - assert.True(t, serviceUnderTest.AreAPIsPublished(&wantedAPILIst)) assert.True(t, serviceUnderTest.IsAPIPublished(aefId, apiName)) serviceRegisterMock.AssertCalled(t, "GetAefsForPublisher", apfId) helmManagerMock.AssertCalled(t, "InstallHelmChart", namespace, repoName, chartName, releaseName) diff --git a/capifcore/internal/publishserviceapi/typeaccess.go b/capifcore/internal/publishserviceapi/typeaccess.go new file mode 100644 index 0000000..32c1a7a --- /dev/null +++ b/capifcore/internal/publishserviceapi/typeaccess.go @@ -0,0 +1,30 @@ +// - +// +// ========================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 publishserviceapi + +func (sd ServiceAPIDescription) GetAefIds() []string { + allIds := []string{} + if sd.AefProfiles != nil { + for _, aefProfile := range *sd.AefProfiles { + allIds = append(allIds, aefProfile.AefId) + } + } + return allIds +} diff --git a/capifcore/internal/publishserviceapi/typeupdate.go b/capifcore/internal/publishserviceapi/typeupdate.go new file mode 100644 index 0000000..98e059c --- /dev/null +++ b/capifcore/internal/publishserviceapi/typeupdate.go @@ -0,0 +1,26 @@ +// - +// ========================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 publishserviceapi + +func (sd *ServiceAPIDescription) PrepareNewService() { + apiName := "api_id_" + sd.ApiName + sd.ApiId = &apiName +} -- 2.16.6 From e499f63287a0786ed1fb94f5bf2cce09422c518f Mon Sep 17 00:00:00 2001 From: elinuxhenrik Date: Tue, 7 Feb 2023 16:50:05 +0100 Subject: [PATCH 02/16] Generate new mock for PublishRegister Issue-ID: NONRTRIC-814 Signed-off-by: elinuxhenrik Change-Id: Ib6ed58f3d777e47d968f5c87925a8044a176905e --- capifcore/internal/publishservice/mocks/PublishRegister.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/capifcore/internal/publishservice/mocks/PublishRegister.go b/capifcore/internal/publishservice/mocks/PublishRegister.go index a798a71..df25620 100644 --- a/capifcore/internal/publishservice/mocks/PublishRegister.go +++ b/capifcore/internal/publishservice/mocks/PublishRegister.go @@ -13,20 +13,6 @@ type PublishRegister struct { mock.Mock } -// AreAPIsPublished provides a mock function with given fields: serviceDescriptions -func (_m *PublishRegister) AreAPIsPublished(serviceDescriptions *[]publishserviceapi.ServiceAPIDescription) bool { - ret := _m.Called(serviceDescriptions) - - var r0 bool - if rf, ok := ret.Get(0).(func(*[]publishserviceapi.ServiceAPIDescription) bool); ok { - r0 = rf(serviceDescriptions) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - // GetAllPublishedServices provides a mock function with given fields: func (_m *PublishRegister) GetAllPublishedServices() []publishserviceapi.ServiceAPIDescription { ret := _m.Called() -- 2.16.6 From bf237808ac109b30461a453c59ff4e9cc9b297f4 Mon Sep 17 00:00:00 2001 From: elinuxhenrik Date: Wed, 8 Feb 2023 14:26:24 +0100 Subject: [PATCH 03/16] Refactor check earlier registration Issue-ID: NONRTRIC-814 Signed-off-by: elinuxhenrik Change-Id: Ic9f6038faa123325a0c8a8be7a7f5cc5f7a586a0 --- capifcore/internal/invokermanagement/invokermanagement.go | 12 ++++++------ capifcore/internal/invokermanagementapi/typevalidation.go | 7 +++++-- .../internal/invokermanagementapi/typevalidation_test.go | 6 +++--- capifcore/internal/providermanagement/providermanagement.go | 12 ++++++------ capifcore/internal/providermanagementapi/typevalidation.go | 7 +++++-- .../internal/providermanagementapi/typevalidation_test.go | 6 +++--- capifcore/internal/publishservice/publishservice.go | 12 ++++++------ capifcore/internal/publishserviceapi/typevalidation.go | 7 +++++-- capifcore/internal/publishserviceapi/typevalidation_test.go | 6 +++--- 9 files changed, 42 insertions(+), 33 deletions(-) diff --git a/capifcore/internal/invokermanagement/invokermanagement.go b/capifcore/internal/invokermanagement/invokermanagement.go index 1fbe2f0..43bdc02 100644 --- a/capifcore/internal/invokermanagement/invokermanagement.go +++ b/capifcore/internal/invokermanagement/invokermanagement.go @@ -106,8 +106,8 @@ func (im *InvokerManager) PostOnboardedInvokers(ctx echo.Context) error { return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "invalid format for invoker")) } - if im.isInvokerOnboarded(newInvoker) { - return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errMsg, "invoker already onboarded")) + if err := im.isInvokerOnboarded(newInvoker); err != nil { + return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errMsg, err)) } if err := im.validateInvoker(newInvoker, ctx); err != nil { @@ -129,13 +129,13 @@ func (im *InvokerManager) PostOnboardedInvokers(ctx echo.Context) error { return nil } -func (im *InvokerManager) isInvokerOnboarded(newInvoker invokerapi.APIInvokerEnrolmentDetails) bool { +func (im *InvokerManager) isInvokerOnboarded(newInvoker invokerapi.APIInvokerEnrolmentDetails) error { for _, invoker := range im.onboardedInvokers { - if invoker.IsOnboarded(newInvoker) { - return true + if err := invoker.ValidateAlreadyOnboarded(newInvoker); err != nil { + return err } } - return false + return nil } func (im *InvokerManager) prepareNewInvoker(newInvoker *invokerapi.APIInvokerEnrolmentDetails) { diff --git a/capifcore/internal/invokermanagementapi/typevalidation.go b/capifcore/internal/invokermanagementapi/typevalidation.go index 8bfb784..10db338 100644 --- a/capifcore/internal/invokermanagementapi/typevalidation.go +++ b/capifcore/internal/invokermanagementapi/typevalidation.go @@ -42,6 +42,9 @@ func (ied *APIInvokerEnrolmentDetails) Validate() error { return nil } -func (ied *APIInvokerEnrolmentDetails) IsOnboarded(otherInvoker APIInvokerEnrolmentDetails) bool { - return ied.OnboardingInformation.ApiInvokerPublicKey == otherInvoker.OnboardingInformation.ApiInvokerPublicKey +func (ied *APIInvokerEnrolmentDetails) ValidateAlreadyOnboarded(otherInvoker APIInvokerEnrolmentDetails) error { + if ied.OnboardingInformation.ApiInvokerPublicKey == otherInvoker.OnboardingInformation.ApiInvokerPublicKey { + return errors.New("invoker with identical public key already onboarded") + } + return nil } diff --git a/capifcore/internal/invokermanagementapi/typevalidation_test.go b/capifcore/internal/invokermanagementapi/typevalidation_test.go index 499b0a1..b6b491d 100644 --- a/capifcore/internal/invokermanagementapi/typevalidation_test.go +++ b/capifcore/internal/invokermanagementapi/typevalidation_test.go @@ -54,7 +54,7 @@ func TestValidateInvoker(t *testing.T) { assert.Nil(t, err) } -func TestIsOnboarded(t *testing.T) { +func TestValidateAlreadyOnboarded(t *testing.T) { publicKey := "publicKey" invokerUnderTest := APIInvokerEnrolmentDetails{ OnboardingInformation: OnboardingInformation{ @@ -67,8 +67,8 @@ func TestIsOnboarded(t *testing.T) { ApiInvokerPublicKey: "otherPublicKey", }, } - assert.False(t, invokerUnderTest.IsOnboarded(otherInvoker)) + assert.Nil(t, invokerUnderTest.ValidateAlreadyOnboarded(otherInvoker)) otherInvoker.OnboardingInformation.ApiInvokerPublicKey = publicKey - assert.True(t, invokerUnderTest.IsOnboarded(otherInvoker)) + assert.NotNil(t, invokerUnderTest.ValidateAlreadyOnboarded(otherInvoker)) } diff --git a/capifcore/internal/providermanagement/providermanagement.go b/capifcore/internal/providermanagement/providermanagement.go index 5e9211e..d5e7a63 100644 --- a/capifcore/internal/providermanagement/providermanagement.go +++ b/capifcore/internal/providermanagement/providermanagement.go @@ -76,8 +76,8 @@ func (pm *ProviderManager) PostRegistrations(ctx echo.Context) error { return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "invalid format for provider")) } - if pm.isProviderRegistered(newProvider) { - return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errMsg, "provider already registered")) + if err := pm.isProviderRegistered(newProvider); err != nil { + return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errMsg, err)) } if err := newProvider.Validate(); err != nil { @@ -95,13 +95,13 @@ func (pm *ProviderManager) PostRegistrations(ctx echo.Context) error { return nil } -func (pm *ProviderManager) isProviderRegistered(newProvider provapi.APIProviderEnrolmentDetails) bool { +func (pm *ProviderManager) isProviderRegistered(newProvider provapi.APIProviderEnrolmentDetails) error { for _, prov := range pm.registeredProviders { - if prov.IsRegistered(newProvider) { - return true + if err := prov.ValidateAlreadyRegistered(newProvider); err != nil { + return err } } - return false + return nil } func (pm *ProviderManager) prepareNewProvider(newProvider *provapi.APIProviderEnrolmentDetails) { diff --git a/capifcore/internal/providermanagementapi/typevalidation.go b/capifcore/internal/providermanagementapi/typevalidation.go index 3c514a8..bb32991 100644 --- a/capifcore/internal/providermanagementapi/typevalidation.go +++ b/capifcore/internal/providermanagementapi/typevalidation.go @@ -68,6 +68,9 @@ func (pd APIProviderEnrolmentDetails) validateFunctions() error { return nil } -func (pd APIProviderEnrolmentDetails) IsRegistered(otherProvider APIProviderEnrolmentDetails) bool { - return pd.RegSec == otherProvider.RegSec +func (pd APIProviderEnrolmentDetails) ValidateAlreadyRegistered(otherProvider APIProviderEnrolmentDetails) error { + if pd.RegSec == otherProvider.RegSec { + return errors.New("provider with identical regSec already registered") + } + return nil } diff --git a/capifcore/internal/providermanagementapi/typevalidation_test.go b/capifcore/internal/providermanagementapi/typevalidation_test.go index b79a13b..a5a4e3c 100644 --- a/capifcore/internal/providermanagementapi/typevalidation_test.go +++ b/capifcore/internal/providermanagementapi/typevalidation_test.go @@ -106,7 +106,7 @@ func TestValidateAPIProviderEnrolmentDetails(t *testing.T) { assert.Nil(t, providerDetailsUnderTest.Validate()) } -func TestIsRegistered(t *testing.T) { +func TestValidateAlreadyRegistered(t *testing.T) { regSec := "regSec" providerUnderTest := APIProviderEnrolmentDetails{ RegSec: regSec, @@ -115,10 +115,10 @@ func TestIsRegistered(t *testing.T) { otherProvider := APIProviderEnrolmentDetails{ RegSec: "otherRegSec", } - assert.False(t, providerUnderTest.IsRegistered(otherProvider)) + assert.Nil(t, providerUnderTest.ValidateAlreadyRegistered(otherProvider)) otherProvider.RegSec = regSec - assert.True(t, providerUnderTest.IsRegistered(otherProvider)) + assert.NotNil(t, providerUnderTest.ValidateAlreadyRegistered(otherProvider)) } func getProvider() APIProviderEnrolmentDetails { diff --git a/capifcore/internal/publishservice/publishservice.go b/capifcore/internal/publishservice/publishservice.go index bf79899..7960f12 100644 --- a/capifcore/internal/publishservice/publishservice.go +++ b/capifcore/internal/publishservice/publishservice.go @@ -118,8 +118,8 @@ func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) e return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errorMsg, "invalid format for service "+apfId)) } - if ps.isServicePublished(newServiceAPIDescription) { - return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errorMsg, "service already published")) + if err := ps.isServicePublished(newServiceAPIDescription); err != nil { + return sendCoreError(ctx, http.StatusForbidden, fmt.Sprintf(errorMsg, err)) } if err := newServiceAPIDescription.Validate(); err != nil { @@ -161,15 +161,15 @@ func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) e return nil } -func (ps *PublishService) isServicePublished(newService publishapi.ServiceAPIDescription) bool { +func (ps *PublishService) isServicePublished(newService publishapi.ServiceAPIDescription) error { for _, services := range ps.publishedServices { for _, service := range services { - if service.IsPublished(newService) { - return true + if err := service.ValidateAlreadyPublished(newService); err != nil { + return err } } } - return false + return nil } func (ps *PublishService) installHelmChart(newServiceAPIDescription publishapi.ServiceAPIDescription, ctx echo.Context) (bool, error) { diff --git a/capifcore/internal/publishserviceapi/typevalidation.go b/capifcore/internal/publishserviceapi/typevalidation.go index ad19324..287a07d 100644 --- a/capifcore/internal/publishserviceapi/typevalidation.go +++ b/capifcore/internal/publishserviceapi/typevalidation.go @@ -32,6 +32,9 @@ func (sd ServiceAPIDescription) Validate() error { return nil } -func (sd ServiceAPIDescription) IsPublished(otherService ServiceAPIDescription) bool { - return sd.ApiName == otherService.ApiName +func (sd ServiceAPIDescription) ValidateAlreadyPublished(otherService ServiceAPIDescription) error { + if sd.ApiName == otherService.ApiName { + return errors.New("service with identical apiName is already published") + } + return nil } diff --git a/capifcore/internal/publishserviceapi/typevalidation_test.go b/capifcore/internal/publishserviceapi/typevalidation_test.go index 39e3619..d7b59d5 100644 --- a/capifcore/internal/publishserviceapi/typevalidation_test.go +++ b/capifcore/internal/publishserviceapi/typevalidation_test.go @@ -39,7 +39,7 @@ func TestValidate(t *testing.T) { } -func TestIsServicePublished(t *testing.T) { +func TestValidateAlreadyPublished(t *testing.T) { apiName := "apiName" serviceUnderTest := ServiceAPIDescription{ ApiName: apiName, @@ -48,8 +48,8 @@ func TestIsServicePublished(t *testing.T) { otherService := ServiceAPIDescription{ ApiName: "otherApiName", } - assert.False(t, serviceUnderTest.IsPublished(otherService)) + assert.Nil(t, serviceUnderTest.ValidateAlreadyPublished(otherService)) otherService.ApiName = apiName - assert.True(t, serviceUnderTest.IsPublished(otherService)) + assert.NotNil(t, serviceUnderTest.ValidateAlreadyPublished(otherService)) } -- 2.16.6 From 8bae1ff85eef426eafced1dabac9fadd733dd29a Mon Sep 17 00:00:00 2001 From: elinuxhenrik Date: Thu, 9 Feb 2023 09:52:33 +0100 Subject: [PATCH 04/16] Remov unnecessary comment Issue-ID: NONRTRIC-814 Signed-off-by: elinuxhenrik Change-Id: I9f034e1aa747f01b60d0bf8028d957206ddb1c5e --- capifcore/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/capifcore/main.go b/capifcore/main.go index 1c1890f..3ee4192 100644 --- a/capifcore/main.go +++ b/capifcore/main.go @@ -83,7 +83,6 @@ func main() { } func getEcho() *echo.Echo { - // This is how you set up a basic Echo router e := echo.New() // Log all requests e.Use(echomiddleware.Logger()) -- 2.16.6 From cfa08775db2ed44e603b0ceccf36a50f59bd679a Mon Sep 17 00:00:00 2001 From: ychacon Date: Wed, 8 Feb 2023 09:41:34 +0100 Subject: [PATCH 05/16] Changes in implementation of Security API - get Token Issue-ID: NONRTRIC-836 Signed-off-by: ychacon Change-Id: I5eb350a63582c6e24651dd89f392dab0a9db1d8b --- capifcore/internal/securityapi/typeaccess.go | 38 +++++++++ capifcore/internal/securityapi/typevalidation.go | 62 ++++++++++++++ .../internal/securityapi/typevalidation_test.go | 97 ++++++++++++++++++++++ capifcore/internal/securityservice/security.go | 79 ++++++++++++------ .../internal/securityservice/security_test.go | 58 ++++++------- 5 files changed, 278 insertions(+), 56 deletions(-) create mode 100644 capifcore/internal/securityapi/typeaccess.go create mode 100644 capifcore/internal/securityapi/typevalidation.go create mode 100644 capifcore/internal/securityapi/typevalidation_test.go diff --git a/capifcore/internal/securityapi/typeaccess.go b/capifcore/internal/securityapi/typeaccess.go new file mode 100644 index 0000000..8b88312 --- /dev/null +++ b/capifcore/internal/securityapi/typeaccess.go @@ -0,0 +1,38 @@ +// - +// ========================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 ( + "github.com/labstack/echo/v4" +) + +func (tokenReq *AccessTokenReq) GetAccessTokenReq(ctx echo.Context) { + clientId := ctx.FormValue("client_id") + clientSecret := ctx.FormValue("client_secret") + scope := ctx.FormValue("scope") + grantType := ctx.FormValue("grant_type") + + tokenReq.ClientId = clientId + tokenReq.ClientSecret = &clientSecret + tokenReq.Scope = &scope + tokenReq.GrantType = AccessTokenReqGrantType(grantType) + +} diff --git a/capifcore/internal/securityapi/typevalidation.go b/capifcore/internal/securityapi/typevalidation.go new file mode 100644 index 0000000..90dbda3 --- /dev/null +++ b/capifcore/internal/securityapi/typevalidation.go @@ -0,0 +1,62 @@ +// - +// ========================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 ( + "strings" +) + +func (tokenReq AccessTokenReq) Validate() (bool, AccessTokenErr) { + + if tokenReq.ClientId == "" { + return false, createAccessTokenError(AccessTokenErrErrorInvalidRequest, "Invalid request") + } + + if tokenReq.GrantType != AccessTokenReqGrantTypeClientCredentials { + return false, createAccessTokenError(AccessTokenErrErrorInvalidGrant, "Invalid value for grant_type") + } + + //3gpp#aefId1:apiName1,apiName2,…apiNameX;aefId2:apiName1,apiName2,…apiNameY;…aefIdN:apiName1,apiName2,…apiNameZ + if tokenReq.Scope != nil { + scope := strings.Split(*tokenReq.Scope, "#") + if len(scope) < 2 { + return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Malformed scope") + } + if scope[0] != "3gpp" { + return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Scope should start with 3gpp") + } + aefList := strings.Split(scope[1], ";") + for _, aef := range aefList { + apiList := strings.Split(aef, ":") + if len(apiList) < 2 { + return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Malformed scope") + } + } + } + return true, AccessTokenErr{} +} + +func createAccessTokenError(err AccessTokenErrError, message string) AccessTokenErr { + return AccessTokenErr{ + Error: err, + ErrorDescription: &message, + } +} diff --git a/capifcore/internal/securityapi/typevalidation_test.go b/capifcore/internal/securityapi/typevalidation_test.go new file mode 100644 index 0000000..0515d06 --- /dev/null +++ b/capifcore/internal/securityapi/typevalidation_test.go @@ -0,0 +1,97 @@ +// - +// ========================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" +) + +func TestValidateClientIdNotPresent(t *testing.T) { + accessTokenUnderTest := AccessTokenReq{} + valid, err := accessTokenUnderTest.Validate() + + assert.Equal(t, false, valid) + assert.Equal(t, AccessTokenErrErrorInvalidRequest, err.Error) + assert.Equal(t, "Invalid request", *err.ErrorDescription) +} + +func TestValidateGrantType(t *testing.T) { + accessTokenUnderTest := AccessTokenReq{ + ClientId: "clientId", + GrantType: AccessTokenReqGrantType(""), + } + valid, err := accessTokenUnderTest.Validate() + + assert.Equal(t, false, valid) + assert.Equal(t, AccessTokenErrErrorInvalidGrant, err.Error) + assert.Equal(t, "Invalid value for grant_type", *err.ErrorDescription) + + accessTokenUnderTest.GrantType = AccessTokenReqGrantType("client_credentials") + valid, err = accessTokenUnderTest.Validate() + assert.Equal(t, true, valid) +} + +func TestValidateScopeNotValid(t *testing.T) { + scope := "scope#aefId:path" + accessTokenUnderTest := AccessTokenReq{ + ClientId: "clientId", + GrantType: ("client_credentials"), + Scope: &scope, + } + valid, err := accessTokenUnderTest.Validate() + + assert.Equal(t, false, valid) + assert.Equal(t, AccessTokenErrErrorInvalidScope, err.Error) + assert.Equal(t, "Scope should start with 3gpp", *err.ErrorDescription) + + scope = "3gpp#aefId:path" + accessTokenUnderTest.Scope = &scope + valid, err = accessTokenUnderTest.Validate() + assert.Equal(t, true, valid) +} + +func TestValidateScopeMalformed(t *testing.T) { + scope := "3gpp" + accessTokenUnderTest := AccessTokenReq{ + ClientId: "clientId", + GrantType: ("client_credentials"), + Scope: &scope, + } + valid, err := accessTokenUnderTest.Validate() + + assert.Equal(t, false, valid) + assert.Equal(t, AccessTokenErrErrorInvalidScope, err.Error) + assert.Equal(t, "Malformed scope", *err.ErrorDescription) + + scope = "3gpp#aefId" + accessTokenUnderTest.Scope = &scope + valid, err = accessTokenUnderTest.Validate() + assert.Equal(t, false, valid) + assert.Equal(t, AccessTokenErrErrorInvalidScope, err.Error) + assert.Equal(t, "Malformed scope", *err.ErrorDescription) + + scope = "3gpp#aefId:path" + accessTokenUnderTest.Scope = &scope + valid, err = accessTokenUnderTest.Validate() + assert.Equal(t, true, valid) +} diff --git a/capifcore/internal/securityservice/security.go b/capifcore/internal/securityservice/security.go index 3df2918..d71ff0d 100644 --- a/capifcore/internal/securityservice/security.go +++ b/capifcore/internal/securityservice/security.go @@ -23,7 +23,9 @@ package security import ( "net/http" "strings" + "time" + "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" "oransc.org/nonrtric/capifcore/internal/common29122" @@ -34,6 +36,8 @@ import ( "oransc.org/nonrtric/capifcore/internal/publishservice" ) +var jwtKey = "my-secret-key" + type Security struct { serviceRegister providermanagement.ServiceRegister publishRegister publishservice.PublishRegister @@ -49,34 +53,62 @@ func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegi } func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId string) error { - clientId := ctx.FormValue("client_id") - clientSecret := ctx.FormValue("client_secret") - scope := ctx.FormValue("scope") + var accessTokenReq securityapi.AccessTokenReq + accessTokenReq.GetAccessTokenReq(ctx) - if !s.invokerRegister.IsInvokerRegistered(clientId) { - return sendCoreError(ctx, http.StatusBadRequest, "Invoker not registered") + if valid, err := accessTokenReq.Validate(); !valid { + return ctx.JSON(http.StatusBadRequest, err) } - if !s.invokerRegister.VerifyInvokerSecret(clientId, clientSecret) { - return sendCoreError(ctx, http.StatusBadRequest, "Invoker secret not valid") + + if !s.invokerRegister.IsInvokerRegistered(accessTokenReq.ClientId) { + return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorInvalidClient, "Invoker not registered") } - if scope != "" { - scopeData := strings.Split(strings.Split(scope, "#")[1], ":") - if !s.serviceRegister.IsFunctionRegistered(scopeData[0]) { - return sendCoreError(ctx, http.StatusBadRequest, "Function not registered") - } - if !s.publishRegister.IsAPIPublished(scopeData[0], scopeData[1]) { - return sendCoreError(ctx, http.StatusBadRequest, "API not published") + + if !s.invokerRegister.VerifyInvokerSecret(accessTokenReq.ClientId, *accessTokenReq.ClientSecret) { + return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorUnauthorizedClient, "Invoker secret not valid") + } + + if accessTokenReq.Scope != nil { + scope := strings.Split(*accessTokenReq.Scope, "#") + aefList := strings.Split(scope[1], ";") + for _, aef := range aefList { + apiList := strings.Split(aef, ":") + if !s.serviceRegister.IsFunctionRegistered(apiList[0]) { + return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorInvalidScope, "AEF Function not registered") + } + for _, api := range strings.Split(apiList[1], ",") { + if !s.publishRegister.IsAPIPublished(apiList[0], api) { + return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorInvalidScope, "API not published") + } + } } } + expirationTime := time.Now().Add(time.Hour).Unix() + + claims := &jwt.MapClaims{ + "iss": accessTokenReq.ClientId, + "exp": expirationTime, + "data": map[string]interface{}{ + "scope": accessTokenReq.Scope, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(jwtKey)) + if err != nil { + // If there is an error in creating the JWT return an internal server error + return err + } + accessTokenResp := securityapi.AccessTokenRsp{ - AccessToken: "asdadfsrt dsr t5", - ExpiresIn: 0, - Scope: &scope, + AccessToken: tokenString, + ExpiresIn: common29122.DurationSec(expirationTime), + Scope: accessTokenReq.Scope, TokenType: "Bearer", } - err := ctx.JSON(http.StatusCreated, accessTokenResp) + err = ctx.JSON(http.StatusCreated, accessTokenResp) if err != nil { // Something really bad happened, tell Echo that our handler failed return err @@ -105,11 +137,10 @@ func (s *Security) PostTrustedInvokersApiInvokerIdUpdate(ctx echo.Context, apiIn return ctx.NoContent(http.StatusNotImplemented) } -func sendCoreError(ctx echo.Context, code int, message string) error { - pd := common29122.ProblemDetails{ - Cause: &message, - Status: &code, +func sendAccessTokenError(ctx echo.Context, code int, err securityapi.AccessTokenErrError, message string) error { + accessTokenErr := securityapi.AccessTokenErr{ + Error: err, + ErrorDescription: &message, } - err := ctx.JSON(code, pd) - return err + return ctx.JSON(code, accessTokenErr) } diff --git a/capifcore/internal/securityservice/security_test.go b/capifcore/internal/securityservice/security_test.go index 7043cca..6c62161 100644 --- a/capifcore/internal/securityservice/security_test.go +++ b/capifcore/internal/securityservice/security_test.go @@ -35,8 +35,6 @@ import ( "github.com/labstack/echo/v4" - "oransc.org/nonrtric/capifcore/internal/common29122" - invokermocks "oransc.org/nonrtric/capifcore/internal/invokermanagement/mocks" servicemocks "oransc.org/nonrtric/capifcore/internal/providermanagement/mocks" publishmocks "oransc.org/nonrtric/capifcore/internal/publishservice/mocks" @@ -65,9 +63,10 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) { aefId := "aefId" path := "path" data.Set("client_id", clientId) - data.Add("client_secret", clientSecret) - data.Add("grant_type", "client_credentials") - data.Add("scope", "scope#"+aefId+":"+path) + data.Set("client_secret", clientSecret) + data.Set("grant_type", "client_credentials") + data.Set("scope", "3gpp#"+aefId+":"+path) + encodedData := data.Encode() result := testutil.NewRequest().Post("/securities/invokerId/token").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(encodedData)).Go(t, requestHandler) @@ -77,9 +76,8 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) { err := result.UnmarshalBodyToObject(&resultResponse) assert.NoError(t, err, "error unmarshaling response") assert.NotEmpty(t, resultResponse.AccessToken) - assert.Equal(t, "scope#"+aefId+":"+path, *resultResponse.Scope) + assert.Equal(t, "3gpp#"+aefId+":"+path, *resultResponse.Scope) assert.Equal(t, securityapi.AccessTokenRspTokenTypeBearer, resultResponse.TokenType) - assert.Equal(t, common29122.DurationSec(0), resultResponse.ExpiresIn) invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", clientId) invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret) serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId) @@ -96,19 +94,18 @@ func TestPostSecurityIdTokenInvokerNotRegistered(t *testing.T) { data.Set("client_id", "id") data.Add("client_secret", "secret") data.Add("grant_type", "client_credentials") - data.Add("scope", "scope#aefId:path") + data.Add("scope", "3gpp#aefId:path") encodedData := data.Encode() result := testutil.NewRequest().Post("/securities/invokerId/token").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(encodedData)).Go(t, requestHandler) assert.Equal(t, http.StatusBadRequest, result.Code()) - var problemDetails common29122.ProblemDetails - err := result.UnmarshalBodyToObject(&problemDetails) + var errDetails securityapi.AccessTokenErr + err := result.UnmarshalBodyToObject(&errDetails) assert.NoError(t, err, "error unmarshaling response") - badRequest := http.StatusBadRequest - assert.Equal(t, &badRequest, problemDetails.Status) + assert.Equal(t, securityapi.AccessTokenErrErrorInvalidClient, errDetails.Error) errMsg := "Invoker not registered" - assert.Equal(t, &errMsg, problemDetails.Cause) + assert.Equal(t, &errMsg, errDetails.ErrorDescription) } func TestPostSecurityIdTokenInvokerSecretNotValid(t *testing.T) { @@ -122,19 +119,18 @@ func TestPostSecurityIdTokenInvokerSecretNotValid(t *testing.T) { data.Set("client_id", "id") data.Add("client_secret", "secret") data.Add("grant_type", "client_credentials") - data.Add("scope", "scope#aefId:path") + data.Add("scope", "3gpp#aefId:path") encodedData := data.Encode() result := testutil.NewRequest().Post("/securities/invokerId/token").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(encodedData)).Go(t, requestHandler) assert.Equal(t, http.StatusBadRequest, result.Code()) - var problemDetails common29122.ProblemDetails - err := result.UnmarshalBodyToObject(&problemDetails) + var errDetails securityapi.AccessTokenErr + err := result.UnmarshalBodyToObject(&errDetails) assert.NoError(t, err, "error unmarshaling response") - badRequest := http.StatusBadRequest - assert.Equal(t, &badRequest, problemDetails.Status) + assert.Equal(t, securityapi.AccessTokenErrErrorUnauthorizedClient, errDetails.Error) errMsg := "Invoker secret not valid" - assert.Equal(t, &errMsg, problemDetails.Cause) + assert.Equal(t, &errMsg, errDetails.ErrorDescription) } func TestPostSecurityIdTokenFunctionNotRegistered(t *testing.T) { @@ -150,19 +146,18 @@ func TestPostSecurityIdTokenFunctionNotRegistered(t *testing.T) { data.Set("client_id", "id") data.Add("client_secret", "secret") data.Add("grant_type", "client_credentials") - data.Add("scope", "scope#aefId:path") + data.Add("scope", "3gpp#aefId:path") encodedData := data.Encode() result := testutil.NewRequest().Post("/securities/invokerId/token").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(encodedData)).Go(t, requestHandler) assert.Equal(t, http.StatusBadRequest, result.Code()) - var problemDetails common29122.ProblemDetails - err := result.UnmarshalBodyToObject(&problemDetails) + var errDetails securityapi.AccessTokenErr + err := result.UnmarshalBodyToObject(&errDetails) assert.NoError(t, err, "error unmarshaling response") - badRequest := http.StatusBadRequest - assert.Equal(t, &badRequest, problemDetails.Status) - errMsg := "Function not registered" - assert.Equal(t, &errMsg, problemDetails.Cause) + assert.Equal(t, securityapi.AccessTokenErrErrorInvalidScope, errDetails.Error) + errMsg := "AEF Function not registered" + assert.Equal(t, &errMsg, errDetails.ErrorDescription) } func TestPostSecurityIdTokenAPINotPublished(t *testing.T) { @@ -180,19 +175,18 @@ func TestPostSecurityIdTokenAPINotPublished(t *testing.T) { data.Set("client_id", "id") data.Add("client_secret", "secret") data.Add("grant_type", "client_credentials") - data.Add("scope", "scope#aefId:path") + data.Add("scope", "3gpp#aefId:path") encodedData := data.Encode() result := testutil.NewRequest().Post("/securities/invokerId/token").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(encodedData)).Go(t, requestHandler) assert.Equal(t, http.StatusBadRequest, result.Code()) - var problemDetails common29122.ProblemDetails - err := result.UnmarshalBodyToObject(&problemDetails) + var errDetails securityapi.AccessTokenErr + err := result.UnmarshalBodyToObject(&errDetails) assert.NoError(t, err, "error unmarshaling response") - badRequest := http.StatusBadRequest - assert.Equal(t, &badRequest, problemDetails.Status) + assert.Equal(t, securityapi.AccessTokenErrErrorInvalidScope, errDetails.Error) errMsg := "API not published" - assert.Equal(t, &errMsg, problemDetails.Cause) + assert.Equal(t, &errMsg, errDetails.ErrorDescription) } func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister) *echo.Echo { -- 2.16.6 From d55825ec39422d2b54da563fb3182f0cde630da3 Mon Sep 17 00:00:00 2001 From: ychacon Date: Thu, 9 Feb 2023 16:48:36 +0100 Subject: [PATCH 06/16] Update Release Notes for G Maintenance Release Issue-ID: NONRTRIC-838 Signed-off-by: ychacon Change-Id: I2ef1670a4d0eaaa58087f7146e783edb861cfcde --- docs/release-notes.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index d2f7d56..e17aa2b 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -20,6 +20,9 @@ Version history capifcore | | | | Initial version | | | | | of capifcore | +------------+----------+------------------+-----------------+ +| 2023-02-10 | 1.0.1 | Yennifer Chacon | G Maintenance | +| | | | Release | ++------------+----------+------------------+-----------------+ Release Data ============ @@ -43,3 +46,23 @@ G Release | **Purpose of the delivery** | Introduction of capifcore | | | | +-----------------------------+---------------------------------------------------+ + +G Maintenance +------------- ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC CAPIF Core | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/plt/sme/ | +| | Ic9f6038faa123325a0c8a8be7a7f5cc5f7a586a0 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | G release | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2023-02-10 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Refactor and improvements of capifcore | +| | | ++-----------------------------+---------------------------------------------------+ \ No newline at end of file -- 2.16.6 From 604e2992bfd31bde571f29f90864ca74306a39af Mon Sep 17 00:00:00 2001 From: ychacon Date: Mon, 13 Feb 2023 16:34:55 +0100 Subject: [PATCH 07/16] Update commitId in release notes Issue-ID: NONRTRIC-838 Signed-off-by: ychacon Change-Id: I5be89734bbcd0a1c9165b7b797baaecebf1630fa --- docs/release-notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index e17aa2b..7cfe5a3 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -54,7 +54,7 @@ G Maintenance | | | +-----------------------------+---------------------------------------------------+ | **Repo/commit-ID** | nonrtric/plt/sme/ | -| | Ic9f6038faa123325a0c8a8be7a7f5cc5f7a586a0 | +| | b4cee94bd251b0bb92fc435fbe74ea1079d43356 | | | | +-----------------------------+---------------------------------------------------+ | **Release designation** | G release | -- 2.16.6 From 1a36c0f848111d82bf58552662f7bff3b05f79ed Mon Sep 17 00:00:00 2001 From: JohnKeeney Date: Tue, 21 Feb 2023 18:22:13 +0000 Subject: [PATCH 08/16] Update Committers - remove Henrik Issue-ID: NONRTRIC-843 Change-Id: I68a41fb1423285674c13639029b4faed15cd9185 Signed-off-by: JohnKeeney --- INFO.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/INFO.yaml b/INFO.yaml index b6339d9..870c84f 100644 --- a/INFO.yaml +++ b/INFO.yaml @@ -33,10 +33,10 @@ repositories: - 'nonrtric/plt/sme' committers: - <<: *oran_nonrtric_ptl - - name: 'Henrik Andersson' - email: 'henrik.b.andersson@est.tech' + - name: 'Yennifer Chacon' + email: 'yennifer.chacon@est.tech' company: 'Ericsson Software Technology' - id: 'elinuxhenrik' + id: 'ychacon' timezone: 'Sweden/Stockholm' - name: 'Patrik Buhr' email: 'patrik.buhr@est.tech' -- 2.16.6 From 4308df0663b45eb9d95b3babdf519a06ee76c15a Mon Sep 17 00:00:00 2001 From: ychacon Date: Thu, 23 Feb 2023 11:13:14 +0100 Subject: [PATCH 09/16] Generating token using keycloak Issue-ID: NONRTRIC-836 Signed-off-by: ychacon Change-Id: If337576ccac816292709c6d7ab30d239a2d8a77b --- capifcore/configs/keycloak.yaml | 23 ++++++ capifcore/internal/config/config.go | 55 +++++++++++++ capifcore/internal/keycloak/keycloak.go | 90 ++++++++++++++++++++++ .../internal/keycloak/mocks/AccessManagement.go | 52 +++++++++++++ capifcore/internal/securityapi/typevalidation.go | 2 +- capifcore/internal/securityservice/security.go | 32 +++----- .../internal/securityservice/security_test.go | 67 ++++++++++++++-- capifcore/main.go | 11 ++- 8 files changed, 299 insertions(+), 33 deletions(-) create mode 100644 capifcore/configs/keycloak.yaml create mode 100644 capifcore/internal/config/config.go create mode 100644 capifcore/internal/keycloak/keycloak.go create mode 100644 capifcore/internal/keycloak/mocks/AccessManagement.go diff --git a/capifcore/configs/keycloak.yaml b/capifcore/configs/keycloak.yaml new file mode 100644 index 0000000..1ca2ba9 --- /dev/null +++ b/capifcore/configs/keycloak.yaml @@ -0,0 +1,23 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= +# + +# Keycloak configurations +authorizationServer: + host: "localhost" + port: "8080" + realms: + invokerrealm: "invokerrealm" diff --git a/capifcore/internal/config/config.go b/capifcore/internal/config/config.go new file mode 100644 index 0000000..4a53022 --- /dev/null +++ b/capifcore/internal/config/config.go @@ -0,0 +1,55 @@ +// - +// ========================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 config + +import ( + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +type AuthorizationServer struct { + Port string `yaml:"port"` + Host string `yaml:"host"` + Realms map[string]string `yaml:"realms"` +} + +type Config struct { + AuthorizationServer AuthorizationServer `yaml:"authorizationServer"` +} + +func ReadKeycloakConfigFile(configFolder string) (*Config, error) { + + f, err := os.Open(filepath.Join(configFolder, "keycloak.yaml")) + if err != nil { + return nil, err + } + defer f.Close() + + var cfg *Config + decoder := yaml.NewDecoder(f) + err = decoder.Decode(&cfg) + if err != nil { + return nil, err + } + return cfg, nil +} diff --git a/capifcore/internal/keycloak/keycloak.go b/capifcore/internal/keycloak/keycloak.go new file mode 100644 index 0000000..16f65c8 --- /dev/null +++ b/capifcore/internal/keycloak/keycloak.go @@ -0,0 +1,90 @@ +// - +// ========================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 keycloak + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + + "oransc.org/nonrtric/capifcore/internal/config" +) + +//go:generate mockery --name AccessManagement +type AccessManagement interface { + // Get JWT token for a client. + // Returns JWT token if client exits and credentials are correct otherwise returns error. + GetToken(clientId, clientPassword, scope string, realm string) (Jwttoken, error) +} + +type KeycloakManager struct { + keycloakServerUrl string + realms map[string]string +} + +func NewKeycloakManager(cfg *config.Config) *KeycloakManager { + + keycloakUrl := "http://" + cfg.AuthorizationServer.Host + ":" + cfg.AuthorizationServer.Port + + return &KeycloakManager{ + keycloakServerUrl: keycloakUrl, + realms: cfg.AuthorizationServer.Realms, + } +} + +type Jwttoken struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + ExpiresIn int `json:"expires_in"` + RefreshExpiresIn int `json:"refresh_expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + NotBeforePolicy int `json:"not-before-policy"` + SessionState string `json:"session_state"` + Scope string `json:"scope"` +} + +func (km *KeycloakManager) GetToken(clientId, clientPassword, scope string, realm string) (Jwttoken, error) { + var jwt Jwttoken + getTokenUrl := km.keycloakServerUrl + "/realms/" + realm + "/protocol/openid-connect/token" + + resp, err := http.PostForm(getTokenUrl, + url.Values{"grant_type": {"client_credentials"}, "client_id": {clientId}, "client_secret": {clientPassword}}) + + if err != nil { + return jwt, err + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + + if err != nil { + return jwt, err + } + if resp.StatusCode != http.StatusOK { + return jwt, errors.New(string(body)) + } + + json.Unmarshal([]byte(body), &jwt) + return jwt, nil +} diff --git a/capifcore/internal/keycloak/mocks/AccessManagement.go b/capifcore/internal/keycloak/mocks/AccessManagement.go new file mode 100644 index 0000000..9ac2179 --- /dev/null +++ b/capifcore/internal/keycloak/mocks/AccessManagement.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + keycloak "oransc.org/nonrtric/capifcore/internal/keycloak" +) + +// AccessManagement is an autogenerated mock type for the AccessManagement type +type AccessManagement struct { + mock.Mock +} + +// GetToken provides a mock function with given fields: clientId, clientPassword, scope, realm +func (_m *AccessManagement) GetToken(clientId string, clientPassword string, scope string, realm string) (keycloak.Jwttoken, error) { + ret := _m.Called(clientId, clientPassword, scope, realm) + + var r0 keycloak.Jwttoken + var r1 error + if rf, ok := ret.Get(0).(func(string, string, string, string) (keycloak.Jwttoken, error)); ok { + return rf(clientId, clientPassword, scope, realm) + } + if rf, ok := ret.Get(0).(func(string, string, string, string) keycloak.Jwttoken); ok { + r0 = rf(clientId, clientPassword, scope, realm) + } else { + r0 = ret.Get(0).(keycloak.Jwttoken) + } + + if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok { + r1 = rf(clientId, clientPassword, scope, realm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewAccessManagement interface { + mock.TestingT + Cleanup(func()) +} + +// NewAccessManagement creates a new instance of AccessManagement. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAccessManagement(t mockConstructorTestingTNewAccessManagement) *AccessManagement { + mock := &AccessManagement{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/capifcore/internal/securityapi/typevalidation.go b/capifcore/internal/securityapi/typevalidation.go index 90dbda3..1241f96 100644 --- a/capifcore/internal/securityapi/typevalidation.go +++ b/capifcore/internal/securityapi/typevalidation.go @@ -35,7 +35,7 @@ func (tokenReq AccessTokenReq) Validate() (bool, AccessTokenErr) { } //3gpp#aefId1:apiName1,apiName2,…apiNameX;aefId2:apiName1,apiName2,…apiNameY;…aefIdN:apiName1,apiName2,…apiNameZ - if tokenReq.Scope != nil { + if tokenReq.Scope != nil && *tokenReq.Scope != "" { scope := strings.Split(*tokenReq.Scope, "#") if len(scope) < 2 { return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Malformed scope") diff --git a/capifcore/internal/securityservice/security.go b/capifcore/internal/securityservice/security.go index d71ff0d..dcf1dbb 100644 --- a/capifcore/internal/securityservice/security.go +++ b/capifcore/internal/securityservice/security.go @@ -23,32 +23,31 @@ package security import ( "net/http" "strings" - "time" - "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" "oransc.org/nonrtric/capifcore/internal/common29122" securityapi "oransc.org/nonrtric/capifcore/internal/securityapi" "oransc.org/nonrtric/capifcore/internal/invokermanagement" + "oransc.org/nonrtric/capifcore/internal/keycloak" "oransc.org/nonrtric/capifcore/internal/providermanagement" "oransc.org/nonrtric/capifcore/internal/publishservice" ) -var jwtKey = "my-secret-key" - type Security struct { serviceRegister providermanagement.ServiceRegister publishRegister publishservice.PublishRegister invokerRegister invokermanagement.InvokerRegister + keycloak keycloak.AccessManagement } -func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister) *Security { +func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, km keycloak.AccessManagement) *Security { return &Security{ serviceRegister: serviceRegister, publishRegister: publishRegister, invokerRegister: invokerRegister, + keycloak: km, } } @@ -68,7 +67,7 @@ func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId st return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorUnauthorizedClient, "Invoker secret not valid") } - if accessTokenReq.Scope != nil { + if accessTokenReq.Scope != nil && *accessTokenReq.Scope != "" { scope := strings.Split(*accessTokenReq.Scope, "#") aefList := strings.Split(scope[1], ";") for _, aef := range aefList { @@ -83,27 +82,14 @@ func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId st } } } - - expirationTime := time.Now().Add(time.Hour).Unix() - - claims := &jwt.MapClaims{ - "iss": accessTokenReq.ClientId, - "exp": expirationTime, - "data": map[string]interface{}{ - "scope": accessTokenReq.Scope, - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(jwtKey)) + jwtToken, err := s.keycloak.GetToken(accessTokenReq.ClientId, *accessTokenReq.ClientSecret, *accessTokenReq.Scope, "invokerrealm") if err != nil { - // If there is an error in creating the JWT return an internal server error - return err + return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorUnauthorizedClient, err.Error()) } accessTokenResp := securityapi.AccessTokenRsp{ - AccessToken: tokenString, - ExpiresIn: common29122.DurationSec(expirationTime), + AccessToken: jwtToken.AccessToken, + ExpiresIn: common29122.DurationSec(jwtToken.ExpiresIn), Scope: accessTokenReq.Scope, TokenType: "Bearer", } diff --git a/capifcore/internal/securityservice/security_test.go b/capifcore/internal/securityservice/security_test.go index 6c62161..13af737 100644 --- a/capifcore/internal/securityservice/security_test.go +++ b/capifcore/internal/securityservice/security_test.go @@ -21,12 +21,14 @@ package security import ( + "errors" "fmt" "net/http" "net/url" "os" "testing" + "oransc.org/nonrtric/capifcore/internal/keycloak" "oransc.org/nonrtric/capifcore/internal/securityapi" "oransc.org/nonrtric/capifcore/internal/invokermanagement" @@ -36,6 +38,7 @@ import ( "github.com/labstack/echo/v4" invokermocks "oransc.org/nonrtric/capifcore/internal/invokermanagement/mocks" + keycloackmocks "oransc.org/nonrtric/capifcore/internal/keycloak/mocks" servicemocks "oransc.org/nonrtric/capifcore/internal/providermanagement/mocks" publishmocks "oransc.org/nonrtric/capifcore/internal/publishservice/mocks" @@ -55,7 +58,15 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("IsAPIPublished", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(true) - requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock) + jwt := keycloak.Jwttoken{ + AccessToken: "eyJhbGNIn0.e3YTQ0xLjEifQ.FcqCwCy7iJiOmw", + ExpiresIn: 300, + Scope: "3gpp#aefIdpath", + } + accessMgmMock := keycloackmocks.AccessManagement{} + accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, nil) + + requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) data := url.Values{} clientId := "id" @@ -76,19 +87,19 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) { err := result.UnmarshalBodyToObject(&resultResponse) assert.NoError(t, err, "error unmarshaling response") assert.NotEmpty(t, resultResponse.AccessToken) - assert.Equal(t, "3gpp#"+aefId+":"+path, *resultResponse.Scope) assert.Equal(t, securityapi.AccessTokenRspTokenTypeBearer, resultResponse.TokenType) invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", clientId) invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret) serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId) publishRegisterMock.AssertCalled(t, "IsAPIPublished", aefId, path) + accessMgmMock.AssertCalled(t, "GetToken", clientId, clientSecret, "3gpp#"+aefId+":"+path, "invokerrealm") } func TestPostSecurityIdTokenInvokerNotRegistered(t *testing.T) { invokerRegisterMock := invokermocks.InvokerRegister{} invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(nil, nil, &invokerRegisterMock) + requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -113,7 +124,7 @@ func TestPostSecurityIdTokenInvokerSecretNotValid(t *testing.T) { invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true) invokerRegisterMock.On("VerifyInvokerSecret", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(nil, nil, &invokerRegisterMock) + requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -140,7 +151,7 @@ func TestPostSecurityIdTokenFunctionNotRegistered(t *testing.T) { serviceRegisterMock := servicemocks.ServiceRegister{} serviceRegisterMock.On("IsFunctionRegistered", mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(&serviceRegisterMock, nil, &invokerRegisterMock) + requestHandler := getEcho(&serviceRegisterMock, nil, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -169,7 +180,7 @@ func TestPostSecurityIdTokenAPINotPublished(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("IsAPIPublished", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock) + requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -189,7 +200,47 @@ func TestPostSecurityIdTokenAPINotPublished(t *testing.T) { assert.Equal(t, &errMsg, errDetails.ErrorDescription) } -func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister) *echo.Echo { +func TestPostSecurityIdTokenInvokerInvalidCredentials(t *testing.T) { + invokerRegisterMock := invokermocks.InvokerRegister{} + invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true) + invokerRegisterMock.On("VerifyInvokerSecret", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(true) + serviceRegisterMock := servicemocks.ServiceRegister{} + serviceRegisterMock.On("IsFunctionRegistered", mock.AnythingOfType("string")).Return(true) + publishRegisterMock := publishmocks.PublishRegister{} + publishRegisterMock.On("IsAPIPublished", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(true) + + jwt := keycloak.Jwttoken{} + accessMgmMock := keycloackmocks.AccessManagement{} + accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, errors.New("invalid_credentials")) + + requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) + + data := url.Values{} + clientId := "id" + clientSecret := "secret" + aefId := "aefId" + path := "path" + data.Set("client_id", clientId) + data.Set("client_secret", clientSecret) + data.Set("grant_type", "client_credentials") + data.Set("scope", "3gpp#"+aefId+":"+path) + + encodedData := data.Encode() + + result := testutil.NewRequest().Post("/securities/invokerId/token").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(encodedData)).Go(t, requestHandler) + + assert.Equal(t, http.StatusBadRequest, result.Code()) + var resultResponse securityapi.AccessTokenErr + err := result.UnmarshalBodyToObject(&resultResponse) + assert.NoError(t, err, "error unmarshaling response") + invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", clientId) + invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret) + serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId) + publishRegisterMock.AssertCalled(t, "IsAPIPublished", aefId, path) + accessMgmMock.AssertCalled(t, "GetToken", clientId, clientSecret, "3gpp#"+aefId+":"+path, "invokerrealm") +} + +func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, keycloakMgm keycloak.AccessManagement) *echo.Echo { swagger, err := securityapi.GetSwagger() if err != nil { fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) @@ -198,7 +249,7 @@ func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister swagger.Servers = nil - s := NewSecurity(serviceRegister, publishRegister, invokerRegister) + s := NewSecurity(serviceRegister, publishRegister, invokerRegister, keycloakMgm) e := echo.New() e.Use(echomiddleware.Logger()) diff --git a/capifcore/main.go b/capifcore/main.go index 3ee4192..f8b9daf 100644 --- a/capifcore/main.go +++ b/capifcore/main.go @@ -32,12 +32,14 @@ import ( "oransc.org/nonrtric/capifcore/internal/discoverserviceapi" "oransc.org/nonrtric/capifcore/internal/eventsapi" "oransc.org/nonrtric/capifcore/internal/invokermanagementapi" + "oransc.org/nonrtric/capifcore/internal/keycloak" "oransc.org/nonrtric/capifcore/internal/providermanagementapi" "oransc.org/nonrtric/capifcore/internal/securityapi" "github.com/deepmap/oapi-codegen/pkg/middleware" echomiddleware "github.com/labstack/echo/v4/middleware" log "github.com/sirupsen/logrus" + config "oransc.org/nonrtric/capifcore/internal/config" "oransc.org/nonrtric/capifcore/internal/discoverservice" "oransc.org/nonrtric/capifcore/internal/eventservice" "oransc.org/nonrtric/capifcore/internal/helmmanagement" @@ -87,6 +89,13 @@ func getEcho() *echo.Echo { // Log all requests e.Use(echomiddleware.Logger()) + // Read configuration file + cfg, err := config.ReadKeycloakConfigFile("configs") + if err != nil { + log.Fatalf("Error loading configuration file\n: %s", err) + } + km := keycloak.NewKeycloakManager(cfg) + var group *echo.Group // Register ProviderManagement providerManagerSwagger, err := providermanagementapi.GetSwagger() @@ -150,7 +159,7 @@ func getEcho() *echo.Echo { log.Fatalf("Error loading Security swagger spec\n: %s", err) } securitySwagger.Servers = nil - securityService := security.NewSecurity(providerManager, publishService, invokerManager) + securityService := security.NewSecurity(providerManager, publishService, invokerManager, km) group = e.Group("/capif-security/v1") group.Use(middleware.OapiRequestValidator(securitySwagger)) securityapi.RegisterHandlersWithBaseURL(e, securityService, "/capif-security/v1") -- 2.16.6 From c865c910a6a04fc202c8eb8b6403544c44784d5f Mon Sep 17 00:00:00 2001 From: ychacon Date: Wed, 1 Mar 2023 19:00:59 +0100 Subject: [PATCH 10/16] Implementation for PUT trustedInvokers endpoint Issue-ID: NONRTRIC-848 Signed-off-by: ychacon Change-Id: Iba953bfb8aa11d77c8aeac906d640fb470769bbc --- capifcore/internal/publishserviceapi/typeaccess.go | 11 ++ capifcore/internal/securityapi/typeupdate.go | 84 +++++++++ capifcore/internal/securityapi/typeupdate_test.go | 106 +++++++++++ capifcore/internal/securityapi/typevalidation.go | 29 +++ .../internal/securityapi/typevalidation_test.go | 48 +++++ capifcore/internal/securityservice/security.go | 69 ++++++- .../internal/securityservice/security_test.go | 207 +++++++++++++++++++++ 7 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 capifcore/internal/securityapi/typeupdate.go create mode 100644 capifcore/internal/securityapi/typeupdate_test.go diff --git a/capifcore/internal/publishserviceapi/typeaccess.go b/capifcore/internal/publishserviceapi/typeaccess.go index 32c1a7a..c8a7afc 100644 --- a/capifcore/internal/publishserviceapi/typeaccess.go +++ b/capifcore/internal/publishserviceapi/typeaccess.go @@ -28,3 +28,14 @@ func (sd ServiceAPIDescription) GetAefIds() []string { } 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 +} diff --git a/capifcore/internal/securityapi/typeupdate.go b/capifcore/internal/securityapi/typeupdate.go new file mode 100644 index 0000000..364c123 --- /dev/null +++ b/capifcore/internal/securityapi/typeupdate.go @@ -0,0 +1,84 @@ +// - +// +// ========================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 +} diff --git a/capifcore/internal/securityapi/typeupdate_test.go b/capifcore/internal/securityapi/typeupdate_test.go new file mode 100644 index 0000000..1ad530d --- /dev/null +++ b/capifcore/internal/securityapi/typeupdate_test.go @@ -0,0 +1,106 @@ +// - +// ========================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) + +} diff --git a/capifcore/internal/securityapi/typevalidation.go b/capifcore/internal/securityapi/typevalidation.go index 1241f96..4be8aee 100644 --- a/capifcore/internal/securityapi/typevalidation.go +++ b/capifcore/internal/securityapi/typevalidation.go @@ -21,6 +21,9 @@ package securityapi import ( + "errors" + "fmt" + "net/url" "strings" ) @@ -54,6 +57,32 @@ func (tokenReq AccessTokenReq) Validate() (bool, AccessTokenErr) { 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, diff --git a/capifcore/internal/securityapi/typevalidation_test.go b/capifcore/internal/securityapi/typevalidation_test.go index 0515d06..f44de2f 100644 --- a/capifcore/internal/securityapi/typevalidation_test.go +++ b/capifcore/internal/securityapi/typevalidation_test.go @@ -24,6 +24,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "oransc.org/nonrtric/capifcore/internal/publishserviceapi" ) func TestValidateClientIdNotPresent(t *testing.T) { @@ -95,3 +96,50 @@ func TestValidateScopeMalformed(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) +} diff --git a/capifcore/internal/securityservice/security.go b/capifcore/internal/securityservice/security.go index dcf1dbb..52d28ce 100644 --- a/capifcore/internal/securityservice/security.go +++ b/capifcore/internal/securityservice/security.go @@ -21,8 +21,11 @@ package security import ( + "fmt" "net/http" + "path" "strings" + "sync" "github.com/labstack/echo/v4" @@ -40,6 +43,8 @@ type Security struct { 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 { @@ -48,6 +53,7 @@ func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegi publishRegister: publishRegister, invokerRegister: invokerRegister, keycloak: km, + trustedInvokers: make(map[string]securityapi.ServiceSecurity), } } @@ -112,7 +118,57 @@ func (s *Security) GetTrustedInvokersApiInvokerId(ctx echo.Context, apiInvokerId } 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 { @@ -130,3 +186,14 @@ func sendAccessTokenError(ctx echo.Context, code int, err securityapi.AccessToke } 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 +} diff --git a/capifcore/internal/securityservice/security_test.go b/capifcore/internal/securityservice/security_test.go index 13af737..d31ff6e 100644 --- a/capifcore/internal/securityservice/security_test.go +++ b/capifcore/internal/securityservice/security_test.go @@ -28,7 +28,9 @@ import ( "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" @@ -240,6 +242,181 @@ func TestPostSecurityIdTokenInvokerInvalidCredentials(t *testing.T) { 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 { @@ -258,3 +435,33 @@ func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister 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", + }, + }, + }, + }, + } +} -- 2.16.6 From 856821490df816d7343d90f76f729240bd4e00d6 Mon Sep 17 00:00:00 2001 From: ychacon Date: Tue, 7 Mar 2023 21:59:08 +0100 Subject: [PATCH 11/16] Implementation for GET/DELETE trustedInvokers endpoint Issue-ID: NONRTRIC-848 Signed-off-by: ychacon Change-Id: I7c3c8c484afa2b56cfabe7d82d47255f91fd726a --- capifcore/internal/securityservice/security.go | 58 ++++++++++++- .../internal/securityservice/security_test.go | 97 +++++++++++++++++++--- capifcore/main_test.go | 2 +- 3 files changed, 140 insertions(+), 17 deletions(-) diff --git a/capifcore/internal/securityservice/security.go b/capifcore/internal/securityservice/security.go index 52d28ce..d3d9026 100644 --- a/capifcore/internal/securityservice/security.go +++ b/capifcore/internal/securityservice/security.go @@ -28,7 +28,7 @@ import ( "sync" "github.com/labstack/echo/v4" - + copystructure "github.com/mitchellh/copystructure" "oransc.org/nonrtric/capifcore/internal/common29122" securityapi "oransc.org/nonrtric/capifcore/internal/securityapi" @@ -110,11 +110,63 @@ func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId st } func (s *Security) DeleteTrustedInvokersApiInvokerId(ctx echo.Context, apiInvokerId string) error { - return ctx.NoContent(http.StatusNotImplemented) + if _, ok := s.trustedInvokers[apiInvokerId]; ok { + s.deleteTrustedInvoker(apiInvokerId) + } + + return ctx.NoContent(http.StatusNoContent) +} + +func (s *Security) deleteTrustedInvoker(apiInvokerId string) { + s.lock.Lock() + defer s.lock.Unlock() + delete(s.trustedInvokers, apiInvokerId) } func (s *Security) GetTrustedInvokersApiInvokerId(ctx echo.Context, apiInvokerId string, params securityapi.GetTrustedInvokersApiInvokerIdParams) error { - return ctx.NoContent(http.StatusNotImplemented) + + if trustedInvoker, ok := s.trustedInvokers[apiInvokerId]; ok { + updatedInvoker := s.checkParams(trustedInvoker, params) + if updatedInvoker != nil { + err := ctx.JSON(http.StatusOK, updatedInvoker) + if err != nil { + return err + } + } + } else { + return sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf("invoker %s not registered as trusted invoker", apiInvokerId)) + } + + return nil +} + +func (s *Security) checkParams(trustedInvoker securityapi.ServiceSecurity, params securityapi.GetTrustedInvokersApiInvokerIdParams) *securityapi.ServiceSecurity { + emptyString := "" + + var sendAuthenticationInfo = (params.AuthenticationInfo != nil) && *params.AuthenticationInfo + var sendAuthorizationInfo = (params.AuthorizationInfo != nil) && *params.AuthorizationInfo + + if sendAuthenticationInfo && sendAuthorizationInfo { + return &trustedInvoker + } + + data, _ := copystructure.Copy(trustedInvoker) + updatedInvoker, ok := data.(securityapi.ServiceSecurity) + if !ok { + return nil + } + + if !sendAuthenticationInfo { + for i := range updatedInvoker.SecurityInfo { + updatedInvoker.SecurityInfo[i].AuthenticationInfo = &emptyString + } + } + if !sendAuthorizationInfo { + for i := range updatedInvoker.SecurityInfo { + updatedInvoker.SecurityInfo[i].AuthorizationInfo = &emptyString + } + } + return &updatedInvoker } func (s *Security) PutTrustedInvokersApiInvokerId(ctx echo.Context, apiInvokerId string) error { diff --git a/capifcore/internal/securityservice/security_test.go b/capifcore/internal/securityservice/security_test.go index d31ff6e..1abb8ae 100644 --- a/capifcore/internal/securityservice/security_test.go +++ b/capifcore/internal/securityservice/security_test.go @@ -68,7 +68,7 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) { accessMgmMock := keycloackmocks.AccessManagement{} accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, nil) - requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) + requestHandler, _ := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) data := url.Values{} clientId := "id" @@ -101,7 +101,7 @@ func TestPostSecurityIdTokenInvokerNotRegistered(t *testing.T) { invokerRegisterMock := invokermocks.InvokerRegister{} invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(nil, nil, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -126,7 +126,7 @@ func TestPostSecurityIdTokenInvokerSecretNotValid(t *testing.T) { invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true) invokerRegisterMock.On("VerifyInvokerSecret", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(nil, nil, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -153,7 +153,7 @@ func TestPostSecurityIdTokenFunctionNotRegistered(t *testing.T) { serviceRegisterMock := servicemocks.ServiceRegister{} serviceRegisterMock.On("IsFunctionRegistered", mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(&serviceRegisterMock, nil, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(&serviceRegisterMock, nil, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -182,7 +182,7 @@ func TestPostSecurityIdTokenAPINotPublished(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("IsAPIPublished", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, nil) data := url.Values{} data.Set("client_id", "id") @@ -215,7 +215,7 @@ func TestPostSecurityIdTokenInvokerInvalidCredentials(t *testing.T) { accessMgmMock := keycloackmocks.AccessManagement{} accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, errors.New("invalid_credentials")) - requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) + requestHandler, _ := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) data := url.Values{} clientId := "id" @@ -263,7 +263,7 @@ func TestPutTrustedInvokerSuccessfully(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices) - requestHandler := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) invokerId := "invokerId" serviceSecurityUnderTest := getServiceSecurity(aefId, apiId) @@ -289,7 +289,7 @@ func TestPutTrustedInkoverNotRegistered(t *testing.T) { invokerRegisterMock := invokermocks.InvokerRegister{} invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(false) - requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(nil, nil, &invokerRegisterMock, nil) invokerId := "invokerId" serviceSecurityUnderTest := getServiceSecurity("aefId", "apiId") @@ -310,7 +310,7 @@ func TestPutTrustedInkoverInvalidInputServiceSecurity(t *testing.T) { invokerRegisterMock := invokermocks.InvokerRegister{} invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true) - requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(nil, nil, &invokerRegisterMock, nil) invokerId := "invokerId" notificationUrl := "url" @@ -350,7 +350,7 @@ func TestPutTrustedInvokerInterfaceDetailsNotNil(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices) - requestHandler := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) invokerId := "invokerId" serviceSecurityUnderTest := getServiceSecurity(aefId, apiId) @@ -399,7 +399,7 @@ func TestPutTrustedInvokerNotFoundSecurityMethod(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices) - requestHandler := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) + requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) invokerId := "invokerId" serviceSecurityUnderTest := getServiceSecurity("aefId", "apiId") @@ -417,7 +417,78 @@ func TestPutTrustedInvokerNotFoundSecurityMethod(t *testing.T) { invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId) } -func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, keycloakMgm keycloak.AccessManagement) *echo.Echo { +func TestDeleteSecurityContext(t *testing.T) { + + requestHandler, securityUnderTest := getEcho(nil, nil, nil, nil) + + aefId := "aefId" + apiId := "apiId" + serviceSecurityUnderTest := getServiceSecurity(aefId, apiId) + serviceSecurityUnderTest.SecurityInfo[0].ApiId = &apiId + + invokerId := "invokerId" + securityUnderTest.trustedInvokers[invokerId] = serviceSecurityUnderTest + + // Delete the security context + result := testutil.NewRequest().Delete("/trustedInvokers/"+invokerId).Go(t, requestHandler) + + assert.Equal(t, http.StatusNoContent, result.Code()) + _, ok := securityUnderTest.trustedInvokers[invokerId] + assert.False(t, ok) +} + +func TestGetSecurityContextByInvokerId(t *testing.T) { + + requestHandler, securityUnderTest := getEcho(nil, nil, nil, nil) + + aefId := "aefId" + apiId := "apiId" + authenticationInfo := "authenticationInfo" + authorizationInfo := "authorizationInfo" + serviceSecurityUnderTest := getServiceSecurity(aefId, apiId) + serviceSecurityUnderTest.SecurityInfo[0].AuthenticationInfo = &authenticationInfo + serviceSecurityUnderTest.SecurityInfo[0].AuthorizationInfo = &authorizationInfo + + invokerId := "invokerId" + securityUnderTest.trustedInvokers[invokerId] = serviceSecurityUnderTest + + // Get security context + result := testutil.NewRequest().Get("/trustedInvokers/"+invokerId).Go(t, requestHandler) + + assert.Equal(t, http.StatusOK, result.Code()) + var resultService securityapi.ServiceSecurity + err := result.UnmarshalBodyToObject(&resultService) + assert.NoError(t, err, "error unmarshaling response") + + for _, secInfo := range resultService.SecurityInfo { + assert.Equal(t, apiId, *secInfo.ApiId) + assert.Equal(t, aefId, *secInfo.AefId) + assert.Equal(t, "", *secInfo.AuthenticationInfo) + assert.Equal(t, "", *secInfo.AuthorizationInfo) + } + + result = testutil.NewRequest().Get("/trustedInvokers/"+invokerId+"?authenticationInfo=true&authorizationInfo=false").Go(t, requestHandler) + assert.Equal(t, http.StatusOK, result.Code()) + err = result.UnmarshalBodyToObject(&resultService) + assert.NoError(t, err, "error unmarshaling response") + + for _, secInfo := range resultService.SecurityInfo { + assert.Equal(t, authenticationInfo, *secInfo.AuthenticationInfo) + assert.Equal(t, "", *secInfo.AuthorizationInfo) + } + + result = testutil.NewRequest().Get("/trustedInvokers/"+invokerId+"?authenticationInfo=true&authorizationInfo=true").Go(t, requestHandler) + assert.Equal(t, http.StatusOK, result.Code()) + err = result.UnmarshalBodyToObject(&resultService) + assert.NoError(t, err, "error unmarshaling response") + + for _, secInfo := range resultService.SecurityInfo { + assert.Equal(t, authenticationInfo, *secInfo.AuthenticationInfo) + assert.Equal(t, authorizationInfo, *secInfo.AuthorizationInfo) + } +} + +func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, keycloakMgm keycloak.AccessManagement) (*echo.Echo, *Security) { swagger, err := securityapi.GetSwagger() if err != nil { fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) @@ -433,7 +504,7 @@ func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister e.Use(middleware.OapiRequestValidator(swagger)) securityapi.RegisterHandlers(e, s) - return e + return e, s } func getServiceSecurity(aefId string, apiId string) securityapi.ServiceSecurity { diff --git a/capifcore/main_test.go b/capifcore/main_test.go index 7d77b92..571d475 100644 --- a/capifcore/main_test.go +++ b/capifcore/main_test.go @@ -101,7 +101,7 @@ func Test_routing(t *testing.T) { name: "Security path", args: args{ url: "/capif-security/v1/trustedInvokers/apiInvokerId", - returnStatus: http.StatusNotImplemented, + returnStatus: http.StatusNotFound, method: "GET", }, }, -- 2.16.6 From b2a87e363829f6447b4f06c1aa4524608bbeb422 Mon Sep 17 00:00:00 2001 From: ychacon Date: Fri, 10 Mar 2023 17:22:27 +0100 Subject: [PATCH 12/16] Implementation for Update and revoke trustedInvokers endpoint Issue-ID: NONRTRIC-848 Signed-off-by: ychacon Change-Id: Iee3c7fbeb8032bda02f65a0ef513ce65d8359b9a --- capifcore/internal/securityapi/typevalidation.go | 21 +++++ capifcore/internal/securityservice/security.go | 80 +++++++++++++++- .../internal/securityservice/security_test.go | 102 +++++++++++++++++++++ 3 files changed, 201 insertions(+), 2 deletions(-) diff --git a/capifcore/internal/securityapi/typevalidation.go b/capifcore/internal/securityapi/typevalidation.go index 4be8aee..4a9ee28 100644 --- a/capifcore/internal/securityapi/typevalidation.go +++ b/capifcore/internal/securityapi/typevalidation.go @@ -83,6 +83,27 @@ func (si SecurityInformation) Validate() error { return nil } +func (sn SecurityNotification) Validate() error { + + if len(strings.TrimSpace(string(sn.ApiInvokerId))) == 0 { + return errors.New("SecurityNotification missing required ApiInvokerId") + } + + if len(sn.ApiIds) < 1 { + return errors.New("SecurityNotification missing required ApiIds") + } + + if len(strings.TrimSpace(string(sn.Cause))) == 0 { + return errors.New("SecurityNotification missing required Cause") + } + + if sn.Cause != CauseOVERLIMITUSAGE && sn.Cause != CauseUNEXPECTEDREASON { + return errors.New("SecurityNotification unexpected value for Cause") + } + + return nil +} + func createAccessTokenError(err AccessTokenErrError, message string) AccessTokenErr { return AccessTokenErr{ Error: err, diff --git a/capifcore/internal/securityservice/security.go b/capifcore/internal/securityservice/security.go index d3d9026..aee022e 100644 --- a/capifcore/internal/securityservice/security.go +++ b/capifcore/internal/securityservice/security.go @@ -29,6 +29,7 @@ import ( "github.com/labstack/echo/v4" copystructure "github.com/mitchellh/copystructure" + "k8s.io/utils/strings/slices" "oransc.org/nonrtric/capifcore/internal/common29122" securityapi "oransc.org/nonrtric/capifcore/internal/securityapi" @@ -224,11 +225,86 @@ func (s *Security) prepareNewSecurityContext(newContext *securityapi.ServiceSecu } func (s *Security) PostTrustedInvokersApiInvokerIdDelete(ctx echo.Context, apiInvokerId string) error { - return ctx.NoContent(http.StatusNotImplemented) + var notification securityapi.SecurityNotification + + errMsg := "Unable to revoke invoker due to %s" + + if err := ctx.Bind(¬ification); err != nil { + return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "invalid format for security notification")) + } + + if err := notification.Validate(); err != nil { + return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err)) + } + + if ss, ok := s.trustedInvokers[apiInvokerId]; ok { + securityInfoCopy := s.revokeTrustedInvoker(&ss, notification, apiInvokerId) + + if len(securityInfoCopy) == 0 { + s.deleteTrustedInvoker(apiInvokerId) + } else { + ss.SecurityInfo = securityInfoCopy + s.updateTrustedInvoker(ss, apiInvokerId) + } + + } else { + return sendCoreError(ctx, http.StatusNotFound, "the invoker is not register as a trusted invoker") + } + + return ctx.NoContent(http.StatusNoContent) + +} + +func (s *Security) revokeTrustedInvoker(ss *securityapi.ServiceSecurity, notification securityapi.SecurityNotification, apiInvokerId string) []securityapi.SecurityInformation { + + data, _ := copystructure.Copy(ss.SecurityInfo) + securityInfoCopy, _ := data.([]securityapi.SecurityInformation) + + for i, context := range ss.SecurityInfo { + if notification.AefId == context.AefId || slices.Contains(notification.ApiIds, *context.ApiId) { + securityInfoCopy = append(securityInfoCopy[:i], securityInfoCopy[i+1:]...) + } + } + + return securityInfoCopy + } func (s *Security) PostTrustedInvokersApiInvokerIdUpdate(ctx echo.Context, apiInvokerId string) error { - return ctx.NoContent(http.StatusNotImplemented) + var serviceSecurity securityapi.ServiceSecurity + + errMsg := "Unable to update service security context due to %s" + + if err := ctx.Bind(&serviceSecurity); err != nil { + return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, "invalid format for service security context")) + } + + if err := serviceSecurity.Validate(); err != nil { + return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err)) + } + + if _, ok := s.trustedInvokers[apiInvokerId]; ok { + s.updateTrustedInvoker(serviceSecurity, apiInvokerId) + } else { + return sendCoreError(ctx, http.StatusNotFound, "the invoker is not register as a trusted invoker") + } + + uri := ctx.Request().Host + ctx.Request().URL.String() + ctx.Response().Header().Set(echo.HeaderLocation, ctx.Scheme()+`://`+path.Join(uri, apiInvokerId)) + + err := ctx.JSON(http.StatusOK, s.trustedInvokers[apiInvokerId]) + if err != nil { + // Something really bad happened, tell Echo that our handler failed + return err + } + + return nil +} + +func (s *Security) updateTrustedInvoker(serviceSecurity securityapi.ServiceSecurity, invokerId string) { + s.lock.Lock() + defer s.lock.Unlock() + s.trustedInvokers[invokerId] = serviceSecurity } func sendAccessTokenError(ctx echo.Context, code int, err securityapi.AccessTokenErrError, message string) error { diff --git a/capifcore/internal/securityservice/security_test.go b/capifcore/internal/securityservice/security_test.go index 1abb8ae..57e9cea 100644 --- a/capifcore/internal/securityservice/security_test.go +++ b/capifcore/internal/securityservice/security_test.go @@ -488,6 +488,108 @@ func TestGetSecurityContextByInvokerId(t *testing.T) { } } +func TestUpdateTrustedInvoker(t *testing.T) { + + requestHandler, securityUnderTest := getEcho(nil, nil, nil, nil) + + aefId := "aefId" + apiId := "apiId" + invokerId := "invokerId" + serviceSecurityTest := getServiceSecurity(aefId, apiId) + serviceSecurityTest.SecurityInfo[0].ApiId = &apiId + securityUnderTest.trustedInvokers[invokerId] = serviceSecurityTest + + // Update the service security with valid invoker, should return 200 with updated service security + newNotifURL := "http://golang.org/" + serviceSecurityTest.NotificationDestination = common29122.Uri(newNotifURL) + result := testutil.NewRequest().Post("/trustedInvokers/"+invokerId+"/update").WithJsonBody(serviceSecurityTest).Go(t, requestHandler) + + var resultResponse securityapi.ServiceSecurity + assert.Equal(t, http.StatusOK, result.Code()) + err := result.UnmarshalBodyToObject(&resultResponse) + assert.NoError(t, err, "error unmarshaling response") + assert.Equal(t, newNotifURL, string(resultResponse.NotificationDestination)) + + // Update with an service security missing required NotificationDestination, should get 400 with problem details + invalidServiceSecurity := securityapi.ServiceSecurity{ + SecurityInfo: []securityapi.SecurityInformation{ + { + AefId: &aefId, + ApiId: &apiId, + PrefSecurityMethods: []publishserviceapi.SecurityMethod{ + publishserviceapi.SecurityMethodOAUTH, + }, + }, + }, + } + + result = testutil.NewRequest().Post("/trustedInvokers/"+invokerId+"/update").WithJsonBody(invalidServiceSecurity).Go(t, requestHandler) + + assert.Equal(t, http.StatusBadRequest, result.Code()) + var problemDetails common29122.ProblemDetails + err = result.UnmarshalBodyToObject(&problemDetails) + assert.NoError(t, err, "error unmarshaling response") + assert.Equal(t, http.StatusBadRequest, *problemDetails.Status) + assert.Contains(t, *problemDetails.Cause, "missing") + assert.Contains(t, *problemDetails.Cause, "notificationDestination") + + // Update a service security that has not been registered, should get 404 with problem details + missingId := "1" + result = testutil.NewRequest().Post("/trustedInvokers/"+missingId+"/update").WithJsonBody(serviceSecurityTest).Go(t, requestHandler) + + assert.Equal(t, http.StatusNotFound, result.Code()) + err = result.UnmarshalBodyToObject(&problemDetails) + assert.NoError(t, err, "error unmarshaling response") + assert.Equal(t, http.StatusNotFound, *problemDetails.Status) + assert.Contains(t, *problemDetails.Cause, "not register") + assert.Contains(t, *problemDetails.Cause, "trusted invoker") +} + +func TestRevokeAuthorizationToInvoker(t *testing.T) { + aefId := "aefId" + apiId := "apiId" + invokerId := "invokerId" + + notification := securityapi.SecurityNotification{ + AefId: &aefId, + ApiInvokerId: invokerId, + ApiIds: []string{apiId}, + Cause: securityapi.CauseUNEXPECTEDREASON, + } + + requestHandler, securityUnderTest := getEcho(nil, nil, nil, nil) + + serviceSecurityTest := getServiceSecurity(aefId, apiId) + serviceSecurityTest.SecurityInfo[0].ApiId = &apiId + + apiIdTwo := "apiIdTwo" + secInfo := securityapi.SecurityInformation{ + AefId: &aefId, + ApiId: &apiIdTwo, + PrefSecurityMethods: []publishserviceapi.SecurityMethod{ + publishserviceapi.SecurityMethodPKI, + }, + } + + serviceSecurityTest.SecurityInfo = append(serviceSecurityTest.SecurityInfo, secInfo) + + securityUnderTest.trustedInvokers[invokerId] = serviceSecurityTest + + // Revoke apiId + result := testutil.NewRequest().Post("/trustedInvokers/"+invokerId+"/delete").WithJsonBody(notification).Go(t, requestHandler) + + assert.Equal(t, http.StatusNoContent, result.Code()) + assert.Equal(t, 1, len(securityUnderTest.trustedInvokers[invokerId].SecurityInfo)) + + notification.ApiIds = []string{apiIdTwo} + // Revoke apiIdTwo + result = testutil.NewRequest().Post("/trustedInvokers/"+invokerId+"/delete").WithJsonBody(notification).Go(t, requestHandler) + + assert.Equal(t, http.StatusNoContent, result.Code()) + _, ok := securityUnderTest.trustedInvokers[invokerId] + assert.False(t, ok) +} + func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, keycloakMgm keycloak.AccessManagement) (*echo.Echo, *Security) { swagger, err := securityapi.GetSwagger() if err != nil { -- 2.16.6 From 051a4a32068b4718ef9ddb1868e532a976de843e Mon Sep 17 00:00:00 2001 From: ychacon Date: Mon, 3 Apr 2023 10:59:06 +0200 Subject: [PATCH 13/16] Integration with keycloak, add client Issue-ID: NONRTRIC-856 Signed-off-by: ychacon Change-Id: I60bba76bf8e1d7c2e907a3a90a307481b589637a --- capifcore/configs/keycloak.yaml | 3 + capifcore/internal/config/config.go | 12 +++- capifcore/internal/keycloak/keycloak.go | 66 ++++++++++++++++++++-- .../internal/keycloak/mocks/AccessManagement.go | 32 ++++++++--- capifcore/internal/restclient/HTTPClient.go | 19 ++++++- capifcore/internal/restclient/HTTPClient_test.go | 2 +- capifcore/internal/securityapi/typeupdate.go | 7 ++- capifcore/internal/securityservice/security.go | 9 ++- .../internal/securityservice/security_test.go | 28 ++++++--- capifcore/main.go | 2 +- 10 files changed, 144 insertions(+), 36 deletions(-) diff --git a/capifcore/configs/keycloak.yaml b/capifcore/configs/keycloak.yaml index 1ca2ba9..86b3905 100644 --- a/capifcore/configs/keycloak.yaml +++ b/capifcore/configs/keycloak.yaml @@ -19,5 +19,8 @@ authorizationServer: host: "localhost" port: "8080" + admin: + user: "admin" + password: "secret" realms: invokerrealm: "invokerrealm" diff --git a/capifcore/internal/config/config.go b/capifcore/internal/config/config.go index 4a53022..f7e07e6 100644 --- a/capifcore/internal/config/config.go +++ b/capifcore/internal/config/config.go @@ -27,10 +27,16 @@ import ( "gopkg.in/yaml.v2" ) +type AdminUser struct { + User string `yaml:"user"` + Password string `yaml:"password"` +} + type AuthorizationServer struct { - Port string `yaml:"port"` - Host string `yaml:"host"` - Realms map[string]string `yaml:"realms"` + Port string `yaml:"port"` + Host string `yaml:"host"` + AdminUser AdminUser `yaml:"admin"` + Realms map[string]string `yaml:"realms"` } type Config struct { diff --git a/capifcore/internal/keycloak/keycloak.go b/capifcore/internal/keycloak/keycloak.go index 16f65c8..3646516 100644 --- a/capifcore/internal/keycloak/keycloak.go +++ b/capifcore/internal/keycloak/keycloak.go @@ -27,28 +27,44 @@ import ( "net/http" "net/url" + log "github.com/sirupsen/logrus" "oransc.org/nonrtric/capifcore/internal/config" + "oransc.org/nonrtric/capifcore/internal/restclient" ) //go:generate mockery --name AccessManagement type AccessManagement interface { // Get JWT token for a client. // Returns JWT token if client exits and credentials are correct otherwise returns error. - GetToken(clientId, clientPassword, scope string, realm string) (Jwttoken, error) + GetToken(realm string, data map[string][]string) (Jwttoken, error) + // Add new client in keycloak + AddClient(clientId string, realm string) error +} + +type AdminUser struct { + User string + Password string } type KeycloakManager struct { keycloakServerUrl string + admin AdminUser realms map[string]string + client restclient.HTTPClient } -func NewKeycloakManager(cfg *config.Config) *KeycloakManager { +func NewKeycloakManager(cfg *config.Config, c restclient.HTTPClient) *KeycloakManager { keycloakUrl := "http://" + cfg.AuthorizationServer.Host + ":" + cfg.AuthorizationServer.Port return &KeycloakManager{ keycloakServerUrl: keycloakUrl, - realms: cfg.AuthorizationServer.Realms, + client: c, + admin: AdminUser{ + User: cfg.AuthorizationServer.AdminUser.User, + Password: cfg.AuthorizationServer.AdminUser.Password, + }, + realms: cfg.AuthorizationServer.Realms, } } @@ -64,12 +80,11 @@ type Jwttoken struct { Scope string `json:"scope"` } -func (km *KeycloakManager) GetToken(clientId, clientPassword, scope string, realm string) (Jwttoken, error) { +func (km *KeycloakManager) GetToken(realm string, data map[string][]string) (Jwttoken, error) { var jwt Jwttoken getTokenUrl := km.keycloakServerUrl + "/realms/" + realm + "/protocol/openid-connect/token" - resp, err := http.PostForm(getTokenUrl, - url.Values{"grant_type": {"client_credentials"}, "client_id": {clientId}, "client_secret": {clientPassword}}) + resp, err := http.PostForm(getTokenUrl, data) if err != nil { return jwt, err @@ -88,3 +103,42 @@ func (km *KeycloakManager) GetToken(clientId, clientPassword, scope string, real json.Unmarshal([]byte(body), &jwt) return jwt, nil } + +type Client struct { + AdminURL string `json:"adminUrl,omitempty"` + BearerOnly bool `json:"bearerOnly,omitempty"` + ClientID string `json:"clientId,omitempty"` + Enabled bool `json:"enabled,omitempty"` + PublicClient bool `json:"publicClient,omitempty"` + RootURL string `json:"rootUrl,omitempty"` + ServiceAccountsEnabled bool `json:"serviceAccountsEnabled,omitempty"` +} + +func (km *KeycloakManager) AddClient(clientId string, realm string) error { + data := url.Values{"grant_type": {"password"}, "username": {km.admin.User}, "password": {km.admin.Password}, "client_id": {"admin-cli"}} + token, err := km.GetToken("master", data) + if err != nil { + log.Errorf("error wrong credentials or url %v\n", err) + return err + } + + createClientUrl := km.keycloakServerUrl + "/admin/realms/" + realm + "/clients" + newClient := Client{ + ClientID: clientId, + Enabled: true, + ServiceAccountsEnabled: true, + BearerOnly: false, + PublicClient: false, + } + + body, _ := json.Marshal(newClient) + var headers = map[string]string{"Content-Type": "application/json", "Authorization": "Bearer " + token.AccessToken} + if error := restclient.Post(createClientUrl, body, headers, km.client); error != nil { + log.Errorf("error with http request: %+v\n", err) + return err + } + + log.Info("Created new client") + return nil + +} diff --git a/capifcore/internal/keycloak/mocks/AccessManagement.go b/capifcore/internal/keycloak/mocks/AccessManagement.go index 9ac2179..59b914a 100644 --- a/capifcore/internal/keycloak/mocks/AccessManagement.go +++ b/capifcore/internal/keycloak/mocks/AccessManagement.go @@ -12,23 +12,37 @@ type AccessManagement struct { mock.Mock } -// GetToken provides a mock function with given fields: clientId, clientPassword, scope, realm -func (_m *AccessManagement) GetToken(clientId string, clientPassword string, scope string, realm string) (keycloak.Jwttoken, error) { - ret := _m.Called(clientId, clientPassword, scope, realm) +// AddClient provides a mock function with given fields: clientId, realm +func (_m *AccessManagement) AddClient(clientId string, realm string) error { + ret := _m.Called(clientId, realm) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(clientId, realm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetToken provides a mock function with given fields: realm, data +func (_m *AccessManagement) GetToken(realm string, data map[string][]string) (keycloak.Jwttoken, error) { + ret := _m.Called(realm, data) var r0 keycloak.Jwttoken var r1 error - if rf, ok := ret.Get(0).(func(string, string, string, string) (keycloak.Jwttoken, error)); ok { - return rf(clientId, clientPassword, scope, realm) + if rf, ok := ret.Get(0).(func(string, map[string][]string) (keycloak.Jwttoken, error)); ok { + return rf(realm, data) } - if rf, ok := ret.Get(0).(func(string, string, string, string) keycloak.Jwttoken); ok { - r0 = rf(clientId, clientPassword, scope, realm) + if rf, ok := ret.Get(0).(func(string, map[string][]string) keycloak.Jwttoken); ok { + r0 = rf(realm, data) } else { r0 = ret.Get(0).(keycloak.Jwttoken) } - if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok { - r1 = rf(clientId, clientPassword, scope, realm) + if rf, ok := ret.Get(1).(func(string, map[string][]string) error); ok { + r1 = rf(realm, data) } else { r1 = ret.Error(1) } diff --git a/capifcore/internal/restclient/HTTPClient.go b/capifcore/internal/restclient/HTTPClient.go index de0ad1d..c771a54 100644 --- a/capifcore/internal/restclient/HTTPClient.go +++ b/capifcore/internal/restclient/HTTPClient.go @@ -45,12 +45,19 @@ func (pe RequestError) Error() string { } func Put(url string, body []byte, client HTTPClient) error { - return do(http.MethodPut, url, body, ContentTypeJSON, client) + var header = map[string]string{"Content-Type": ContentTypeJSON} + return do(http.MethodPut, url, body, header, client) } -func do(method string, url string, body []byte, contentType string, client HTTPClient) error { +func Post(url string, body []byte, header map[string]string, client HTTPClient) error { + return do(http.MethodPost, url, body, header, client) +} + +func do(method string, url string, body []byte, header map[string]string, client HTTPClient) error { if req, reqErr := http.NewRequest(method, url, bytes.NewBuffer(body)); reqErr == nil { - req.Header.Set("Content-Type", contentType) + if len(header) > 0 { + setHeader(req, header) + } if response, respErr := client.Do(req); respErr == nil { if isResponseSuccess(response.StatusCode) { return nil @@ -65,6 +72,12 @@ func do(method string, url string, body []byte, contentType string, client HTTPC } } +func setHeader(req *http.Request, header map[string]string) { + for key, element := range header { + req.Header.Set(key, element) + } +} + func isResponseSuccess(statusCode int) bool { return statusCode >= http.StatusOK && statusCode <= 299 } diff --git a/capifcore/internal/restclient/HTTPClient_test.go b/capifcore/internal/restclient/HTTPClient_test.go index 21186aa..e390686 100644 --- a/capifcore/internal/restclient/HTTPClient_test.go +++ b/capifcore/internal/restclient/HTTPClient_test.go @@ -109,7 +109,7 @@ func Test_doErrorCases(t *testing.T) { StatusCode: tt.args.mockReturnStatus, Body: io.NopCloser(bytes.NewReader(tt.args.mockReturnBody)), }, tt.args.mockReturnError) - err := do("PUT", tt.args.url, nil, "", &clientMock) + err := do("PUT", tt.args.url, nil, map[string]string{}, &clientMock) assertions.Equal(tt.wantErr, err, tt.name) }) } diff --git a/capifcore/internal/securityapi/typeupdate.go b/capifcore/internal/securityapi/typeupdate.go index 364c123..3402b8e 100644 --- a/capifcore/internal/securityapi/typeupdate.go +++ b/capifcore/internal/securityapi/typeupdate.go @@ -31,7 +31,6 @@ 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) @@ -39,9 +38,11 @@ func (newContext *ServiceSecurity) PrepareNewSecurityContext(services []publishs checkNil := securityInfo.ApiId != nil && securityInfo.AefId != nil if checkNil { service := getServiceByApiId(&services, securityInfo.ApiId) - afpProfile := service.GetAefProfileById(securityInfo.AefId) + if service != nil { + afpProfile := service.GetAefProfileById(securityInfo.AefId) + addSecurityMethodsFromAefProfile(afpProfile) + } - addSecurityMethodsFromAefProfile(afpProfile) } } diff --git a/capifcore/internal/securityservice/security.go b/capifcore/internal/securityservice/security.go index aee022e..e211f67 100644 --- a/capifcore/internal/securityservice/security.go +++ b/capifcore/internal/securityservice/security.go @@ -23,6 +23,7 @@ package security import ( "fmt" "net/http" + "net/url" "path" "strings" "sync" @@ -89,7 +90,8 @@ func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId st } } } - jwtToken, err := s.keycloak.GetToken(accessTokenReq.ClientId, *accessTokenReq.ClientSecret, *accessTokenReq.Scope, "invokerrealm") + data := url.Values{"grant_type": {"client_credentials"}, "client_id": {accessTokenReq.ClientId}, "client_secret": {*accessTokenReq.ClientSecret}} + jwtToken, err := s.keycloak.GetToken("invokerrealm", data) if err != nil { return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorUnauthorizedClient, err.Error()) } @@ -190,6 +192,11 @@ func (s *Security) PutTrustedInvokersApiInvokerId(ctx echo.Context, apiInvokerId return sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf(errMsg, err)) } + err = s.keycloak.AddClient(apiInvokerId, "invokerrealm") + 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)) diff --git a/capifcore/internal/securityservice/security_test.go b/capifcore/internal/securityservice/security_test.go index 57e9cea..1dda127 100644 --- a/capifcore/internal/securityservice/security_test.go +++ b/capifcore/internal/securityservice/security_test.go @@ -66,7 +66,7 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) { Scope: "3gpp#aefIdpath", } accessMgmMock := keycloackmocks.AccessManagement{} - accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, nil) + accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("map[string][]string")).Return(jwt, nil) requestHandler, _ := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) @@ -94,7 +94,7 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) { invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret) serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId) publishRegisterMock.AssertCalled(t, "IsAPIPublished", aefId, path) - accessMgmMock.AssertCalled(t, "GetToken", clientId, clientSecret, "3gpp#"+aefId+":"+path, "invokerrealm") + accessMgmMock.AssertNumberOfCalls(t, "GetToken", 1) } func TestPostSecurityIdTokenInvokerNotRegistered(t *testing.T) { @@ -213,7 +213,7 @@ func TestPostSecurityIdTokenInvokerInvalidCredentials(t *testing.T) { jwt := keycloak.Jwttoken{} accessMgmMock := keycloackmocks.AccessManagement{} - accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, errors.New("invalid_credentials")) + accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("map[string][]string")).Return(jwt, errors.New("invalid_credentials")) requestHandler, _ := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) @@ -239,7 +239,7 @@ func TestPostSecurityIdTokenInvokerInvalidCredentials(t *testing.T) { invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret) serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId) publishRegisterMock.AssertCalled(t, "IsAPIPublished", aefId, path) - accessMgmMock.AssertCalled(t, "GetToken", clientId, clientSecret, "3gpp#"+aefId+":"+path, "invokerrealm") + accessMgmMock.AssertNumberOfCalls(t, "GetToken", 1) } func TestPutTrustedInvokerSuccessfully(t *testing.T) { @@ -263,7 +263,10 @@ func TestPutTrustedInvokerSuccessfully(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices) - requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) + accessMgmMock := keycloackmocks.AccessManagement{} + accessMgmMock.On("AddClient", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil) + + requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) invokerId := "invokerId" serviceSecurityUnderTest := getServiceSecurity(aefId, apiId) @@ -282,6 +285,7 @@ func TestPutTrustedInvokerSuccessfully(t *testing.T) { assert.Equal(t, *security.SelSecurityMethod, publishserviceapi.SecurityMethodPKI) } invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId) + accessMgmMock.AssertCalled(t, "AddClient", invokerId, "invokerrealm") } @@ -350,7 +354,10 @@ func TestPutTrustedInvokerInterfaceDetailsNotNil(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices) - requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) + accessMgmMock := keycloackmocks.AccessManagement{} + accessMgmMock.On("AddClient", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil) + + requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) invokerId := "invokerId" serviceSecurityUnderTest := getServiceSecurity(aefId, apiId) @@ -379,7 +386,7 @@ func TestPutTrustedInvokerInterfaceDetailsNotNil(t *testing.T) { assert.Equal(t, publishserviceapi.SecurityMethodPSK, *security.SelSecurityMethod) } invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", invokerId) - + accessMgmMock.AssertCalled(t, "AddClient", invokerId, "invokerrealm") } func TestPutTrustedInvokerNotFoundSecurityMethod(t *testing.T) { @@ -399,7 +406,10 @@ func TestPutTrustedInvokerNotFoundSecurityMethod(t *testing.T) { publishRegisterMock := publishmocks.PublishRegister{} publishRegisterMock.On("GetAllPublishedServices").Return(publishedServices) - requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, nil) + accessMgmMock := keycloackmocks.AccessManagement{} + accessMgmMock.On("AddClient", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil) + + requestHandler, _ := getEcho(nil, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock) invokerId := "invokerId" serviceSecurityUnderTest := getServiceSecurity("aefId", "apiId") @@ -510,7 +520,7 @@ func TestUpdateTrustedInvoker(t *testing.T) { assert.NoError(t, err, "error unmarshaling response") assert.Equal(t, newNotifURL, string(resultResponse.NotificationDestination)) - // Update with an service security missing required NotificationDestination, should get 400 with problem details + // Update with a service security missing required NotificationDestination, should get 400 with problem details invalidServiceSecurity := securityapi.ServiceSecurity{ SecurityInfo: []securityapi.SecurityInformation{ { diff --git a/capifcore/main.go b/capifcore/main.go index f8b9daf..5ba3923 100644 --- a/capifcore/main.go +++ b/capifcore/main.go @@ -94,7 +94,7 @@ func getEcho() *echo.Echo { if err != nil { log.Fatalf("Error loading configuration file\n: %s", err) } - km := keycloak.NewKeycloakManager(cfg) + km := keycloak.NewKeycloakManager(cfg, &http.Client{}) var group *echo.Group // Register ProviderManagement -- 2.16.6 From b01ea504c8924bbf86355d6a888b767e6af853c0 Mon Sep 17 00:00:00 2001 From: ychacon Date: Tue, 4 Apr 2023 15:48:26 +0200 Subject: [PATCH 14/16] Adding docker-compose file for keycloak and postgres DB Issue-ID: NONRTRIC-856 Signed-off-by: ychacon Change-Id: I6c59369cc54dc2c67584901755c1a9370b6085dc --- capifcore/docker-compose.yml | 84 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 capifcore/docker-compose.yml diff --git a/capifcore/docker-compose.yml b/capifcore/docker-compose.yml new file mode 100644 index 0000000..da2be5b --- /dev/null +++ b/capifcore/docker-compose.yml @@ -0,0 +1,84 @@ +# Copyright (C) 2023 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= +# +version: '3.5' + +services: + postgres: + container_name: postgres_container + image: postgres:latest + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: "exit 0" + ports: + - "5432:5432" + networks: + - capif + restart: unless-stopped + + pgadmin: + container_name: pgadmin_container + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - pgadmin:/var/lib/pgadmin + + ports: + - "${PGADMIN_PORT:-5050}:80" + networks: + - capif + restart: unless-stopped + + keycloak: + container_name: keycloak + image: quay.io/keycloak/keycloak:20.0.3 + environment: + KC_DB: postgres + KC_DB_URL_HOST: postgres_container + KC_DB_URL_DATABASE: keycloak + KC_DB_PASSWORD: password + KC_DB_USERNAME: keycloak + KC_DB_SCHEMA: public + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: secret + ports: + - 8080:8080 + depends_on: + - postgres + healthcheck: + test: curl --fail --silent http://localhost:8180/health/ready 2>&1 || exit 1 + interval: 10s + timeout: 10s + retries: 5 + entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"] + networks: + - capif + +networks: + capif: + driver: bridge + +volumes: + postgres_data: + driver: local + pgadmin: \ No newline at end of file -- 2.16.6 From 777987abe351d45394cda0404f863147e83cb53e Mon Sep 17 00:00:00 2001 From: ychacon Date: Tue, 25 Apr 2023 10:02:56 +0200 Subject: [PATCH 15/16] Bug fix Issue-ID: NONRTRIC-861 Signed-off-by: ychacon Change-Id: Id77be8cfb5d857cb722277d11fda33b464c09a64 --- capifcore/internal/providermanagementapi/typeaccess.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capifcore/internal/providermanagementapi/typeaccess.go b/capifcore/internal/providermanagementapi/typeaccess.go index 78b68c0..5257701 100644 --- a/capifcore/internal/providermanagementapi/typeaccess.go +++ b/capifcore/internal/providermanagementapi/typeaccess.go @@ -22,7 +22,7 @@ package providermanagementapi func (ed APIProviderEnrolmentDetails) GetExposingFunctionIdsForPublisher(apfId string) []string { for _, registeredFunc := range *ed.ApiProvFuncs { - if *registeredFunc.ApiProvFuncId == apfId && registeredFunc.isProvidingFunction() { + if *registeredFunc.ApiProvFuncId == apfId { return ed.getExposingFunctionIds() } } -- 2.16.6 From b873255143602920b7b0728392fbd8fe304b73d3 Mon Sep 17 00:00:00 2001 From: ychacon Date: Wed, 26 Apr 2023 09:11:45 +0200 Subject: [PATCH 16/16] Adding Capif provider Issue-ID: NONRTRIC-861 Signed-off-by: ychacon Change-Id: If80624eccbbafd8c9ae9d14658a3bb5599fdec2f --- provider/Dockerfile | 38 +++++ provider/generate.sh | 156 +++++++++++++++++++++ .../common/generator_settings.yaml | 29 ++++ .../common29122/generator_settings.yaml | 30 ++++ .../common29571/generator_settings.yaml | 27 ++++ .../generator_settings_types.yaml | 28 ++++ .../generator_settings_types.yaml | 29 ++++ provider/handler/common_handler.go | 81 +++++++++++ provider/handler/getapi_handler.go | 80 +++++++++++ provider/handler/home_handler.go | 32 +++++ provider/handler/publishapi_handler.go | 98 +++++++++++++ provider/handler/registration_handler.go | 88 ++++++++++++ .../gentools/commoncollector/commoncollector.go | 119 ++++++++++++++++ .../gentools/commoncollector/definitions.txt | 38 +++++ provider/internal/gentools/enumfixer/enumfixer.go | 117 ++++++++++++++++ .../specificationfixer/specificationfixer.go | 80 +++++++++++ provider/main.go | 89 ++++++++++++ provider/view/base.html | 53 +++++++ provider/view/css/style.css | 41 ++++++ provider/view/getapi.html | 109 ++++++++++++++ provider/view/home.html | 70 +++++++++ provider/view/js/script.js | 121 ++++++++++++++++ provider/view/publishapi.html | 111 +++++++++++++++ provider/view/registration.html | 119 ++++++++++++++++ 24 files changed, 1783 insertions(+) create mode 100644 provider/Dockerfile create mode 100755 provider/generate.sh create mode 100644 provider/gogeneratorspecs/common/generator_settings.yaml create mode 100644 provider/gogeneratorspecs/common29122/generator_settings.yaml create mode 100644 provider/gogeneratorspecs/common29571/generator_settings.yaml create mode 100644 provider/gogeneratorspecs/providermanagementapi/generator_settings_types.yaml create mode 100644 provider/gogeneratorspecs/publishserviceapi/generator_settings_types.yaml create mode 100644 provider/handler/common_handler.go create mode 100644 provider/handler/getapi_handler.go create mode 100644 provider/handler/home_handler.go create mode 100644 provider/handler/publishapi_handler.go create mode 100644 provider/handler/registration_handler.go create mode 100644 provider/internal/gentools/commoncollector/commoncollector.go create mode 100644 provider/internal/gentools/commoncollector/definitions.txt create mode 100644 provider/internal/gentools/enumfixer/enumfixer.go create mode 100644 provider/internal/gentools/specificationfixer/specificationfixer.go create mode 100644 provider/main.go create mode 100644 provider/view/base.html create mode 100644 provider/view/css/style.css create mode 100644 provider/view/getapi.html create mode 100644 provider/view/home.html create mode 100644 provider/view/js/script.js create mode 100644 provider/view/publishapi.html create mode 100644 provider/view/registration.html diff --git a/provider/Dockerfile b/provider/Dockerfile new file mode 100644 index 0000000..bfadfb8 --- /dev/null +++ b/provider/Dockerfile @@ -0,0 +1,38 @@ +#================================================================================== +# 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. +# +# This source code is part of the near-RT RIC (RAN Intelligent Controller) +# platform project (RICP). +#================================================================================== + +## +## Build +## +FROM nexus3.o-ran-sc.org:10001/golang:1.19.2-bullseye AS build +WORKDIR /app +COPY go.mod . +COPY go.sum . +RUN go mod download +COPY . . +RUN go build -o /capifprovider +## +## Deploy +## +FROM gcr.io/distroless/base-debian11 +WORKDIR / +## Copy from "build" stage +COPY --from=build /capifprovider . +USER nonroot:nonroot +ENTRYPOINT ["/capifprovider"] diff --git a/provider/generate.sh b/provider/generate.sh new file mode 100755 index 0000000..36dfe59 --- /dev/null +++ b/provider/generate.sh @@ -0,0 +1,156 @@ +# - +# ========================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=================================== +# + +cwd=$(pwd) + +mkdir -p specs + +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.222/29222-h60.zip -o specs/apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.122/29122-h70.zip -o specs/common29122apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.508/29508-h80.zip -o specs/common29508apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.510/29510-h70.zip -o specs/common29510apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.512/29512-h80.zip -o specs/common29512apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.514/29514-h60.zip -o specs/common29514apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.517/29517-h70.zip -o specs/common29517apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.518/29518-h70.zip -o specs/common29518apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.522/29522-h70.zip -o specs/common29522apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.523/29523-h80.zip -o specs/common29523apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.554/29554-h40.zip -o specs/common29554apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.571/29571-h70.zip -o specs/common29571apidef.zip +curl https://www.3gpp.org/ftp/Specs/archive/29_series/29.572/29572-h60.zip -o specs/common29572apidef.zip + +cd specs/ + +jar xvf apidef.zip +jar xvf common29122apidef.zip +jar xvf common29508apidef.zip +jar xvf common29510apidef.zip +jar xvf common29512apidef.zip +jar xvf common29514apidef.zip +jar xvf common29517apidef.zip +jar xvf common29518apidef.zip +jar xvf common29522apidef.zip +jar xvf common29523apidef.zip +jar xvf common29554apidef.zip +jar xvf common29571apidef.zip +jar xvf common29572apidef.zip + +# Remove types that are not used by CAPIF that have dependencies to other specifications. +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\CivicAddress/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\ExternalMbsServiceArea/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\GeographicArea/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\GeoServiceArea/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\MbsMediaComp/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\MbsMediaCompRm/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\MbsMediaInfo/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\MbsServiceInfo/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\MbsSession/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed -e 'H;x;/^\( *\)\n\1/{s/\n.*//;x;d;}' -e 's/.*//;x;/\SpatialValidityCond/{s/^\( *\).*/ \1/;x;d;}' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml + +# Remove attributes that can not be generated easily. +sed '/accessTokenError.*/,+3d' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml +sed '/accessTokenRequest.*/,+3d' TS29571_CommonData.yaml >temp.yaml +mv temp.yaml TS29571_CommonData.yaml + + +sed '/oneOf.*/,+2d' TS29222_CAPIF_Publish_Service_API.yaml >temp.yaml +mv temp.yaml TS29222_CAPIF_Publish_Service_API.yaml + +# Replace references to external specs that are collected to the common spec by the commoncollector +# +cat TS29122_CommonData.yaml | sed 's/TS29572_Nlmf_Location/CommonData/g' > temp.yaml +mv temp.yaml TS29122_CommonData.yaml +cat TS29122_CommonData.yaml | sed 's/TS29554_Npcf_BDTPolicyControl/CommonData/g' > temp.yaml +mv temp.yaml TS29122_CommonData.yaml +cat TS29122_CommonData.yaml | sed 's/TS29514_Npcf_PolicyAuthorization/CommonData/g' > temp.yaml +mv temp.yaml TS29122_CommonData.yaml +cat TS29571_CommonData.yaml | sed 's/TS29514_Npcf_PolicyAuthorization/CommonData/g' > temp.yaml +mv temp.yaml TS29571_CommonData.yaml +cat TS29571_CommonData.yaml | sed 's/TS29572_Nlmf_Location/CommonData/g' > temp.yaml +mv temp.yaml TS29571_CommonData.yaml +cat TS29222_CAPIF_Publish_Service_API.yaml | sed 's/TS29572_Nlmf_Location/CommonData/g' > temp.yaml +mv temp.yaml TS29222_CAPIF_Publish_Service_API.yaml +# + +# This spec has references to itself that need to be removed +cat TS29571_CommonData.yaml | sed 's/TS29571_CommonData.yaml//g' > temp.yaml +mv temp.yaml TS29571_CommonData.yaml + +cd $cwd + +echo "Fixing enums" +cd internal/gentools/enumfixer +go build . +./enumfixer -apidir=../../../specs + +cd $cwd +echo "Gathering common references" +cd internal/gentools/commoncollector +go build . +./commoncollector -apidir=../../../specs + +cd $cwd +echo "Fixing misc in specifications" +cd internal/gentools/specificationfixer +go build . +./specificationfixer -apidir=../../../specs + +cd $cwd + +go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.10.1 +PATH=$PATH:~/go/bin + +echo "Generating TS29122_CommonData" +mkdir -p internal/common29122 +oapi-codegen --config gogeneratorspecs/common29122/generator_settings.yaml specs/TS29122_CommonData.yaml + +echo "Generating aggregated CommonData" +mkdir -p internal/common +oapi-codegen --config gogeneratorspecs/common/generator_settings.yaml specs/CommonData.yaml + +echo "Generating TS29571_CommonData" +mkdir -p internal/common29571 +oapi-codegen --config gogeneratorspecs/common29571/generator_settings.yaml specs/TS29571_CommonData.yaml + + +echo "Generating TS29222_CAPIF_API_Provider_Management_API" +mkdir -p internal/providermanagementapi +oapi-codegen --config gogeneratorspecs/providermanagementapi/generator_settings_types.yaml specs/TS29222_CAPIF_API_Provider_Management_API.yaml + +echo "Generating TS29222_CAPIF_Publish_Service_API" +mkdir -p internal/publishserviceapi +oapi-codegen --config gogeneratorspecs/publishserviceapi/generator_settings_types.yaml specs/TS29222_CAPIF_Publish_Service_API.yaml + +echo "Cleanup" +rm -rf specs + +echo "Generating mocks." +go generate ./... \ No newline at end of file diff --git a/provider/gogeneratorspecs/common/generator_settings.yaml b/provider/gogeneratorspecs/common/generator_settings.yaml new file mode 100644 index 0000000..af302a1 --- /dev/null +++ b/provider/gogeneratorspecs/common/generator_settings.yaml @@ -0,0 +1,29 @@ +# - +# ========================LICENSE_START================================= +# O-RAN-SC +# %% +# Copyright (C) 2022: 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=================================== +# + +output: + internal/common/common.gen.go +package: common +generate: + - types + - skip-prune + - spec +import-mapping: + TS29571_CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common29571 diff --git a/provider/gogeneratorspecs/common29122/generator_settings.yaml b/provider/gogeneratorspecs/common29122/generator_settings.yaml new file mode 100644 index 0000000..69f77e2 --- /dev/null +++ b/provider/gogeneratorspecs/common29122/generator_settings.yaml @@ -0,0 +1,30 @@ +# - +# ========================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=================================== +# + +output: + internal/common29122/common29122.gen.go +package: common29122 +generate: + - types + - skip-prune + - spec +import-mapping: + TS29571_CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common29571 + CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common \ No newline at end of file diff --git a/provider/gogeneratorspecs/common29571/generator_settings.yaml b/provider/gogeneratorspecs/common29571/generator_settings.yaml new file mode 100644 index 0000000..c83b629 --- /dev/null +++ b/provider/gogeneratorspecs/common29571/generator_settings.yaml @@ -0,0 +1,27 @@ +# - +# ========================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=================================== +# + +output: + internal/common29571/common29571.gen.go +package: common29571 +generate: + - types + - skip-prune + - spec \ No newline at end of file diff --git a/provider/gogeneratorspecs/providermanagementapi/generator_settings_types.yaml b/provider/gogeneratorspecs/providermanagementapi/generator_settings_types.yaml new file mode 100644 index 0000000..fd9bd0a --- /dev/null +++ b/provider/gogeneratorspecs/providermanagementapi/generator_settings_types.yaml @@ -0,0 +1,28 @@ +# - +# ========================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=================================== +# + +output: + internal/providermanagementapi/providermanagementapi-types.gen.go +package: providermanagementapi +generate: + - types +import-mapping: + TS29122_CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common29122 + TS29571_CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common29571 \ No newline at end of file diff --git a/provider/gogeneratorspecs/publishserviceapi/generator_settings_types.yaml b/provider/gogeneratorspecs/publishserviceapi/generator_settings_types.yaml new file mode 100644 index 0000000..1a5f416 --- /dev/null +++ b/provider/gogeneratorspecs/publishserviceapi/generator_settings_types.yaml @@ -0,0 +1,29 @@ +# - +# ========================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=================================== +# + +output: + internal/publishserviceapi/publishserviceapi-types.gen.go +package: publishserviceapi +generate: + - types +import-mapping: + TS29122_CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common29122 + TS29571_CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common29571 + CommonData.yaml: oransc.org/nonrtric/capifprov/internal/common \ No newline at end of file diff --git a/provider/handler/common_handler.go b/provider/handler/common_handler.go new file mode 100644 index 0000000..73952ee --- /dev/null +++ b/provider/handler/common_handler.go @@ -0,0 +1,81 @@ +// - +// +// ========================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 handler + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +func makeRequest(method, url string, headers map[string]string, data interface{}) ([]byte, error) { + client := &http.Client{} + + // Create a new HTTP request with the specified method and URL + req, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, err + } + + // Set any headers specified in the headers map + for k, v := range headers { + req.Header.Set(k, v) + } + + // If there is data to send, marshal it to JSON and set it as the request body + if data != nil { + jsonBytes, err := json.Marshal(data) + if err != nil { + return nil, err + } + req.Body = io.NopCloser(bytes.NewReader(jsonBytes)) + } + + // Send the request and get the response + if resp, err := client.Do(req); err == nil { + if isResponseSuccess(resp.StatusCode) { + defer resp.Body.Close() + + // Read the response body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return respBody, nil + } else { + return nil, getRequestError(resp) + } + } else { + return nil, err + } +} + +func isResponseSuccess(statusCode int) bool { + return statusCode >= http.StatusOK && statusCode <= 299 +} + +func getRequestError(response *http.Response) error { + defer response.Body.Close() + responseData, _ := io.ReadAll(response.Body) + + return fmt.Errorf("message: %v code: %v", string(responseData), response.StatusCode) +} diff --git a/provider/handler/getapi_handler.go b/provider/handler/getapi_handler.go new file mode 100644 index 0000000..01c33e6 --- /dev/null +++ b/provider/handler/getapi_handler.go @@ -0,0 +1,80 @@ +// - +// +// ========================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 handler + +import ( + "encoding/json" + "fmt" + "net/http" + + log "github.com/sirupsen/logrus" + + "github.com/labstack/echo/v4" + "oransc.org/nonrtric/capifprov/internal/publishserviceapi" +) + +func GetApiRequest(server string) echo.HandlerFunc { + return func(c echo.Context) error { + + aefId := c.FormValue("apfId") + if aefId == "" { + return c.Render(http.StatusOK, "getapi.html", map[string]interface{}{ + "isResponse": false, + "isError": false, + }) + } + + //server format: http://localhost:8090 + url := server + "/published-apis/v1/" + aefId + "/service-apis" + log.Infof("[Get API] to %v for aefId: %v", url, aefId) + + headers := map[string]string{ + "Content-Type": "text/plain", + } + resp, err := makeRequest("GET", url, headers, nil) + if err != nil { + log.Errorf("[Get API] %v", fmt.Sprintf("error: %v", err)) + return c.Render(http.StatusBadRequest, "getapi.html", map[string]interface{}{ + "response": fmt.Sprintf("error: %v", err), + "isError": true, + "isResponse": false, + }) + } + log.Infof("[Get API] Response from service: %+v error: %v\n", string(resp), err) + + var resAPIs []publishserviceapi.ServiceAPIDescription + err = json.Unmarshal(resp, &resAPIs) + if err != nil { + log.Error("[Get API] error unmarshaling parameter ServiceAPIDescription as JSON") + return c.Render(http.StatusBadRequest, "getapi.html", map[string]interface{}{ + "isResponse": false, + "isError": true, + "response": "error unmarshaling parameter ServiceAPIDescription as JSON", + }) + } + + bytes, _ := json.Marshal(resAPIs) + log.Infof("[Get API] There are %v ServiceAPIDescription objects available", len(resAPIs)) + return c.Render(http.StatusOK, "getapi.html", map[string]interface{}{ + "isResponse": true, + "response": string(bytes), + }) + } +} diff --git a/provider/handler/home_handler.go b/provider/handler/home_handler.go new file mode 100644 index 0000000..619d624 --- /dev/null +++ b/provider/handler/home_handler.go @@ -0,0 +1,32 @@ +// - +// +// ========================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 handler + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +func HomeHandler(c echo.Context) error { + return c.Render(http.StatusOK, "home.html", map[string]interface{}{ + "name": "HOME", + }) +} diff --git a/provider/handler/publishapi_handler.go b/provider/handler/publishapi_handler.go new file mode 100644 index 0000000..99e6775 --- /dev/null +++ b/provider/handler/publishapi_handler.go @@ -0,0 +1,98 @@ +// - +// +// ========================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 handler + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + log "github.com/sirupsen/logrus" + "oransc.org/nonrtric/capifprov/internal/publishserviceapi" +) + +func PublishapiHandler(c echo.Context) error { + return c.Render(http.StatusOK, "publishapi.html", map[string]interface{}{ + "isError": false, + "isResponse": false, + }) +} + +func PublishApiFormHandler(server string) echo.HandlerFunc { + return func(c echo.Context) error { + + apfId := c.FormValue("apfId") + if apfId == "" { + return c.Render(http.StatusBadRequest, "publishapi.html", map[string]interface{}{ + "isError": true, + "isResponse": false, + "response": "field apfId is needed", + }) + } + + //server format: http://localhost:8090 + url := server + "/published-apis/v1/" + apfId + "/service-apis" + + log.Infof("[Publish API] url to capif core %v for aefId: %v", url, apfId) + var apiDescription publishserviceapi.ServiceAPIDescription + + err := json.Unmarshal([]byte(c.FormValue("apiDescription")), &apiDescription) + if err != nil { + log.Error("[Publish API] error unmarshaling parameter ServiceAPIDescription as JSON") + return c.Render(http.StatusBadRequest, "publishapi.html", map[string]interface{}{ + "isResponse": false, + "isError": true, + "response": "error unmarshaling parameter ServiceAPIDescription as JSON", + }) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + resp, err := makeRequest("POST", url, headers, apiDescription) + if err != nil { + log.Errorf("[Publish API] %v", fmt.Sprintf("error: %v", err)) + return c.Render(http.StatusBadRequest, "publishapi.html", map[string]interface{}{ + "isResponse": false, + "isError": true, + "response": fmt.Sprintf("error: %v", err), + }) + } + + var resAPI publishserviceapi.ServiceAPIDescription + err = json.Unmarshal(resp, &resAPI) + if err != nil { + log.Error("[Publish API] error unmarshaling parameter ServiceAPIDescription as JSON") + return c.Render(http.StatusBadRequest, "publishapi.html", map[string]interface{}{ + "isResponse": false, + "isError": true, + "response": "Error unmarshaling parameter ServiceAPIDescription as JSON", + }) + } + + bytes, _ := json.Marshal(resAPI) + log.Infof("[Publish API] API %v with the id: %v has been register", resAPI.ApiName, *resAPI.ApiId) + return c.Render(http.StatusOK, "publishapi.html", map[string]interface{}{ + "isResponse": true, + "response": string(bytes), + }) + } +} diff --git a/provider/handler/registration_handler.go b/provider/handler/registration_handler.go new file mode 100644 index 0000000..0259999 --- /dev/null +++ b/provider/handler/registration_handler.go @@ -0,0 +1,88 @@ +// - +// +// ========================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 handler + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + log "github.com/sirupsen/logrus" + "oransc.org/nonrtric/capifprov/internal/providermanagementapi" +) + +func RegistrationHandler(c echo.Context) error { + return c.Render(http.StatusOK, "registration.html", map[string]interface{}{ + "isError": false, + "isResponse": false, + }) +} + +func RegistrationFormHandler(server string) echo.HandlerFunc { + return func(c echo.Context) error { + + url := server + "/api-provider-management/v1/registrations" + log.Infof("[Register provider] url to capif core %v\n", url) + + var newProvider providermanagementapi.APIProviderEnrolmentDetails + err := json.Unmarshal([]byte(c.FormValue("enrolmentDetails")), &newProvider) + if err != nil { + log.Error("[Register provider] error unmarshaling parameter enrolmentDetails as JSON") + return c.Render(http.StatusBadRequest, "registration.html", map[string]interface{}{ + "isResponse": false, + "isError": true, + "response": "Error unmarshaling parameter enrolmentDetails as JSON", + }) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + resp, err := makeRequest("POST", url, headers, newProvider) + if err != nil { + log.Errorf("[Register provider] %v", fmt.Sprintf("error: %v", err)) + return c.Render(http.StatusBadRequest, "registration.html", map[string]interface{}{ + "isResponse": false, + "isError": true, + "response": fmt.Sprintf("error: %v", err), + }) + } + + var resProvider providermanagementapi.APIProviderEnrolmentDetails + err = json.Unmarshal(resp, &resProvider) + if err != nil { + log.Error("[Register provider] error unmarshaling parameter enrolmentDetails as JSON") + return c.Render(http.StatusBadRequest, "registration.html", map[string]interface{}{ + "isResponse": false, + "isError": true, + "response": "error unmarshaling parameter enrolmentDetails as JSON", + }) + } + + bytes, _ := json.Marshal(resProvider) + log.Infof("[Register provider] Api Provider domain %v has been register\n", resProvider.ApiProvDomId) + return c.Render(http.StatusOK, "registration.html", map[string]interface{}{ + "isResponse": true, + "isError": false, + "response": string(bytes), + }) + } +} diff --git a/provider/internal/gentools/commoncollector/commoncollector.go b/provider/internal/gentools/commoncollector/commoncollector.go new file mode 100644 index 0000000..ab85e0a --- /dev/null +++ b/provider/internal/gentools/commoncollector/commoncollector.go @@ -0,0 +1,119 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 main + +import ( + "bufio" + "flag" + "io/ioutil" + "log" + "os" + "strings" + + "gopkg.in/yaml.v2" +) + +var apiDir *string +var common = map[interface{}]interface{}{ + "openapi": "3.0.0", + "info": map[interface{}]interface{}{ + "title": "Common", + "version": "1.0.0", + }, + "components": map[interface{}]interface{}{ + "schemas": map[interface{}]interface{}{}, + }, +} + +func main() { + apiDir = flag.String("apidir", "", "Directory containing API definitions to fix") + flag.Parse() + + file, err := os.Open("definitions.txt") + if err != nil { + log.Fatalf("Error opening file: %v", err) + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + scanner := bufio.NewScanner(file) + components := common["components"] + cMap := components.(map[interface{}]interface{}) + schemas := cMap["schemas"].(map[interface{}]interface{}) + for scanner.Scan() { + name, data := getDependency(scanner.Text()) + if name == "EthFlowDescription" { + changeToLocalReference("fDir", "FlowDirection", data) + } + if name == "ReportingInformation" { + changeToLocalReference("notifMethod", "NotificationMethod", data) + } + if name == "RelativeCartesianLocation" { + properties := data["properties"].(map[interface{}]interface{}) + delete(properties, true) + data["required"] = remove(data["required"].([]interface{}), 1) + } + schemas[name] = data + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + modCommon, err := yaml.Marshal(common) + if err != nil { + log.Fatalf("Marshal: #%v ", err) + } + err = ioutil.WriteFile(*apiDir+"/"+"CommonData.yaml", modCommon, 0644) + if err != nil { + log.Fatalf("Error writing yamlFile. #%v ", err) + } +} + +func changeToLocalReference(attrname, refName string, data map[interface{}]interface{}) { + properties := data["properties"].(map[interface{}]interface{}) + ref := properties[attrname].(map[interface{}]interface{}) + ref["$ref"] = "#/components/schemas/" + refName +} + +func getDependency(s string) (string, map[interface{}]interface{}) { + info := strings.Split(s, "#") + yamlFile, err := ioutil.ReadFile(*apiDir + "/" + info[0]) + if err != nil { + log.Fatalf("Error reading yamlFile. #%v ", err) + } + m := make(map[string]interface{}) + err = yaml.Unmarshal(yamlFile, m) + if err != nil { + log.Fatalf("Unmarshal: %v", err) + } + components := m["components"] + cMap := components.(map[interface{}]interface{}) + schemas := cMap["schemas"].(map[interface{}]interface{}) + component := strings.Split(info[1], "/") + dep := schemas[component[3]].(map[interface{}]interface{}) + return component[3], dep +} + +func remove(slice []interface{}, s int) []interface{} { + return append(slice[:s], slice[s+1:]...) +} diff --git a/provider/internal/gentools/commoncollector/definitions.txt b/provider/internal/gentools/commoncollector/definitions.txt new file mode 100644 index 0000000..a2f3f25 --- /dev/null +++ b/provider/internal/gentools/commoncollector/definitions.txt @@ -0,0 +1,38 @@ +TS29508_Nsmf_EventExposure.yaml#/components/schemas/NotificationMethod +TS29510_Nnrf_NFManagement.yaml#/components/schemas/Ipv4AddressRange +TS29512_Npcf_SMPolicyControl.yaml#/components/schemas/FlowDirection +TS29514_Npcf_PolicyAuthorization.yaml#/components/schemas/ContentVersion +TS29514_Npcf_PolicyAuthorization.yaml#/components/schemas/EthFlowDescription +TS29514_Npcf_PolicyAuthorization.yaml#/components/schemas/FlowDescription +TS29514_Npcf_PolicyAuthorization.yaml#/components/schemas/TscaiInputContainer +TS29517_Naf_EventExposure.yaml#/components/schemas/AddrFqdn +TS29518_Namf_EventExposure.yaml#/components/schemas/CommunicationFailure +TS29522_TrafficInfluence.yaml#/components/schemas/AfResultInfo +TS29522_TrafficInfluence.yaml#/components/schemas/AfResultStatus +TS29523_Npcf_EventExposure.yaml#/components/schemas/ReportingInformation +TS29554_Npcf_BDTPolicyControl.yaml#/components/schemas/NetworkAreaInfo +TS29572_Nlmf_Location.yaml#/components/schemas/Altitude +TS29572_Nlmf_Location.yaml#/components/schemas/Angle +TS29572_Nlmf_Location.yaml#/components/schemas/CivicAddress +TS29572_Nlmf_Location.yaml#/components/schemas/Confidence +TS29572_Nlmf_Location.yaml#/components/schemas/EllipsoidArc +TS29572_Nlmf_Location.yaml#/components/schemas/GADShape +TS29572_Nlmf_Location.yaml#/components/schemas/GeographicArea +TS29572_Nlmf_Location.yaml#/components/schemas/GeographicalCoordinates +TS29572_Nlmf_Location.yaml#/components/schemas/InnerRadius +TS29572_Nlmf_Location.yaml#/components/schemas/Local2dPointUncertaintyEllipse +TS29572_Nlmf_Location.yaml#/components/schemas/Local3dPointUncertaintyEllipsoid +TS29572_Nlmf_Location.yaml#/components/schemas/LocalOrigin +TS29572_Nlmf_Location.yaml#/components/schemas/Orientation +TS29572_Nlmf_Location.yaml#/components/schemas/Point +TS29572_Nlmf_Location.yaml#/components/schemas/PointAltitude +TS29572_Nlmf_Location.yaml#/components/schemas/PointAltitudeUncertainty +TS29572_Nlmf_Location.yaml#/components/schemas/PointList +TS29572_Nlmf_Location.yaml#/components/schemas/PointUncertaintyCircle +TS29572_Nlmf_Location.yaml#/components/schemas/PointUncertaintyEllipse +TS29572_Nlmf_Location.yaml#/components/schemas/Polygon +TS29572_Nlmf_Location.yaml#/components/schemas/RelativeCartesianLocation +TS29572_Nlmf_Location.yaml#/components/schemas/Uncertainty +TS29572_Nlmf_Location.yaml#/components/schemas/UncertaintyEllipsoid +TS29572_Nlmf_Location.yaml#/components/schemas/SupportedGADShapes +TS29572_Nlmf_Location.yaml#/components/schemas/UncertaintyEllipse \ No newline at end of file diff --git a/provider/internal/gentools/enumfixer/enumfixer.go b/provider/internal/gentools/enumfixer/enumfixer.go new file mode 100644 index 0000000..e722875 --- /dev/null +++ b/provider/internal/gentools/enumfixer/enumfixer.go @@ -0,0 +1,117 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + + "gopkg.in/yaml.v2" +) + +type Enum struct { + Enum []string `yaml:"enum"` + Type string `yaml:"type"` + Description string `yaml:"description"` +} + +func main() { + var apiDir = flag.String("apidir", "", "Directory containing API definitions to fix") + flag.Parse() + err := filepath.Walk(*apiDir, fixEnums) + if err != nil { + fmt.Println(err) + } +} + +func fixEnums(path string, info os.FileInfo, _ error) error { + if !info.IsDir() && strings.HasSuffix(info.Name(), ".yaml") { + yamlFile, err := ioutil.ReadFile(path) + if err != nil { + log.Printf("yamlFile. Get err #%v ", err) + } + m := make(map[string]interface{}) + err = yaml.Unmarshal(yamlFile, m) + if err != nil { + log.Fatalf("Unmarshal: %v", err) + } + components := m["components"] + if components != nil { + cMap := components.(map[interface{}]interface{}) + if _, ok := cMap["schemas"].(map[interface{}]interface{}); ok { + schemas := cMap["schemas"].(map[interface{}]interface{}) + for typeName, typeDef := range schemas { + tDMap := typeDef.(map[interface{}]interface{}) + anyOf, ok := tDMap["anyOf"] + if ok { + aOSlice := anyOf.([]interface{}) + correctEnum := Enum{} + mapInterface := aOSlice[0].(map[interface{}]interface{}) + enumInterface := mapInterface["enum"] + if enumInterface != nil { + is := enumInterface.([]interface{}) + var enumVals []string + for i := 0; i < len(is); i++ { + if reflect.TypeOf(is[i]).Kind() == reflect.String { + enumVals = append(enumVals, is[i].(string)) + + } else if reflect.TypeOf(is[1]).Kind() == reflect.Int { + enumVals = append(enumVals, strconv.Itoa(is[i].(int))) + } + } + correctEnum.Enum = enumVals + correctEnum.Type = "string" + description := tDMap["description"] + if description != nil { + correctEnum.Description = description.(string) + } else { + if aOSlice[1] != nil { + mapInterface = aOSlice[1].(map[interface{}]interface{}) + description := mapInterface["description"] + if description != nil { + correctEnum.Description = description.(string) + } + } + } + schemas[typeName] = correctEnum + } + } + } + modM, err := yaml.Marshal(m) + if err != nil { + log.Printf("yamlFile. Get err #%v ", err) + } + err = ioutil.WriteFile(path, modM, 0644) + if err != nil { + log.Printf("yamlFile. Get err #%v ", err) + } + } + } + } + return nil +} diff --git a/provider/internal/gentools/specificationfixer/specificationfixer.go b/provider/internal/gentools/specificationfixer/specificationfixer.go new file mode 100644 index 0000000..4257b65 --- /dev/null +++ b/provider/internal/gentools/specificationfixer/specificationfixer.go @@ -0,0 +1,80 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 main + +import ( + "flag" + "io/ioutil" + "log" + + "gopkg.in/yaml.v2" +) + +var apiDir *string + +func main() { + apiDir = flag.String("apidir", "", "Directory containing API definitions to fix") + flag.Parse() + + m := getData("TS29571_CommonData.yaml") + components := m["components"] + cMap := components.(map[interface{}]interface{}) + schemas := cMap["schemas"].(map[interface{}]interface{}) + snssaiExtensionData := schemas["SnssaiExtension"].(map[interface{}]interface{}) + props := snssaiExtensionData["properties"].(map[interface{}]interface{}) + wildcardSdData := props["wildcardSd"].(map[interface{}]interface{}) + delete(wildcardSdData, "enum") + + writeFile("TS29571_CommonData.yaml", m) + + m = getData("TS29222_CAPIF_Security_API.yaml") + components = m["components"] + cMap = components.(map[interface{}]interface{}) + schemas = cMap["schemas"].(map[interface{}]interface{}) + accessTokenReq := schemas["AccessTokenReq"].(map[interface{}]interface{}) + accessTokenReq["type"] = "object" + + writeFile("TS29222_CAPIF_Security_API.yaml", m) +} + +func getData(filename string) map[string]interface{} { + yamlFile, err := ioutil.ReadFile(*apiDir + "/" + filename) + if err != nil { + log.Fatalf("Error reading yamlFile. #%v ", err) + } + m := make(map[string]interface{}) + err = yaml.Unmarshal(yamlFile, m) + if err != nil { + log.Fatalf("Unmarshal: %v", err) + } + return m +} + +func writeFile(filename string, data map[string]interface{}) { + modCommon, err := yaml.Marshal(data) + if err != nil { + log.Fatalf("Marshal: #%v ", err) + } + err = ioutil.WriteFile(*apiDir+"/"+filename, modCommon, 0644) + if err != nil { + log.Fatalf("Error writing yamlFile. #%v ", err) + } +} diff --git a/provider/main.go b/provider/main.go new file mode 100644 index 0000000..b7661ea --- /dev/null +++ b/provider/main.go @@ -0,0 +1,89 @@ +// - +// ========================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 main + +import ( + "errors" + "flag" + "fmt" + "html/template" + "io" + + log "github.com/sirupsen/logrus" + + "github.com/labstack/echo/v4" + "oransc.org/nonrtric/capifprov/handler" +) + +type TemplateRegistry struct { + templates map[string]*template.Template +} + +func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + tmpl, ok := t.templates[name] + if !ok { + err := errors.New("Template not found -> " + name) + return err + } + return tmpl.ExecuteTemplate(w, "base", data) +} + +func main() { + + // Echo instance + e := echo.New() + e.Static("/", "view") + var capifCoreUrl string + flag.StringVar(&capifCoreUrl, "capifCoreUrl", "http://localhost:8090", "Url for CAPIF core") + var logLevelStr = flag.String("loglevel", "Info", "Log level") + var port = flag.Int("port", 9090, "Port for CAPIF Provider") + + flag.Parse() + + if loglevel, err := log.ParseLevel(*logLevelStr); err == nil { + log.SetLevel(loglevel) + } + + templates := make(map[string]*template.Template) + templates["home.html"] = template.Must(template.ParseFiles("view/home.html", "view/base.html")) + templates["registration.html"] = template.Must(template.ParseFiles("view/registration.html", "view/base.html")) + templates["publishapi.html"] = template.Must(template.ParseFiles("view/publishapi.html", "view/base.html")) + templates["getapi.html"] = template.Must(template.ParseFiles("view/getapi.html", "view/base.html")) + + e.Renderer = &TemplateRegistry{ + templates: templates, + } + + // Route => handler + e.GET("/", handler.HomeHandler) + e.POST("/", handler.HomeHandler) + + e.GET("/registration", handler.RegistrationHandler) + e.POST("/registration", handler.RegistrationFormHandler(capifCoreUrl)) + + e.GET("/publishapi", handler.PublishapiHandler) + e.POST("/publishapi", handler.PublishApiFormHandler(capifCoreUrl)) + + e.GET("/getapi", handler.GetApiRequest(capifCoreUrl)) + + // Start the web server + e.Logger.Fatal(e.Start(fmt.Sprintf("0.0.0.0:%d", *port))) +} diff --git a/provider/view/base.html b/provider/view/base.html new file mode 100644 index 0000000..7d6d2b2 --- /dev/null +++ b/provider/view/base.html @@ -0,0 +1,53 @@ + +{{define "base"}} + + + + + + + + + + + + + + {{template "title" .}} + + +
+
+
+ +
+ {{template "body" .}} +
+
+ + + + +{{end}} \ No newline at end of file diff --git a/provider/view/css/style.css b/provider/view/css/style.css new file mode 100644 index 0000000..1654f0c --- /dev/null +++ b/provider/view/css/style.css @@ -0,0 +1,41 @@ +/* + ========================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=================================== +*/ +.callout { + padding: 20px; + margin: 20px 0; + border: 1px solid #eee; + border-left-width: 5px; + border-radius: 3px; +} + +.bs-callout { + margin-top: -5px; +} +.callout-info { + border-left-color: #5bc0de; +} + +.callout-info h4 { + color: #5bc0de; +} + +.hiddenRow { + padding: 0 !important; +} \ No newline at end of file diff --git a/provider/view/getapi.html b/provider/view/getapi.html new file mode 100644 index 0000000..9ae04e3 --- /dev/null +++ b/provider/view/getapi.html @@ -0,0 +1,109 @@ + +{{define "title"}} + CAPIF Provider | {{index . "name"}} +{{end}} + +{{define "body"}} + +{{if .isResponse}} +
+
+

Response from CAPIF core

+
+
ServiceAPIDescription
+
+
+
+ +
+
+ +
+
+
+
+ +{{- else}} +
+
+ {{if .isError}} + + {{end}} +
API publishing functions> Get APIs
+
+
+ + +
+
+ + +
+
+
+
+{{- end}} +{{end}} + + diff --git a/provider/view/home.html b/provider/view/home.html new file mode 100644 index 0000000..9d9ff55 --- /dev/null +++ b/provider/view/home.html @@ -0,0 +1,70 @@ + +{{define "title"}} + CAPIF Provider +{{end}} + +{{define "body"}} +
+
+ +
+

+ +

+ +
+
+

+ +

+
+ +
+
+
+
+{{end}} + + diff --git a/provider/view/js/script.js b/provider/view/js/script.js new file mode 100644 index 0000000..2b25652 --- /dev/null +++ b/provider/view/js/script.js @@ -0,0 +1,121 @@ +/* + ========================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=================================== +*/ +const isObject = (value) => typeof value === "object" && value !== null + +function checkValue(value){ + return (isObject(value) ? value : ""); +} + +function printResources(resources) { + let res = Object.values(checkValue(resources)); + let out = `

Resources:

    `; + res.forEach((r) => { + out += `
  • +

    + CommType: ${r.commType} + CustOpName: ${r.custOpName} + ResourceName: ${r.resourceName} + Uri: ${r.uri} + Description: ${r.description} + Operations: ${Object.values(checkValue(r.operations))} +

    +
  • `; + }); + out += `
`; + return out; +} + +function printCustomOperations(custOperations) { + let operations = Object.values(checkValue(custOperations)); + let out = `

Custom Operations:

    `; + operations.forEach((o) => { + out += `
  • +

    + CommType: ${o.commType} + CustOpName: ${o.custOpName} + Description: ${o.description} + Operations: ${Object.values(checkValue(o.operations))} +

    +
  • `; + }); + out += `
`; + return out; +} + +function printVersions(versions) { + let vers = Object.values(checkValue(versions)); + let out = `

Versions:

    `; + vers.forEach((v) => { + out += `
  • +

    + ApiVersion: ${v.apiVersion} + ${printCustomOperations(v.custOperations)} + ${printResources(v.resources)} +

    +
  • `; + }); + out += `
`; + return out; +} + +function printInterfaceDescription(description) { + let interfaceDescriptions = Object.values(checkValue(description)); + let out = `

Interface Description:

    `; + interfaceDescriptions.forEach((d) => { + out += `
  • +

    + Ipv4Addr: ${d.ipv4Addr} + Ipv6Addr: ${d.ipv6Addr} + Port: ${d.port} + SecurityMethods: ${Object.values(checkValue(d.securityMethods))} +

    +
  • `; + }); + out += `
`; + return out; +} + +function printAefProfiles(aefProfiles){ + let out = ""; + let index = 0; + aefProfiles.forEach((aef) => { + out += ` + + ${aef.aefId} + ${aef.aefLocation} + ${aef.domainName} + ${aef.protocol} + ${Object.values(checkValue(aef.securityMethods))} + + + +
+ ${printInterfaceDescription(aef.interfaceDescriptions)} + ${printVersions(aef.versions)} +
+ + + `; + index++; + }); + return out; +} + + diff --git a/provider/view/publishapi.html b/provider/view/publishapi.html new file mode 100644 index 0000000..3f71ed6 --- /dev/null +++ b/provider/view/publishapi.html @@ -0,0 +1,111 @@ + +{{define "title"}} + CAPIF Provider +{{end}} + +{{define "body"}} + {{if .isResponse}} +
+
+

Response from CAPIF core

+
+
ServiceAPIDescription
+
+
+ ApiId: + +
+
+ ApiName: + +
+
+ Description: + +
+ +
AefProfiles:
+
+ + + + + + + + + + + + + +
AefIdAefLocationDomainNameProtocolSecurityMethods
+
+
+
+ +
+
+ +
+
+
+
+ + {{- else}} +
+
+ {{if .isError}} + + {{end}} +
API publishing functions> Publish API
+
+
+ + +
+
+ + +
+ +
+ + +
+
+
+
+ {{- end}} +{{end}} + + diff --git a/provider/view/registration.html b/provider/view/registration.html new file mode 100644 index 0000000..d0e381d --- /dev/null +++ b/provider/view/registration.html @@ -0,0 +1,119 @@ + +{{define "title"}} + CAPIF Provider | {{index . "name"}} +{{end}} + +{{define "body"}} +{{if .isResponse}} +
+
+

Response from CAPIF core

+
+
APIProviderEnrolmentDetails
+
+
+ ApiProvDomId: + +
+
+ ApiProvDomInfo: + +
+
+ RegSec: + +
+
APIProviderFunctionDetails:
+ +
+ + + + + + + + + + + + +
ApiProvFuncIdApiProvFuncInfoApiProvFuncRoleRegistrationInformation
+
+
+
+ +
+
+ +
+
+
+
+ +{{- else}} +
+
+ {{if .isError}} + + {{end}} +
API management functions> Registrations
+
+
+ + +
+ +
+ + +
+
+
+
+{{- end}} +{{end}} + + -- 2.16.6