[ISSUE-67869881] add decryption/encryption
diff --git a/accessEntity/data.go b/accessEntity/data.go index 3981014..6af4391 100644 --- a/accessEntity/data.go +++ b/accessEntity/data.go
@@ -16,6 +16,8 @@ import ( "database/sql" "fmt" + "github.com/apid/apid-core/cipher" + "github.com/apid/apid-core/util" "github.com/apid/apidApiMetadata/common" "strings" ) @@ -27,7 +29,6 @@ type DbManager struct { common.DbManager - cipherManager common.CipherManagerInterface } func (d *DbManager) GetApiProductNames(id string, idType string) ([]string, error) { @@ -209,7 +210,12 @@ } else if priKey == IdentifierAppName { switch secKey { case IdentifierDeveloperEmail: - apiProducts, err = d.getApiProductsByAppName(priVal, secVal, "", "", org) + var email string + email, err = d.CipherManager.EncryptBase64(secVal, org, cipher.ModeEcb, cipher.PaddingPKCS5) + if err != nil { + return + } + apiProducts, err = d.getApiProductsByAppName(priVal, email, "", "", org) case IdentifierDeveloperId: apiProducts, err = d.getApiProductsByAppName(priVal, "", secVal, "", org) case IdentifierCompanyName: @@ -242,7 +248,12 @@ case IdentifierAppName: switch secKey { case IdentifierDeveloperEmail: - return d.getAppByAppName(priVal, secVal, "", "", org) + var email string + email, err = d.CipherManager.EncryptBase64(secVal, org, cipher.ModeEcb, cipher.PaddingPKCS5) + if err != nil { + return + } + return d.getAppByAppName(priVal, email, "", "", org) case IdentifierDeveloperId: return d.getAppByAppName(priVal, "", secVal, "", org) case IdentifierCompanyName: @@ -279,24 +290,47 @@ 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) } + + 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) + var email string + email, err = d.CipherManager.EncryptBase64(priVal, org, cipher.ModeEcb, cipher.PaddingPKCS5) + if err != nil { + return + } + developers, err = d.getDeveloperByEmail(email, 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) } + + var plaintext string + for i := range developers { + if plaintext, err = d.CipherManager.TryDecryptBase64(developers[i].Email, org); err != nil { + return + } + developers[i].Email = plaintext + } + return } @@ -746,19 +780,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..4a9c592 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)
diff --git a/accessEntity/mock_test.go b/accessEntity/mock_test.go index 5a30e27..b9e1367 100644 --- a/accessEntity/mock_test.go +++ b/accessEntity/mock_test.go
@@ -14,9 +14,24 @@ package accessEntity import ( + "github.com/apid/apid-core/cipher" "github.com/apid/apidApiMetadata/common" ) +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 +} + type DummyDbMan struct { apiProducts []common.ApiProduct apps []common.App @@ -33,6 +48,10 @@ err error } +func (d *DummyDbMan) GetOrgs() (orgs []string, err error) { + return +} + func (d *DummyDbMan) SetDbVersion(string) { }
diff --git a/common/cipher.go b/common/cipher.go index dcd3eec..5562c02 100644 --- a/common/cipher.go +++ b/common/cipher.go
@@ -22,71 +22,121 @@ "regexp" "strings" "sync" + "time" ) const RegEncrypted = `^\{[0-9A-Za-z]+/[0-9A-Za-z]+/[0-9A-Za-z]+\}.` -const retrieveEncryptKeyPath = "" +const retrieveEncryptKeyPath = "/encryption/key" +const EncryptAes = "AES" +const retrieveKeyRetryInterval = time.Duration(5 * time.Second) +const retrieveKeyTimeout = time.Duration(5 * time.Minute) var RegexpEncrypted = regexp.MustCompile(RegEncrypted) -const ( - EncryptAes = "AES" -) +func CreateCipherManager(client *http.Client, serverUrlBase string) *KmsCipherManager { + return &KmsCipherManager{ + serverUrlBase: serverUrlBase, + key: make(map[string][]byte), + aes: make(map[string]*cipher.AesCipher), + mutex: &sync.RWMutex{}, + client: client, + interval: retrieveKeyRetryInterval, + timeout: retrieveKeyTimeout, + } +} type KmsCipherManager struct { + serverUrlBase string // org-level key map {organization: key} key map[string][]byte // org-level AesCipher map {organization: AesCipher} - aes map[string]*cipher.AesCipher - mutex *sync.RWMutex - client *http.Client + aes map[string]*cipher.AesCipher + mutex *sync.RWMutex + client *http.Client + interval time.Duration + timeout time.Duration } -func (c *KmsCipherManager) retrieveKey(org string) (key []byte, err error) { - req, err := http.NewRequest(http.MethodGet, retrieveEncryptKeyPath, nil) - if err != nil { +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) { + + if err := c.retrieveKey(org); err != nil { + log.Error(err) + } else { return } + ticker := time.NewTicker(interval) + for { + select { + case <-time.After(timeout): + 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 + log.Debugf("Retrieving key from: %s", c.serverUrlBase+retrieveEncryptKeyPath) + 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) + } res, err := c.client.Do(req) if err != nil { - return + return fmt.Errorf("failed to retrieve key for org=%s : %v", org, err) } if res.StatusCode != http.StatusOK { - err = fmt.Errorf("retrieve encryption key failed for org [%v] with status: %v", org, res.Status) - return + err = fmt.Errorf("failed to retrieve key for org [%v] with status: %v", org, res.Status) + return fmt.Errorf("failed to create retrieving key request for org=%s : %v", org, err) } - defer res.Body.Close() + + 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)) - return + 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.key[org] = key + c.aes[org] = a + c.mutex.Unlock() + return nil } -func (c *KmsCipherManager) getAesCipher(org string) (*cipher.AesCipher, error) { +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, nil + return a } - c.mutex.RUnlock() // if not exists - key, err := c.retrieveKey(org) - if err != nil { - log.Errorf("getAesCipher error for org [%v] when retrieveKey: %v", org, err) - return nil, err - } - - c.mutex.Lock() - defer c.mutex.Unlock() - - c.key[org] = key - a, err := cipher.CreateAesCipher(key) - if err != nil { - log.Errorf("getAesCipher error for org [%v] when CreateAesCipher: %v", org, err) - return nil, err - } - c.aes[org] = a - return a, nil + c.mutex.RUnlock() + c.startRetrieve(org, c.interval, c.timeout) + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.aes[org] } // If input is encrypted, it decodes the input with base64, @@ -109,11 +159,7 @@ log.Errorf("Decode base64 of [%v] failed: [%v], considered as unencrypted!", text, err) return } - aes, err := c.getAesCipher(org) - if err != nil { - return - } - plaintext, err := aes.Decrypt(bytes, mode, padding) + plaintext, err := c.getAesCipher(org).Decrypt(bytes, mode, padding) if err != nil { log.Errorf("Decrypt of [%v] failed: [%v], considered as unencrypted!", bytes, err) return @@ -126,11 +172,7 @@ // 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, err := c.getAesCipher(org) - if err != nil { - return - } - ciphertext, err := aes.Encrypt([]byte(input), mode, padding) + ciphertext, err := c.getAesCipher(org).Encrypt([]byte(input), mode, padding) if err != nil { return } @@ -156,13 +198,13 @@ return } // encryption algorithm - if strings.ToUpper(l[0]) != EncryptAes { + if l[0] != EncryptAes { err = fmt.Errorf("unsupported algorithm for GetCiphertext: %v", l[0]) return } // mode - mode = cipher.Mode(strings.ToUpper(l[1])) + mode = cipher.Mode(l[1]) // padding - padding = cipher.Padding(strings.ToUpper(l[2])) + padding = cipher.Padding(l[2]) return }
diff --git a/common/cipher_test.go b/common/cipher_test.go new file mode 100644 index 0000000..4ff12b9 --- /dev/null +++ b/common/cipher_test.go
@@ -0,0 +1,88 @@ +// 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" + "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 + testCipherMan.key[testOrg] = key + 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 + BeforeEach(func() { + // set key server + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer GinkgoRecover() + 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)) + }) + }) +})
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..ef8434f 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 name FROM kms_organization`) + 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..4a3ecdc --- /dev/null +++ b/common/data_test.go
@@ -0,0 +1,94 @@ +// 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" + "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)) + }) + + }) + + 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..1a12c2f --- /dev/null +++ b/common/data_test.sql
@@ -0,0 +1,27 @@ +-- 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'); +COMMIT;
diff --git a/common/interfaces.go b/common/interfaces.go index fa49689..859e4bc 100644 --- a/common/interfaces.go +++ b/common/interfaces.go
@@ -23,9 +23,11 @@ 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.
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 9aada4e..b900cd6 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 verifyApiKey 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..c2169bb 100644 --- a/verifyApiKey/data.go +++ b/verifyApiKey/data.go
@@ -68,6 +68,19 @@ return errors.New("InvalidApiKey") } + email, err := dbc.CipherManager.TryDecryptBase64(dataWrapper.tempDeveloperDetails.Email, + dataWrapper.verifyApiKeyRequest.OrganizationName) + if err != nil { + return err + } + secret, err := dbc.CipherManager.TryDecryptBase64(dataWrapper.verifyApiKeySuccessResponse.ClientId.ClientSecret, + dataWrapper.verifyApiKeyRequest.OrganizationName) + if err != nil { + return err + } + dataWrapper.tempDeveloperDetails.Email = email + 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()) - }) - -})