From 6d658e8115a19e36495cb5805900dbda0399876a Mon Sep 17 00:00:00 2001 From: Scott Ganyo Date: Mon, 29 Jun 2020 09:34:56 -0700 Subject: [PATCH] internal JWT tokens for hybrid always emit CRD format change --truncate -to --rotate in provision remove --verify-only option in provision eliminate `token create-secret` cmd --- .gitignore | 1 + cmd/bindings/bindings.go | 8 +- cmd/provision/legacy.go | 244 ++++++++ cmd/provision/provision.go | 972 +++++++------------------------- cmd/provision/provision_test.go | 16 +- cmd/provision/proxy.go | 260 +++++++++ cmd/root.go | 2 +- cmd/token/token.go | 82 +-- go.mod | 4 +- go.sum | 15 +- shared/jwks.go | 93 +-- shared/shared.go | 67 +-- 12 files changed, 812 insertions(+), 952 deletions(-) create mode 100644 cmd/provision/legacy.go create mode 100644 cmd/provision/proxy.go diff --git a/.gitignore b/.gitignore index 0a0bf4e..a4b66a4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ coverage.txt dist/ # Misc +/.vscode /apigee-remote-service-cli /config.yaml /configmap.yaml diff --git a/cmd/bindings/bindings.go b/cmd/bindings/bindings.go index ac01d2f..ba107ea 100644 --- a/cmd/bindings/bindings.go +++ b/cmd/bindings/bindings.go @@ -156,7 +156,7 @@ func (b *bindings) getProducts() ([]product.APIProduct, error) { if b.products != nil { return b.products, nil } - req, err := b.Client.NewRequest(http.MethodGet, "", nil) + req, err := b.ApigeeClient.NewRequest(http.MethodGet, "", nil) if err != nil { return nil, errors.Wrap(err, "creating request") } @@ -164,7 +164,7 @@ func (b *bindings) getProducts() ([]product.APIProduct, error) { req.URL.RawQuery = "expand=true" var res product.APIResponse - resp, err := b.Client.Do(req, &res) + resp, err := b.ApigeeClient.Do(req, &res) if err != nil { return nil, errors.Wrap(err, "retrieving products") } @@ -264,14 +264,14 @@ func (b *bindings) updateTargetBindings(p *product.APIProduct, bindings []string newAttrs := attrUpdate{ Attributes: attributes, } - req, err := b.Client.NewRequest(http.MethodPost, "", newAttrs) + req, err := b.ApigeeClient.NewRequest(http.MethodPost, "", newAttrs) if err != nil { return err } path := fmt.Sprintf(productAttrPathFormat, b.Org, p.Name) req.URL.Path = path // hack: negate client's base URL var attrResult attrUpdate - _, err = b.Client.Do(req, &attrResult) + _, err = b.ApigeeClient.Do(req, &attrResult) return err } diff --git a/cmd/provision/legacy.go b/cmd/provision/legacy.go new file mode 100644 index 0000000..c7954c4 --- /dev/null +++ b/cmd/provision/legacy.go @@ -0,0 +1,244 @@ +// Copyright 2018 Google LLC +// +// 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 provision + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "encoding/xml" + "fmt" + "io/ioutil" + rnd "math/rand" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/apigee/apigee-remote-service-cli/apigee" + "github.com/apigee/apigee-remote-service-cli/shared" + "github.com/pkg/errors" + "go.uber.org/multierr" +) + +const ( + legacyCredentialURLFormat = "%s/credential/organization/%s/environment/%s" // InternalProxyURL, org, env + analyticsURLFormat = "%s/analytics/organization/%s/environment/%s" // InternalProxyURL, org, env + legacyAnalyticURLFormat = "%s/axpublisher/organization/%s/environment/%s" // InternalProxyURL, org, env + legacyAuthProxyZip = "remote-service-legacy.zip" + + // virtualHost is only necessary for legacy + virtualHostDeleteText = "secure" + virtualHostReplaceText = "default" + virtualHostReplacementFmt = "%s" // each virtualHost + + internalProxyName = "edgemicro-internal" + internalProxyZip = "internal.zip" +) + +func (p *provision) deployInternalProxy(replaceVirtualHosts func(proxyDir string) error, tempDir string, verbosef shared.FormatFn) error { + + customizedZip, err := getCustomizedProxy(tempDir, internalProxyZip, func(proxyDir string) error { + + // change server locations + calloutFile := filepath.Join(proxyDir, "policies", "Callout.xml") + bytes, err := ioutil.ReadFile(calloutFile) + if err != nil { + return errors.Wrapf(err, "reading file %s", calloutFile) + } + var callout JavaCallout + if err := xml.Unmarshal(bytes, &callout); err != nil { + return errors.Wrapf(err, "unmarshalling %s", calloutFile) + } + setMgmtURL := false + for i, cp := range callout.Properties { + if cp.Name == "REGION_MAP" { + callout.Properties[i].Value = fmt.Sprintf("DN=%s", p.RuntimeBase) + } + if cp.Name == "MGMT_URL_PREFIX" { + setMgmtURL = true + callout.Properties[i].Value = p.ManagementBase + } + } + if !setMgmtURL { + callout.Properties = append(callout.Properties, + javaCalloutProperty{ + Name: "MGMT_URL_PREFIX", + Value: p.ManagementBase, + }) + } + + writer, err := os.OpenFile(calloutFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0) + if err != nil { + return errors.Wrapf(err, "writing file %s", calloutFile) + } + writer.WriteString(xml.Header) + encoder := xml.NewEncoder(writer) + encoder.Indent("", " ") + err = encoder.Encode(callout) + if err != nil { + return errors.Wrapf(err, "encoding xml to %s", calloutFile) + } + err = writer.Close() + if err != nil { + return errors.Wrapf(err, "closing file %s", calloutFile) + } + + return replaceVirtualHosts(proxyDir) + }) + if err != nil { + return err + } + + return p.checkAndDeployProxy(internalProxyName, customizedZip, verbosef) +} + +//check if the KVM exists, if it doesn't, create a new one and sets certs for JWT +func (p *provision) getOrCreateKVM(cred *keySecret, printf shared.FormatFn) error { + + kid, keyBytes, jwksBytes, err := p.CreateJWKS(1, printf) + if err != nil { + return err + } + + kvm := apigee.KVM{ + Name: kvmName, + Encrypted: encryptKVM, + Entries: []apigee.Entry{ + { + Name: "private_key", + Value: string(keyBytes), + }, + { + Name: "jwks", + Value: string(jwksBytes), + }, + { + Name: "kid", + Value: kid, + }, + }, + } + + resp, err := p.ApigeeClient.KVMService.Create(kvm) + if err != nil && (resp == nil || resp.StatusCode != http.StatusConflict) { // http.StatusConflict == already exists + return err + } + if resp.StatusCode == http.StatusConflict { + printf("kvm %s already exists", kvmName) + return nil + } + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("creating kvm %s, status code: %v", kvmName, resp.StatusCode) + } + printf("kvm %s created", kvmName) + + printf("new private key:\n%s", string(keyBytes)) + printf("new jwks:\n%s", string(jwksBytes)) + + return nil +} + +// hash for key and secret +func newHash() string { + // use crypto seed + var seed int64 + binary.Read(rand.Reader, binary.BigEndian, &seed) + rnd.Seed(seed) + + t := time.Now() + h := sha256.New() + h.Write([]byte(t.String() + string(rnd.Int()))) + str := hex.EncodeToString(h.Sum(nil)) + return str +} + +func (p *provision) createLegacyCredential(printf shared.FormatFn) (*keySecret, error) { + printf("creating credential...") + cred := &keySecret{ + Key: newHash(), + Secret: newHash(), + } + + credentialURL := fmt.Sprintf(legacyCredentialURLFormat, p.InternalProxyURL, p.Org, p.Env) + + req, err := p.ApigeeClient.NewRequest(http.MethodPost, credentialURL, cred) + if err != nil { + return nil, err + } + req.URL, err = url.Parse(credentialURL) // override client's munged URL + if err != nil { + return nil, err + } + + resp, err := p.ApigeeClient.Do(req, nil) + if err != nil { + return nil, err + } + if resp.StatusCode > 299 { + return nil, fmt.Errorf("creating credential, status: %d", resp.StatusCode) + } + printf("credential created") + return cred, nil +} + +// verify POST internalProxyURL/analytics/organization/%s/environment/%s +// verify POST internalProxyURL/quotas/organization/%s/environment/%s +func (p *provision) verifyInternalProxy(client *http.Client, printf shared.FormatFn) error { + var verifyErrors error + + var req *http.Request + var err error + var res *http.Response + if p.IsOPDK { + analyticsURL := fmt.Sprintf(legacyAnalyticURLFormat, p.InternalProxyURL, p.Org, p.Env) + req, err = http.NewRequest(http.MethodPost, analyticsURL, strings.NewReader("{}")) + } else { + analyticsURL := fmt.Sprintf(analyticsURLFormat, p.InternalProxyURL, p.Org, p.Env) + req, err = http.NewRequest(http.MethodGet, analyticsURL, nil) + q := req.URL.Query() + q.Add("tenant", fmt.Sprintf("%s~%s", p.Org, p.Env)) + q.Add("relative_file_path", "fake") + q.Add("file_content_type", "application/x-gzip") + q.Add("encrypt", "true") + req.URL.RawQuery = q.Encode() + } + if err != nil { + res, err = client.Do(req) + if res != nil { + defer res.Body.Close() + } + } + if err != nil { + verifyErrors = multierr.Append(verifyErrors, err) + } + + return verifyErrors +} + +// JavaCallout must be capitalized to ensure correct generation +type JavaCallout struct { + Name string `xml:"name,attr"` + DisplayName, ClassName, ResourceURL string + Properties []javaCalloutProperty `xml:"Properties>Property"` +} + +type javaCalloutProperty struct { + Name string `xml:"name,attr"` + Value string `xml:",chardata"` +} diff --git a/cmd/provision/provision.go b/cmd/provision/provision.go index bd3448b..f327556 100644 --- a/cmd/provision/provision.go +++ b/cmd/provision/provision.go @@ -15,27 +15,21 @@ package provision import ( - "archive/zip" "bytes" - "crypto/rand" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/xml" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" "fmt" - "io" "io/ioutil" - rnd "math/rand" + "net" "net/http" - "net/url" "os" "path/filepath" - "sort" "strings" "time" - "github.com/apigee/apigee-remote-service-cli/apigee" - "github.com/apigee/apigee-remote-service-cli/proxies" "github.com/apigee/apigee-remote-service-cli/shared" "github.com/apigee/apigee-remote-service-envoy/server" "github.com/pkg/errors" @@ -44,22 +38,7 @@ import ( "gopkg.in/yaml.v3" ) -const ( // legacy - legacyCredentialURLFormat = "%s/credential/organization/%s/environment/%s" // InternalProxyURL, org, env - analyticsURLFormat = "%s/analytics/organization/%s/environment/%s" // InternalProxyURL, org, env - legacyAnalyticURLFormat = "%s/axpublisher/organization/%s/environment/%s" // InternalProxyURL, org, env - legacyAuthProxyZip = "remote-service-legacy.zip" - - // virtualHost is only necessary for legacy - virtualHostDeleteText = "secure" - virtualHostReplaceText = "default" - virtualHostReplacementFmt = "%s" // each virtualHost - - internalProxyName = "edgemicro-internal" // legacy - internalProxyZip = "internal.zip" -) - -const ( // modern +const ( kvmName = "remote-service" cacheName = "remote-service" encryptKVM = true @@ -67,12 +46,6 @@ const ( // modern remoteServiceProxyZip = "remote-service-gcp.zip" - apiProductsPath = "apiproducts" - developersPath = "developers" - applicationsPathFormat = "developers/%s/apps" // developer email - keyCreatePathFormat = "developers/%s/apps/%s/keys/create" // developer email, app ID - keyPathFormat = "developers/%s/apps/%s/keys/%s" // developer email, app ID, key ID - certsURLFormat = "%s/certs" // RemoteServiceProxyURL productsURLFormat = "%s/products" // RemoteServiceProxyURL verifyAPIKeyURLFormat = "%s/verifyApiKey" // RemoteServiceProxyURL @@ -85,17 +58,17 @@ const ( // modern defaultApigeeCAFile = "/opt/apigee/tls/ca.crt" defaultApigeeCertFile = "/opt/apigee/tls/tls.crt" defaultApigeeKeyFile = "/opt/apigee/tls/tls.key" + + policySecretNameFormat = "%s-%s-policy-secret" ) type provision struct { *shared.RootArgs forceProxyInstall bool virtualHosts string - verifyOnly bool provisionKey string provisionSecret string - developerEmail string - namespace string + rotate int } // Cmd returns base command @@ -112,24 +85,22 @@ to your organization and environment.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { err := rootArgs.Resolve(false, true) if err == nil { - if p.IsGCPManaged && !p.verifyOnly { - missingFlagNames := []string{} + missingFlagNames := []string{} + if p.IsGCPManaged { if p.Token == "" { missingFlagNames = append(missingFlagNames, "token") } - if p.developerEmail == "" { - missingFlagNames = append(missingFlagNames, "developer-email") + } else { + if p.rotate > 0 { + return fmt.Errorf(`--rotate only valid for hybrid, use 'token rotate-cert' for others`) } - err = p.PrintMissingFlags(missingFlagNames) } + err = p.PrintMissingFlags(missingFlagNames) } return err }, RunE: func(cmd *cobra.Command, _ []string) error { - if p.verifyOnly && (p.provisionKey == "" || p.provisionSecret == "") { - return fmt.Errorf("--verify-only requires values for --key and --secret") - } return p.run(printf) }, } @@ -148,515 +119,239 @@ to your organization and environment.`, c.Flags().StringVarP(&rootArgs.Password, "password", "p", "", "Apigee password (legacy or OPDK only)") - c.Flags().StringVarP(&p.developerEmail, "developer-email", "d", "", - "email used to create a developer (ignored for --legacy or --opdk)") c.Flags().BoolVarP(&p.forceProxyInstall, "force-proxy-install", "f", false, "force new proxy install (upgrades proxy)") c.Flags().StringVarP(&p.virtualHosts, "virtual-hosts", "", "default,secure", "override proxy virtualHosts") - c.Flags().BoolVarP(&p.verifyOnly, "verify-only", "", false, - "verify only, don’t provision anything") - c.Flags().StringVarP(&p.namespace, "namespace", "n", "", - "emit configuration as an Envoy ConfigMap in the specified namespace") + c.Flags().StringVarP(&p.Namespace, "namespace", "n", "apigee", + "emit configuration in the specified namespace") - c.Flags().StringVarP(&p.provisionKey, "key", "k", "", "gateway key (for --verify-only)") - c.Flags().StringVarP(&p.provisionSecret, "secret", "s", "", "gateway secret (for --verify-only)") + c.Flags().IntVarP(&p.rotate, "rotate", "", 0, "if n > 0, generate new private key and keep n public keys (hybrid only)") return c } func (p *provision) run(printf shared.FormatFn) error { - var cred *credential + var cred *keySecret var verbosef = shared.NoPrintf - if p.Verbose || p.verifyOnly { + if p.Verbose { verbosef = printf } - if !p.verifyOnly { + tempDir, err := ioutil.TempDir("", "apigee") + if err != nil { + return errors.Wrap(err, "creating temp dir") + } + defer os.RemoveAll(tempDir) - tempDir, err := ioutil.TempDir("", "apigee") + replaceVH := func(proxyDir string) error { + proxiesFile := filepath.Join(proxyDir, "proxies", "default.xml") + bytes, err := ioutil.ReadFile(proxiesFile) if err != nil { - return errors.Wrap(err, "creating temp dir") + return errors.Wrapf(err, "reading file %s", proxiesFile) } - defer os.RemoveAll(tempDir) - - replaceVH := func(proxyDir string) error { - proxiesFile := filepath.Join(proxyDir, "proxies", "default.xml") - bytes, err := ioutil.ReadFile(proxiesFile) - if err != nil { - return errors.Wrapf(err, "reading file %s", proxiesFile) + newVH := "" + for _, vh := range strings.Split(p.virtualHosts, ",") { + if strings.TrimSpace(vh) != "" { + newVH = newVH + fmt.Sprintf(virtualHostReplacementFmt, vh) } - newVH := "" - for _, vh := range strings.Split(p.virtualHosts, ",") { - if strings.TrimSpace(vh) != "" { - newVH = newVH + fmt.Sprintf(virtualHostReplacementFmt, vh) - } - } - // remove all "secure" virtualhost - bytes = []byte(strings.ReplaceAll(string(bytes), virtualHostDeleteText, "")) - // replace the "default" virtualhost - bytes = []byte(strings.Replace(string(bytes), virtualHostReplaceText, newVH, 1)) - if err := ioutil.WriteFile(proxiesFile, bytes, 0); err != nil { - return errors.Wrapf(err, "writing file %s", proxiesFile) - } - return nil } - - replaceInFile := func(file, old, new string) error { - bytes, err := ioutil.ReadFile(file) - if err != nil { - return errors.Wrapf(err, "reading file %s", file) - } - bytes = []byte(strings.Replace(string(bytes), old, new, 1)) - if err := ioutil.WriteFile(file, bytes, 0); err != nil { - return errors.Wrapf(err, "writing file %s", file) - } - return nil - } - - replaceVHAndAuthTarget := func(proxyDir string) error { - if err := replaceVH(proxyDir); err != nil { - return err - } - - if p.IsOPDK { - // OPDK must target local internal proxy - authFile := filepath.Join(proxyDir, "policies", "Authenticate-Call.xml") - oldTarget := "https://edgemicroservices.apigee.net" - newTarget := p.RuntimeBase - if err := replaceInFile(authFile, oldTarget, newTarget); err != nil { - return err - } - - // OPDK must have org.noncps = true for products callout - calloutFile := filepath.Join(proxyDir, "policies", "JavaCallout.xml") - oldValue := "" - newValue := `true - ` - if err := replaceInFile(calloutFile, oldValue, newValue); err != nil { - return err - } - } - return nil - } - - if p.IsOPDK { - if err := p.deployInternalProxy(replaceVH, tempDir, verbosef); err != nil { - return errors.Wrap(err, "deploying internal proxy") - } + // remove all "secure" virtualhost + bytes = []byte(strings.ReplaceAll(string(bytes), virtualHostDeleteText, "")) + // replace the "default" virtualhost + bytes = []byte(strings.Replace(string(bytes), virtualHostReplaceText, newVH, 1)) + if err := ioutil.WriteFile(proxiesFile, bytes, 0); err != nil { + return errors.Wrapf(err, "writing file %s", proxiesFile) } + return nil + } - // input remote-service proxy - var customizedProxy string - if p.IsGCPManaged { - customizedProxy, err = getCustomizedProxy(tempDir, remoteServiceProxyZip, nil) - } else { - customizedProxy, err = getCustomizedProxy(tempDir, legacyAuthProxyZip, replaceVHAndAuthTarget) - } + replaceInFile := func(file, old, new string) error { + bytes, err := ioutil.ReadFile(file) if err != nil { - return err + return errors.Wrapf(err, "reading file %s", file) } - - if err := p.checkAndDeployProxy(authProxyName, customizedProxy, verbosef); err != nil { - return errors.Wrapf(err, "deploying proxy %s", authProxyName) + bytes = []byte(strings.Replace(string(bytes), old, new, 1)) + if err := ioutil.WriteFile(file, bytes, 0); err != nil { + return errors.Wrapf(err, "writing file %s", file) } + return nil + } - if p.IsGCPManaged { - cred, err = p.createGCPCredential(verbosef) - } else { - cred, err = p.createLegacyCredential(verbosef) - } - if err != nil { - return errors.Wrapf(err, "generating credential") + replaceVHAndAuthTarget := func(proxyDir string) error { + if err := replaceVH(proxyDir); err != nil { + return err } - if !p.IsGCPManaged { - if err := p.getOrCreateKVM(cred, verbosef); err != nil { - return errors.Wrapf(err, "retrieving or creating kvm") + if p.IsOPDK { + // OPDK must target local internal proxy + authFile := filepath.Join(proxyDir, "policies", "Authenticate-Call.xml") + oldTarget := "https://edgemicroservices.apigee.net" + newTarget := p.RuntimeBase + if err := replaceInFile(authFile, oldTarget, newTarget); err != nil { + return err } - } - } else { // verifyOnly == true - cred = &credential{ - Key: p.provisionKey, - Secret: p.provisionSecret, + // OPDK must have org.noncps = true for products callout + calloutFile := filepath.Join(proxyDir, "policies", "JavaCallout.xml") + oldValue := "" + newValue := `true + ` + if err := replaceInFile(calloutFile, oldValue, newValue); err != nil { + return err + } } + return nil } - // use generated credentials - opts := *p.ClientOpts - if cred != nil { - opts.Auth = &apigee.EdgeAuth{ - Username: cred.Key, - Password: cred.Secret, - } - var err error - if p.Client, err = apigee.NewEdgeClient(&opts); err != nil { - return errors.Wrapf(err, "creating new client") + if p.IsOPDK { + if err := p.deployInternalProxy(replaceVH, tempDir, verbosef); err != nil { + return errors.Wrap(err, "deploying internal proxy") } } - var verifyErrors error - if p.IsLegacySaaS || p.IsOPDK { - verbosef("verifying internal proxy...") - verifyErrors = p.verifyInternalProxy(opts.Auth, verbosef) - } - - verbosef("verifying remote-service proxy...") - verifyErrors = multierr.Combine(verifyErrors, p.verifyRemoteServiceProxy(opts.Auth, verbosef)) - - if verifyErrors != nil { - shared.Errorf("\nWARNING: Apigee may not be provisioned properly.") - shared.Errorf("Unable to verify proxy endpoint(s). Errors:\n") - for _, err := range multierr.Errors(verifyErrors) { - shared.Errorf(" %s", err) - } - shared.Errorf("\n") + // input remote-service proxy + var customizedProxy string + if p.IsGCPManaged { + customizedProxy, err = getCustomizedProxy(tempDir, remoteServiceProxyZip, nil) + } else { + customizedProxy, err = getCustomizedProxy(tempDir, legacyAuthProxyZip, replaceVHAndAuthTarget) } - - if !p.verifyOnly { - if err := p.printConfig(cred, printf, verifyErrors); err != nil { - return errors.Wrapf(err, "generating config") - } + if err != nil { + return err } - if verifyErrors != nil { - os.Exit(1) + if err := p.checkAndDeployProxy(authProxyName, customizedProxy, verbosef); err != nil { + return errors.Wrapf(err, "deploying proxy %s", authProxyName) } - verbosef("provisioning verified OK") - return nil -} + if !p.IsGCPManaged { + cred, err = p.createLegacyCredential(verbosef) // TODO: on missing or force new cred + if err != nil { + return errors.Wrapf(err, "generating credential") + } -// ensures that there's a product, developer, and app -func (p *provision) createGCPCredential(verbosef shared.FormatFn) (*credential, error) { - const removeServiceName = "remote-service" - - // create product - product := apiProduct{ - Name: removeServiceName, - DisplayName: removeServiceName, - ApprovalType: "auto", - Attributes: []attribute{ - {Name: "access", Value: "internal"}, - }, - Description: removeServiceName + " access", - APIResources: []string{"/**"}, - Environments: []string{p.Env}, - Proxies: []string{removeServiceName}, - } - req, err := p.Client.NewRequestNoEnv(http.MethodPost, apiProductsPath, product) - if err != nil { - return nil, err - } - res, err := p.Client.Do(req, nil) - if err != nil { - if res.StatusCode != http.StatusConflict { // exists - return nil, err + if err := p.getOrCreateKVM(cred, verbosef); err != nil { + return errors.Wrapf(err, "retrieving or creating kvm") } - verbosef("product %s already exists", removeServiceName) } - // create developer - devEmail := p.developerEmail - dev := developer{ - Email: devEmail, - FirstName: removeServiceName, - LastName: removeServiceName, - UserName: removeServiceName, + config := p.ServerConfig + if config == nil { + config = p.createConfig(cred) } - req, err = p.Client.NewRequestNoEnv(http.MethodPost, developersPath, dev) - if err != nil { - return nil, err - } - res, err = p.Client.Do(req, nil) - if err != nil { - if res.StatusCode != http.StatusConflict { // exists - return nil, err + + if p.IsGCPManaged && config.Tenant.PrivateKey == nil { + keyID, privateKey, jwks, err := p.CreateNewKey() + if err != nil { + return err } - verbosef("developer %s already exists", devEmail) - } + config.Tenant.PrivateKey = privateKey + config.Tenant.PrivateKeyID = keyID - // create application - app := application{ - Name: removeServiceName, - APIProducts: []string{removeServiceName}, - } - applicationsPath := fmt.Sprintf(applicationsPathFormat, devEmail) - req, err = p.Client.NewRequestNoEnv(http.MethodPost, applicationsPath, &app) - if err != nil { - return nil, err - } - res, err = p.Client.Do(req, &app) - if err == nil { - appCred := app.Credentials[0] - cred := &credential{ - Key: appCred.Key, - Secret: appCred.Secret, + if jwks, err = p.RotateJKWS(jwks, p.rotate); err != nil { + return err } - verbosef("credentials created: %v", cred) - return cred, nil - } - if res == nil || res.StatusCode != http.StatusConflict { - return nil, err + config.Tenant.JWKS = jwks } - // http.StatusConflict == app exists, create a new credential - verbosef("app %s already exists", removeServiceName) - appCred := appCredential{ - Key: newHash(), - Secret: newHash(), - } - createKeyPath := fmt.Sprintf(keyCreatePathFormat, devEmail, removeServiceName) - if req, err = p.Client.NewRequestNoEnv(http.MethodPost, createKeyPath, &appCred); err != nil { - return nil, err - } - if res, err = p.Client.Do(req, &appCred); err != nil { - return nil, err - } + verifyErrors := p.verify(config, verbosef) - // adding product to the credential requires a separate call - appCredDetails := appCredentialDetails{ - APIProducts: []string{removeServiceName}, - } - keyPath := fmt.Sprintf(keyPathFormat, devEmail, removeServiceName, appCred.Key) - if req, err = p.Client.NewRequestNoEnv(http.MethodPost, keyPath, &appCredDetails); err != nil { - return nil, err - } - if res, err = p.Client.Do(req, &appCred); err != nil { - return nil, err + if err := p.printConfig(config, printf, verifyErrors); err != nil { + return errors.Wrapf(err, "generating config") } - cred := &credential{ - Key: appCred.Key, - Secret: appCred.Secret, + if verifyErrors != nil { + os.Exit(1) } - verbosef("credentials created: %v", cred) - return cred, nil + verbosef("provisioning verified OK") + return nil } -func (p *provision) deployInternalProxy(replaceVirtualHosts func(proxyDir string) error, tempDir string, verbosef shared.FormatFn) error { - - customizedZip, err := getCustomizedProxy(tempDir, internalProxyZip, func(proxyDir string) error { +func (p *provision) createAuthorizedClient(config *server.Config) (*http.Client, error) { - // change server locations - calloutFile := filepath.Join(proxyDir, "policies", "Callout.xml") - bytes, err := ioutil.ReadFile(calloutFile) - if err != nil { - return errors.Wrapf(err, "reading file %s", calloutFile) - } - var callout JavaCallout - if err := xml.Unmarshal(bytes, &callout); err != nil { - return errors.Wrapf(err, "unmarshalling %s", calloutFile) + // add authorization to transport + tr := http.DefaultTransport + if config.Tenant.AllowUnverifiedSSLCert { + tr = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - setMgmtURL := false - for i, cp := range callout.Properties { - if cp.Name == "REGION_MAP" { - callout.Properties[i].Value = fmt.Sprintf("DN=%s", p.RuntimeBase) - } - if cp.Name == "MGMT_URL_PREFIX" { - setMgmtURL = true - callout.Properties[i].Value = p.ManagementBase - } - } - if !setMgmtURL { - callout.Properties = append(callout.Properties, - javaCalloutProperty{ - Name: "MGMT_URL_PREFIX", - Value: p.ManagementBase, - }) - } - - writer, err := os.OpenFile(calloutFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0) - if err != nil { - return errors.Wrapf(err, "writing file %s", calloutFile) - } - writer.WriteString(xml.Header) - encoder := xml.NewEncoder(writer) - encoder.Indent("", " ") - err = encoder.Encode(callout) - if err != nil { - return errors.Wrapf(err, "encoding xml to %s", calloutFile) - } - err = writer.Close() - if err != nil { - return errors.Wrapf(err, "closing file %s", calloutFile) - } - - return replaceVirtualHosts(proxyDir) - }) - if err != nil { - return err - } - - return p.checkAndDeployProxy(internalProxyName, customizedZip, verbosef) -} - -type proxyModFunc func(name string) error - -// returns filename of zipped proxy -func getCustomizedProxy(tempDir, name string, modFunc proxyModFunc) (string, error) { - if err := proxies.RestoreAsset(tempDir, name); err != nil { - return "", errors.Wrapf(err, "restoring asset %s", name) - } - zipFile := filepath.Join(tempDir, name) - if modFunc == nil { - return zipFile, nil } - extractDir, err := ioutil.TempDir(tempDir, "proxy") + tr, err := server.AuthorizationRoundTripper(config, tr) if err != nil { - return "", errors.Wrap(err, "creating temp dir") - } - if err := unzipFile(zipFile, extractDir); err != nil { - return "", errors.Wrapf(err, "extracting %s to %s", zipFile, extractDir) - } - - if err := modFunc(filepath.Join(extractDir, "apiproxy")); err != nil { - return "", err - } - - // write zip - customizedZip := filepath.Join(tempDir, "customized.zip") - if err := zipDir(extractDir, customizedZip); err != nil { - return "", errors.Wrapf(err, "zipping dir %s to file %s", extractDir, customizedZip) + return nil, err } - return customizedZip, nil -} - -// hash for key and secret -func newHash() string { - // use crypto seed - var seed int64 - binary.Read(rand.Reader, binary.BigEndian, &seed) - rnd.Seed(seed) - - t := time.Now() - h := sha256.New() - h.Write([]byte(t.String() + string(rnd.Int()))) - str := hex.EncodeToString(h.Sum(nil)) - return str + return &http.Client{ + Timeout: config.Tenant.ClientTimeout, + Transport: tr, + }, nil } -//check if the KVM exists, if it doesn't, create a new one and sets certs for JWT -func (p *provision) getOrCreateKVM(cred *credential, printf shared.FormatFn) error { +func (p *provision) verify(config *server.Config, verbosef shared.FormatFn) error { - kid, keyBytes, jwksBytes, err := p.CreateJWKS(1, printf) + client, err := p.createAuthorizedClient(config) if err != nil { return err } - kvm := apigee.KVM{ - Name: kvmName, - Encrypted: encryptKVM, - Entries: []apigee.Entry{ - { - Name: "private_key", - Value: string(keyBytes), - }, - { - Name: "jwks", - Value: string(jwksBytes), - }, - { - Name: "kid", - Value: kid, - }, - }, - } - - resp, err := p.Client.KVMService.Create(kvm) - if err != nil && (resp == nil || resp.StatusCode != http.StatusConflict) { // http.StatusConflict == already exists - return err - } - if resp.StatusCode == http.StatusConflict { - printf("kvm %s already exists", kvmName) - return nil - } - if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("creating kvm %s, status code: %v", kvmName, resp.StatusCode) - } - printf("kvm %s created", kvmName) - - printf("new private key:\n%s", string(keyBytes)) - printf("new jwks:\n%s", string(jwksBytes)) - - return nil -} - -func (p *provision) createLegacyCredential(printf shared.FormatFn) (*credential, error) { - printf("creating credential...") - cred := &credential{ - Key: newHash(), - Secret: newHash(), + var verifyErrors error + if p.IsLegacySaaS || p.IsOPDK { + verbosef("verifying internal proxy...") + verifyErrors = p.verifyInternalProxy(client, verbosef) } - credentialURL := fmt.Sprintf(legacyCredentialURLFormat, p.InternalProxyURL, p.Org, p.Env) + verbosef("verifying remote-service proxy...") + verifyErrors = multierr.Combine(verifyErrors, p.verifyRemoteServiceProxy(client, verbosef)) - req, err := p.Client.NewRequest(http.MethodPost, credentialURL, cred) - if err != nil { - return nil, err - } - req.URL, err = url.Parse(credentialURL) // override client's munged URL - if err != nil { - return nil, err + if verifyErrors != nil { + shared.Errorf("\nWARNING: Apigee may not be provisioned properly.") + shared.Errorf("Unable to verify proxy endpoint(s). Errors:\n") + for _, err := range multierr.Errors(verifyErrors) { + shared.Errorf(" %s", err) + } + shared.Errorf("\n") } - resp, err := p.Client.Do(req, nil) - if err != nil { - return nil, err - } - if resp.StatusCode > 299 { - return nil, fmt.Errorf("creating credential, status: %d", resp.StatusCode) - } - printf("credential created") - return cred, nil + return verifyErrors } -/* -# Example ConfigMap for apigee-remote-service-envoy configuration in SaaS. -# You must update the tenant values appropriately. -# Note: Alternatively, you can use the generated config from the CLI directly. -# Direct the output to `config.yaml` and run the follow command on it: -# `kubectl -n apigee create configmap apigee-remote-service-envoy --from-file=config.yaml` -apiVersion: v1 -kind: ConfigMap -metadata: - name: apigee-remote-service-envoy - namespace: apigee -data: - config.yaml: | - xxxx... -*/ -func (p *provision) printConfig(cred *credential, printf shared.FormatFn, verifyErrors error) error { - - config := server.Config{ +func (p *provision) createConfig(cred *keySecret) *server.Config { + config := &server.Config{ Tenant: server.TenantConfig{ InternalAPI: p.InternalProxyURL, RemoteServiceAPI: p.RemoteServiceProxyURL, OrgName: p.Org, EnvName: p.Env, - Key: cred.Key, - Secret: cred.Secret, AllowUnverifiedSSLCert: p.InsecureSkipVerify, }, } + if cred != nil { + config.Tenant.Key = cred.Key + config.Tenant.Secret = cred.Secret + } + if p.IsGCPManaged { config.Tenant.InternalAPI = "" // no internal API for GCP config.Analytics.CollectionInterval = 10 * time.Second - // assumes the same mesh and tls files are mounted properly - fluentdNS := p.namespace - if fluentdNS == "" { - fluentdNS = "apigee" - } - config.Analytics.FluentdEndpoint = fmt.Sprintf(fluentdInternalFormat, p.Org, p.Env, fluentdNS) + config.Analytics.FluentdEndpoint = fmt.Sprintf(fluentdInternalFormat, p.Org, p.Env, p.Namespace) config.Analytics.TLS.CAFile = defaultApigeeCAFile config.Analytics.TLS.CertFile = defaultApigeeCertFile config.Analytics.TLS.KeyFile = defaultApigeeKeyFile @@ -666,6 +361,10 @@ func (p *provision) printConfig(cred *credential, printf shared.FormatFn, verify config.Analytics.LegacyEndpoint = true } + return config +} + +func (p *provision) printConfig(config *server.Config, printf shared.FormatFn, verifyErrors error) error { // encode config var yamlBuffer bytes.Buffer yamlEncoder := yaml.NewEncoder(&yamlBuffer) @@ -676,28 +375,13 @@ func (p *provision) printConfig(cred *credential, printf shared.FormatFn, verify } configYAML := yamlBuffer.String() - print := func(config string) error { - printf("# Configuration for apigee-remote-service-envoy") - printf("# generated by apigee-remote-service-cli provision on %s", time.Now().Format("2006-01-02 15:04:05")) - if verifyErrors != nil { - printf("# WARNING: verification of provision failed. May not be valid.") - } - printf(config) - return nil - } - - if p.namespace == "" { - return print(configYAML) - } - - // ConfigMap data := map[string]string{"config.yaml": configYAML} - crd := shared.KubernetesCRD{ + configCRD := server.ConfigMapCRD{ APIVersion: "v1", Kind: "ConfigMap", - Metadata: shared.Metadata{ + Metadata: server.Metadata{ Name: "apigee-remote-service-envoy", - Namespace: p.namespace, + Namespace: p.Namespace, }, Data: data, } @@ -705,172 +389,82 @@ func (p *provision) printConfig(cred *credential, printf shared.FormatFn, verify yamlBuffer.Reset() yamlEncoder = yaml.NewEncoder(&yamlBuffer) yamlEncoder.SetIndent(2) - err = yamlEncoder.Encode(crd) + err = yamlEncoder.Encode(configCRD) if err != nil { return err } - configMapYAML := yamlBuffer.String() - - return print(configMapYAML) -} -func (p *provision) checkAndDeployProxy(name, file string, printf shared.FormatFn) error { - printf("checking if proxy %s deployment exists...", name) - var oldRev *apigee.Revision - var err error + // secret for IsGCPManaged if p.IsGCPManaged { - oldRev, err = p.Client.Proxies.GetGCPDeployedRevision(name) - } else { - oldRev, err = p.Client.Proxies.GetDeployedRevision(name) - } - if err != nil { - return err - } - if oldRev != nil { - if p.forceProxyInstall { - printf("replacing proxy %s revision %s in %s", name, oldRev, p.Env) - } else { - printf("proxy %s revision %s already deployed to %s", name, oldRev, p.Env) - return nil - } - } + privateKeyBytes := pem.EncodeToMemory(&pem.Block{Type: server.PEMKeyType, + Bytes: x509.MarshalPKCS1PrivateKey(config.Tenant.PrivateKey)}) - printf("checking proxy %s status...", name) - var resp *apigee.Response - proxy, resp, err := p.Client.Proxies.Get(name) - if err != nil && (resp == nil || resp.StatusCode != 404) { - return err - } - - return p.importAndDeployProxy(name, proxy, oldRev, file, printf) -} - -func (p *provision) importAndDeployProxy(name string, proxy *apigee.Proxy, oldRev *apigee.Revision, file string, printf shared.FormatFn) error { - var newRev apigee.Revision = 1 - if proxy != nil && len(proxy.Revisions) > 0 { - sort.Sort(apigee.RevisionSlice(proxy.Revisions)) - newRev = proxy.Revisions[len(proxy.Revisions)-1] + 1 - printf("proxy %s exists. highest revision is: %d", name, newRev-1) - } - - // create a new client to avoid dumping the proxy binary to stdout during Import - noDebugClient := p.Client - if p.Verbose { - opts := *p.ClientOpts - opts.Debug = false - var err error - noDebugClient, err = apigee.NewEdgeClient(&opts) + // create CRD for secret + kidProp := fmt.Sprintf(server.SecretKIDFormat, config.Tenant.PrivateKeyID) + jwksBytes, err := json.Marshal(config.Tenant.JWKS) if err != nil { return err } - } - - printf("creating new proxy %s revision: %d...", name, newRev) - _, res, err := noDebugClient.Proxies.Import(name, file) - if res != nil { - defer res.Body.Close() - } - if err != nil { - return errors.Wrapf(err, "importing proxy %s", name) - } - - if oldRev != nil && !p.IsGCPManaged { // it's not necessary to undeploy first with GCP - printf("undeploying proxy %s revision %d on env %s...", - name, oldRev, p.Env) - _, res, err = p.Client.Proxies.Undeploy(name, p.Env, *oldRev) - if res != nil { - defer res.Body.Close() - } - if err != nil { - return errors.Wrapf(err, "undeploying proxy %s", name) + secretData := map[string]string{ + server.SecretJKWSKey: base64.StdEncoding.EncodeToString(jwksBytes), + server.SecretPrivateKey: base64.StdEncoding.EncodeToString(privateKeyBytes), + server.SecretKIDKey: base64.StdEncoding.EncodeToString([]byte(kidProp)), } - } - if !p.IsGCPManaged { - cache := apigee.Cache{ - Name: cacheName, + secretCRD := server.SecretCRD{ + APIVersion: "v1", + Kind: "Secret", + Type: "Opaque", + Metadata: server.Metadata{ + Name: fmt.Sprintf(policySecretNameFormat, p.Org, p.Env), + Namespace: p.Namespace, + }, + Data: secretData, } - res, err = p.Client.CacheService.Create(cache) - if err != nil && (res == nil || res.StatusCode != http.StatusConflict) { // http.StatusConflict == already exists + + err = yamlEncoder.Encode(secretCRD) + if err != nil { return err } - if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusConflict { - return fmt.Errorf("creating cache %s, status code: %v", cacheName, res.StatusCode) - } - if res.StatusCode == http.StatusConflict { - printf("cache %s already exists", cacheName) - } else { - printf("cache %s created", cacheName) - } } - printf("deploying proxy %s revision %d to env %s...", name, newRev, p.Env) - _, res, err = p.Client.Proxies.Deploy(name, p.Env, newRev) - if res != nil { - defer res.Body.Close() + platform := "GCP" + if p.IsLegacySaaS { + platform = "SaaS" } - if err != nil { - return errors.Wrapf(err, "deploying proxy %s", name) - } - - return nil -} - -// verify POST internalProxyURL/analytics/organization/%s/environment/%s -// verify POST internalProxyURL/quotas/organization/%s/environment/%s -func (p *provision) verifyInternalProxy(auth *apigee.EdgeAuth, printf shared.FormatFn) error { - var verifyErrors error - - var req *http.Request - var err error - var res *apigee.Response if p.IsOPDK { - analyticsURL := fmt.Sprintf(legacyAnalyticURLFormat, p.InternalProxyURL, p.Org, p.Env) - req, err = http.NewRequest(http.MethodPost, analyticsURL, strings.NewReader("{}")) - } else { - analyticsURL := fmt.Sprintf(analyticsURLFormat, p.InternalProxyURL, p.Org, p.Env) - req, err = http.NewRequest(http.MethodGet, analyticsURL, nil) - q := req.URL.Query() - q.Add("tenant", fmt.Sprintf("%s~%s", p.Org, p.Env)) - q.Add("relative_file_path", "fake") - q.Add("file_content_type", "application/x-gzip") - q.Add("encrypt", "true") - req.URL.RawQuery = q.Encode() - } - if err != nil { - auth.ApplyTo(req) - res, err = p.Client.Do(req, nil) - if res != nil { - defer res.Body.Close() - } + platform = "OPDK" } - if err != nil { - verifyErrors = multierr.Append(verifyErrors, err) + + printf("# Configuration for apigee-remote-service-envoy (platform: %s)", platform) + printf("# generated by apigee-remote-service-cli provision on %s", time.Now().Format("2006-01-02 15:04:05")) + if verifyErrors != nil { + printf("# WARNING: verification of provision failed. May not be valid.") } + printf(yamlBuffer.String()) - return verifyErrors + return nil } // verify GET RemoteServiceProxyURL/certs // verify GET RemoteServiceProxyURL/products // verify POST RemoteServiceProxyURL/verifyApiKey // verify POST RemoteServiceProxyURL/quotas -func (p *provision) verifyRemoteServiceProxy(auth *apigee.EdgeAuth, printf shared.FormatFn) error { +func (p *provision) verifyRemoteServiceProxy(client *http.Client, printf shared.FormatFn) error { verifyGET := func(targetURL string) error { req, err := http.NewRequest(http.MethodGet, targetURL, nil) if err != nil { return errors.Wrapf(err, "creating request") } - auth.ApplyTo(req) - res, err := p.Client.Do(req, nil) + res, err := client.Do(req) if res != nil { defer res.Body.Close() } return err } - var res *apigee.Response + var res *http.Response var verifyErrors error certsURL := fmt.Sprintf(certsURLFormat, p.RemoteServiceProxyURL) err := verifyGET(certsURL) @@ -881,17 +475,15 @@ func (p *provision) verifyRemoteServiceProxy(auth *apigee.EdgeAuth, printf share verifyErrors = multierr.Append(verifyErrors, err) verifyAPIKeyURL := fmt.Sprintf(verifyAPIKeyURLFormat, p.RemoteServiceProxyURL) - body := fmt.Sprintf(`{ "apiKey": "%s" }`, auth.Username) - req, err := http.NewRequest(http.MethodPost, verifyAPIKeyURL, strings.NewReader(body)) + req, err := http.NewRequest(http.MethodPost, verifyAPIKeyURL, strings.NewReader(`{ "apiKey": "x" }`)) if err == nil { req.Header.Add("Content-Type", "application/json") - auth.ApplyTo(req) - res, err = p.Client.Do(req, nil) + res, err = client.Do(req) if res != nil { defer res.Body.Close() } } - if err != nil && (res == nil || res.StatusCode != 401) { // 401 is ok, we don't actually have a valid api key to test + if err != nil && (res == nil || res.StatusCode != 401) { // 401 is ok, we didn't use a valid api key verifyErrors = multierr.Append(verifyErrors, err) } @@ -899,8 +491,7 @@ func (p *provision) verifyRemoteServiceProxy(auth *apigee.EdgeAuth, printf share req, err = http.NewRequest(http.MethodPost, quotasURL, strings.NewReader("{}")) if err == nil { req.Header.Add("Content-Type", "application/json") - auth.ApplyTo(req) - res, err = p.Client.Do(req, nil) + res, err = client.Do(req) if res != nil { defer res.Body.Close() } @@ -912,162 +503,7 @@ func (p *provision) verifyRemoteServiceProxy(auth *apigee.EdgeAuth, printf share return verifyErrors } -func unzipFile(src, dest string) error { - r, err := zip.OpenReader(src) - if err != nil { - return err - } - defer r.Close() - - os.MkdirAll(dest, 0755) - - extract := func(f *zip.File) error { - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - - path := filepath.Join(dest, f.Name) - - if f.FileInfo().IsDir() { - os.MkdirAll(path, f.Mode()) - } else { - os.MkdirAll(filepath.Dir(path), f.Mode()) - f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return err - } - defer f.Close() - - _, err = io.Copy(f, rc) - if err != nil { - return err - } - } - return nil - } - - for _, f := range r.File { - err := extract(f) - if err != nil { - return err - } - } - - return nil -} - -func zipDir(source, file string) error { - zipFile, err := os.Create(file) - if err != nil { - return err - } - defer zipFile.Close() - - w := zip.NewWriter(zipFile) - - var addFiles func(w *zip.Writer, fileBase, zipBase string) error - addFiles = func(w *zip.Writer, fileBase, zipBase string) error { - files, err := ioutil.ReadDir(fileBase) - if err != nil { - return err - } - - for _, file := range files { - fqName := filepath.Join(fileBase, file.Name()) - zipFQName := filepath.Join(zipBase, file.Name()) - - if file.IsDir() { - addFiles(w, fqName, zipFQName) - continue - } - - bytes, err := ioutil.ReadFile(fqName) - if err != nil { - return err - } - f, err := w.Create(zipFQName) - if err != nil { - return err - } - if _, err = f.Write(bytes); err != nil { - return err - } - } - return nil - } - - err = addFiles(w, source, "") - if err != nil { - return err - } - - return w.Close() -} - -type credential struct { +type keySecret struct { Key string `json:"key"` Secret string `json:"secret"` } - -// JavaCallout must be capitalized to ensure correct generation -type JavaCallout struct { - Name string `xml:"name,attr"` - DisplayName, ClassName, ResourceURL string - Properties []javaCalloutProperty `xml:"Properties>Property"` -} - -type javaCalloutProperty struct { - Name string `xml:"name,attr"` - Value string `xml:",chardata"` -} - -type connection struct { - Address string `yaml:"address"` -} - -type apiProduct struct { - Name string `json:"name,omitempty"` - DisplayName string `json:"displayName,omitempty"` - ApprovalType string `json:"approvalType,omitempty"` - Attributes []attribute `json:"attributes,omitempty"` - Description string `json:"description,omitempty"` - APIResources []string `json:"apiResources,omitempty"` - Environments []string `json:"environments,omitempty"` - Proxies []string `json:"proxies,omitempty"` -} - -type attribute struct { - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` -} - -type developer struct { - Email string `json:"email,omitempty"` - FirstName string `json:"firstName,omitempty"` - LastName string `json:"lastName,omitempty"` - UserName string `json:"userName,omitempty"` -} - -type application struct { - Name string `json:"name,omitempty"` - APIProducts []string `json:"apiProducts,omitempty"` - Credentials []appCredential `json:"credentials,omitempty"` -} - -type appCredential struct { - Key string `json:"consumerKey,omitempty"` - Secret string `json:"consumerSecret,omitempty"` -} - -type rotateRequest struct { - PrivateKey string `json:"private_key"` - Certificate string `json:"certificate"` - KeyID string `json:"kid"` -} - -type appCredentialDetails struct { - APIProducts []string `json:"apiProducts,omitempty"` - Attributes []attribute `json:"attributes,omitempty"` -} diff --git a/cmd/provision/provision_test.go b/cmd/provision/provision_test.go index 9297791..390bf7b 100644 --- a/cmd/provision/provision_test.go +++ b/cmd/provision/provision_test.go @@ -19,7 +19,6 @@ import ( "net/http/httptest" "testing" - "github.com/apigee/apigee-remote-service-cli/apigee" "github.com/apigee/apigee-remote-service-cli/shared" ) @@ -32,32 +31,35 @@ func TestVerifyRemoteServiceProxyTLS(t *testing.T) { count++ })) - auth := &apigee.EdgeAuth{} - // try without InsecureSkipVerify p := &provision{ RootArgs: &shared.RootArgs{ RuntimeBase: ts.URL, Token: "-", InsecureSkipVerify: false, + IsLegacySaaS: true, }, - verifyOnly: true, } if err := p.Resolve(false, false); err != nil { t.Fatal(err) } - if err := p.verifyRemoteServiceProxy(auth, shared.Printf); err == nil { + client, err := p.createAuthorizedClient(p.createConfig(nil)) + if err != nil { + t.Fatal(err) + } + if err := p.verifyRemoteServiceProxy(client, shared.Printf); err == nil { t.Errorf("got nil error, want TLS failure") } // try with InsecureSkipVerify p.InsecureSkipVerify = true + client, err = p.createAuthorizedClient(p.createConfig(nil)) if err := p.Resolve(false, false); err != nil { t.Fatal(err) } - if err := p.verifyRemoteServiceProxy(auth, shared.Printf); err != nil { - t.Errorf("unexpected error: %v", err) + if err := p.verifyRemoteServiceProxy(client, shared.Printf); err != nil { + t.Errorf("unexpected: %v", err) } if count != 4 { t.Errorf("got %d, want %d", count, 4) diff --git a/cmd/provision/proxy.go b/cmd/provision/proxy.go new file mode 100644 index 0000000..3fe5413 --- /dev/null +++ b/cmd/provision/proxy.go @@ -0,0 +1,260 @@ +// Copyright 2020 Google LLC +// +// 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 provision + +import ( + "archive/zip" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "sort" + + "github.com/apigee/apigee-remote-service-cli/apigee" + "github.com/apigee/apigee-remote-service-cli/proxies" + "github.com/apigee/apigee-remote-service-cli/shared" + "github.com/pkg/errors" +) + +type proxyModFunc func(name string) error + +// returns filename of zipped proxy +func getCustomizedProxy(tempDir, name string, modFunc proxyModFunc) (string, error) { + if err := proxies.RestoreAsset(tempDir, name); err != nil { + return "", errors.Wrapf(err, "restoring asset %s", name) + } + zipFile := filepath.Join(tempDir, name) + if modFunc == nil { + return zipFile, nil + } + + extractDir, err := ioutil.TempDir(tempDir, "proxy") + if err != nil { + return "", errors.Wrap(err, "creating temp dir") + } + if err := unzipFile(zipFile, extractDir); err != nil { + return "", errors.Wrapf(err, "extracting %s to %s", zipFile, extractDir) + } + + if err := modFunc(filepath.Join(extractDir, "apiproxy")); err != nil { + return "", err + } + + // write zip + customizedZip := filepath.Join(tempDir, "customized.zip") + if err := zipDir(extractDir, customizedZip); err != nil { + return "", errors.Wrapf(err, "zipping dir %s to file %s", extractDir, customizedZip) + } + + return customizedZip, nil +} + +func (p *provision) checkAndDeployProxy(name, file string, printf shared.FormatFn) error { + printf("checking if proxy %s deployment exists...", name) + var oldRev *apigee.Revision + var err error + if p.IsGCPManaged { + oldRev, err = p.ApigeeClient.Proxies.GetGCPDeployedRevision(name) + } else { + oldRev, err = p.ApigeeClient.Proxies.GetDeployedRevision(name) + } + if err != nil { + return err + } + if oldRev != nil { + if p.forceProxyInstall { + printf("replacing proxy %s revision %s in %s", name, oldRev, p.Env) + } else { + printf("proxy %s revision %s already deployed to %s", name, oldRev, p.Env) + return nil + } + } + + printf("checking proxy %s status...", name) + var resp *apigee.Response + proxy, resp, err := p.ApigeeClient.Proxies.Get(name) + if err != nil && (resp == nil || resp.StatusCode != 404) { + return err + } + + return p.importAndDeployProxy(name, proxy, oldRev, file, printf) +} + +func (p *provision) importAndDeployProxy(name string, proxy *apigee.Proxy, oldRev *apigee.Revision, file string, printf shared.FormatFn) error { + var newRev apigee.Revision = 1 + if proxy != nil && len(proxy.Revisions) > 0 { + sort.Sort(apigee.RevisionSlice(proxy.Revisions)) + newRev = proxy.Revisions[len(proxy.Revisions)-1] + 1 + printf("proxy %s exists. highest revision is: %d", name, newRev-1) + } + + // create a new client to avoid dumping the proxy binary to stdout during Import + noDebugClient := p.ApigeeClient + if p.Verbose { + opts := *p.ClientOpts + opts.Debug = false + var err error + noDebugClient, err = apigee.NewEdgeClient(&opts) + if err != nil { + return err + } + } + + printf("creating new proxy %s revision: %d...", name, newRev) + _, res, err := noDebugClient.Proxies.Import(name, file) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return errors.Wrapf(err, "importing proxy %s", name) + } + + if oldRev != nil && !p.IsGCPManaged { // it's not necessary to undeploy first with GCP + printf("undeploying proxy %s revision %d on env %s...", + name, oldRev, p.Env) + _, res, err = p.ApigeeClient.Proxies.Undeploy(name, p.Env, *oldRev) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return errors.Wrapf(err, "undeploying proxy %s", name) + } + } + + if !p.IsGCPManaged { + cache := apigee.Cache{ + Name: cacheName, + } + res, err = p.ApigeeClient.CacheService.Create(cache) + if err != nil && (res == nil || res.StatusCode != http.StatusConflict) { // http.StatusConflict == already exists + return err + } + if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusConflict { + return fmt.Errorf("creating cache %s, status code: %v", cacheName, res.StatusCode) + } + if res.StatusCode == http.StatusConflict { + printf("cache %s already exists", cacheName) + } else { + printf("cache %s created", cacheName) + } + } + + printf("deploying proxy %s revision %d to env %s...", name, newRev, p.Env) + _, res, err = p.ApigeeClient.Proxies.Deploy(name, p.Env, newRev) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return errors.Wrapf(err, "deploying proxy %s", name) + } + + return nil +} + +func unzipFile(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + os.MkdirAll(dest, 0755) + + extract := func(f *zip.File) error { + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + path := filepath.Join(dest, f.Name) + + if f.FileInfo().IsDir() { + os.MkdirAll(path, f.Mode()) + } else { + os.MkdirAll(filepath.Dir(path), f.Mode()) + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, rc) + if err != nil { + return err + } + } + return nil + } + + for _, f := range r.File { + err := extract(f) + if err != nil { + return err + } + } + + return nil +} + +func zipDir(source, file string) error { + zipFile, err := os.Create(file) + if err != nil { + return err + } + defer zipFile.Close() + + w := zip.NewWriter(zipFile) + + var addFiles func(w *zip.Writer, fileBase, zipBase string) error + addFiles = func(w *zip.Writer, fileBase, zipBase string) error { + files, err := ioutil.ReadDir(fileBase) + if err != nil { + return err + } + + for _, file := range files { + fqName := filepath.Join(fileBase, file.Name()) + zipFQName := filepath.Join(zipBase, file.Name()) + + if file.IsDir() { + addFiles(w, fqName, zipFQName) + continue + } + + bytes, err := ioutil.ReadFile(fqName) + if err != nil { + return err + } + f, err := w.Create(zipFQName) + if err != nil { + return err + } + if _, err = f.Write(bytes); err != nil { + return err + } + } + return nil + } + + err = addFiles(w, source, "") + if err != nil { + return err + } + + return w.Close() +} diff --git a/cmd/root.go b/cmd/root.go index 6093053..b76605a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -75,7 +75,7 @@ func version(rootArgs *shared.RootArgs, printf shared.FormatFn) *cobra.Command { return errors.Wrap(err, "error creating request") } var version versionResponse - resp, err := rootArgs.Client.Do(req, &version) + resp, err := rootArgs.ApigeeClient.Do(req, &version) if err != nil { if resp == nil { return errors.Wrap(err, "error getting proxy version") diff --git a/cmd/token/token.go b/cmd/token/token.go index 337184c..1893cf7 100644 --- a/cmd/token/token.go +++ b/cmd/token/token.go @@ -16,7 +16,6 @@ package token import ( "bytes" - "encoding/base64" "encoding/json" "fmt" "io" @@ -31,7 +30,6 @@ import ( "github.com/lestrrat-go/jwx/jwt" "github.com/pkg/errors" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) const ( @@ -43,11 +41,7 @@ const ( commonName = "apigee-remote-service" orgName = "Google LLC" - // hybrid forces specific file extensions! https://docs.apigee.com/hybrid/v1.2/k8s-secrets - jwksSecretKey = "remote-service.crt" // obviously not a .crt, but hybrid will treat as blob - keySecretKey = "remote-service.key" - kidSecretKey = "remote-service.properties" - kidSecretPropFormat = "kid=%s" // KID + secretKIDFormat = "kid=%s" ) type token struct { @@ -75,7 +69,6 @@ func Cmd(rootArgs *shared.RootArgs, printf shared.FormatFn) *cobra.Command { c.AddCommand(cmdCreateToken(t, printf)) c.AddCommand(cmdInspectToken(t, printf)) c.AddCommand(cmdRotateCert(t, printf)) - c.AddCommand(cmdCreateSecret(t, printf)) return c } @@ -127,29 +120,6 @@ func cmdInspectToken(t *token, printf shared.FormatFn) *cobra.Command { return c } -func cmdCreateSecret(t *token, printf shared.FormatFn) *cobra.Command { - c := &cobra.Command{ - Use: "create-secret", - Short: "create Kubernetes CRDs for JWT tokens (hybrid only)", - Long: "Creates a new Kubernetes Secret CRD for JWT tokens, maintains prior cert(s) for rotation.", - Args: cobra.NoArgs, - - Run: func(cmd *cobra.Command, _ []string) { - if t.ServerConfig != nil { - t.clientID = t.ServerConfig.Tenant.Key - t.clientSecret = t.ServerConfig.Tenant.Secret - } - - t.createSecret(printf) - }, - } - - c.Flags().StringVarP(&t.namespace, "namespace", "n", "apigee", "emit Secret in the specified namespace") - c.Flags().IntVarP(&t.truncate, "truncate", "", 2, "number of certs to keep in jwks") - - return c -} - func cmdRotateCert(t *token, printf shared.FormatFn) *cobra.Command { c := &cobra.Command{ Use: "rotate-cert", @@ -209,7 +179,7 @@ func (t *token) createToken(printf shared.FormatFn) (string, error) { req.Header.Set("Accept", "application/json") var tokenRes tokenResponse - resp, err := t.Client.Do(req, &tokenRes) + resp, err := t.ApigeeClient.Do(req, &tokenRes) if err != nil { return "", errors.Wrap(err, "creating token") } @@ -298,7 +268,7 @@ func (t *token) rotateCert(printf shared.FormatFn) error { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - resp, err := t.Client.Do(req, nil) + resp, err := t.ApigeeClient.Do(req, nil) if err != nil { if resp != nil && resp.StatusCode == 401 { return errors.Wrap(err, "authentication failed, check your key and secret") @@ -314,52 +284,6 @@ func (t *token) rotateCert(printf shared.FormatFn) error { return nil } -// createSecret is called by `token create-secret` -func (t *token) createSecret(printf shared.FormatFn) error { - var verbosef = shared.NoPrintf - if t.Verbose { - verbosef = printf - } - - kid, keyBytes, jwksBytes, err := t.CreateJWKS(t.truncate, verbosef) - if err != nil { - return err - } - - // create CRD for secret - kidProp := fmt.Sprintf(kidSecretPropFormat, kid) - data := map[string]string{ - jwksSecretKey: base64.StdEncoding.EncodeToString(jwksBytes), - keySecretKey: base64.StdEncoding.EncodeToString(keyBytes), - kidSecretKey: base64.StdEncoding.EncodeToString([]byte(kidProp)), - } - - crd := shared.KubernetesCRD{ - APIVersion: "v1", - Kind: "Secret", - Type: "Opaque", - Metadata: shared.Metadata{ - Name: fmt.Sprintf(policySecretNameFormat, t.Org, t.Env), - Namespace: t.namespace, - }, - Data: data, - } - - // encode as YAML - var yamlBuffer bytes.Buffer - yamlEncoder := yaml.NewEncoder(&yamlBuffer) - yamlEncoder.SetIndent(2) - err = yamlEncoder.Encode(crd) - if err != nil { - return errors.Wrap(err, "encoding YAML") - } - - printf("# Secret for apigee-remote-service-envoy") - printf("# generated by apigee-remote-service-cli provision on %s", time.Now().Format("2006-01-02 15:04:05")) - printf(yamlBuffer.String()) - return nil -} - type rotateRequest struct { PrivateKey string `json:"private_key"` JWKS string `json:"jwks"` diff --git a/go.mod b/go.mod index c010621..be214e6 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ go 1.13 // replace github.com/apigee/apigee-remote-service-envoy => ../apigee-remote-service-envoy require ( - github.com/apigee/apigee-remote-service-envoy v1.0.0-beta.3.0.20200618203739-37ac2b14898d - github.com/apigee/apigee-remote-service-golib v1.0.0-beta.3.0.20200618203547-765ca9c46796 + github.com/apigee/apigee-remote-service-envoy v1.0.0-beta.3.0.20200629162443-e791f23f4f50 + github.com/apigee/apigee-remote-service-golib v1.0.0-beta.3.0.20200626215319-903f25c9a17d github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d github.com/google/go-querystring v1.0.0 github.com/lestrrat-go/jwx v1.0.2 diff --git a/go.sum b/go.sum index f8e3302..8586ddc 100644 --- a/go.sum +++ b/go.sum @@ -3,13 +3,15 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/apigee/apigee-remote-service-envoy v1.0.0-beta.3.0.20200618203739-37ac2b14898d h1:vYnJE2Np+o+q4ZcOu2aWAOH+IYs1JH8JJKpjQKLbxmw= -github.com/apigee/apigee-remote-service-envoy v1.0.0-beta.3.0.20200618203739-37ac2b14898d/go.mod h1:ywqRCY4XF4SUoWEG4+Rns22EwzTQ7NQm59KpFqlW54Y= -github.com/apigee/apigee-remote-service-golib v1.0.0-beta.3.0.20200618203547-765ca9c46796 h1:j+J2shJQvtiL3+wR1i9YtgApVJxS6GDshSLLq8CCQdc= -github.com/apigee/apigee-remote-service-golib v1.0.0-beta.3.0.20200618203547-765ca9c46796/go.mod h1:ppZ7rYK/RjFYZlQVDEq6xiemo1zg7OTUILeT9RFxq1E= +github.com/apigee/apigee-remote-service-envoy v1.0.0-beta.3.0.20200629162443-e791f23f4f50 h1:lzPBznFXHtsj9Z4LA/Sbr3GYfgpvYkibzE+ZFGF4aRI= +github.com/apigee/apigee-remote-service-envoy v1.0.0-beta.3.0.20200629162443-e791f23f4f50/go.mod h1:wkSo8RK8M1p+jhlLIdeQ/tPYNipo4QRuyx99laxsuEw= +github.com/apigee/apigee-remote-service-golib v1.0.0-beta.3.0.20200626215319-903f25c9a17d h1:wO4kMYNLu7P663W1+rJEUqxDUwILrMY6lKdWRg6Kfmw= +github.com/apigee/apigee-remote-service-golib v1.0.0-beta.3.0.20200626215319-903f25c9a17d/go.mod h1:ppZ7rYK/RjFYZlQVDEq6xiemo1zg7OTUILeT9RFxq1E= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -112,6 +114,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -149,6 +152,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U= github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -170,6 +175,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -296,6 +302,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/shared/jwks.go b/shared/jwks.go index 9611712..d941d2e 100644 --- a/shared/jwks.go +++ b/shared/jwks.go @@ -32,64 +32,75 @@ import ( const ( certsURLFormat = "%s/certs" // RemoteServiceProxyURL certKeyLength = 2048 + pemType = "RSA PRIVATE KEY" ) -// CreateJWKS returns keyID, private key, jwks, error -func (r *RootArgs) CreateJWKS(truncate int, verbosef FormatFn) (keyID string, pkBytes, jwksBytes []byte, err error) { - jwkSet := &jwk.Set{} - verbosef("retrieving existing certificates...") - fail := func(err error, msg string) (string, []byte, []byte, error) { - return "", nil, nil, errors.Wrap(err, msg) - } - - if truncate > 1 { // if 1, just skip old stuff - // get old jwks - jwksURL := fmt.Sprintf(certsURLFormat, r.RemoteServiceProxyURL) - jwkSet, err = jwk.FetchHTTP(jwksURL) - if err != nil { - return fail(err, "fetching jwks") - } - jwksBytes, err := json.Marshal(jwkSet) - if err != nil { - return fail(err, "marshalling JSON") - } - verbosef("old jkws...\n%s", string(jwksBytes)) - } - - // gen kid, key +// CreateNewKey returns keyID, private key, jwks, error +func (r *RootArgs) CreateNewKey() (keyID string, privateKey *rsa.PrivateKey, jwks *jwk.Set, err error) { keyID = time.Now().Format(time.RFC3339) - privateKey, err := rsa.GenerateKey(rand.Reader, certKeyLength) + privateKey, err = rsa.GenerateKey(rand.Reader, certKeyLength) if err != nil { - return fail(err, "generating key") + return } - // update jwks with new key - jwkKey, err := jwk.New(&privateKey.PublicKey) + var jwkKey jwk.Key + jwkKey, err = jwk.New(&privateKey.PublicKey) if err != nil { - return fail(err, "generating jwks") + return } jwkKey.Set(jwk.KeyIDKey, keyID) jwkKey.Set(jwk.AlgorithmKey, jwa.RS256) - jwkSet.Keys = append(jwkSet.Keys, jwkKey) + jwks = &jwk.Set{ + Keys: []jwk.Key{jwkKey}, + } + return +} + +// RotateJKWS returns a jwk.Set including passed keys and keys from existing endpoint, +// sorted by key ID and truncated per the truncate param. +func (r *RootArgs) RotateJKWS(jwks *jwk.Set, truncate int) (*jwk.Set, error) { + + keys := jwks.Keys + + if truncate > 1 { // if 1, just skip getting old + var oldJWKS *jwk.Set + var err error + certsURL := fmt.Sprintf(certsURLFormat, r.RemoteServiceProxyURL) + if oldJWKS, err = jwk.FetchHTTP(certsURL); err != nil { + return nil, errors.Wrapf(err, "retrieving JWKs from: %s", certsURL) + } + keys = append(keys, oldJWKS.Keys...) + } - // sort ascending and truncate - sort.Sort(sort.Reverse(byKID(jwkSet.Keys))) - if truncate > 0 { - jwkSet.Keys = jwkSet.Keys[:truncate] + sort.Sort(sort.Reverse(byKID(keys))) + if truncate > 0 && len(keys) > truncate { + keys = keys[:truncate] } - jwksBytes, err = json.Marshal(jwkSet) - if err != nil { - return fail(err, "marshalling JSON") + return &jwk.Set{Keys: keys}, nil +} + +// CreateJWKS returns keyID, private key, jwks, error +func (r *RootArgs) CreateJWKS(truncate int, verbosef FormatFn) (keyID string, pkBytes, jwksBytes []byte, err error) { + + var privateKey *rsa.PrivateKey + var jwks *jwk.Set + if keyID, privateKey, jwks, err = r.CreateNewKey(); err != nil { + return + } + + if jwks, err = r.RotateJKWS(jwks, truncate); err != nil { + return } - verbosef("new jkws...\n%s", string(jwksBytes)) - // get private key bytes - pkBytes = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) + if jwksBytes, err = json.Marshal(jwks); err != nil { + return + } + verbosef("new jkws...\n%s", string(jwksBytes)) - return keyID, pkBytes, jwksBytes, nil + pkBytes = pem.EncodeToMemory(&pem.Block{Type: pemType, Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) + return } type byKID []jwk.Key diff --git a/shared/shared.go b/shared/shared.go index 0686b57..7361519 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/url" "os" "reflect" @@ -28,7 +27,6 @@ import ( "github.com/apigee/apigee-remote-service-cli/testutil" "github.com/apigee/apigee-remote-service-envoy/server" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) const ( @@ -44,9 +42,6 @@ const ( // RuntimeBaseFormat is a format for base of the organization runtime URL (legacy SaaS and OPDK) RuntimeBaseFormat = "https://%s-%s.apigee.net" - // LegacySaaSInternalBase is the internal API used for auth and analytics - LegacySaaSInternalBase = "https://istioservices.apigee.net/edgemicro" - internalProxyURLFormat = "%s://istioservices.%s/edgemicro" // runtime scheme, runtime domain (legacy SaaS and OPDK) internalProxyURLFormatOPDK = "%s/edgemicro" // runtimeBase remoteServicePath = "/remote-service" @@ -79,13 +74,14 @@ type RootArgs struct { IsGCPManaged bool ConfigPath string InsecureSkipVerify bool + Namespace string ServerConfig *server.Config // config loaded from ConfigPath // the following is derived in Resolve() InternalProxyURL string RemoteServiceProxyURL string - Client *apigee.EdgeClient + ApigeeClient *apigee.EdgeClient ClientOpts *apigee.EdgeClientOptions } @@ -187,7 +183,7 @@ func (r *RootArgs) Resolve(skipAuth, requireRuntime bool) error { } var err error - r.Client, err = apigee.NewEdgeClient(r.ClientOpts) + r.ApigeeClient, err = apigee.NewEdgeClient(r.ClientOpts) if err != nil { if strings.Contains(err.Error(), ".netrc") { // no .netrc and no auth baseURL, err := url.Parse(r.ManagementBase) @@ -261,39 +257,33 @@ func (r *RootArgs) loadConfig() error { return nil } - yamlFile, err := ioutil.ReadFile(r.ConfigPath) + r.ServerConfig = &server.Config{} + err := r.ServerConfig.Load(r.ConfigPath, "") if err != nil { return err } - // load as either CRD or raw config - cm := &KubernetesCRD{} - c := &server.Config{} - err = yaml.Unmarshal(yamlFile, cm) - if err == nil { - if cm.Data == nil { - err = yaml.Unmarshal(yamlFile, c) - } else { - err = yaml.Unmarshal([]byte(cm.Data["config.yaml"]), c) - } - } - if err != nil { - return err - } - - r.ServerConfig = c - r.RuntimeBase = strings.Split(c.Tenant.RemoteServiceAPI, remoteServicePath)[0] - r.Org = c.Tenant.OrgName - r.Env = c.Tenant.EnvName + r.RuntimeBase = strings.Split(r.ServerConfig.Tenant.RemoteServiceAPI, remoteServicePath)[0] + r.Org = r.ServerConfig.Tenant.OrgName + r.Env = r.ServerConfig.Tenant.EnvName + r.InsecureSkipVerify = r.ServerConfig.Tenant.AllowUnverifiedSSLCert + r.Namespace = r.ServerConfig.Global.Namespace - switch c.Tenant.InternalAPI { - case "": + if r.ServerConfig.IsGCPManaged() { r.ManagementBase = GCPExperienceBase r.IsGCPManaged = true - case LegacySaaSInternalBase: + + if r.ServerConfig.Tenant.PrivateKey == nil || r.ServerConfig.Tenant.PrivateKeyID == "" { + return fmt.Errorf("Secret CRD not found in file: %s", r.ConfigPath) + } + } + + if r.ServerConfig.IsApigeeManaged() { r.ManagementBase = LegacySaaSManagementBase r.IsLegacySaaS = true - default: + } + + if r.ServerConfig.IsOPDK() { r.ManagementBase = r.RuntimeBase r.IsOPDK = true } @@ -317,18 +307,3 @@ func (r *RootArgs) PrintMissingFlags(missingFlagNames []string) error { } return nil } - -// KubernetesCRD has generic Kubernetes headers for CRD generation -type KubernetesCRD struct { - APIVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata Metadata `yaml:"metadata"` - Type string `yaml:"type,omitempty"` - Data map[string]string `yaml:"data"` -} - -// Metadata is for Kubernetes CRD generation -type Metadata struct { - Name string `yaml:"name"` - Namespace string `yaml:"namespace"` -}