Issue 67869881 Add decryption/encryption (#32)
* [ISSUE-67869881] add encryption/decryption
* [ISSUE-67869881] add decryption/encryption
* [ISSUE-67869881] update retrieve key request
* [ISSUE-67869881] support orgs without encryption
* [ISSUE-67869881] update url path
* [ISSUE-67869881] update request
* [ISSUE-67869881] address comments, add test cases
* [ISSUE-67869881] add more test cases
diff --git a/accessEntity/api.go b/accessEntity/api.go
index b239409..fb49fca 100644
--- a/accessEntity/api.go
+++ b/accessEntity/api.go
@@ -246,7 +246,7 @@
log.Errorf("getCompanyDeveloper: %v", err)
return nil, newDbError(err)
}
- email, err := a.DbMan.GetDevEmailByDevId(dev.DeveloperId)
+ email, err := a.DbMan.GetDevEmailByDevId(dev.DeveloperId, org)
if err != nil {
log.Errorf("getCompanyDeveloper: %v", err)
return nil, newDbError(err)
diff --git a/accessEntity/data.go b/accessEntity/data.go
index 3e808b0..4634f61 100644
--- a/accessEntity/data.go
+++ b/accessEntity/data.go
@@ -16,6 +16,7 @@
import (
"database/sql"
"fmt"
+ "github.com/apid/apid-core/util"
"github.com/apid/apidApiMetadata/common"
"strings"
)
@@ -84,7 +85,7 @@
return name.String, nil
}
-func (d *DbManager) GetDevEmailByDevId(devId string) (string, error) {
+func (d *DbManager) GetDevEmailByDevId(devId string, org string) (string, error) {
query := selectDeveloperById(
"?",
"email",
@@ -94,7 +95,7 @@
if err != nil || !email.Valid {
return "", err
}
- return email.String, nil
+ return email.String, err
}
func (d *DbManager) GetComNames(id string, idType string) ([]string, error) {
@@ -278,23 +279,36 @@
switch priKey {
case IdentifierConsumerKey:
- return d.getAppCredentialByConsumerKey(priVal, org)
+ appCredentials, err = d.getAppCredentialByConsumerKey(priVal, org)
case IdentifierAppId:
- return d.getAppCredentialByAppId(priVal, org)
+ appCredentials, err = d.getAppCredentialByAppId(priVal, org)
}
+
+ if err != nil {
+ return
+ }
+
+ var plaintext string
+ for i := range appCredentials {
+ if plaintext, err = d.CipherManager.TryDecryptBase64(appCredentials[i].ConsumerSecret, org); err != nil {
+ return
+ }
+ appCredentials[i].ConsumerSecret = plaintext
+ }
+
return
}
func (d *DbManager) GetDevelopers(org, priKey, priVal, secKey, secVal string) (developers []common.Developer, err error) {
switch priKey {
case IdentifierAppId:
- return d.getDeveloperByAppId(priVal, org)
+ developers, err = d.getDeveloperByAppId(priVal, org)
case IdentifierDeveloperEmail:
- return d.getDeveloperByEmail(priVal, org)
+ developers, err = d.getDeveloperByEmail(priVal, org)
case IdentifierConsumerKey:
- return d.getDeveloperByConsumerKey(priVal, org)
+ developers, err = d.getDeveloperByConsumerKey(priVal, org)
case IdentifierDeveloperId:
- return d.getDeveloperById(priVal, org)
+ developers, err = d.getDeveloperById(priVal, org)
}
return
}
@@ -745,19 +759,10 @@
var prods []common.ApiProduct
for _, prod := range apiProducts {
resources := common.JsonToStringArray(prod.ApiResources)
- if Contains(resources, resource) {
+ if util.Contains(resources, resource) {
prods = append(prods, prod)
}
}
//log.Debugf("After filter: %v", prods)
return prods
}
-
-func Contains(sl []string, str string) bool {
- for _, s := range sl {
- if s == str {
- return true
- }
- }
- return false
-}
diff --git a/accessEntity/data_test.go b/accessEntity/data_test.go
index 9b9924d..aa3f51c 100644
--- a/accessEntity/data_test.go
+++ b/accessEntity/data_test.go
@@ -43,8 +43,9 @@
dbMan = &DbManager{
DbManager: common.DbManager{
- Data: services.Data(),
- DbMux: sync.RWMutex{},
+ Data: services.Data(),
+ DbMux: sync.RWMutex{},
+ CipherManager: &DummyCipherMan{},
},
}
dbMan.SetDbVersion(dataTestTempDir)
@@ -529,7 +530,7 @@
It("GetDevEmailByDevId", func() {
data := "e41f04e8-9d3f-470a-8bfd-c7939945896c"
expected := "bar@google.com"
- Expect(dbMan.GetDevEmailByDevId(data)).Should(Equal(expected))
+ Expect(dbMan.GetDevEmailByDevId(data, "apid-haoming")).Should(Equal(expected))
})
It("GetStatus", func() {
diff --git a/accessEntity/data_test.sql b/accessEntity/data_test.sql
index 8b1590a..7f01503 100644
--- a/accessEntity/data_test.sql
+++ b/accessEntity/data_test.sql
@@ -49,9 +49,9 @@
INSERT INTO "kms_api_product" VALUES('db90a25a-15c8-42ad-96c1-63ed9682b5a9','515211e9','apigee-remote-proxy','apigee-remote-proxy','','{/**,/}','AUTO','{""}','{apigee-remote-proxy}','{prod,test}','','',NULL,'2017-09-20 23:05:09.234+00:00','haoming@apid.git','2017-09-20 23:05:09.234+00:00','haoming@apid.git','515211e9');
INSERT INTO "kms_api_product" VALUES('fea8a6d5-8d34-477f-ac82-c397eaec06af','515211e9','testproductsdljnkpt','testproductsdljnkpt','','{/res1}','AUTO','{}','{}','{test}','','',NULL,'2017-11-02 16:00:15.608+00:00','haoming@apid.git','2017-11-02 16:00:18.125+00:00','haoming@apid.git','515211e9');
CREATE TABLE kms_app_credential (id text,tenant_id text,consumer_secret text,app_id text,method_type text,status text,issued_at blob,expires_at blob,app_status text,scopes text,created_at blob,created_by text,updated_at blob,updated_by text,_change_selector text, primary key (id,tenant_id));
-INSERT INTO "kms_app_credential" VALUES('abcd','515211e9','secret1','408ad853-3fa0-402f-90ee-103de98d71a5','','APPROVED','2017-08-18 22:13:18.35+00:00','','','{}','2017-08-18 22:13:18.35+00:00','-NA-','2017-08-18 22:13:18.352+00:00','-NA-','515211e9');
-INSERT INTO "kms_app_credential" VALUES('dcba','515211e9','secret2','ae053aee-f12d-4591-84ef-2e6ae0d4205d','','APPROVED','2017-09-20 23:05:59.148+00:00','','','{}','2017-09-20 23:05:59.148+00:00','-NA-','2017-09-20 23:05:59.151+00:00','-NA-','515211e9');
-INSERT INTO "kms_app_credential" VALUES('wxyz','515211e9','secret3','35608afe-2715-4064-bb4d-3cbb4e82c474','','APPROVED','2017-11-02 16:00:16.512+00:00','','','{}','2017-11-02 16:00:16.512+00:00','-NA-','2017-11-02 16:00:16.514+00:00','-NA-','515211e9');
+INSERT INTO "kms_app_credential" VALUES('abcd','515211e9','encrypted:secret1','408ad853-3fa0-402f-90ee-103de98d71a5','','APPROVED','2017-08-18 22:13:18.35+00:00','','','{}','2017-08-18 22:13:18.35+00:00','-NA-','2017-08-18 22:13:18.352+00:00','-NA-','515211e9');
+INSERT INTO "kms_app_credential" VALUES('dcba','515211e9','encrypted:secret2','ae053aee-f12d-4591-84ef-2e6ae0d4205d','','APPROVED','2017-09-20 23:05:59.148+00:00','','','{}','2017-09-20 23:05:59.148+00:00','-NA-','2017-09-20 23:05:59.151+00:00','-NA-','515211e9');
+INSERT INTO "kms_app_credential" VALUES('wxyz','515211e9','encrypted:secret3','35608afe-2715-4064-bb4d-3cbb4e82c474','','APPROVED','2017-11-02 16:00:16.512+00:00','','','{}','2017-11-02 16:00:16.512+00:00','-NA-','2017-11-02 16:00:16.514+00:00','-NA-','515211e9');
CREATE TABLE kms_app_credential_apiproduct_mapper (tenant_id text,appcred_id text,app_id text,apiprdt_id text,status text,_change_selector text, primary key (tenant_id,appcred_id,app_id,apiprdt_id));
INSERT INTO "kms_app_credential_apiproduct_mapper" VALUES('515211e9','abcd','408ad853-3fa0-402f-90ee-103de98d71a5','b7e0970c-4677-4b05-8105-5ea59fdcf4e7','APPROVED','515211e9');
INSERT INTO "kms_app_credential_apiproduct_mapper" VALUES('515211e9','dcba','ae053aee-f12d-4591-84ef-2e6ae0d4205d','db90a25a-15c8-42ad-96c1-63ed9682b5a9','APPROVED','515211e9');
diff --git a/accessEntity/interfaces.go b/accessEntity/interfaces.go
index ff28bf2..e12cb08 100644
--- a/accessEntity/interfaces.go
+++ b/accessEntity/interfaces.go
@@ -35,6 +35,6 @@
GetApiProductNames(id string, idType string) ([]string, error)
GetAppNames(id string, idType string) ([]string, error)
GetComNames(id string, idType string) ([]string, error)
- GetDevEmailByDevId(devId string) (string, error)
+ GetDevEmailByDevId(devId string, org string) (string, error)
GetStatus(id, t string) (string, error)
}
diff --git a/accessEntity/mock_test.go b/accessEntity/mock_test.go
index 5a30e27..2564f83 100644
--- a/accessEntity/mock_test.go
+++ b/accessEntity/mock_test.go
@@ -14,9 +14,30 @@
package accessEntity
import (
+ "github.com/apid/apid-core/cipher"
"github.com/apid/apidApiMetadata/common"
+ "strings"
)
+const dummyEncryptPrefix = "encrypted:"
+
+type DummyCipherMan struct {
+}
+
+func (c *DummyCipherMan) AddOrgs(orgs []string) {
+}
+
+func (d *DummyCipherMan) TryDecryptBase64(input string, org string) (string, error) {
+ if strings.HasPrefix(input, dummyEncryptPrefix) {
+ return input[len(dummyEncryptPrefix):], nil
+ }
+ return input, nil
+}
+
+func (d *DummyCipherMan) EncryptBase64(input string, org string, mode cipher.Mode, padding cipher.Padding) (string, error) {
+ return dummyEncryptPrefix + input, nil
+}
+
type DummyDbMan struct {
apiProducts []common.ApiProduct
apps []common.App
@@ -33,6 +54,10 @@
err error
}
+func (d *DummyDbMan) GetOrgs() (orgs []string, err error) {
+ return
+}
+
func (d *DummyDbMan) SetDbVersion(string) {
}
@@ -80,7 +105,7 @@
return d.comNames, d.err
}
-func (d *DummyDbMan) GetDevEmailByDevId(devId string) (string, error) {
+func (d *DummyDbMan) GetDevEmailByDevId(devId string, org string) (string, error) {
return d.email, d.err
}
diff --git a/common/cipher.go b/common/cipher.go
new file mode 100644
index 0000000..9235937
--- /dev/null
+++ b/common/cipher.go
@@ -0,0 +1,273 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package common
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "github.com/apid/apid-core/cipher"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+)
+
+const regEncrypted = `^\{[0-9A-Za-z]+/[0-9A-Za-z]+/[0-9A-Za-z]+\}.`
+const retrieveEncryptKeyPath = "/encryptionkey"
+const EncryptAes = "AES"
+
+const (
+ retrieveKeyRetryInterval = time.Duration(5 * time.Second)
+ retrieveKeyTimeout = time.Duration(5 * time.Minute)
+)
+const parameterOrganization = "organization"
+const configBearerToken = "apigeesync_bearer_token"
+const headerContentType = "Content-Type"
+const (
+ typeJson = "application/json"
+ typeXml = "text/xml"
+)
+const errorCodeNoKey = "organizations.EncryptionKeyDoesNotExist"
+
+var RegexpEncrypted = regexp.MustCompile(regEncrypted)
+
+func CreateCipherManager(client *http.Client, serverUrlBase string) *KmsCipherManager {
+ return &KmsCipherManager{
+ serverUrlBase: serverUrlBase,
+ aes: make(map[string]*cipher.AesCipher),
+ mutex: &sync.RWMutex{},
+ client: client,
+ interval: retrieveKeyRetryInterval,
+ timeout: retrieveKeyTimeout,
+ }
+}
+
+type KmsCipherManager struct {
+ serverUrlBase string
+ // org-level AesCipher map {organization: AesCipher}
+ aes map[string]*cipher.AesCipher
+ mutex *sync.RWMutex
+ client *http.Client
+ interval time.Duration
+ timeout time.Duration
+}
+
+func (c *KmsCipherManager) AddOrgs(orgs []string) {
+ for _, org := range orgs {
+ go c.startRetrieve(org, c.interval, c.timeout)
+ }
+}
+
+func (c *KmsCipherManager) startRetrieve(org string, interval time.Duration, timeout time.Duration) {
+ timeoutChan := time.After(timeout)
+ if err := c.retrieveKey(org); err != nil {
+ log.Error(err)
+ } else {
+ return
+ }
+ ticker := time.NewTicker(interval)
+ for {
+ select {
+ case <-timeoutChan:
+ log.Error("timeout when retrieving key")
+ return
+ case <-ticker.C:
+ if err := c.retrieveKey(org); err != nil {
+ log.Error(err)
+ } else {
+ return
+ }
+ }
+ }
+}
+
+func (c *KmsCipherManager) retrieveKey(org string) error {
+ var key []byte
+ req, err := http.NewRequest(http.MethodGet, c.serverUrlBase+retrieveEncryptKeyPath, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create retrieving key request for org=%s : %v", org, err)
+ }
+ pars := req.URL.Query()
+ pars[parameterOrganization] = []string{org}
+ req.URL.RawQuery = pars.Encode()
+ req.Header.Set("Authorization", "Bearer "+services.Config().GetString(configBearerToken))
+ log.Debugf("Retrieving key: %s", req.URL.String())
+ res, err := c.client.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve key for org=%s : %v", org, err)
+ }
+
+ // if 404
+ if res.StatusCode == http.StatusNotFound {
+ e, err := parseErrorResponse(res)
+ if err != nil {
+ log.Errorf("Failed to parse 404 error response for org %s: %v", org, err)
+ return err
+ }
+ // is this org has no key, stop retrying
+ if e.Code == errorCodeNoKey {
+ log.Debugf("No key is associated with org %v", org)
+ return nil
+ }
+ }
+
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("failed to retrieve key for org [%v] with status: %v", org, res.Status)
+ }
+
+ log.Debugf("Downloaded Encryption Key for org %s", org)
+ key64, err := ioutil.ReadAll(res.Body)
+ res.Body.Close()
+ if err != nil {
+ return fmt.Errorf("error reading encryption key: %v", err)
+ }
+ key, err = base64.StdEncoding.DecodeString(string(key64))
+ if err != nil {
+ return fmt.Errorf("error decoding encryption key: %v", err)
+ }
+ log.Debugf("Encryption Key successfully retrieved for org %s", org)
+ a, err := cipher.CreateAesCipher(key)
+ if err != nil {
+ return fmt.Errorf("CreateAesCipher error for org [%v] when CreateAesCipher: %v", org, err)
+ }
+ c.mutex.Lock()
+ c.aes[org] = a
+ c.mutex.Unlock()
+ return nil
+}
+
+// return val is nullable
+func (c *KmsCipherManager) getAesCipher(org string) *cipher.AesCipher {
+ // if exists
+ c.mutex.RLock()
+ if a := c.aes[org]; a != nil {
+ c.mutex.RUnlock()
+ return a
+ }
+ // if not exists
+ c.mutex.RUnlock()
+ if err := c.retrieveKey(org); err != nil {
+ log.Errorf("Failed to get encryption key for org=%s : %v", org, err)
+ return nil
+ }
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+ return c.aes[org]
+}
+
+// If input is encrypted, it decodes the input with base64,
+// and then decrypt it. Otherwise, original input is returned.
+// An encrypted input should be ciphertext prepended with algorithm. An unencrypted input can have any other format.
+// An example of encrypted input is "{AES/ECB/PKCS5Padding}2jX3V3dQ5xB9C9Zl9sqyo8pmkvVP10rkEVPVhmnLHw4=".
+func (c *KmsCipherManager) TryDecryptBase64(input string, org string) (output string, err error) {
+ if !IsEncrypted(input) {
+ output = input
+ return
+ }
+
+ text, mode, padding, err := GetCiphertext(input)
+ if err != nil {
+ log.Errorf("Get ciphertext of [%v] failed: [%v], considered as unencrypted!", input, err)
+ return
+ }
+ bytes, err := base64.StdEncoding.DecodeString(text)
+ if err != nil {
+ log.Errorf("Decode base64 of [%v] failed: [%v], considered as unencrypted!", text, err)
+ return
+ }
+ aes := c.getAesCipher(org)
+ if aes == nil {
+ err = fmt.Errorf("failed to get decryption key for org: %s", org)
+ return
+ }
+ plaintext, err := aes.Decrypt(bytes, mode, padding)
+ if err != nil {
+ log.Errorf("Decrypt of [%v] failed: [%v], considered as unencrypted!", bytes, err)
+ return
+ }
+ output = string(plaintext)
+ return
+}
+
+// It encrypts the input, and then encodes the ciphertext with base64.
+// The returned string is the base64 encoding of the encrypted input, prepended with algorithm.
+// An example output is "{AES/ECB/PKCS5Padding}2jX3V3dQ5xB9C9Zl9sqyo8pmkvVP10rkEVPVhmnLHw4="
+func (c *KmsCipherManager) EncryptBase64(input string, org string, mode cipher.Mode, padding cipher.Padding) (output string, err error) {
+ aes := c.getAesCipher(org)
+ // TODO: make sure this logic is expected
+ // if failed to get key and cipher, considered this org as unencrypted
+ if aes == nil {
+ return input, nil
+ }
+ ciphertext, err := aes.Encrypt([]byte(input), mode, padding)
+ if err != nil {
+ return
+ }
+ output = fmt.Sprintf("{%s/%s/%s}%s", EncryptAes, mode, padding, base64.StdEncoding.EncodeToString(ciphertext))
+ return
+}
+
+func IsEncrypted(input string) (encrypted bool) {
+ return RegexpEncrypted.Match([]byte(input))
+}
+
+func GetCiphertext(input string) (ciphertext string, mode cipher.Mode, padding cipher.Padding, err error) {
+ list := strings.SplitN(input, "}", 2)
+ if len(list) != 2 {
+ err = fmt.Errorf("invalid input for GetCiphertext: %v", input)
+ return
+ }
+ ciphertext = list[1]
+ list = strings.Split(strings.TrimLeft(list[0], "{"), "/")
+ if len(list) != 3 {
+ err = fmt.Errorf("invalid input for GetCiphertext: %v", input)
+ return
+ }
+ // encryption algorithm
+ if list[0] != EncryptAes {
+ err = fmt.Errorf("unsupported algorithm for GetCiphertext: %v", list[0])
+ return
+ }
+ // mode
+ mode = cipher.Mode(list[1])
+ // padding
+ padding = cipher.Padding(list[2])
+ return
+}
+
+type KeyErrorResponse struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+}
+
+func parseErrorResponse(res *http.Response) (*KeyErrorResponse, error) {
+ contentType := res.Header.Get(headerContentType)
+ defer res.Body.Close()
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+ ret := &KeyErrorResponse{}
+ if contentType == typeJson {
+ return ret, json.Unmarshal(body, ret)
+ } else if contentType == typeXml {
+ return ret, xml.Unmarshal(body, ret)
+ } else {
+ return nil, fmt.Errorf("unknown error: %v", string(body))
+ }
+}
diff --git a/common/cipher_test.go b/common/cipher_test.go
new file mode 100644
index 0000000..71ff7d4
--- /dev/null
+++ b/common/cipher_test.go
@@ -0,0 +1,233 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package common
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "github.com/apid/apid-core/cipher"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "net/http"
+ "net/http/httptest"
+ "time"
+)
+
+var _ = Describe("Cipher Test", func() {
+ var testCipherMan *KmsCipherManager
+ var testCount int
+ var testOrg string
+ plaingtext := "aUWQKgAwmaR0p2kY"
+ cipher64 := "{AES/ECB/PKCS5Padding}2jX3V3dQ5xB9C9Zl9sqyo8pmkvVP10rkEVPVhmnLHw4="
+ key := []byte{2, 122, 212, 83, 150, 164, 180, 4, 148, 242, 65, 189, 3, 188, 76, 247}
+ BeforeEach(func() {
+ testCount++
+ testOrg = fmt.Sprintf("org%d", testCount)
+ })
+
+ Context("Encryption/Decryption", func() {
+ BeforeEach(func() {
+ testCipherMan = CreateCipherManager(nil, "")
+ // set key locally
+ var err error
+ testCipherMan.aes[testOrg], err = cipher.CreateAesCipher(key)
+ Expect(err).Should(Succeed())
+ })
+
+ It("Encryption", func() {
+ Expect(testCipherMan.EncryptBase64(plaingtext, testOrg, cipher.ModeEcb, cipher.PaddingPKCS5)).
+ Should(Equal(cipher64))
+ })
+
+ It("Decryption", func() {
+ Expect(testCipherMan.TryDecryptBase64(cipher64, testOrg)).Should(Equal(plaingtext))
+ })
+
+ It("Try to decrypt unencrypted input", func() {
+ Expect(testCipherMan.TryDecryptBase64(plaingtext, testOrg)).Should(Equal(plaingtext))
+ })
+ })
+
+ Context("Retrieve new key", func() {
+ var server *httptest.Server
+ Context("Retrieve new key with lazy method", func() {
+ BeforeEach(func() {
+ // set key server
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer GinkgoRecover()
+ Expect(r.URL.Path).Should(Equal(retrieveEncryptKeyPath))
+ Expect(r.URL.Query().Get(parameterOrganization)).Should(Equal(testOrg))
+ Expect(w.Write([]byte(base64.StdEncoding.EncodeToString(key)))).Should(Equal(24))
+ }))
+ time.Sleep(100 * time.Millisecond)
+ testCipherMan = CreateCipherManager(&http.Client{}, server.URL)
+ })
+
+ AfterEach(func() {
+ server.Close()
+ })
+
+ It("Encryption", func() {
+ Expect(testCipherMan.EncryptBase64(plaingtext, testOrg, cipher.ModeEcb, cipher.PaddingPKCS5)).
+ Should(Equal(cipher64))
+ })
+
+ It("Decryption", func() {
+ Expect(testCipherMan.TryDecryptBase64(cipher64, testOrg)).Should(Equal(plaingtext))
+ })
+ })
+
+ Context("Retrieve new keys during initialization", func() {
+
+ AfterEach(func() {
+ server.Close()
+ })
+
+ It("Retrieve Key happy path", func() {
+ // set key server
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer GinkgoRecover()
+ Expect(r.URL.Path).Should(Equal(retrieveEncryptKeyPath))
+ Expect(r.URL.Query().Get(parameterOrganization)).Should(HavePrefix(testOrg))
+ Expect(w.Write([]byte(base64.StdEncoding.EncodeToString(key)))).Should(Equal(24))
+ }))
+ time.Sleep(100 * time.Millisecond)
+ testCipherMan = CreateCipherManager(&http.Client{}, server.URL)
+
+ //test 2 orgs
+ testOrg1 := testOrg + "_1"
+ testCipherMan.AddOrgs([]string{testOrg, testOrg1})
+ for {
+ time.Sleep(100 * time.Millisecond)
+ testCipherMan.mutex.RLock()
+ l := len(testCipherMan.aes)
+ testCipherMan.mutex.RUnlock()
+ if l == 2 {
+ //close server to make sure key was retrieved by "AddOrgs"
+ server.Close()
+ Expect(testCipherMan.EncryptBase64(plaingtext, testOrg, cipher.ModeEcb, cipher.PaddingPKCS5)).
+ Should(Equal(cipher64))
+ Expect(testCipherMan.EncryptBase64(plaingtext, testOrg1, cipher.ModeEcb, cipher.PaddingPKCS5)).
+ Should(Equal(cipher64))
+ return
+ }
+ }
+ }, 2)
+
+ It("Retrieve Key should retry for internal server error", func() {
+ // set key server
+ count := 0
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer GinkgoRecover()
+ Expect(r.URL.Path).Should(Equal(retrieveEncryptKeyPath))
+ Expect(r.URL.Query().Get(parameterOrganization)).Should(Equal(testOrg))
+ count++
+ if count == 1 {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ if count == 2 {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ Expect(w.Write([]byte(base64.StdEncoding.EncodeToString(key)))).Should(Equal(24))
+ }))
+ time.Sleep(100 * time.Millisecond)
+ testCipherMan = CreateCipherManager(&http.Client{}, server.URL)
+ testCipherMan.interval = 100 * time.Millisecond
+ //should retry in case of error
+ testCipherMan.AddOrgs([]string{testOrg})
+ for {
+ time.Sleep(100 * time.Millisecond)
+ testCipherMan.mutex.RLock()
+ aes := testCipherMan.aes[testOrg]
+ testCipherMan.mutex.RUnlock()
+ if aes != nil {
+ //close server to make sure key was retrieved by "AddOrgs"
+ server.Close()
+ Expect(testCipherMan.EncryptBase64(plaingtext, testOrg, cipher.ModeEcb, cipher.PaddingPKCS5)).
+ Should(Equal(cipher64))
+ return
+ }
+ }
+ }, 2)
+
+ It("Retrieve Key should stop retrying for JSON organizations.EncryptionKeyDoesNotExist", func() {
+ // set key server
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer GinkgoRecover()
+ Expect(r.URL.Path).Should(Equal(retrieveEncryptKeyPath))
+ Expect(r.URL.Query().Get(parameterOrganization)).Should(Equal(testOrg))
+
+ response := KeyErrorResponse{
+ Code: errorCodeNoKey,
+ Message: fmt.Sprintf("Encryption key does not exist for the org [%s].", testOrg),
+ }
+ bytes, err := json.Marshal(response)
+ Expect(err).Should(Succeed())
+ w.Header().Set(headerContentType, typeJson)
+ w.WriteHeader(http.StatusNotFound)
+ Expect(w.Write(bytes)).Should(Equal(len(bytes)))
+ }))
+ time.Sleep(100 * time.Millisecond)
+ testCipherMan = CreateCipherManager(&http.Client{}, server.URL)
+ //should stop retrying after one try
+ testCipherMan.startRetrieve(testOrg, 100*time.Millisecond, 10*time.Minute)
+ }, 2)
+
+ It("Retrieve Key should stop retrying for XML organizations.EncryptionKeyDoesNotExist", func() {
+ // set key server
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer GinkgoRecover()
+ Expect(r.URL.Path).Should(Equal(retrieveEncryptKeyPath))
+ Expect(r.URL.Query().Get(parameterOrganization)).Should(Equal(testOrg))
+
+ response := KeyErrorResponse{
+ Code: errorCodeNoKey,
+ Message: fmt.Sprintf("Encryption key does not exist for the org [%s].", testOrg),
+ }
+ bytes, err := xml.Marshal(response)
+ Expect(err).Should(Succeed())
+ w.Header().Set(headerContentType, typeXml)
+ w.WriteHeader(http.StatusNotFound)
+ Expect(w.Write(bytes)).Should(Equal(len(bytes)))
+ }))
+ time.Sleep(100 * time.Millisecond)
+ testCipherMan = CreateCipherManager(&http.Client{}, server.URL)
+ //should stop retrying after one try
+ testCipherMan.startRetrieve(testOrg, 100*time.Millisecond, 10*time.Minute)
+ }, 2)
+ })
+
+ })
+
+ Context("IsEncrypted", func() {
+ It("IsEncrypted", func() {
+ testData := [][]interface{}{
+ {"{AES/ECB/PKCS5Padding}foo", true},
+ {"AES/ECB/PKCS5Padding}foo", false},
+ {"{AES/ECB/PKCS5Paddingfoo", false},
+ {"{AES/ECB/}foo", false},
+ {"{AES/PKCS5Padding}foo", false},
+ {"{AES//PKCS5Padding}foo", false},
+ {"foo", false},
+ }
+ for i := range testData {
+ Expect(IsEncrypted(testData[i][0].(string))).Should(Equal(testData[i][1]))
+ }
+ })
+ })
+})
diff --git a/common/common_suite_test.go b/common/common_suite_test.go
new file mode 100644
index 0000000..724b2b5
--- /dev/null
+++ b/common/common_suite_test.go
@@ -0,0 +1,30 @@
+package common
+
+import (
+ "github.com/apid/apid-core"
+ "github.com/apid/apid-core/factory"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+var testTempDirBase string
+
+var _ = BeforeSuite(func() {
+ apid.Initialize(factory.DefaultServicesFactory())
+ SetApidServices(apid.AllServices(), apid.Log().ForModule("apidApiMetadata"))
+ var err error
+ testTempDirBase, err = ioutil.TempDir("", "verify_apikey_")
+ Expect(err).Should(Succeed())
+})
+
+func TestCommon(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "ApiMetadata Common Suite")
+}
+
+var _ = AfterSuite(func() {
+ Expect(os.RemoveAll(testTempDirBase)).Should(Succeed())
+})
diff --git a/common/data.go b/common/data.go
index a592f97..f7c9ba6 100644
--- a/common/data.go
+++ b/common/data.go
@@ -23,10 +23,11 @@
)
type DbManager struct {
- Data apid.DataService
- Db apid.DB
- DbMux sync.RWMutex
- dbVersion string
+ Data apid.DataService
+ Db apid.DB
+ DbMux sync.RWMutex
+ CipherManager CipherManagerInterface
+ dbVersion string
}
const (
@@ -96,6 +97,26 @@
return mapOfAttributes
}
+func (dbc *DbManager) GetOrgs() (orgs []string, err error) {
+ db := dbc.GetDb()
+ rows, err := db.Query(`SELECT DISTINCT org FROM edgex_data_scope`)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ for rows.Next() {
+ var tmp sql.NullString
+ if err = rows.Scan(&tmp); err != nil {
+ return nil, err
+ }
+ if tmp.Valid {
+ orgs = append(orgs, tmp.String)
+ }
+ }
+ err = rows.Err()
+ return
+}
+
func JsonToStringArray(fjson string) []string {
var array []string
if err := json.Unmarshal([]byte(fjson), &array); err == nil {
diff --git a/common/data_test.go b/common/data_test.go
new file mode 100644
index 0000000..7070890
--- /dev/null
+++ b/common/data_test.go
@@ -0,0 +1,102 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package common
+
+import (
+ "github.com/apid/apid-core"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "io/ioutil"
+ "reflect"
+ "sort"
+ "sync"
+)
+
+const fileDataTest = "data_test.sql"
+
+var _ = Describe("DataTest", func() {
+
+ Context("DB", func() {
+ var dataTestTempDir string
+ var testDbMan *DbManager
+ BeforeEach(func() {
+ var err error
+ dataTestTempDir, err = ioutil.TempDir(testTempDirBase, "sqlite3")
+ Expect(err).NotTo(HaveOccurred())
+ services.Config().Set("local_storage_path", dataTestTempDir)
+
+ testDbMan = &DbManager{
+ Data: services.Data(),
+ DbMux: sync.RWMutex{},
+ }
+ testDbMan.SetDbVersion(dataTestTempDir)
+ Expect(testDbMan.GetDbVersion()).Should(Equal(dataTestTempDir))
+ setupTestDb(testDbMan.GetDb())
+ })
+
+ It("should get kms attributes", func() {
+ attributes := testDbMan.GetKmsAttributes("bc811169", "40753e12-a50a-429d-9121-e571eb4e43a9", "85629786-37c5-4e8c-bb45-208f3360d005", "50321842-d6ee-4e92-91b9-37234a7920c1", "test-invalid")
+ Expect(len(attributes)).Should(BeEquivalentTo(3))
+ Expect(len(attributes["40753e12-a50a-429d-9121-e571eb4e43a9"])).Should(BeEquivalentTo(1))
+ Expect(len(attributes["85629786-37c5-4e8c-bb45-208f3360d005"])).Should(BeEquivalentTo(2))
+ Expect(len(attributes["50321842-d6ee-4e92-91b9-37234a7920c1"])).Should(BeEquivalentTo(5))
+ Expect(len(attributes["test-invalid"])).Should(BeEquivalentTo(0))
+ })
+
+ It("Should get all orgs", func() {
+ orgs, err := testDbMan.GetOrgs()
+ Expect(err).Should(Succeed())
+ sort.Strings(orgs)
+ Expect(orgs).Should(Equal([]string{"apid-haoming", "apid-test"}))
+ })
+
+ })
+
+ Context("Validate common.JsonToStringArray", func() {
+
+ It("should transform simple valid json", func() {
+ array := JsonToStringArray("[\"test-1\", \"test-2\"]")
+ Expect(reflect.DeepEqual(array, []string{"test-1", "test-2"})).Should(BeTrue())
+ })
+ It("should transform simple single valid json", func() {
+ array := JsonToStringArray("[\"test-1\"]")
+ Expect(reflect.DeepEqual(array, []string{"test-1"})).Should(BeTrue())
+ })
+ It("should transform simple fake json", func() {
+ s := JsonToStringArray("{test-1,test-2}")
+ Expect(reflect.DeepEqual(s, []string{"test-1", "test-2"})).Should(BeTrue())
+ })
+ It("should transform simple single valued fake json", func() {
+ s := JsonToStringArray("{test-1}")
+ Expect(reflect.DeepEqual(s, []string{"test-1"})).Should(BeTrue())
+ })
+ It("space between fields considered as valid char", func() {
+ s := JsonToStringArray("{test-1, test-2}")
+ Expect(reflect.DeepEqual(s, []string{"test-1", " test-2"})).Should(BeTrue())
+ })
+ It("remove only last braces", func() {
+ s := JsonToStringArray("{test-1,test-2}}")
+ Expect(reflect.DeepEqual(s, []string{"test-1", "test-2}"})).Should(BeTrue())
+ })
+
+ })
+})
+
+func setupTestDb(db apid.DB) {
+ bytes, err := ioutil.ReadFile(fileDataTest)
+ Expect(err).Should(Succeed())
+ query := string(bytes)
+ _, err = db.Exec(query)
+ Expect(err).Should(Succeed())
+}
diff --git a/common/data_test.sql b/common/data_test.sql
new file mode 100644
index 0000000..1b5349b
--- /dev/null
+++ b/common/data_test.sql
@@ -0,0 +1,32 @@
+-- Copyright 2017 Google Inc.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE kms_attributes (tenant_id text,entity_id text,cust_id text,org_id text,dev_id text,comp_id text,apiprdt_id text,app_id text,appcred_id text,name text,type text,value text,_change_selector text, primary key (tenant_id,entity_id,name,type));
+INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','RateLimit','APIPRODUCT','RX100','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','85629786-37c5-4e8c-bb45-208f3360d005','','85629786-37c5-4e8c-bb45-208f3360d005','','','','','','features.isEdgexEnabled','ORGANIZATION','true','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','85629786-37c5-4e8c-bb45-208f3360d005','','85629786-37c5-4e8c-bb45-208f3360d005','','','','','','features.isCpsEnabled','ORGANIZATION','true','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','developer.quota.limit','APIPRODUCT','100','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','developer.quota.interval','APIPRODUCT','10','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','developer.quota.timeunit','APIPRODUCT','minute','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','Threshold','APIPRODUCT','TX100','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','40753e12-a50a-429d-9121-e571eb4e43a9','','','','','40753e12-a50a-429d-9121-e571eb4e43a9','','','access','APIPRODUCT','public','bc811169');
+INSERT INTO "kms_attributes" VALUES('bc811169','2d373ed6-e38f-453b-bb34-6d731d9c4815','','','','','','2d373ed6-e38f-453b-bb34-6d731d9c4815','','DisplayName','APP','demo-app','bc811169');
+CREATE TABLE kms_organization (id text,name text,display_name text,type text,tenant_id text,customer_id text,description text,created_at blob,created_by text,updated_at blob,updated_by text,_change_selector text, primary key (id,tenant_id));
+INSERT INTO "kms_organization" VALUES('e2cc4caf-40d6-4ecb-8149-ed32d04184b2','apid-haoming','apid-haoming','paid','515211e9','94cd5075-7f33-4afb-9545-a53a254277a1','','2017-08-16 22:16:06.544+00:00','foobar@google.com','2017-08-16 22:29:23.046+00:00','foobar@google.com','515211e9');
+CREATE TABLE edgex_data_scope (id text,apid_cluster_id text,scope text,org text,env text,created blob,created_by text,updated blob,updated_by text,_change_selector text,org_scope text,env_scope text, primary key (id));
+INSERT INTO "edgex_data_scope" VALUES('cc066263-6355-416d-9d59-7f3135d64953','543230f1-8c41-4bf5-94a3-f10c104ff5d4','155211e9','apid-haoming','test','2017-08-27 22:53:33.859+00:00','foobar@google.com','2017-08-27 22:53:33.859+00:00','foobar@google.com','543230f1-8c41-4bf5-94a3-f10c104ff5d4','12344caf-40d6-4ecb-8149-ed32d04184b2','1234203e-ba88-4cd5-967d-4caa88f64909');
+INSERT INTO "edgex_data_scope" VALUES('08c81eeb-57ec-43fe-8fed-cdff5494406f','123430f1-8c41-4bf5-94a3-f10c104ff5d4','165211e9','apid-test','prod','2017-08-29 02:39:34.093+00:00','foobar@google.com','2017-08-29 02:39:34.093+00:00','foobar@google.com','123430f1-8c41-4bf5-94a3-f10c104ff5d4','43214caf-40d6-4ecb-8149-ed32d04184b2','43211cae-f2a6-4663-9f36-eb17d76e6c32');
+COMMIT;
diff --git a/common/interfaces.go b/common/interfaces.go
index 59249b5..859e4bc 100644
--- a/common/interfaces.go
+++ b/common/interfaces.go
@@ -13,6 +13,8 @@
// limitations under the License.
package common
+import "github.com/apid/apid-core/cipher"
+
type ApiManagerInterface interface {
InitAPI()
}
@@ -21,4 +23,18 @@
SetDbVersion(string)
GetDbVersion() string
GetKmsAttributes(tenantId string, entities ...string) map[string][]Attribute
+ GetOrgs() (orgs []string, err error)
+}
+
+type CipherManagerInterface interface {
+ AddOrgs(orgs []string)
+ // If input is encrypted, it decodes the input with base64,
+ // and then decrypt it. Otherwise, original input is returned.
+ // An encrypted input should be ciphertext prepended with algorithm. An unencrypted input can have any other format.
+ // An example input is "{AES/ECB/PKCS5Padding}2jX3V3dQ5xB9C9Zl9sqyo8pmkvVP10rkEVPVhmnLHw4=".
+ TryDecryptBase64(input string, org string) (output string, err error)
+ // It encrypts the input, and then encodes the ciphertext with base64.
+ // The returned string is the base64 encoding of the encrypted input, prepended with algorithm.
+ // An example output is "{AES/ECB/PKCS5Padding}2jX3V3dQ5xB9C9Zl9sqyo8pmkvVP10rkEVPVhmnLHw4="
+ EncryptBase64(input string, org string, mode cipher.Mode, padding cipher.Padding) (output string, err error)
}
diff --git a/glide.yaml b/glide.yaml
index abe2d4a..9e5bcb4 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -15,7 +15,7 @@
package: github.com/apid/apidApiMetadata
import:
- package: github.com/apid/apid-core
- version: master
+ version: ISSUE-67869881
- package: github.com/apid/apidApigeeSync
version: master
testImport:
diff --git a/init.go b/init.go
index 158a948..8083500 100644
--- a/init.go
+++ b/init.go
@@ -16,10 +16,20 @@
import (
"github.com/apid/apid-core"
+ "github.com/apid/apid-core/util"
"github.com/apid/apidApiMetadata/accessEntity"
"github.com/apid/apidApiMetadata/common"
"github.com/apid/apidApiMetadata/verifyApiKey"
+ "net/http"
"sync"
+ "time"
+)
+
+const (
+ maxIdleConnsPerHost = 1
+ httpTimeout = 5 * time.Minute
+ configBearerToken = "apigeesync_bearer_token"
+ configRetrieveEncKeyBase = "apimetadata_encryption_key_server_base"
)
var (
@@ -44,11 +54,30 @@
return common.PluginData, nil
}
-func initManagers(services apid.Services) apigeeSyncHandler {
+// init http client
+func createHttpClient() *http.Client {
+ tr := util.Transport(services.Config().GetString(util.ConfigfwdProxyPortURL))
+ tr.MaxIdleConnsPerHost = maxIdleConnsPerHost
+ client := &http.Client{
+ Transport: tr,
+ Timeout: httpTimeout,
+ CheckRedirect: func(req *http.Request, _ []*http.Request) error {
+ req.Header.Set("Authorization", "Bearer "+services.Config().GetString(configBearerToken))
+ return nil
+ },
+ }
+ return client
+}
+
+func initManagers(services apid.Services) *apigeeSyncHandler {
+
+ cipherMan := common.CreateCipherManager(createHttpClient(), services.Config().GetString(configRetrieveEncKeyBase))
+
verifyDbMan := &verifyApiKey.DbManager{
DbManager: common.DbManager{
- Data: services.Data(),
- DbMux: sync.RWMutex{},
+ Data: services.Data(),
+ DbMux: sync.RWMutex{},
+ CipherManager: cipherMan,
},
}
verifyApiMan := &verifyApiKey.ApiManager{
@@ -58,8 +87,9 @@
entityDbMan := &accessEntity.DbManager{
DbManager: common.DbManager{
- Data: services.Data(),
- DbMux: sync.RWMutex{},
+ Data: services.Data(),
+ DbMux: sync.RWMutex{},
+ CipherManager: cipherMan,
},
}
@@ -68,9 +98,10 @@
AccessEntityPath: accessEntity.AccessEntityPath,
}
- syncHandler := apigeeSyncHandler{
- dbMans: []common.DbManagerInterface{verifyDbMan, entityDbMan},
- apiMans: []common.ApiManagerInterface{verifyApiMan, entityApiMan},
+ syncHandler := &apigeeSyncHandler{
+ dbMans: []common.DbManagerInterface{verifyDbMan, entityDbMan},
+ apiMans: []common.ApiManagerInterface{verifyApiMan, entityApiMan},
+ cipherMan: cipherMan,
}
syncHandler.initListener(services)
return syncHandler
diff --git a/listener.go b/listener.go
index 9f6e7e2..4253cfc 100644
--- a/listener.go
+++ b/listener.go
@@ -25,8 +25,9 @@
)
type apigeeSyncHandler struct {
- dbMans []common.DbManagerInterface
- apiMans []common.ApiManagerInterface
+ dbMans []common.DbManagerInterface
+ apiMans []common.ApiManagerInterface
+ cipherMan common.CipherManagerInterface
}
func (h *apigeeSyncHandler) initListener(services apid.Services) {
@@ -43,6 +44,12 @@
for _, dbMan := range h.dbMans {
dbMan.SetDbVersion(snapshot.SnapshotInfo)
}
+ // retrieve encryption keys
+ orgs, err := h.dbMans[0].GetOrgs()
+ if err != nil {
+ log.Panicf("Failed to get orgs: %v", err)
+ }
+ h.cipherMan.AddOrgs(orgs)
// idempotent init api for all packages
for _, apiMan := range h.apiMans {
apiMan.InitAPI()
@@ -54,7 +61,7 @@
if snapData, ok := e.(*tran.Snapshot); ok {
h.processSnapshot(snapData)
- } else {
+ } else { //TODO handle changelist and retrieve key for new orgs
log.Debugf("Received event. No action required for apiMetadata plugin. Ignoring. %v", e)
}
}
diff --git a/listener_test.go b/listener_test.go
index 3b2a77a..27756fd 100644
--- a/listener_test.go
+++ b/listener_test.go
@@ -17,6 +17,7 @@
import (
"github.com/apid/apid-core"
"github.com/apid/apid-core/factory"
+ "github.com/apid/apidApiMetadata/common"
tran "github.com/apigee-labs/transicator/common"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -26,7 +27,7 @@
var _ = Describe("listener", func() {
- var listnerTestSyncHandler apigeeSyncHandler
+ var listenerTestSyncHandler *apigeeSyncHandler
var listnerTestTempDir string
var _ = BeforeEach(func() {
var err error
@@ -38,7 +39,12 @@
Expect(err).NotTo(HaveOccurred())
apid.InitializePlugins("")
- listnerTestSyncHandler = initManagers(s)
+ listenerTestSyncHandler = &apigeeSyncHandler{
+ dbMans: []common.DbManagerInterface{&DummyDbMan{}, &DummyDbMan{}},
+ apiMans: []common.ApiManagerInterface{},
+ cipherMan: &DummyCipherMan{},
+ }
+ listenerTestSyncHandler.initListener(services)
})
var _ = AfterEach(func() {
@@ -52,22 +58,22 @@
SnapshotInfo: "test_snapshot",
Tables: []tran.Table{},
}
- listnerTestSyncHandler.Handle(s)
- for _, dbMan := range listnerTestSyncHandler.dbMans {
+ listenerTestSyncHandler.Handle(s)
+ for _, dbMan := range listenerTestSyncHandler.dbMans {
Expect(dbMan.GetDbVersion()).Should(BeEquivalentTo(s.SnapshotInfo))
}
})
- It("should not change version for chang event", func() {
+ It("should not change version for change event", func() {
- version := listnerTestSyncHandler.dbMans[0].GetDbVersion()
+ version := listenerTestSyncHandler.dbMans[0].GetDbVersion()
s := &tran.Change{
ChangeSequence: 12321,
Table: "",
}
- listnerTestSyncHandler.Handle(s)
- for _, dbMan := range listnerTestSyncHandler.dbMans {
+ listenerTestSyncHandler.Handle(s)
+ for _, dbMan := range listenerTestSyncHandler.dbMans {
Expect(dbMan.GetDbVersion() == version).Should(BeTrue())
}
diff --git a/mock_test.go b/mock_test.go
new file mode 100644
index 0000000..525edff
--- /dev/null
+++ b/mock_test.go
@@ -0,0 +1,52 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package apidApiMetadata
+
+import (
+ "github.com/apid/apid-core/cipher"
+ "github.com/apid/apidApiMetadata/common"
+)
+
+type DummyDbMan struct {
+ version string
+}
+
+func (d *DummyDbMan) GetOrgs() (orgs []string, err error) {
+ return
+}
+
+func (d *DummyDbMan) SetDbVersion(v string) {
+ d.version = v
+}
+func (d *DummyDbMan) GetDbVersion() string {
+ return d.version
+}
+
+func (d *DummyDbMan) GetKmsAttributes(tenantId string, entities ...string) map[string][]common.Attribute {
+ return nil
+}
+
+type DummyCipherMan struct {
+}
+
+func (c *DummyCipherMan) AddOrgs(orgs []string) {
+}
+
+func (d *DummyCipherMan) TryDecryptBase64(input string, org string) (string, error) {
+ return input, nil
+}
+
+func (d *DummyCipherMan) EncryptBase64(input string, org string, mode cipher.Mode, padding cipher.Padding) (string, error) {
+ return input, nil
+}
diff --git a/verifyApiKey/api.go b/verifyApiKey/api.go
index e06c58c..e6fcb32 100644
--- a/verifyApiKey/api.go
+++ b/verifyApiKey/api.go
@@ -16,6 +16,7 @@
import (
"encoding/json"
+ "github.com/apid/apid-core/util"
"github.com/apid/apidApiMetadata/common"
"io"
"io/ioutil"
@@ -173,8 +174,8 @@
for _, apiProd := range details {
if len(apiProd.Resources) == 0 || validatePath(apiProd.Resources, verifyApiKeyReq.UriPath) {
- if len(apiProd.Apiproxies) == 0 || contains(apiProd.Apiproxies, verifyApiKeyReq.ApiProxyName) {
- if len(apiProd.Environments) == 0 || contains(apiProd.Environments, verifyApiKeyReq.EnvironmentName) {
+ if len(apiProd.Apiproxies) == 0 || util.Contains(apiProd.Apiproxies, verifyApiKeyReq.ApiProxyName) {
+ if len(apiProd.Environments) == 0 || util.Contains(apiProd.Environments, verifyApiKeyReq.EnvironmentName) {
bestMathcedProduct = apiProd
return bestMathcedProduct
// set rank 1 or just return
@@ -254,7 +255,7 @@
return &ee
}
- if verifyApiKeyReq.ValidateAgainstApiProxiesAndEnvs && (len(apiProductDetails.Apiproxies) > 0 && !contains(apiProductDetails.Apiproxies, verifyApiKeyReq.ApiProxyName)) {
+ if verifyApiKeyReq.ValidateAgainstApiProxiesAndEnvs && (len(apiProductDetails.Apiproxies) > 0 && !util.Contains(apiProductDetails.Apiproxies, verifyApiKeyReq.ApiProxyName)) {
reason = "Proxy Validation Failed (" + strings.Join(apiProductDetails.Apiproxies, ", ") + " vs " + verifyApiKeyReq.ApiProxyName + ")"
errorCode = "oauth.v2.InvalidApiKeyForGivenResource"
log.Debug("Validation error occoured ", errorCode, " ", reason)
@@ -262,7 +263,7 @@
return &ee
}
/* Verify if the ENV matches */
- if verifyApiKeyReq.ValidateAgainstApiProxiesAndEnvs && (len(apiProductDetails.Environments) > 0 && !contains(apiProductDetails.Environments, verifyApiKeyReq.EnvironmentName)) {
+ if verifyApiKeyReq.ValidateAgainstApiProxiesAndEnvs && (len(apiProductDetails.Environments) > 0 && !util.Contains(apiProductDetails.Environments, verifyApiKeyReq.EnvironmentName)) {
reason = "ENV Validation Failed (" + strings.Join(apiProductDetails.Environments, ", ") + " vs " + verifyApiKeyReq.EnvironmentName + ")"
errorCode = "oauth.v2.InvalidApiKeyForGivenResource"
log.Debug("Validation error occoured ", errorCode, " ", reason)
diff --git a/verifyApiKey/api_test.go b/verifyApiKey/api_test.go
index 9e1795c..9cce413 100644
--- a/verifyApiKey/api_test.go
+++ b/verifyApiKey/api_test.go
@@ -59,8 +59,9 @@
dbMan = &DbManager{
DbManager: common.DbManager{
- Data: serviceFactoryForTest.Data(),
- DbMux: sync.RWMutex{},
+ Data: serviceFactoryForTest.Data(),
+ DbMux: sync.RWMutex{},
+ CipherManager: &DummyCipherMan{},
},
}
dbMan.SetDbVersion(dataTestTempDir)
diff --git a/verifyApiKey/data.go b/verifyApiKey/data.go
index 8e70faf..a5d3708 100644
--- a/verifyApiKey/data.go
+++ b/verifyApiKey/data.go
@@ -68,6 +68,13 @@
return errors.New("InvalidApiKey")
}
+ secret, err := dbc.CipherManager.TryDecryptBase64(dataWrapper.verifyApiKeySuccessResponse.ClientId.ClientSecret,
+ dataWrapper.verifyApiKeyRequest.OrganizationName)
+ if err != nil {
+ return err
+ }
+ dataWrapper.verifyApiKeySuccessResponse.ClientId.ClientSecret = secret
+
if dataWrapper.verifyApiKeySuccessResponse.App.CallbackUrl != "" {
dataWrapper.verifyApiKeySuccessResponse.ClientId.RedirectURIs = []string{dataWrapper.verifyApiKeySuccessResponse.App.CallbackUrl}
}
diff --git a/verifyApiKey/data_helper_test.go b/verifyApiKey/data_helper_test.go
index 74c19a5..57951e9 100644
--- a/verifyApiKey/data_helper_test.go
+++ b/verifyApiKey/data_helper_test.go
@@ -116,47 +116,3 @@
Expect(err).NotTo(HaveOccurred())
Expect(tx.Commit()).NotTo(HaveOccurred())
}
-
-func setupKmsAttributesdata(db apid.DB) {
- tx, err := db.Begin()
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`CREATE TABLE kms_attributes (tenant_id text,entity_id text,cust_id text,org_id text,dev_id text,comp_id text,apiprdt_id text,app_id text,appcred_id text,name text,type text,value text,_change_selector text, primary key (tenant_id,entity_id,name,type));`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','RateLimit','APIPRODUCT','RX100','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','85629786-37c5-4e8c-bb45-208f3360d005','','85629786-37c5-4e8c-bb45-208f3360d005','','','','','','features.isEdgexEnabled','ORGANIZATION','true','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','85629786-37c5-4e8c-bb45-208f3360d005','','85629786-37c5-4e8c-bb45-208f3360d005','','','','','','features.isCpsEnabled','ORGANIZATION','true','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','developer.quota.limit','APIPRODUCT','100','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','developer.quota.interval','APIPRODUCT','10','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','developer.quota.timeunit','APIPRODUCT','minute','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','50321842-d6ee-4e92-91b9-37234a7920c1','','','','','50321842-d6ee-4e92-91b9-37234a7920c1','','','Threshold','APIPRODUCT','TX100','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','40753e12-a50a-429d-9121-e571eb4e43a9','','','','','40753e12-a50a-429d-9121-e571eb4e43a9','','','access','APIPRODUCT','public','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(``)
- Expect(err).NotTo(HaveOccurred())
- _, err = tx.Exec(`INSERT INTO "kms_attributes" VALUES('bc811169','2d373ed6-e38f-453b-bb34-6d731d9c4815','','','','','','2d373ed6-e38f-453b-bb34-6d731d9c4815','','DisplayName','APP','demo-app','bc811169');`)
- Expect(err).NotTo(HaveOccurred())
- Expect(tx.Commit()).NotTo(HaveOccurred())
-}
diff --git a/verifyApiKey/data_test.go b/verifyApiKey/data_test.go
index d81914a..54d920b 100644
--- a/verifyApiKey/data_test.go
+++ b/verifyApiKey/data_test.go
@@ -40,8 +40,9 @@
dbMan = &DbManager{
DbManager: common.DbManager{
- Data: s.Data(),
- DbMux: sync.RWMutex{},
+ Data: s.Data(),
+ DbMux: sync.RWMutex{},
+ CipherManager: &DummyCipherMan{},
},
}
dbMan.SetDbVersion(dataTestTempDir)
@@ -183,18 +184,5 @@
Expect(len(apiProducts)).Should(BeEquivalentTo(0))
})
-
- It("should get kms attributes", func() {
-
- setupKmsAttributesdata(dbMan.Db)
- attributes := dbMan.GetKmsAttributes("bc811169", "40753e12-a50a-429d-9121-e571eb4e43a9", "85629786-37c5-4e8c-bb45-208f3360d005", "50321842-d6ee-4e92-91b9-37234a7920c1", "test-invalid")
- Expect(len(attributes)).Should(BeEquivalentTo(3))
- Expect(len(attributes["40753e12-a50a-429d-9121-e571eb4e43a9"])).Should(BeEquivalentTo(1))
- Expect(len(attributes["85629786-37c5-4e8c-bb45-208f3360d005"])).Should(BeEquivalentTo(2))
- Expect(len(attributes["50321842-d6ee-4e92-91b9-37234a7920c1"])).Should(BeEquivalentTo(5))
- Expect(len(attributes["test-invalid"])).Should(BeEquivalentTo(0))
-
- })
-
})
})
diff --git a/verifyApiKey/mock_test.go b/verifyApiKey/mock_test.go
new file mode 100644
index 0000000..731a244
--- /dev/null
+++ b/verifyApiKey/mock_test.go
@@ -0,0 +1,30 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package verifyApiKey
+
+import "github.com/apid/apid-core/cipher"
+
+type DummyCipherMan struct {
+}
+
+func (c *DummyCipherMan) AddOrgs(orgs []string) {
+}
+
+func (d *DummyCipherMan) TryDecryptBase64(input string, org string) (string, error) {
+ return input, nil
+}
+
+func (d *DummyCipherMan) EncryptBase64(input string, org string, mode cipher.Mode, padding cipher.Padding) (string, error) {
+ return input, nil
+}
diff --git a/verifyApiKey/verifyApiKeyUtil.go b/verifyApiKey/verifyApiKeyUtil.go
index 85aa8c5..fcfaf2e 100644
--- a/verifyApiKey/verifyApiKeyUtil.go
+++ b/verifyApiKey/verifyApiKeyUtil.go
@@ -53,12 +53,3 @@
/* if the i/p resource is empty, no checks need to be made */
return len(fs) == 0
}
-
-func contains(givenArray []string, searchString string) bool {
- for _, element := range givenArray {
- if element == searchString {
- return true
- }
- }
- return false
-}
diff --git a/verifyApiKey/verifyApiKeyUtil_test.go b/verifyApiKey/verifyApiKeyUtil_test.go
deleted file mode 100644
index 1c26982..0000000
--- a/verifyApiKey/verifyApiKeyUtil_test.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package verifyApiKey
-
-import (
- "github.com/apid/apidApiMetadata/common"
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/gomega"
- "reflect"
-)
-
-var _ = Describe("Validate Env", func() {
-
- It("validation1", func() {
- s := contains([]string{"foo", "bar"}, "foo")
- Expect(s).Should(BeTrue())
- })
- It("validation2", func() {
- s := contains([]string{"foo", "bar"}, "bar")
- Expect(s).Should(BeTrue())
- })
- It("validation3", func() {
- s := contains([]string{"foo", "bar"}, "xxx")
- Expect(s).Should(BeFalse())
- })
- It("validation4", func() {
- s := contains([]string{}, "xxx")
- Expect(s).Should(BeFalse())
- })
-})
-
-var _ = Describe("Validate common.JsonToStringArray", func() {
-
- It("should transform simple valid json", func() {
- array := common.JsonToStringArray("[\"test-1\", \"test-2\"]")
- Expect(reflect.DeepEqual(array, []string{"test-1", "test-2"})).Should(BeTrue())
- })
- It("should transform simple single valid json", func() {
- array := common.JsonToStringArray("[\"test-1\"]")
- Expect(reflect.DeepEqual(array, []string{"test-1"})).Should(BeTrue())
- })
- It("should transform simple fake json", func() {
- s := common.JsonToStringArray("{test-1,test-2}")
- Expect(reflect.DeepEqual(s, []string{"test-1", "test-2"})).Should(BeTrue())
- })
- It("should transform simple single valued fake json", func() {
- s := common.JsonToStringArray("{test-1}")
- Expect(reflect.DeepEqual(s, []string{"test-1"})).Should(BeTrue())
- })
- It("space between fields considered as valid char", func() {
- s := common.JsonToStringArray("{test-1, test-2}")
- Expect(reflect.DeepEqual(s, []string{"test-1", " test-2"})).Should(BeTrue())
- })
- It("remove only last braces", func() {
- s := common.JsonToStringArray("{test-1,test-2}}")
- Expect(reflect.DeepEqual(s, []string{"test-1", "test-2}"})).Should(BeTrue())
- })
-
-})