From 051a4a32068b4718ef9ddb1868e532a976de843e Mon Sep 17 00:00:00 2001 From: ychacon Date: Mon, 3 Apr 2023 10:59:06 +0200 Subject: [PATCH] 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