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()) - }) - -})