From 8d9ce4be013658f22f8bc8113ce53db7fa71e4a2 Mon Sep 17 00:00:00 2001 From: Lindsey Cheng Date: Tue, 24 Dec 2024 15:02:46 +0800 Subject: [PATCH] feat: Add model/DTOs and HTTP client for security-proxy-auth Closes #963. Add model/DTOs and HTTP client for security-proxy-auth. Signed-off-by: Lindsey Cheng --- clients/http/auth.go | 50 ++++++++++++ clients/http/auth_test.go | 43 +++++++++++ clients/interfaces/auth.go | 23 ++++++ clients/interfaces/mocks/AuthClient.go | 96 +++++++++++++++++++++++ common/constants.go | 10 +++ dtos/keydata.go | 27 +++++++ dtos/keydata_test.go | 33 ++++++++ dtos/requests/keydata.go | 44 +++++++++++ dtos/requests/keydata_test.go | 101 +++++++++++++++++++++++++ dtos/responses/keydata.go | 24 ++++++ dtos/responses/keydata_test.go | 31 ++++++++ models/keydata.go | 13 ++++ 12 files changed, 495 insertions(+) create mode 100644 clients/http/auth.go create mode 100644 clients/http/auth_test.go create mode 100644 clients/interfaces/auth.go create mode 100644 clients/interfaces/mocks/AuthClient.go create mode 100644 dtos/keydata.go create mode 100644 dtos/keydata_test.go create mode 100644 dtos/requests/keydata.go create mode 100644 dtos/requests/keydata_test.go create mode 100644 dtos/responses/keydata.go create mode 100644 dtos/responses/keydata_test.go create mode 100644 models/keydata.go diff --git a/clients/http/auth.go b/clients/http/auth.go new file mode 100644 index 00000000..0ce859e7 --- /dev/null +++ b/clients/http/auth.go @@ -0,0 +1,50 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/http/utils" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" +) + +type AuthClient struct { + baseUrl string + authInjector interfaces.AuthenticationInjector +} + +// NewAuthClient creates an instance of AuthClient +func NewAuthClient(baseUrl string, authInjector interfaces.AuthenticationInjector) interfaces.AuthClient { + return &AuthClient{ + baseUrl: baseUrl, + authInjector: authInjector, + } +} + +// AddKey adds new key +func (ac *AuthClient) AddKey(ctx context.Context, req requests.AddKeyDataRequest) (dtoCommon.BaseResponse, errors.EdgeX) { + var response dtoCommon.BaseResponse + err := utils.PostRequestWithRawData(ctx, &response, ac.baseUrl, common.ApiKeyRoute, nil, req, ac.authInjector) + if err != nil { + return response, errors.NewCommonEdgeXWrapper(err) + } + return response, nil +} + +func (ac *AuthClient) VerificationKeyByIssuer(ctx context.Context, issuer string) (res responses.KeyDataResponse, err errors.EdgeX) { + path := common.NewPathBuilder().SetPath(common.ApiKeyRoute).SetPath(common.VerificationKeyType).SetPath(common.Issuer).SetNameFieldPath(issuer).BuildPath() + err = utils.GetRequest(ctx, &res, ac.baseUrl, path, nil, ac.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} diff --git a/clients/http/auth_test.go b/clients/http/auth_test.go new file mode 100644 index 00000000..ecef05a8 --- /dev/null +++ b/clients/http/auth_test.go @@ -0,0 +1,43 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + "net/http" + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/responses" + + "github.com/stretchr/testify/require" +) + +func TestAddKey(t *testing.T) { + ts := newTestServer(http.MethodPost, common.ApiKeyRoute, dtoCommon.BaseResponse{}) + defer ts.Close() + + client := NewAuthClient(ts.URL, NewNullAuthenticationInjector()) + res, err := client.AddKey(context.Background(), requests.AddKeyDataRequest{}) + require.NoError(t, err) + require.IsType(t, dtoCommon.BaseResponse{}, res) +} + +func TestVerificationKeyByIssuer(t *testing.T) { + mockIssuer := "mockIssuer" + + path := common.NewPathBuilder().EnableNameFieldEscape(false). + SetPath(common.ApiKeyRoute).SetPath(common.VerificationKeyType).SetPath(common.Issuer).SetNameFieldPath(mockIssuer).BuildPath() + ts := newTestServer(http.MethodGet, path, responses.KeyDataResponse{}) + defer ts.Close() + + client := NewAuthClient(ts.URL, NewNullAuthenticationInjector()) + res, err := client.VerificationKeyByIssuer(context.Background(), mockIssuer) + require.NoError(t, err) + require.IsType(t, responses.KeyDataResponse{}, res) +} diff --git a/clients/interfaces/auth.go b/clients/interfaces/auth.go new file mode 100644 index 00000000..2b22714c --- /dev/null +++ b/clients/interfaces/auth.go @@ -0,0 +1,23 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package interfaces + +import ( + "context" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" +) + +// AuthClient defines the interface for interactions with the auth API endpoint on the security-proxy-auth service. +type AuthClient interface { + // AddKey adds the JWT signing or verification key + AddKey(ctx context.Context, req requests.AddKeyDataRequest) (common.BaseResponse, errors.EdgeX) + // VerificationKeyByIssuer returns the JWT verification key by the specified issuer + VerificationKeyByIssuer(ctx context.Context, issuer string) (res responses.KeyDataResponse, err errors.EdgeX) +} diff --git a/clients/interfaces/mocks/AuthClient.go b/clients/interfaces/mocks/AuthClient.go new file mode 100644 index 00000000..54f03351 --- /dev/null +++ b/clients/interfaces/mocks/AuthClient.go @@ -0,0 +1,96 @@ +// Code generated by mockery v2.49.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + common "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" + + errors "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + + mock "github.com/stretchr/testify/mock" + + requests "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/requests" + + responses "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/responses" +) + +// AuthClient is an autogenerated mock type for the AuthClient type +type AuthClient struct { + mock.Mock +} + +// AddKey provides a mock function with given fields: ctx, req +func (_m *AuthClient) AddKey(ctx context.Context, req requests.AddKeyDataRequest) (common.BaseResponse, errors.EdgeX) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for AddKey") + } + + var r0 common.BaseResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, requests.AddKeyDataRequest) (common.BaseResponse, errors.EdgeX)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, requests.AddKeyDataRequest) common.BaseResponse); ok { + r0 = rf(ctx, req) + } else { + r0 = ret.Get(0).(common.BaseResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, requests.AddKeyDataRequest) errors.EdgeX); ok { + r1 = rf(ctx, req) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// VerificationKeyByIssuer provides a mock function with given fields: ctx, issuer +func (_m *AuthClient) VerificationKeyByIssuer(ctx context.Context, issuer string) (responses.KeyDataResponse, errors.EdgeX) { + ret := _m.Called(ctx, issuer) + + if len(ret) == 0 { + panic("no return value specified for VerificationKeyByIssuer") + } + + var r0 responses.KeyDataResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string) (responses.KeyDataResponse, errors.EdgeX)); ok { + return rf(ctx, issuer) + } + if rf, ok := ret.Get(0).(func(context.Context, string) responses.KeyDataResponse); ok { + r0 = rf(ctx, issuer) + } else { + r0 = ret.Get(0).(responses.KeyDataResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) errors.EdgeX); ok { + r1 = rf(ctx, issuer) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// NewAuthClient creates a new instance of AuthClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAuthClient(t interface { + mock.TestingT + Cleanup(func()) +}) *AuthClient { + mock := &AuthClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/common/constants.go b/common/constants.go index 6ea3e0ed..1128a4df 100644 --- a/common/constants.go +++ b/common/constants.go @@ -132,6 +132,9 @@ const ( ApiAllRegistrationsRoute = ApiRegisterRoute + "/" + All ApiKVSByKeyRoute = ApiKVSRoute + "/" + Key + "/:" + Key ApiRegistrationByServiceIdRoute = ApiRegisterRoute + "/" + ServiceId + "/:" + ServiceId + + ApiKeyRoute = ApiBase + "/key" + ApiVerificationKeyByIssuerRoute = ApiKeyRoute + "/" + VerificationKeyType + "/" + Issuer + "/:" + Issuer ) // Constants related to defined url path names and parameters in the v3 service APIs @@ -394,3 +397,10 @@ const ( // App Service Topics // App Service topics remain configurable inorder to filter by subscription. ) + +// Constants related to the security-proxy-auth service +const ( + VerificationKeyType = "verification" + SigningKeyType = "signing" + Issuer = "issuer" +) diff --git a/dtos/keydata.go b/dtos/keydata.go new file mode 100644 index 00000000..329d2b23 --- /dev/null +++ b/dtos/keydata.go @@ -0,0 +1,27 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package dtos + +import ( + "strings" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/models" +) + +type KeyData struct { + Issuer string `json:"issuer" validate:"required"` + Type string `json:"type" validate:"omitempty,oneof=verification signing"` + Key string `json:"key" validate:"required"` +} + +// ToKeyDataModel transforms the KeyData DTO to the KeyData Model +func ToKeyDataModel(keyData KeyData) models.KeyData { + return models.KeyData{ + Issuer: keyData.Issuer, + Type: strings.ToLower(keyData.Type), + Key: keyData.Key, + } +} diff --git a/dtos/keydata_test.go b/dtos/keydata_test.go new file mode 100644 index 00000000..125d932c --- /dev/null +++ b/dtos/keydata_test.go @@ -0,0 +1,33 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package dtos + +import ( + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/models" + + "github.com/stretchr/testify/require" +) + +func TestToKeyDataModel(t *testing.T) { + mockIssuer := "mockIssuer" + mockType := "verification" + mockKey := "mockKey" + mockKeyDataDTO := KeyData{ + Issuer: mockIssuer, + Type: mockType, + Key: mockKey, + } + mockModel := models.KeyData{ + Issuer: mockIssuer, + Type: mockType, + Key: mockKey, + } + + model := ToKeyDataModel(mockKeyDataDTO) + require.Equal(t, mockModel, model) +} diff --git a/dtos/requests/keydata.go b/dtos/requests/keydata.go new file mode 100644 index 00000000..cade8f93 --- /dev/null +++ b/dtos/requests/keydata.go @@ -0,0 +1,44 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package requests + +import ( + "encoding/json" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" +) + +// AddKeyDataRequest defines the Request Content for POST Key DTO. +type AddKeyDataRequest struct { + dtoCommon.BaseRequest `json:",inline"` + KeyData dtos.KeyData `json:"keyData"` +} + +// Validate satisfies the Validator interface +func (a *AddKeyDataRequest) Validate() error { + err := common.Validate(a) + return err +} + +// UnmarshalJSON implements the Unmarshaler interface for the AddUserRequest type +func (a *AddKeyDataRequest) UnmarshalJSON(b []byte) error { + var alias struct { + dtoCommon.BaseRequest + KeyData dtos.KeyData + } + if err := json.Unmarshal(b, &alias); err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal request body as JSON.", err) + } + + *a = AddKeyDataRequest(alias) + if err := a.Validate(); err != nil { + return err + } + return nil +} diff --git a/dtos/requests/keydata_test.go b/dtos/requests/keydata_test.go new file mode 100644 index 00000000..bcf5e7fc --- /dev/null +++ b/dtos/requests/keydata_test.go @@ -0,0 +1,101 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package requests + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" + + "github.com/stretchr/testify/require" +) + +var ( + mockIssuer = "mockIssuer" + mockType = "verification" + mockKey = "mockKey" + mockKeyData = dtos.KeyData{ + Issuer: mockIssuer, + Type: mockType, + Key: mockKey, + } + testAddKeyDataRequest = AddKeyDataRequest{ + BaseRequest: dtoCommon.BaseRequest{ + RequestId: ExampleUUID, + Versionable: dtoCommon.NewVersionable(), + }, + KeyData: mockKeyData, + } +) + +func TestAddKeyDataRequest_Validate(t *testing.T) { + valid := testAddKeyDataRequest + + emptyIssuer := valid + emptyIssuer.KeyData.Issuer = "" + + invalidType := valid + invalidType.KeyData.Type = "invalidType" + + emptyKey := valid + emptyKey.KeyData.Key = "" + + tests := []struct { + name string + KeyData AddKeyDataRequest + expectError bool + }{ + {"valid AddKeyDataRequest", valid, false}, + {"invalid AddKeyDataRequest, empty issuer", emptyIssuer, true}, + {"invalid AddKeyDataRequest, invalid type", invalidType, true}, + {"invalid AddKeyDataRequest, empty key", emptyKey, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.KeyData.Validate() + if tt.expectError { + require.Error(t, err, fmt.Sprintf("expect error but not : %s", tt.name)) + } else { + require.NoError(t, err, fmt.Sprintf("unexpected error occurs : %s", tt.name)) + } + }) + } +} + +func TestAddKeyDataRequest_UnmarshalJSON(t *testing.T) { + valid := testAddKeyDataRequest + resultTestBytes, _ := json.Marshal(testAddKeyDataRequest) + + type args struct { + data []byte + } + tests := []struct { + name string + addDevice AddKeyDataRequest + args args + wantErr bool + }{ + {"unmarshal AddKeyDataRequest with success", valid, args{resultTestBytes}, false}, + {"unmarshal invalid AddKeyDataRequest, empty data", AddKeyDataRequest{}, args{[]byte{}}, true}, + {"unmarshal invalid AddKeyDataRequest, string data", AddKeyDataRequest{}, args{[]byte("Invalid AddKeyDataRequest")}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var expected = tt.addDevice + err := tt.addDevice.UnmarshalJSON(tt.args.data) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, expected, tt.addDevice, "Unmarshal did not result in expected AddKeyDataRequest.") + } + }) + } +} diff --git a/dtos/responses/keydata.go b/dtos/responses/keydata.go new file mode 100644 index 00000000..770cd54e --- /dev/null +++ b/dtos/responses/keydata.go @@ -0,0 +1,24 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package responses + +import ( + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" +) + +// KeyDataResponse defines the Response Content for GET KeyData DTOs. +type KeyDataResponse struct { + dtoCommon.BaseResponse `json:",inline"` + KeyData dtos.KeyData `json:"keyData"` +} + +func NewKeyDataResponse(requestId string, message string, statusCode int, keyData dtos.KeyData) KeyDataResponse { + return KeyDataResponse{ + BaseResponse: dtoCommon.NewBaseResponse(requestId, message, statusCode), + KeyData: keyData, + } +} diff --git a/dtos/responses/keydata_test.go b/dtos/responses/keydata_test.go new file mode 100644 index 00000000..3211dde9 --- /dev/null +++ b/dtos/responses/keydata_test.go @@ -0,0 +1,31 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package responses + +import ( + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + + "github.com/stretchr/testify/assert" +) + +func TestKeyDataResponse(t *testing.T) { + expectedIssuer := "mockIssuer" + expectedType := "verification" + expectedKey := "mockKey" + expectedKeyData := dtos.KeyData{ + Issuer: expectedIssuer, + Type: expectedType, + Key: expectedKey, + } + actual := NewKeyDataResponse(expectedRequestId, expectedMessage, expectedStatusCode, expectedKeyData) + + assert.Equal(t, expectedRequestId, actual.RequestId) + assert.Equal(t, expectedStatusCode, actual.StatusCode) + assert.Equal(t, expectedMessage, actual.Message) + assert.Equal(t, expectedKeyData, actual.KeyData) +} diff --git a/models/keydata.go b/models/keydata.go new file mode 100644 index 00000000..1d50f82e --- /dev/null +++ b/models/keydata.go @@ -0,0 +1,13 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package models + +// KeyData contains the signing or verification key for the JWT token +type KeyData struct { + Issuer string + Type string + Key string +}