From c38af36419feae83a0e8abbe505ba551fb0c9bf6 Mon Sep 17 00:00:00 2001 From: Lindsey Cheng Date: Fri, 27 Dec 2024 11:16:28 +0800 Subject: [PATCH] feat: Add key-related APIs in security-proxy-auth Resolves #5038. Add key-related APIs in security-proxy-auth to enable support for external JWT verification. Signed-off-by: Lindsey Cheng --- .../res/configuration.yaml | 6 + .../res/db/sql/00-utils.sql | 7 + .../res/db/sql/01-tables.sql | 13 + .../res/configuration.yaml | 3 + go.mod | 4 +- go.sum | 8 +- internal/core/command/router.go | 7 +- internal/core/data/router.go | 7 +- internal/core/keeper/router.go | 7 +- internal/core/metadata/router.go | 7 +- .../pkg/infrastructure/postgres/consts.go | 5 +- .../pkg/infrastructure/postgres/keystore.go | 67 ++ internal/pkg/utils/crypto/aes.go | 107 +++ internal/pkg/utils/crypto/aes_test.go | 24 + .../pkg/utils/crypto/interfaces/crypto.go | 13 + .../utils/crypto/interfaces/mocks/Crypto.go | 90 +++ .../security/proxyauth/application/key.go | 83 ++ .../proxyauth/application/key_test.go | 172 ++++ internal/security/proxyauth/config/config.go | 15 +- .../security/proxyauth/container/cryptor.go | 20 + .../security/proxyauth/container/database.go | 20 + .../proxyauth/controller/controller.go | 24 + .../proxyauth/controller/controller_test.go | 32 + internal/security/proxyauth/controller/key.go | 73 ++ .../security/proxyauth/controller/key_test.go | 179 +++++ .../proxyauth/infrastructure/interfaces/db.go | 15 + .../interfaces/mocks/DBClient.go | 128 +++ internal/security/proxyauth/init.go | 19 +- internal/security/proxyauth/main.go | 11 +- internal/security/proxyauth/router.go | 38 + internal/security/proxyauth/utils/key.go | 16 + internal/security/proxyauth/utils/key_test.go | 28 + internal/support/notifications/router.go | 5 +- internal/support/scheduler/router.go | 7 +- openapi/security-proxy-auth.yaml | 740 ++++++++++++++++++ 35 files changed, 1940 insertions(+), 60 deletions(-) create mode 100644 cmd/security-proxy-auth/res/db/sql/00-utils.sql create mode 100644 cmd/security-proxy-auth/res/db/sql/01-tables.sql create mode 100644 internal/pkg/infrastructure/postgres/keystore.go create mode 100644 internal/pkg/utils/crypto/aes.go create mode 100644 internal/pkg/utils/crypto/aes_test.go create mode 100644 internal/pkg/utils/crypto/interfaces/crypto.go create mode 100644 internal/pkg/utils/crypto/interfaces/mocks/Crypto.go create mode 100644 internal/security/proxyauth/application/key.go create mode 100644 internal/security/proxyauth/application/key_test.go create mode 100644 internal/security/proxyauth/container/cryptor.go create mode 100644 internal/security/proxyauth/container/database.go create mode 100644 internal/security/proxyauth/controller/controller.go create mode 100644 internal/security/proxyauth/controller/controller_test.go create mode 100644 internal/security/proxyauth/controller/key.go create mode 100644 internal/security/proxyauth/controller/key_test.go create mode 100644 internal/security/proxyauth/infrastructure/interfaces/db.go create mode 100644 internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go create mode 100644 internal/security/proxyauth/router.go create mode 100644 internal/security/proxyauth/utils/key.go create mode 100644 internal/security/proxyauth/utils/key_test.go create mode 100644 openapi/security-proxy-auth.yaml diff --git a/cmd/core-common-config-bootstrapper/res/configuration.yaml b/cmd/core-common-config-bootstrapper/res/configuration.yaml index fd72372b8c..c48d0505b8 100644 --- a/cmd/core-common-config-bootstrapper/res/configuration.yaml +++ b/cmd/core-common-config-bootstrapper/res/configuration.yaml @@ -73,6 +73,12 @@ all-services: DefaultPubRetryAttempts: "2" Subject: "edgex/#" # Required for NATS JetStream only for stream auto-provisioning + Clients: + security-proxy-auth: + Protocol: http + Host: localhost + Port: 59842 + app-services: Writable: StoreAndForward: diff --git a/cmd/security-proxy-auth/res/db/sql/00-utils.sql b/cmd/security-proxy-auth/res/db/sql/00-utils.sql new file mode 100644 index 0000000000..4700ba17e0 --- /dev/null +++ b/cmd/security-proxy-auth/res/db/sql/00-utils.sql @@ -0,0 +1,7 @@ +-- +-- Copyright (C) 2025 IOTech Ltd +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- schema for proxy-auth related tables +CREATE SCHEMA IF NOT EXISTS security_proxy_auth; diff --git a/cmd/security-proxy-auth/res/db/sql/01-tables.sql b/cmd/security-proxy-auth/res/db/sql/01-tables.sql new file mode 100644 index 0000000000..4d50ca99cf --- /dev/null +++ b/cmd/security-proxy-auth/res/db/sql/01-tables.sql @@ -0,0 +1,13 @@ +-- +-- Copyright (C) 2025 IOTech Ltd +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- security_proxy_auth.key_store is used to store the key file +CREATE TABLE IF NOT EXISTS security_proxy_auth.key_store ( + id UUID PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + content TEXT NOT NULL, + created timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc'), + modified timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc') +); diff --git a/cmd/security-secretstore-setup/res/configuration.yaml b/cmd/security-secretstore-setup/res/configuration.yaml index 92928f028f..7897e646f2 100644 --- a/cmd/security-secretstore-setup/res/configuration.yaml +++ b/cmd/security-secretstore-setup/res/configuration.yaml @@ -64,6 +64,9 @@ Databases: scheduler: Service: support-scheduler Username: support_scheduler + securityproxyauth: + Service: security-proxy-auth + Username: security_proxy_auth SecureMessageBus: Type: none KuiperConfigPath: /tmp/kuiper/edgex.yaml diff --git a/go.mod b/go.mod index 3feda1dad5..78c6a6d594 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.23 require ( github.com/eclipse/paho.mqtt.golang v1.5.0 - github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.15 + github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.16 github.com/edgexfoundry/go-mod-configuration/v4 v4.0.0-dev.10 - github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.18 + github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.19 github.com/edgexfoundry/go-mod-messaging/v4 v4.0.0-dev.10 github.com/edgexfoundry/go-mod-secrets/v4 v4.0.0-dev.5 github.com/fxamacker/cbor/v2 v2.7.0 diff --git a/go.sum b/go.sum index 602d51d943..d89111b0fb 100644 --- a/go.sum +++ b/go.sum @@ -68,12 +68,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= -github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.15 h1:oeSDtoah8q3sBo8huqNdRjjxRF5IcsLh0kby0gJW/o4= -github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.15/go.mod h1:D+fSf0PWO9E4nz+1tVe0OGYnBeRQ1nHdF3B1tnYrq60= +github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.16 h1:NSEoo/YnSDNWQcZpLAtXqDKHAId5mecTVBxk9zs/ROg= +github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.16/go.mod h1:66I+qRA22YkjA/SYw4F9R0avZ9oNYM8bg2qhwePTFkI= github.com/edgexfoundry/go-mod-configuration/v4 v4.0.0-dev.10 h1:DMv5LZDxcqUeb1dREMd/vK+reXmZYlpafgtm8XhYdHQ= github.com/edgexfoundry/go-mod-configuration/v4 v4.0.0-dev.10/go.mod h1:ltUpMcOpJSzmabBtZox5qg1AK2wEikvZJyIBXtJ7mUQ= -github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.18 h1:yGXoMQd1XFM924NYukAagbZ4xLYFCzMg0bjGWZtStCM= -github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.18/go.mod h1:M5JXcRrmnIVNAmqeDNVXd0PSOGdq96fgrEmzivx02c8= +github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.19 h1:uXZml7n/I/+c7k3eZRuJRrlWjYx/Euk8tlnBqeitvB8= +github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.19/go.mod h1:M5JXcRrmnIVNAmqeDNVXd0PSOGdq96fgrEmzivx02c8= github.com/edgexfoundry/go-mod-messaging/v4 v4.0.0-dev.10 h1:xvDQDIJtmj/ZCmKzbAzg3h1F2ZdWz1MPoJSNfYZANGc= github.com/edgexfoundry/go-mod-messaging/v4 v4.0.0-dev.10/go.mod h1:ibaiw7r3RgLYDuuFfWT1kh//bjP+onDOOQsnSsdD4E8= github.com/edgexfoundry/go-mod-registry/v4 v4.0.0-dev.3 h1:6tw6JqEJDOqo2lEgxjZ+scvsub5R20WGpInCuoxS6zE= diff --git a/internal/core/command/router.go b/internal/core/command/router.go index 278f32ea0c..512d324157 100644 --- a/internal/core/command/router.go +++ b/internal/core/command/router.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2021-2023 IOTech Ltd +// Copyright (C) 2021-2025 IOTech Ltd // Copyright (C) 2023 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 @@ -9,7 +9,6 @@ package command import ( "github.com/edgexfoundry/edgex-go" commandController "github.com/edgexfoundry/edgex-go/internal/core/command/controller/http" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" @@ -19,9 +18,7 @@ import ( ) func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) { - lc := container.LoggingClientFrom(dic.Get) - secretProvider := container.SecretProviderExtFrom(dic.Get) - authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc) + authenticationHook := handlers.AutoConfigAuthenticationFunc(dic) // Common _ = controller.NewCommonController(dic, r, serviceName, edgex.Version) diff --git a/internal/core/data/router.go b/internal/core/data/router.go index 31f3e755d0..081be09964 100644 --- a/internal/core/data/router.go +++ b/internal/core/data/router.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2021-2023 IOTech Ltd +// Copyright (C) 2021-2025 IOTech Ltd // Copyright (C) 2023 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 @@ -8,7 +8,6 @@ package data import ( "github.com/edgexfoundry/edgex-go" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" @@ -20,9 +19,7 @@ import ( ) func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) { - lc := container.LoggingClientFrom(dic.Get) - secretProvider := container.SecretProviderExtFrom(dic.Get) - authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc) + authenticationHook := handlers.AutoConfigAuthenticationFunc(dic) // Common _ = controller.NewCommonController(dic, r, serviceName, edgex.Version) diff --git a/internal/core/keeper/router.go b/internal/core/keeper/router.go index 93c9c46583..f7980ddda7 100644 --- a/internal/core/keeper/router.go +++ b/internal/core/keeper/router.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2024 IOTech Ltd +// Copyright (C) 2024-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -7,7 +7,6 @@ package keeper import ( "github.com/edgexfoundry/edgex-go" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" @@ -19,9 +18,7 @@ import ( ) func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) { - lc := container.LoggingClientFrom(dic.Get) - secretProvider := container.SecretProviderExtFrom(dic.Get) - authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc) + authenticationHook := handlers.AutoConfigAuthenticationFunc(dic) // Common _ = controller.NewCommonController(dic, r, serviceName, edgex.Version) diff --git a/internal/core/metadata/router.go b/internal/core/metadata/router.go index 8b788e7f05..cf041d88c0 100644 --- a/internal/core/metadata/router.go +++ b/internal/core/metadata/router.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2021-2024 IOTech Ltd +// Copyright (C) 2021-2025 IOTech Ltd // Copyright (C) 2023 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 @@ -8,7 +8,6 @@ package metadata import ( "github.com/edgexfoundry/edgex-go" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" @@ -21,9 +20,7 @@ import ( ) func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) { - lc := container.LoggingClientFrom(dic.Get) - secretProvider := container.SecretProviderExtFrom(dic.Get) - authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc) + authenticationHook := handlers.AutoConfigAuthenticationFunc(dic) // Common _ = controller.NewCommonController(dic, r, serviceName, edgex.Version) diff --git a/internal/pkg/infrastructure/postgres/consts.go b/internal/pkg/infrastructure/postgres/consts.go index 3701dcd5c5..88593d02df 100644 --- a/internal/pkg/infrastructure/postgres/consts.go +++ b/internal/pkg/infrastructure/postgres/consts.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2024 IOTech Ltd +// Copyright (C) 2024-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -12,6 +12,7 @@ const ( coreMetaDataSchema = "core_metadata" supportNotificationsSchema = "support_notifications" supportSchedulerSchema = "support_scheduler" + ProxyAuthSchema = "security_proxy_auth" ) // constants relate to the postgres db table names @@ -29,6 +30,7 @@ const ( scheduleJobTableName = supportSchedulerSchema + ".job" subscriptionTableName = supportNotificationsSchema + ".subscription" transmissionTableName = supportNotificationsSchema + ".transmission" + keyStoreTableName = ProxyAuthSchema + ".key_store" ) // constants relate to the common db table column names @@ -38,6 +40,7 @@ const ( idCol = "id" modifiedCol = "modified" statusCol = "status" + nameCol = "name" ) // constants relate to the event/reading postgres db table column names diff --git a/internal/pkg/infrastructure/postgres/keystore.go b/internal/pkg/infrastructure/postgres/keystore.go new file mode 100644 index 0000000000..8fd9953bd8 --- /dev/null +++ b/internal/pkg/infrastructure/postgres/keystore.go @@ -0,0 +1,67 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "context" + "fmt" + "time" + + pgClient "github.com/edgexfoundry/edgex-go/internal/pkg/db/postgres" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + + "github.com/google/uuid" +) + +// AddKey adds a new key to the database +func (c *Client) AddKey(name, content string) errors.EdgeX { + exists, err := c.KeyExists(name) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } else if exists { + return errors.NewCommonEdgeX(errors.KindDuplicateName, fmt.Sprintf("key '%s' already exists", name), nil) + } + + _, pgxErr := c.ConnPool.Exec( + context.Background(), sqlInsert(keyStoreTableName, idCol, nameCol, contentCol), + uuid.New().String(), name, content) + if pgxErr != nil { + return pgClient.WrapDBError("failed to insert row to key_store table", pgxErr) + } + return nil +} + +// UpdateKey updates the key by name +func (c *Client) UpdateKey(name, content string) errors.EdgeX { + _, pgxErr := c.ConnPool.Exec( + context.Background(), sqlUpdateColsByCondCol(keyStoreTableName, nameCol, contentCol, modifiedCol), content, time.Now().UTC(), name) + if pgxErr != nil { + return pgClient.WrapDBError("failed to update row to key_store table", pgxErr) + } + return nil +} + +// ReadKeyContent reads key content from the database +func (c *Client) ReadKeyContent(name string) (string, errors.EdgeX) { + var fileContent string + row := c.ConnPool.QueryRow(context.Background(), + fmt.Sprintf("SELECT %s FROM %s WHERE %s = $1", contentCol, keyStoreTableName, nameCol), name) + if err := row.Scan(&fileContent); err != nil { + return fileContent, pgClient.WrapDBError("failed to query key content", err) + } + return fileContent, nil +} + +// KeyExists check whether the key file exits +func (c *Client) KeyExists(filename string) (bool, errors.EdgeX) { + var exists bool + err := c.ConnPool.QueryRow(context.Background(), sqlCheckExistsByCol(keyStoreTableName, nameCol), filename).Scan(&exists) + if err != nil { + return false, pgClient.WrapDBError(fmt.Sprintf("failed to check key by name '%s' from %s table", nameCol, keyStoreTableName), err) + } + return exists, nil +} diff --git a/internal/pkg/utils/crypto/aes.go b/internal/pkg/utils/crypto/aes.go new file mode 100644 index 0000000000..f68505cb15 --- /dev/null +++ b/internal/pkg/utils/crypto/aes.go @@ -0,0 +1,107 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package crypto + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "io" + + "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" +) + +const aesKey = "RO6gGYKocUahpdX15k9gYvbLuSxbKrPz" + +// AESCryptor defined the AES cryptor struct +type AESCryptor struct { + key []byte +} + +func NewAESCryptor() interfaces.Crypto { + return &AESCryptor{ + key: []byte(aesKey), + } +} + +// Encrypt encrypts the given plaintext with AES-CBC mode and returns a string in base64 encoding +func (c *AESCryptor) Encrypt(plaintext string) (string, errors.EdgeX) { + bytePlaintext := []byte(plaintext) + block, err := aes.NewCipher(c.key) + if err != nil { + return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err) + } + + // CBC mode works on blocks so plaintexts may need to be padded to the next whole block + paddedPlaintext := pkcs7Pad(bytePlaintext, block.BlockSize()) + + ciphertext := make([]byte, aes.BlockSize+len(paddedPlaintext)) + // attach a random iv ahead of the ciphertext + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err) + } + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext[aes.BlockSize:], paddedPlaintext) + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt decrypts the given ciphertext with AES-CBC mode and returns the original value as string +func (c *AESCryptor) Decrypt(ciphertext string) ([]byte, errors.EdgeX) { + decodedCipherText, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err) + } + + block, err := aes.NewCipher(c.key) + if err != nil { + return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err) + } + + if len(decodedCipherText) < aes.BlockSize { + return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err) + } + + // get the iv from the cipher text + iv := decodedCipherText[:aes.BlockSize] + decodedCipherText = decodedCipherText[aes.BlockSize:] + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(decodedCipherText, decodedCipherText) + + // If the original plaintext lengths are not a multiple of the block + // size, padding would have to be added when encrypting, which would be + // removed at this point + plaintext, e := pkcs7Unpad(decodedCipherText) + if e != nil { + return nil, errors.NewCommonEdgeXWrapper(err) + } + + return plaintext, nil +} + +// pkcs7Pad implements the PKCS7 padding +func pkcs7Pad(data []byte, blockSize int) []byte { + padding := blockSize - (len(data) % blockSize) + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) +} + +// pkcs7Unpad implements the PKCS7 unpadding +func pkcs7Unpad(data []byte) ([]byte, errors.EdgeX) { + length := len(data) + unpadding := int(data[length-1]) + if unpadding > length { + return nil, errors.NewCommonEdgeX(errors.KindServerError, "invalid padding", nil) + } + return data[:(length - unpadding)], nil +} diff --git a/internal/pkg/utils/crypto/aes_test.go b/internal/pkg/utils/crypto/aes_test.go new file mode 100644 index 0000000000..75c1628ff6 --- /dev/null +++ b/internal/pkg/utils/crypto/aes_test.go @@ -0,0 +1,24 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package crypto + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAESCryptor_Encryption(t *testing.T) { + testData := "test data" + aesCryptor := NewAESCryptor() + + encrypted, err := aesCryptor.Encrypt(testData) + require.NoError(t, err) + + decrypted, err := aesCryptor.Decrypt(encrypted) + require.NoError(t, err) + require.Equal(t, testData, string(decrypted)) +} diff --git a/internal/pkg/utils/crypto/interfaces/crypto.go b/internal/pkg/utils/crypto/interfaces/crypto.go new file mode 100644 index 0000000000..d58d0285ac --- /dev/null +++ b/internal/pkg/utils/crypto/interfaces/crypto.go @@ -0,0 +1,13 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package interfaces + +import "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + +type Crypto interface { + Encrypt(string) (string, errors.EdgeX) + Decrypt(string) ([]byte, errors.EdgeX) +} diff --git a/internal/pkg/utils/crypto/interfaces/mocks/Crypto.go b/internal/pkg/utils/crypto/interfaces/mocks/Crypto.go new file mode 100644 index 0000000000..c540f8ddfb --- /dev/null +++ b/internal/pkg/utils/crypto/interfaces/mocks/Crypto.go @@ -0,0 +1,90 @@ +// Code generated by mockery v2.49.0. DO NOT EDIT. + +package mocks + +import ( + errors "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + + mock "github.com/stretchr/testify/mock" +) + +// Crypto is an autogenerated mock type for the Crypto type +type Crypto struct { + mock.Mock +} + +// Decrypt provides a mock function with given fields: _a0 +func (_m *Crypto) Decrypt(_a0 string) ([]byte, errors.EdgeX) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 []byte + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) ([]byte, errors.EdgeX)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// Encrypt provides a mock function with given fields: _a0 +func (_m *Crypto) Encrypt(_a0 string) (string, errors.EdgeX) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + + var r0 string + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) (string, errors.EdgeX)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// NewCrypto creates a new instance of Crypto. 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 NewCrypto(t interface { + mock.TestingT + Cleanup(func()) +}) *Crypto { + mock := &Crypto{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/security/proxyauth/application/key.go b/internal/security/proxyauth/application/key.go new file mode 100644 index 0000000000..270927a59c --- /dev/null +++ b/internal/security/proxyauth/application/key.go @@ -0,0 +1,83 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package application + +import ( + "fmt" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" + proxyAuthUtils "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/utils" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + "github.com/edgexfoundry/go-mod-core-contracts/v4/models" +) + +// The AddKey function accepts the new KeyData model from the controller function +// and then invokes AddKey function of infrastructure layer to add new user +func AddKey(dic *di.Container, keyData models.KeyData) errors.EdgeX { + dbClient := container.DBClientFrom(dic.Get) + cryptor := container.CryptoFrom(dic.Get) + + keyName := "" + if len(keyData.Type) == 0 { + keyData.Type = common.VerificationKeyType + } + switch keyData.Type { + case common.VerificationKeyType: + keyName = proxyAuthUtils.VerificationKeyName(keyData.Issuer) + case common.SigningKeyType: + keyName = proxyAuthUtils.SigningKeyName(keyData.Issuer) + default: + return errors.NewCommonEdgeX( + errors.KindContractInvalid, + fmt.Sprintf("key type should be one of the '%s' or '%s'", common.VerificationKeyType, common.SigningKeyType), nil) + } + + encryptedKey, err := cryptor.Encrypt(keyData.Key) + if err != nil { + return errors.NewCommonEdgeX(errors.Kind(err), "failed to encrypt the key", err) + } + + exists, edgexErr := dbClient.KeyExists(keyName) + if edgexErr != nil { + return errors.NewCommonEdgeXWrapper(edgexErr) + } + if exists { + err = dbClient.UpdateKey(keyName, encryptedKey) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + } else { + err = dbClient.AddKey(keyName, encryptedKey) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + } + return nil +} + +// VerificationKeyByIssuer returns the verification key by issuer +func VerificationKeyByIssuer(dic *di.Container, issuer string) (dtos.KeyData, errors.EdgeX) { + if issuer == "" { + return dtos.KeyData{}, errors.NewCommonEdgeX(errors.KindContractInvalid, "issuer is empty", nil) + } + keyName := proxyAuthUtils.VerificationKeyName(issuer) + dbClient := container.DBClientFrom(dic.Get) + cryptor := container.CryptoFrom(dic.Get) + + keyData, err := dbClient.ReadKeyContent(keyName) + if err != nil { + return dtos.KeyData{}, errors.NewCommonEdgeXWrapper(err) + } + + decryptedKey, err := cryptor.Decrypt(keyData) + if err != nil { + return dtos.KeyData{}, errors.NewCommonEdgeX(errors.Kind(err), "failed to decrypt the key", err) + } + + return dtos.KeyData{Issuer: issuer, Type: common.VerificationKeyType, Key: string(decryptedKey)}, nil +} diff --git a/internal/security/proxyauth/application/key_test.go b/internal/security/proxyauth/application/key_test.go new file mode 100644 index 0000000000..293d88cd26 --- /dev/null +++ b/internal/security/proxyauth/application/key_test.go @@ -0,0 +1,172 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package application + +import ( + "testing" + + cryptoMocks "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces/mocks" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/infrastructure/interfaces/mocks" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + "github.com/edgexfoundry/go-mod-core-contracts/v4/models" + + "github.com/stretchr/testify/require" +) + +func mockDic() *di.Container { + return di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) +} + +func TestAddKey(t *testing.T) { + dic := mockDic() + + validNewKey := "validNewKey" + validIssuer := "testIssuer" + validKeyData := models.KeyData{ + Type: common.VerificationKeyType, + Issuer: validIssuer, + Key: validNewKey, + } + validKeyName := validKeyData.Issuer + "/" + validKeyData.Type + validEncryptedKey := "encryptedValidNewKey" + + validUpdateKey := "validUpdateKey" + updateKeyData := models.KeyData{ + Type: common.SigningKeyType, + Issuer: "issuer2", + Key: validUpdateKey, + } + validUpdateKeyName := updateKeyData.Issuer + "/" + updateKeyData.Type + validUpdateEncryptedKey := "encryptedValidUpdateKey" + + invalidKeyData := models.KeyData{ + Type: "invalidKeyType", + Issuer: "issuer2", + Key: validUpdateKey, + } + + encryptFailedKey := "encryptFailedKey" + encryptFailedKeyData := models.KeyData{ + Type: common.SigningKeyType, + Issuer: "issuer3", + Key: encryptFailedKey, + } + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("KeyExists", validKeyName).Return(false, nil) + dbClientMock.On("AddKey", validKeyName, validEncryptedKey).Return(nil) + dbClientMock.On("KeyExists", validUpdateKeyName).Return(true, nil) + dbClientMock.On("UpdateKey", validUpdateKeyName, validUpdateEncryptedKey).Return(nil) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Encrypt", validKeyData.Key).Return(validEncryptedKey, nil) + cryptoMock.On("Encrypt", updateKeyData.Key).Return(validUpdateEncryptedKey, nil) + cryptoMock.On("Encrypt", encryptFailedKeyData.Key).Return("", errors.NewCommonEdgeX(errors.KindServerError, "failed to encrypt the key", nil)) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + keyData models.KeyData + errorExpected bool + errKind errors.ErrKind + }{ + {"Valid - Add new verification key", validKeyData, false, ""}, + {"Valid - Update existing signing key", updateKeyData, false, ""}, + {"Invalid - Invalid key type", invalidKeyData, true, errors.KindContractInvalid}, + {"Invalid - Encryption Error", encryptFailedKeyData, true, errors.KindServerError}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := AddKey(dic, test.keyData) + if test.errorExpected { + require.Error(t, err) + require.Equal(t, test.errKind, errors.Kind(err)) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestVerificationKeyByIssuer(t *testing.T) { + dic := mockDic() + + validIssuer := "issuer1" + validEncryptedKey := "encryptedKey" + expectedKeyName := validIssuer + "/" + common.VerificationKeyType + expectedKeyData := dtos.KeyData{Issuer: validIssuer, Type: common.VerificationKeyType, Key: "decryptedKey"} + + invalidIssuer := "invalidIssuer" + invalidKeyName := invalidIssuer + "/" + common.VerificationKeyType + + decryptErrIssuer := "decryptErrIssuer" + decryptErrKeyName := decryptErrIssuer + "/" + common.VerificationKeyType + decryptErrKey := "decryptErrKey" + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("ReadKeyContent", expectedKeyName).Return(validEncryptedKey, nil) + dbClientMock.On("ReadKeyContent", invalidKeyName).Return("", errors.NewCommonEdgeX(errors.KindServerError, "read key error", nil)) + dbClientMock.On("ReadKeyContent", decryptErrKeyName).Return(decryptErrKey, nil) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Decrypt", validEncryptedKey).Return([]byte("decryptedKey"), nil) + cryptoMock.On("Decrypt", decryptErrKey).Return([]byte{}, errors.NewCommonEdgeX(errors.KindServerError, "decrypt key error", nil)) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + issuer string + expectedKeyData dtos.KeyData + errorExpected bool + errKind errors.ErrKind + }{ + {"Valid - Valid key", validIssuer, expectedKeyData, false, ""}, + {"Invalid - Empty issuer", "", dtos.KeyData{}, true, errors.KindContractInvalid}, + {"Invalid - Key read error", invalidIssuer, dtos.KeyData{}, true, errors.KindServerError}, + {"Invalid - Decryption error", decryptErrIssuer, dtos.KeyData{}, true, errors.KindServerError}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := VerificationKeyByIssuer(dic, test.issuer) + if test.errorExpected { + require.Error(t, err) + require.Equal(t, test.errKind, errors.Kind(err)) + } else { + require.NoError(t, err) + require.Equal(t, test.expectedKeyData, result) + } + }) + } +} diff --git a/internal/security/proxyauth/config/config.go b/internal/security/proxyauth/config/config.go index eb486287fb..f8a1e289f4 100644 --- a/internal/security/proxyauth/config/config.go +++ b/internal/security/proxyauth/config/config.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2018 Dell Inc. - * Copyright 2022 IOTech Ltd. + * Copyright 2022-2025 IOTech Ltd. * Copyright 2023 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -25,11 +25,14 @@ type ConfigurationStruct struct { Writable WritableInfo Registry bootstrapConfig.RegistryInfo Service bootstrapConfig.ServiceInfo + Database bootstrapConfig.Database } // WritableInfo contains configuration properties that can be updated and applied without restarting the service. type WritableInfo struct { - LogLevel string + LogLevel string + InsecureSecrets bootstrapConfig.InsecureSecrets + Telemetry bootstrapConfig.TelemetryInfo } // UpdateFromRaw converts configuration received from the registry to a service-specific configuration struct which is @@ -86,10 +89,14 @@ func (c *ConfigurationStruct) GetRegistryInfo() bootstrapConfig.RegistryInfo { // GetInsecureSecrets returns the service's InsecureSecrets. func (c *ConfigurationStruct) GetInsecureSecrets() bootstrapConfig.InsecureSecrets { - return nil + return c.Writable.InsecureSecrets } // GetTelemetryInfo returns the service's Telemetry settings. func (c *ConfigurationStruct) GetTelemetryInfo() *bootstrapConfig.TelemetryInfo { - return nil + return &c.Writable.Telemetry +} + +func (c *ConfigurationStruct) GetDatabaseInfo() bootstrapConfig.Database { + return c.Database } diff --git a/internal/security/proxyauth/container/cryptor.go b/internal/security/proxyauth/container/cryptor.go new file mode 100644 index 0000000000..bda89c819c --- /dev/null +++ b/internal/security/proxyauth/container/cryptor.go @@ -0,0 +1,20 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" +) + +// CryptoInterfaceName contains the name of the interfaces.Crypto implementation in the DIC. +var CryptoInterfaceName = di.TypeInstanceToName((*interfaces.Crypto)(nil)) + +// CryptoFrom helper function queries the DIC and returns the interfaces.Cryptor implementation. +func CryptoFrom(get di.Get) interfaces.Crypto { + return get(CryptoInterfaceName).(interfaces.Crypto) +} diff --git a/internal/security/proxyauth/container/database.go b/internal/security/proxyauth/container/database.go new file mode 100644 index 0000000000..f06fccb96d --- /dev/null +++ b/internal/security/proxyauth/container/database.go @@ -0,0 +1,20 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/infrastructure/interfaces" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" +) + +// DBClientInterfaceName contains the name of the interfaces.DBClient implementation in the DIC. +var DBClientInterfaceName = di.TypeInstanceToName((*interfaces.DBClient)(nil)) + +// DBClientFrom helper function queries the DIC and returns the interfaces.DBClient implementation. +func DBClientFrom(get di.Get) interfaces.DBClient { + return get(DBClientInterfaceName).(interfaces.DBClient) +} diff --git a/internal/security/proxyauth/controller/controller.go b/internal/security/proxyauth/controller/controller.go new file mode 100644 index 0000000000..d0d94aaf22 --- /dev/null +++ b/internal/security/proxyauth/controller/controller.go @@ -0,0 +1,24 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "github.com/edgexfoundry/edgex-go/internal/io" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" +) + +type AuthController struct { + dic *di.Container + reader io.DtoReader +} + +func NewAuthController(dic *di.Container) *AuthController { + return &AuthController{ + dic: dic, + reader: io.NewJsonDtoReader(), + } +} diff --git a/internal/security/proxyauth/controller/controller_test.go b/internal/security/proxyauth/controller/controller_test.go new file mode 100644 index 0000000000..2ac102674c --- /dev/null +++ b/internal/security/proxyauth/controller/controller_test.go @@ -0,0 +1,32 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "testing" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" + + "github.com/stretchr/testify/require" +) + +func mockDic() *di.Container { + return di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) +} + +func TestNewAuthController(t *testing.T) { + dic := mockDic() + controller := NewAuthController(dic) + + require.NotNil(t, controller) + require.NotNil(t, controller.reader) +} diff --git a/internal/security/proxyauth/controller/key.go b/internal/security/proxyauth/controller/key.go new file mode 100644 index 0000000000..99b89ae331 --- /dev/null +++ b/internal/security/proxyauth/controller/key.go @@ -0,0 +1,73 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "net/http" + + "github.com/edgexfoundry/edgex-go/internal/pkg" + "github.com/edgexfoundry/edgex-go/internal/pkg/utils" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/application" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + commonDTO "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/labstack/echo/v4" +) + +func (a *AuthController) AddKey(c echo.Context) error { + r := c.Request() + w := c.Response() + if r.Body != nil { + defer func() { _ = r.Body.Close() }() + } + + lc := bootstrapContainer.LoggingClientFrom(a.dic.Get) + ctx := r.Context() + + var req requests.AddKeyDataRequest + err := a.reader.Read(r.Body, &req) + if err != nil { + return utils.WriteErrorResponse(w, ctx, lc, err, "") + } + + var response any + reqId := req.RequestId + + err = application.AddKey(a.dic, dtos.ToKeyDataModel(req.KeyData)) + if err != nil { + return utils.WriteErrorResponse(w, ctx, lc, err, "") + } + + response = commonDTO.NewBaseResponse( + reqId, + "", + http.StatusCreated) + utils.WriteHttpHeader(w, ctx, http.StatusCreated) + return pkg.EncodeAndWriteResponse(response, w, lc) +} + +func (a *AuthController) VerificationKeyByIssuer(c echo.Context) error { + lc := bootstrapContainer.LoggingClientFrom(a.dic.Get) + r := c.Request() + w := c.Response() + ctx := r.Context() + + issuer := c.Param(common.Issuer) + + keyData, err := application.VerificationKeyByIssuer(a.dic, issuer) + if err != nil { + return utils.WriteErrorResponse(w, ctx, lc, err, "") + } + + response := responses.NewKeyDataResponse("", "", http.StatusOK, keyData) + utils.WriteHttpHeader(w, ctx, http.StatusOK) + return pkg.EncodeAndWriteResponse(response, w, lc) +} diff --git a/internal/security/proxyauth/controller/key_test.go b/internal/security/proxyauth/controller/key_test.go new file mode 100644 index 0000000000..c8dad1b569 --- /dev/null +++ b/internal/security/proxyauth/controller/key_test.go @@ -0,0 +1,179 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + cryptoMocks "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces/mocks" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/infrastructure/interfaces/mocks" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + commonDTO "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" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/require" +) + +func TestAuthController_AddKey(t *testing.T) { + dic := mockDic() + + e := echo.New() + controller := NewAuthController(dic) + + validNewKey := "validNewKey" + validIssuer := "testIssuer" + validKeyData := dtos.KeyData{ + Type: common.VerificationKeyType, + Issuer: validIssuer, + Key: validNewKey, + } + validKeyName := validKeyData.Issuer + "/" + validKeyData.Type + validEncryptedKey := "encryptedValidNewKey" + validReq := requests.AddKeyDataRequest{ + BaseRequest: commonDTO.BaseRequest{ + Versionable: commonDTO.NewVersionable(), + }, + KeyData: validKeyData, + } + + noIssuerReq := validReq + noIssuerReq.KeyData.Issuer = "" + + invalidTypeReq := validReq + invalidTypeReq.KeyData.Type = "invalidType" + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("KeyExists", validKeyName).Return(false, nil) + dbClientMock.On("AddKey", validKeyName, validEncryptedKey).Return(nil) + dbClientMock.On("KeyExists", validKeyName).Return(false, nil) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Encrypt", validKeyData.Key).Return(validEncryptedKey, nil) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + request requests.AddKeyDataRequest + expectedStatus int + }{ + {"Valid - Successful add key", validReq, http.StatusCreated}, + {"Invalid - no issuer request", noIssuerReq, http.StatusBadRequest}, + {"Invalid - invalid type", invalidTypeReq, http.StatusBadRequest}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + jsonBytes, err := json.Marshal(test.request) + require.NoError(t, err) + + reader := strings.NewReader(string(jsonBytes)) + req, err := http.NewRequest(http.MethodPost, common.ApiKeyRoute, reader) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + ctx := e.NewContext(req, recorder) + + edgexErr := controller.AddKey(ctx) + require.NoError(t, edgexErr) + require.Equal(t, test.expectedStatus, recorder.Code) + + var res commonDTO.BaseResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + require.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected") + require.Equal(t, test.expectedStatus, res.StatusCode, "BaseResponse status code not as expected") + }) + } +} + +func TestAuthController_VerificationKeyByIssuer(t *testing.T) { + dic := mockDic() + controller := NewAuthController(dic) + + validIssuer := "issuer1" + validEncryptedKey := "encryptedKey" + expectedKeyName := validIssuer + "/" + common.VerificationKeyType + expectedKeyData := dtos.KeyData{Issuer: validIssuer, Type: common.VerificationKeyType, Key: "decryptedKey"} + + invalidIssuer := "invalidIssuer" + invalidKeyName := invalidIssuer + "/" + common.VerificationKeyType + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("ReadKeyContent", expectedKeyName).Return(validEncryptedKey, nil) + dbClientMock.On("ReadKeyContent", invalidKeyName).Return("", errors.NewCommonEdgeX(errors.KindServerError, "read key error", nil)) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Decrypt", validEncryptedKey).Return([]byte("decryptedKey"), nil) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + issuer string + expectedStatus int + expectedKeyData dtos.KeyData + }{ + {"Valid - valid issuer", validIssuer, http.StatusOK, expectedKeyData}, + {"Invalid - no issuer request", "", http.StatusBadRequest, dtos.KeyData{}}, + {"Invalid - failed to read key by issuer", invalidIssuer, http.StatusInternalServerError, dtos.KeyData{}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + e := echo.New() + req, err := http.NewRequest(http.MethodGet, common.ApiVerificationKeyByIssuerRoute, http.NoBody) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + c := e.NewContext(req, recorder) + c.SetParamNames(common.Issuer) + c.SetParamValues(test.issuer) + + edgexErr := controller.VerificationKeyByIssuer(c) + require.NoError(t, edgexErr) + require.Equal(t, test.expectedStatus, recorder.Code) + if test.expectedStatus == http.StatusOK { + var res responses.KeyDataResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + require.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected") + require.Equal(t, expectedKeyData, res.KeyData, "KeyData response not as expected") + } else { + var res commonDTO.BaseResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + require.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected") + require.Equal(t, test.expectedStatus, res.StatusCode, "BaseResponse status code not as expected") + } + }) + } +} diff --git a/internal/security/proxyauth/infrastructure/interfaces/db.go b/internal/security/proxyauth/infrastructure/interfaces/db.go new file mode 100644 index 0000000000..79b562f9e8 --- /dev/null +++ b/internal/security/proxyauth/infrastructure/interfaces/db.go @@ -0,0 +1,15 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package interfaces + +import "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + +type DBClient interface { + AddKey(name string, content string) errors.EdgeX + UpdateKey(name string, content string) errors.EdgeX + ReadKeyContent(name string) (string, errors.EdgeX) + KeyExists(name string) (bool, errors.EdgeX) +} diff --git a/internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go b/internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go new file mode 100644 index 0000000000..d65b72251f --- /dev/null +++ b/internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go @@ -0,0 +1,128 @@ +// Code generated by mockery v2.49.0. DO NOT EDIT. + +package mocks + +import ( + errors "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + + mock "github.com/stretchr/testify/mock" +) + +// DBClient is an autogenerated mock type for the DBClient type +type DBClient struct { + mock.Mock +} + +// AddKey provides a mock function with given fields: name, content +func (_m *DBClient) AddKey(name string, content string) errors.EdgeX { + ret := _m.Called(name, content) + + if len(ret) == 0 { + panic("no return value specified for AddKey") + } + + var r0 errors.EdgeX + if rf, ok := ret.Get(0).(func(string, string) errors.EdgeX); ok { + r0 = rf(name, content) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(errors.EdgeX) + } + } + + return r0 +} + +// KeyExists provides a mock function with given fields: name +func (_m *DBClient) KeyExists(name string) (bool, errors.EdgeX) { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for KeyExists") + } + + var r0 bool + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) (bool, errors.EdgeX)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// ReadKeyContent provides a mock function with given fields: name +func (_m *DBClient) ReadKeyContent(name string) (string, errors.EdgeX) { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for ReadKeyContent") + } + + var r0 string + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) (string, errors.EdgeX)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// UpdateKey provides a mock function with given fields: name, content +func (_m *DBClient) UpdateKey(name string, content string) errors.EdgeX { + ret := _m.Called(name, content) + + if len(ret) == 0 { + panic("no return value specified for UpdateKey") + } + + var r0 errors.EdgeX + if rf, ok := ret.Get(0).(func(string, string) errors.EdgeX); ok { + r0 = rf(name, content) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(errors.EdgeX) + } + } + + return r0 +} + +// NewDBClient creates a new instance of DBClient. 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 NewDBClient(t interface { + mock.TestingT + Cleanup(func()) +}) *DBClient { + mock := &DBClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/security/proxyauth/init.go b/internal/security/proxyauth/init.go index ababfbe83b..ba36e19d82 100644 --- a/internal/security/proxyauth/init.go +++ b/internal/security/proxyauth/init.go @@ -20,10 +20,6 @@ import ( "context" "sync" - "github.com/edgexfoundry/edgex-go" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/startup" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" @@ -44,20 +40,9 @@ func NewBootstrap(router *echo.Echo, serviceName string) *Bootstrap { } } -// BootstrapHandler fulfills the BootstrapHandler contract and performs initialization needed by the command service. -// Authentication is always on for this service, -// as it is called by NGINX to authenticate requests -// and must always authenticate even if the rest of EdgeX does not +// BootstrapHandler fulfills the BootstrapHandler contract and performs initialization needed by the security-proxy-auth service. func (b *Bootstrap) BootstrapHandler(ctx context.Context, wg *sync.WaitGroup, _ startup.Timer, dic *di.Container) bool { - lc := container.LoggingClientFrom(dic.Get) - secretProvider := container.SecretProviderExtFrom(dic.Get) - authenticationHook := handlers.SecretStoreAuthenticationHandlerFunc(secretProvider, lc) - - // Common - _ = controller.NewCommonController(dic, b.router, b.serviceName, edgex.Version) - - // Run authentication hook for a nil route - b.router.GET("/auth", emptyHandler, authenticationHook) + LoadRestRoutes(b.router, dic, b.serviceName) return true } diff --git a/internal/security/proxyauth/main.go b/internal/security/proxyauth/main.go index 88d3eab95a..c4f922e795 100644 --- a/internal/security/proxyauth/main.go +++ b/internal/security/proxyauth/main.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2020 Dell Inc. - * Copyright 2022-2024 IOTech Ltd. + * Copyright 2022-2025 IOTech Ltd. * Copyright 2023 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -18,6 +18,7 @@ package proxyauth import ( "context" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/flags" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" @@ -27,6 +28,8 @@ import ( "github.com/edgexfoundry/go-mod-bootstrap/v4/di" "github.com/edgexfoundry/edgex-go" + pkgHandlers "github.com/edgexfoundry/edgex-go/internal/pkg/bootstrap/handlers" + "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto" "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/config" "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" @@ -53,6 +56,9 @@ func Main(ctx context.Context, cancel context.CancelFunc, router *echo.Echo, arg container.ConfigurationName: func(get di.Get) interface{} { return configuration }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return crypto.NewAESCryptor() + }, }) httpServer := handlers.NewHttpServer(router, true, common.SecurityProxyAuthServiceKey) @@ -69,10 +75,9 @@ func Main(ctx context.Context, cancel context.CancelFunc, router *echo.Echo, arg true, bootstrapConfig.ServiceTypeOther, []interfaces.BootstrapHandler{ + pkgHandlers.NewDatabase(httpServer, configuration, container.DBClientInterfaceName).BootstrapHandler, // add db client bootstrap handler NewBootstrap(router, common.SecurityProxyAuthServiceKey).BootstrapHandler, httpServer.BootstrapHandler, handlers.NewStartMessage(common.SecurityProxyAuthServiceKey, edgex.Version).BootstrapHandler, }) - - // code here! } diff --git a/internal/security/proxyauth/router.go b/internal/security/proxyauth/router.go new file mode 100644 index 0000000000..a64f82ab2f --- /dev/null +++ b/internal/security/proxyauth/router.go @@ -0,0 +1,38 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package proxyauth + +import ( + "github.com/edgexfoundry/edgex-go" + spaController "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/controller" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + + "github.com/labstack/echo/v4" +) + +// LoadRestRoutes generates the routing for API requests +// Authentication is always on for this service, +// as it is called by NGINX to authenticate requests +// and must always authenticate even if the rest of EdgeX does not +func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) { + authenticationHook := handlers.AutoConfigAuthenticationFunc(dic) + + // Common + _ = controller.NewCommonController(dic, r, serviceName, edgex.Version) + + // Run authentication hook for a nil route + r.GET("/auth", emptyHandler, authenticationHook) + + ac := spaController.NewAuthController(dic) + + r.POST(common.ApiKeyRoute, ac.AddKey, authenticationHook) + // This API will be called within the authenticationHook function itself + r.GET(common.ApiVerificationKeyByIssuerRoute, ac.VerificationKeyByIssuer) +} diff --git a/internal/security/proxyauth/utils/key.go b/internal/security/proxyauth/utils/key.go new file mode 100644 index 0000000000..a08a1a7ca4 --- /dev/null +++ b/internal/security/proxyauth/utils/key.go @@ -0,0 +1,16 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + +func SigningKeyName(issuer string) string { + return issuer + "/" + common.SigningKeyType +} + +func VerificationKeyName(issuer string) string { + return issuer + "/" + common.VerificationKeyType +} diff --git a/internal/security/proxyauth/utils/key_test.go b/internal/security/proxyauth/utils/key_test.go new file mode 100644 index 0000000000..4f4f042749 --- /dev/null +++ b/internal/security/proxyauth/utils/key_test.go @@ -0,0 +1,28 @@ +// +// Copyright (C) 2025 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + + "github.com/stretchr/testify/require" +) + +func TestSigningKeyName(t *testing.T) { + mockIssuer := "mockIssuer" + expected := mockIssuer + "/" + common.SigningKeyType + result := SigningKeyName(mockIssuer) + require.Equal(t, expected, result) +} + +func TestVerificationKeyName(t *testing.T) { + mockIssuer := "mockIssuer" + expected := mockIssuer + "/" + common.VerificationKeyType + result := VerificationKeyName(mockIssuer) + require.Equal(t, expected, result) +} diff --git a/internal/support/notifications/router.go b/internal/support/notifications/router.go index 49e6410b85..5171fe2ffd 100644 --- a/internal/support/notifications/router.go +++ b/internal/support/notifications/router.go @@ -7,7 +7,6 @@ package notifications import ( "github.com/edgexfoundry/edgex-go" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" @@ -19,9 +18,7 @@ import ( ) func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) { - lc := container.LoggingClientFrom(dic.Get) - secretProvider := container.SecretProviderExtFrom(dic.Get) - authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc) + authenticationHook := handlers.AutoConfigAuthenticationFunc(dic) // Common _ = controller.NewCommonController(dic, r, serviceName, edgex.Version) diff --git a/internal/support/scheduler/router.go b/internal/support/scheduler/router.go index b9c85d9a85..22f854008a 100644 --- a/internal/support/scheduler/router.go +++ b/internal/support/scheduler/router.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2024 IOTech Ltd +// Copyright (C) 2024-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -8,7 +8,6 @@ package scheduler import ( "github.com/labstack/echo/v4" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" @@ -19,9 +18,7 @@ import ( ) func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) { - lc := container.LoggingClientFrom(dic.Get) - secretProvider := container.SecretProviderExtFrom(dic.Get) - authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc) + authenticationHook := handlers.AutoConfigAuthenticationFunc(dic) // Common _ = controller.NewCommonController(dic, r, serviceName, edgex.Version) diff --git a/openapi/security-proxy-auth.yaml b/openapi/security-proxy-auth.yaml new file mode 100644 index 0000000000..bf77a8cc9e --- /dev/null +++ b/openapi/security-proxy-auth.yaml @@ -0,0 +1,740 @@ +openapi: 3.1.0 +info: + title: Edge Foundry - Proxy Authentication API + description: This is the definition of the API for the Security Proxy Auth service in the EdgeX Foundry IOT microservice platform. + version: 4.0.0 + +servers: + - url: http://localhost:59842/api/v3 + description: URL for local development and testing + +components: + schemas: + BaseResponse: + description: "Defines basic properties which all use-case specific response DTO instances should support" + type: object + properties: + apiVersion: + description: "A version number shows the API version in DTOs." + type: string + example: v3 + requestId: + description: "Uniquely identifies the request that resulted in this response." + type: string + format: uuid + example: "e6e8a2f4-eb14-4649-9e2b-175247911369" + message: + description: "A field that can contain a free-form message, such as an error message." + type: string + statusCode: + description: "A numeric code signifying the operational status of the response." + type: integer + ErrorResponse: + allOf: + - $ref: '#/components/schemas/BaseResponse' + description: "A response type for returning a generic error to the caller." + type: object + PingResponse: + type: object + properties: + apiVersion: + description: "A version number shows the API version in DTOs." + type: string + timestamp: + description: "Outputs the current server timestamp in RFC1123 format" + example: "Mon, 02 Jan 2006 15:04:05 MST" + type: string + serviceName: + description: "Outputs the name of the service the response is from" + type: string + VersionResponse: + description: "A response returned from the /version endpoint whose purpose is to report out the latest version supported by the service." + type: object + properties: + apiVersion: + description: "A version number shows the API version in DTOs." + type: string + version: + description: "The latest version supported by the service." + type: string + serviceName: + description: "Outputs the name of the service the response is from" + type: string + ConfigResponse: + description: "Provides a response containing the configuration for the targeted service." + type: object + properties: + apiVersion: + description: "A version number shows the API version in DTOs." + type: string + serviceName: + description: "Outputs the name of the service the response is from" + type: string + config: + description: "An object containing the service's configuration. Please refer the configuration documentation of each service for more details at [EdgeX Foundry Documentation](https://docs.edgexfoundry.org)." + type: object + BaseRequest: + description: "Defines basic properties which all use-case specific request DTO instances should support." + type: object + properties: + requestId: + description: "Uniquely identifies this request. For implementation, recommend this value be generated by the type's constructor." + type: string + format: uuid + example: "e6e8a2f4-eb14-4649-9e2b-175247911369" + apiVersion: + description: "A version number shows the API version in DTOs." + type: string + example: "v3" + SecretRequest: + allOf: + - $ref: '#/components/schemas/BaseRequest' + description: Defines the secret data to be stored + type: object + properties: + secretName: + description: Specifies the location within the secret to store + type: string + example: "credentials" + secretData: + description: A list of the key/value pairs of secret data to store + type: array + items: + $ref: '#/components/schemas/SecretDataKeyValue' + SecretDataKeyValue: + description: Defines a key/value pair of the secret data + type: object + properties: + key: + description: The key to identify the secret + type: string + example: "username" + value: + description: The value of the secret + type: string + example: "mqtt-user" + required: + - key + - value + AddKeyDataRequest: + allOf: + - $ref: '#/components/schemas/BaseRequest' + description: "A request for ingesting a new key" + type: object + properties: + keyData: + $ref: '#/components/schemas/KeyData' + required: + - keyData + KeyData: + description: "Indicates the information of a key." + properties: + issuer: + description: "The issuer of the key" + type: string + type: + description: "The type of the key. Default value verification if it is empty." + enum: + - verification + - signing + key: + description: "The key content" + type: string + required: + - issuer + - password + - role + GetKeyDataResponse: + type: object + properties: + apiVersion: + description: "A version number shows the API version in DTOs." + type: string + keyData: + $ref: '#/components/schemas/KeyDataResponse' + KeyDataResponse: + type: object + properties: + issuer: + description: "The issuer of the key" + type: string + type: + description: "The type of the key" + type: string + key: + description: "The key content" + type: string + + parameters: + correlatedRequestHeader: + in: header + name: X-Correlation-ID + description: "A unique identifier correlating a request to its associated response, facilitating tracing through being included on requests originating from the initiating request." + schema: + type: string + format: uuid + required: true + example: "14a42ea6-c394-41c3-8bcd-a29b9f5e6835" + OriginalRequestUriHeader: + in: header + name: X-Forwarded-Uri + required: false + description: original request URI + schema: + type: string + OriginalRequestMethodHeader: + in: header + name: X-Forwarded-Method + required: false + description: original request method + schema: + type: string + BearerTokenHeader: + in: header + name: Authorization + required: true + description: Bearer token authentication + schema: + type: string + example: "Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiOWQ0ZDhhYi0wMzk5LTQ0ZmYtYWQ5Ni1hNGUyMTE3MmM1YzIiLCJleHAiOjE2NTcwODQ5OTksIm5iZiI6MTY1NzA4MTM5OSwiaWF0IjoxNjU3MDgxMzk5fQ.WvuSULWKfROzygV-V1LobsjlK5aU88CRNnOgabfzdB6bD5IK2HXw8tD3XLL3xOfTBNnyQkRnqFVbZJ9zkyW_Ng" + offsetParam: + in: query + name: offset + required: false + schema: + type: integer + minimum: 0 + default: 0 + description: "The number of items to skip before starting to collect the result set." + limitParam: + in: query + name: limit + required: false + schema: + type: integer + minimum: -1 + default: 20 + description: "The numbers of items to return. Specify -1 will return all remaining items after offset. The maximum will be the MaxResultCount as defined in the configuration of service." + + headers: + correlatedResponseHeader: + description: "A response header that returns the unique correlation ID used to initiate the request." + schema: + type: string + format: uuid + example: "14a42ea6-c394-41c3-8bcd-a29b9f5e6835" + examples: + RolePolicyExample: + value: + apiVersion: v3 + rolePolicy: + role: device-cmd-dev + description: The device command developer role which can execute all the device commands + accessPolicies: + - path: /core-command/* + httpMethods: + - GET + - PUT + effect: allow + - path: /core-metadata/api/v3/device/* + httpMethods: + - GET + - PUT + - POST + effect: deny + GetRolePolicyExample: + value: + apiVersion: v3 + statusCode: 200 + rolePolicy: + id: 2faeb932-fc82-4c3c-8de5-4f5a191240ec + role: device-cmd-dev + description: The device command developer role which can execute all the device commands + created: 1600927134931 + modified: 1600927134931 + accessPolicies: + - path: /core-command/* + httpMethods: + - GET + - PUT + effect: allow + - path: /core-metadata/api/v3/device/* + httpMethods: + - GET + - PUT + - POST + effect: deny + MultiRolePolicyExample: + value: + apiVersion: v3 + statusCode: 200 + totalCount: 1 + rolePolicies: + - id: 2faeb932-fc82-4c3c-8de5-4f5a191240ec + role: device-cmd-dev + created: 1600927134931 + modified: 1600927134931 + accessPolicies: + - path: /core-command/* + httpMethods: + - GET + - PUT + - path: /core-metadata/api/v3/device/* + httpMethods: + - GET + - PUT + - POST + effect: allow + 400Example: + value: + apiVersion: "v3" + requestId: "73f0932c-0148-11eb-adc1-0242ac120002" + statusCode: 400 + message: "Bad Request" + 401Example: + value: + apiVersion: "v3" + requestId: "73f0932c-0148-11eb-adc1-0242ac120002" + statusCode: 401 + message: "Unauthorized" + 403Example: + value: + apiVersion: "v3" + requestId: "73f0932c-0148-11eb-adc1-0242ac120002" + statusCode: 403 + message: "Forbidden" + 404Example: + value: + apiVersion: "v3" + requestId: "84c9489c-0148-11eb-adc1-0242ac120002" + statusCode: 404 + message: "Not Found" + 409Example: + value: + apiVersion: "v3" + statusCode: 409 + message: "Data Duplicate" + 416Example: + value: + apiVersion: "v3" + statusCode: 416 + message: "Range Not Satisfiable" + 500Example: + value: + apiVersion: "v3" + statusCode: 500 + message: "Interval Server Error" + AddUserExample: + value: + - apiVersion: v3 + user: + name: bob + displayName: Bob Myers + description: A device user + password: password + roles: + - device-admin + LoginRequestExample: + value: + username: "alice" + password: "***************" + LoginRespExample: + value: + apiVersion: v3 + statusCode: 200 + jwt: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzA4ODIxNjIsImlzcyI6IklPVGVjaCIsInRva2VuX2lkIjoiZTcxMDMwYzQtZDgxMS00Y2Y1LThmMjktYTg3YWE2ZDY0NTRkIn0.TPlUmcwAkWUC3_WRFC1Pp3sjbG4EmXLXrqow387et0N3z9tNZ-zqH6jfNzOATcj5I7-Wrkp-T0fFfbhGidPYBg + RefreshTokenRespExample: + value: + apiVersion: v3 + statusCode: 200 + jwt: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzA4ODIxNjIsImlzcyI6IklPVGVjaCIsInRva2VuX2lkIjoiZTcxMDMwYzQtZDgxMS00Y2Y1LThmMjktYTg3YWE2ZDY0NTRkIn0.TPlUmcwAkWUC3_WRFC1Pp3sjbG4EmXLXrqow387et0N3z9tNZ-zqH6jfNzOATcj5I7-Wrkp-T0fFfbhGidPYBg + GetUserExample: + value: + apiVersion: v3 + statusCode: 200 + user: + name: bob + displayName: Bob Myers + description: A device user + roles: + - device-admin + MultiAddUserExample: + value: + - apiVersion: "v3" + requestId: "592b98aa-1e4e-46f2-992a-9e6ef844270f" + statusCode: 201 + - apiVersion: "v3" + requestId: "8e025b94-1512-4ec4-a9bd-3111cb278379" + statusCode: 400 + message: "role policy 'not-exists-role' not exists" + - apiVersion: "v3" + requestId: "8e025b94-1512-4ec4-a9bd-3111cb278379" + statusCode: 409 + message: "user name 'user' already exists" + MultiUpdateUserExample: + value: + - apiVersion: "v3" + requestId: "592b98aa-1e4e-46f2-992a-9e6ef844270f" + statusCode: 200 + - apiVersion: "v3" + requestId: "8e025b94-1512-4ec4-a9bd-3111cb278379" + statusCode: 400 + message: "role policy 'not-exists-role' not exists" + - apiVersion: "v3" + requestId: "8e025b94-1512-4ec4-a9bd-3111cb278379" + statusCode: 404 + message: "user 'user' does not exist" + MultiUserExample: + value: + apiVersion: v3 + statusCode: 200 + totalCount: 2 + users: + - name: bob + displayName: Bob Myers + description: A device user + roles: + - device-admin + - name: alice + displayName: Alice Masur + description: A device profile user + roles: + - device-profile-admin + MultiAuthRouteRequestExample: + value: + - apiVersion: v3 + authRoute: + path: /core-metadata/api/v3/device/* + httpMethod: GET + - apiVersion: v3 + authRoute: + path: /core-data/api/v3/event + httpMethod: POST + MultiAuthRouteResponseExample: + value: + apiVersion: v3 + statusCode: 200 + authResponses: + - path: /core-metadata/api/v3/device/* + httpMethod: GET + authResult: true + - path: /core-data/api/v3/event + httpMethod: POST + authResult: false + AddKeyDataExample: + value: + apiVersion: v3 + keyData: + issuer: bob + type: verification + key: g2ymJgt6hlYk92My89wpeYL3yH0WoiM9 + GetKeyDataExample: + value: + apiVersion: v3 + statusCode: 200 + keyData: + issuer: bob + type: verification + key: g2ymJgt6hlYk92My89wpeYL3yH0WoiM9 + +paths: + /auth: + parameters: + - $ref: '#/components/parameters/BearerTokenHeader' + - $ref: '#/components/parameters/OriginalRequestUriHeader' + - $ref: '#/components/parameters/OriginalRequestMethodHeader' + get: + summary: Authenticate the user + description: | + Authenticate the user based on the JWT provided in the headers. + responses: + '204': + description: "The user is authorized with the requested URI and the operation." + '401': + description: "Unauthorized" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 401Example: + $ref: '#/components/examples/401Example' + '403': + description: "Forbidden" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 403Example: + $ref: '#/components/examples/403Example' + /key: + post: + summary: Add a new key + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddKeyDataRequest' + examples: + addKeyDataExample: + $ref: '#/components/examples/AddKeyDataExample' + responses: + '201': + description: "Created" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/BaseResponse' + example: + apiVersion: "v3" + statusCode: 201 + '400': + description: "Bad Request" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 400Example: + $ref: '#/components/examples/400Example' + '409': + description: "Conflicted" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 409Example: + $ref: '#/components/examples/409Example' + '500': + description: "Internal Server Error" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 500Example: + $ref: '#/components/examples/500Example' + /key/verification/issuer/{issuer}: + parameters: + - name: issuer + in: path + required: true + schema: + type: string + description: "Specifies the issuer of the key" + get: + summary: Get a key information by the specified issuer + responses: + '200': + description: "OK" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/GetKeyDataResponse' + examples: + LoginRespExample: + $ref: '#/components/examples/GetKeyDataExample' + '400': + description: "Bad Request" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 400Example: + $ref: '#/components/examples/400Example' + '404': + description: "Not Found" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 404Example: + $ref: '#/components/examples/404Example' + '500': + description: "Internal Server Error" + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 500Example: + $ref: '#/components/examples/500Example' + /secret: + parameters: + - $ref: '#/components/parameters/correlatedRequestHeader' + post: + summary: Stores a secret to the secure Secret Store + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SecretRequest' + required: true + responses: + '201': + description: "Created" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/BaseResponse' + '400': + description: "Invalid request." + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 400Example: + $ref: '#/components/examples/400Example' + '500': + description: "An unexpected error happened on the server." + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 500Example: + $ref: '#/components/examples/500Example' + /config: + get: + summary: "Returns the current configuration of the service." + responses: + '200': + description: "OK" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/ConfigResponse' + example: + apiVersion: "v3" + config: + Writeable: + LogLevel: "INFO" + Databases: + Primary: + Type: "redisdb" + Timeout: 5000 + Host: "edgex-redis" + Port: 6379 + Name: "proxy-auth" + Registry: + Host: "localhost" + Port: 8500 + Type: "consul" + Service: + HealthCheckInterval: "10s" + Host: "proxy-auth" + Port: 59842 + SeverBindAddr: "" + StartupMsg: "This is the Core Data Microservice" + MaxResultCount: 50000 + MaxRequestSize: 0 + RequestTimeout: "5s" + SecretStore: + Type: "vault" + Host: "edgex-vault" + Port: 8200 + Path: "security-proxy-auth" + Protocol: "http" + Namespace: "" + RootCaCertPath: "" + ServerName: "" + Authentication: + AuthType: "X-Vault-Token" + AuthToken: "" + TokenFile: "/tmp/edgex/secrets/security-proxy-auth/secrets-token.json" + serviceName: "proxy-auth" + '500': + description: "Interval Server Error" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 500Example: + $ref: '#/components/examples/500Example' + /ping: + get: + summary: "A simple 'ping' endpoint that can be used as a service healthcheck" + responses: + '200': + description: "OK" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/PingResponse' + example: + apiVersion: "v3" + timestamp: "Mon, 02 Jan 2006 15:04:05 MST" + serviceName: "proxy-auth" + '500': + description: "Interval Server Error" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 500Example: + $ref: '#/components/examples/500Example' + /version: + get: + summary: "A simple 'version' endpoint that will return the current version of the service" + responses: + '200': + description: "OK" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/VersionResponse' + example: + apiVersion: "v3" + version: "master" + serviceName: "proxy-auth" + '500': + description: "Interval Server Error" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 500Example: + $ref: '#/components/examples/500Example'