Integration with keycloak, add client 31/10831/1
authorychacon <yennifer.chacon@est.tech>
Mon, 3 Apr 2023 08:59:06 +0000 (10:59 +0200)
committerychacon <yennifer.chacon@est.tech>
Mon, 3 Apr 2023 08:59:06 +0000 (10:59 +0200)
Issue-ID: NONRTRIC-856
Signed-off-by: ychacon <yennifer.chacon@est.tech>
Change-Id: I60bba76bf8e1d7c2e907a3a90a307481b589637a

capifcore/configs/keycloak.yaml
capifcore/internal/config/config.go
capifcore/internal/keycloak/keycloak.go
capifcore/internal/keycloak/mocks/AccessManagement.go
capifcore/internal/restclient/HTTPClient.go
capifcore/internal/restclient/HTTPClient_test.go
capifcore/internal/securityapi/typeupdate.go
capifcore/internal/securityservice/security.go
capifcore/internal/securityservice/security_test.go
capifcore/main.go

index 1ca2ba9..86b3905 100644 (file)
@@ -19,5 +19,8 @@
 authorizationServer:
   host: "localhost"
   port: "8080"
+  admin:
+    user: "admin"
+    password: "secret"
   realms:
    invokerrealm: "invokerrealm"
index 4a53022..f7e07e6 100644 (file)
@@ -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 {
index 16f65c8..3646516 100644 (file)
@@ -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
+
+}
index 9ac2179..59b914a 100644 (file)
@@ -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)
        }
index de0ad1d..c771a54 100644 (file)
@@ -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
 }
index 21186aa..e390686 100644 (file)
@@ -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)
                })
        }
index 364c123..3402b8e 100644 (file)
@@ -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)
                        }
                }
 
index aee022e..e211f67 100644 (file)
@@ -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))
 
index 57e9cea..1dda127 100644 (file)
@@ -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{
                        {
index f8b9daf..5ba3923 100644 (file)
@@ -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