From 3c51d97ee946e07fc1b9e33985d3e24ce2028ec2 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 21 Mar 2018 23:07:16 -0400 Subject: [PATCH] Add gcp secrets --- command/commands.go | 2 + .../hashicorp/go-gcp-common/LICENSE | 363 +++++ .../go-gcp-common/gcputil/compute.go | 111 ++ .../go-gcp-common/gcputil/credentials.go | 178 +++ .../go-gcp-common/gcputil/iam_admin.go | 51 + .../go-gcp-common/gcputil/resource_name.go | 124 ++ .../vault-plugin-secrets-gcp/LICENSE | 363 +++++ .../plugin/backend.go | 114 ++ .../plugin/iamutil/iam_handle.go | 77 + .../plugin/iamutil/iam_policy.go | 100 ++ .../plugin/iamutil/iam_resources.go | 253 +++ .../plugin/iamutil/iam_resources_generated.go | 1365 +++++++++++++++++ .../plugin/path_config.go | 136 ++ .../plugin/path_role_set.go | 520 +++++++ .../plugin/role_set.go | 412 +++++ .../plugin/rollback.go | 276 ++++ .../plugin/secrets_access_token.go | 162 ++ .../plugin/secrets_service_account_key.go | 245 +++ .../plugin/util/bindings_template | 12 + .../plugin/util/parse_bindings.go | 135 ++ .../plugin/util/string_set.go | 80 + .../plugin/util/testing.go | 31 + vendor/vendor.json | 24 + 23 files changed, 5134 insertions(+) create mode 100644 vendor/github.com/hashicorp/go-gcp-common/LICENSE create mode 100644 vendor/github.com/hashicorp/go-gcp-common/gcputil/compute.go create mode 100644 vendor/github.com/hashicorp/go-gcp-common/gcputil/credentials.go create mode 100644 vendor/github.com/hashicorp/go-gcp-common/gcputil/iam_admin.go create mode 100644 vendor/github.com/hashicorp/go-gcp-common/gcputil/resource_name.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/LICENSE create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/backend.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_handle.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_policy.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources_generated.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_config.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_role_set.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/role_set.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/rollback.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_access_token.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_service_account_key.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/bindings_template create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/parse_bindings.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/string_set.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/testing.go diff --git a/command/commands.go b/command/commands.go index 7dd306ddf7..2232b90f60 100644 --- a/command/commands.go +++ b/command/commands.go @@ -6,6 +6,7 @@ import ( "os/signal" "syscall" + gcp "github.com/hashicorp/vault-plugin-secrets-gcp/plugin" kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/logical" @@ -113,6 +114,7 @@ var ( "cassandra": cassandra.Factory, "consul": consul.Factory, "database": database.Factory, + "gcp": gcp.Factory, "kv": kv.Factory, "mongodb": mongodb.Factory, "mssql": mssql.Factory, diff --git a/vendor/github.com/hashicorp/go-gcp-common/LICENSE b/vendor/github.com/hashicorp/go-gcp-common/LICENSE new file mode 100644 index 0000000000..e87a115e46 --- /dev/null +++ b/vendor/github.com/hashicorp/go-gcp-common/LICENSE @@ -0,0 +1,363 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/vendor/github.com/hashicorp/go-gcp-common/gcputil/compute.go b/vendor/github.com/hashicorp/go-gcp-common/gcputil/compute.go new file mode 100644 index 0000000000..f318ecef94 --- /dev/null +++ b/vendor/github.com/hashicorp/go-gcp-common/gcputil/compute.go @@ -0,0 +1,111 @@ +package gcputil + +import ( + "fmt" + "google.golang.org/api/compute/v1" + "regexp" + "time" +) + +func ParseGcpLabels(labels []string) (parsed map[string]string, invalid []string) { + parsed = map[string]string{} + invalid = []string{} + + re := regexp.MustCompile(labelRegex) + for _, labelStr := range labels { + matches := re.FindStringSubmatch(labelStr) + if len(matches) == 0 { + invalid = append(invalid, labelStr) + continue + } + + captureNames := re.SubexpNames() + var keyPtr, valPtr *string + for i, name := range captureNames { + if name == "key" { + keyPtr = &matches[i] + } else if name == "value" { + valPtr = &matches[i] + } + } + + if keyPtr == nil || valPtr == nil || len(*keyPtr) < 1 { + invalid = append(invalid, labelStr) + continue + } else { + parsed[*keyPtr] = *valPtr + } + } + + return parsed, invalid +} + +type CustomJWTClaims struct { + Google *GoogleJWTClaims `json:"google,omitempty"` +} + +type GoogleJWTClaims struct { + Compute *GCEIdentityMetadata `json:"compute_engine,omitempty"` +} + +type GCEIdentityMetadata struct { + // ProjectId is the ID for the project where you created the instance. + ProjectId string `json:"project_id" structs:"project_id" mapstructure:"project_id"` + + // ProjectNumber is the unique ID for the project where you created the instance. + ProjectNumber int64 `json:"project_number" structs:"project_number" mapstructure:"project_number"` + + // Zone is the zone where the instance is located. + Zone string `json:"zone" structs:"zone" mapstructure:"zone"` + + // InstanceId is the unique ID for the instance to which this token belongs. This ID is unique and never reused. + InstanceId string `json:"instance_id" structs:"instance_id" mapstructure:"instance_id"` + + // InstanceName is the name of the instance to which this token belongs. This name can be reused by several + // instances over time, so use the instance_id value to identify a unique instance ID. + InstanceName string `json:"instance_name" structs:"instance_name" mapstructure:"instance_name"` + + // CreatedAt is a unix timestamp indicating when you created the instance. + CreatedAt int64 `json:"instance_creation_timestamp" structs:"instance_creation_timestamp" mapstructure:"instance_creation_timestamp"` +} + +// GetVerifiedInstance returns the Instance as described by the identity metadata or an error. +// If the instance has an invalid status or its creation timestamp does not match the metadata value, +// this will return nil and an error. +func (meta *GCEIdentityMetadata) GetVerifiedInstance(gceClient *compute.Service) (*compute.Instance, error) { + instance, err := gceClient.Instances.Get(meta.ProjectId, meta.Zone, meta.InstanceName).Do() + if err != nil { + return nil, fmt.Errorf("unable to find instance associated with token: %v", err) + } + + if !IsValidInstanceStatus(instance.Status) { + return nil, fmt.Errorf("authenticating instance %s found but has invalid status '%s'", instance.Name, instance.Status) + } + + // Parse the metadata CreatedAt into time. + metaTime := time.Unix(meta.CreatedAt, 0) + + // Parse instance creationTimestamp into time. + actualTime, err := time.Parse(time.RFC3339Nano, instance.CreationTimestamp) + if err != nil { + return nil, fmt.Errorf("instance 'creationTimestamp' field could not be parsed into time: %s", instance.CreationTimestamp) + } + + // Return an error if the metadata creation timestamp is before the instance creation timestamp. + delta := float64(metaTime.Sub(actualTime)) / float64(time.Second) + if delta < -1 { + return nil, fmt.Errorf("metadata instance_creation_timestamp %d is before instance's creation time %d", actualTime.Unix(), metaTime.Unix()) + } + return instance, nil +} + +var validInstanceStates map[string]struct{} = map[string]struct{}{ + "PROVISIONING": struct{}{}, + "RUNNING": struct{}{}, + "STAGING": struct{}{}, +} + +func IsValidInstanceStatus(status string) bool { + _, ok := validInstanceStates[status] + return ok +} diff --git a/vendor/github.com/hashicorp/go-gcp-common/gcputil/credentials.go b/vendor/github.com/hashicorp/go-gcp-common/gcputil/credentials.go new file mode 100644 index 0000000000..b0bc0cb7cc --- /dev/null +++ b/vendor/github.com/hashicorp/go-gcp-common/gcputil/credentials.go @@ -0,0 +1,178 @@ +package gcputil + +import ( + "context" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "github.com/hashicorp/go-cleanhttp" + "github.com/mitchellh/go-homedir" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" + googleoauth2 "google.golang.org/api/oauth2/v2" + "gopkg.in/square/go-jose.v2" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" +) + +const ( + labelRegex string = "^(?P[a-z]([\\w-]+)?):(?P[\\w-]*)$" + defaultHomeCredentialsFile = ".gcp/credentials" +) + +// GcpCredentials represents a simplified version of the Google Cloud Platform credentials file format. +type GcpCredentials struct { + ClientEmail string `json:"client_email" structs:"client_email" mapstructure:"client_email"` + ClientId string `json:"client_id" structs:"client_id" mapstructure:"client_id"` + PrivateKeyId string `json:"private_key_id" structs:"private_key_id" mapstructure:"private_key_id"` + PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"` + ProjectId string `json:"project_id" structs:"project_id" mapstructure:"project_id"` +} + +// FindCredentials attempts to obtain GCP credentials in the +// following ways: +// * Parse JSON from provided credentialsJson +// * Parse JSON from the environment variables GOOGLE_CREDENTIALS or GOOGLE_CLOUD_KEYFILE_JSON +// * Parse JSON file ~/.gcp/credentials +// * Google Application Default Credentials (see https://developers.google.com/identity/protocols/application-default-credentials) +func FindCredentials(credsJson string, ctx context.Context, scopes ...string) (*GcpCredentials, oauth2.TokenSource, error) { + var creds *GcpCredentials + var err error + // 1. Parse JSON from provided credentialsJson + if credsJson == "" { + // 2. JSON from env var GOOGLE_CREDENTIALS + credsJson = os.Getenv("GOOGLE_CREDENTIALS") + } + + if credsJson == "" { + // 3. JSON from env var GOOGLE_CLOUD_KEYFILE_JSON + credsJson = os.Getenv("GOOGLE_CLOUD_KEYFILE_JSON") + } + + if credsJson == "" { + // 4. JSON from ~/.gcp/credentials + home, err := homedir.Dir() + if err == nil { + return nil, nil, errors.New("could not find home directory") + } + credBytes, err := ioutil.ReadFile(filepath.Join(home, defaultHomeCredentialsFile)) + if err == nil { + credsJson = string(credBytes) + } + } + + // Parse JSON into credentials. + if credsJson != "" { + creds, err = Credentials(credsJson) + if err == nil { + conf := jwt.Config{ + Email: creds.ClientEmail, + PrivateKey: []byte(creds.PrivateKey), + Scopes: scopes, + TokenURL: "https://accounts.google.com/o/oauth2/token", + } + return creds, conf.TokenSource(ctx), nil + } + } + + // 5. Use Application default credentials. + defaultCreds, err := google.FindDefaultCredentials(ctx, scopes...) + if err != nil { + return nil, nil, err + } + + if defaultCreds.JSON != nil { + creds, err = Credentials(string(defaultCreds.JSON)) + if err != nil { + return nil, nil, errors.New("could not read credentials from application default credential JSON") + } + } + + return creds, defaultCreds.TokenSource, nil +} + +// Credentials attempts to parse GcpCredentials from a JSON string. +func Credentials(credentialsJson string) (*GcpCredentials, error) { + credentials := &GcpCredentials{} + if err := json.Unmarshal([]byte(credentialsJson), &credentials); err != nil { + return nil, err + } + return credentials, nil +} + +// GetHttpClient creates an HTTP client from the given Google credentials and scopes. +func GetHttpClient(credentials *GcpCredentials, clientScopes ...string) (*http.Client, error) { + conf := jwt.Config{ + Email: credentials.ClientEmail, + PrivateKey: []byte(credentials.PrivateKey), + Scopes: clientScopes, + TokenURL: "https://accounts.google.com/o/oauth2/token", + } + + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient()) + client := conf.Client(ctx) + return client, nil +} + +// PublicKey returns a public key from a Google PEM key file (type TYPE_X509_PEM_FILE). +func PublicKey(pemString string) (interface{}, error) { + pemBytes, err := base64.StdEncoding.DecodeString(pemString) + if err != nil { + return nil, err + } + + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, errors.New("Unable to find pem block in key") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + return cert.PublicKey, nil +} + +// OAuth2RSAPublicKey returns the PEM key file string for Google Oauth2 public cert for the given 'kid' id. +func OAuth2RSAPublicKey(kid, oauth2BasePath string) (interface{}, error) { + oauth2Client, err := googleoauth2.New(cleanhttp.DefaultClient()) + if err != nil { + return "", err + } + + if len(oauth2BasePath) > 0 { + oauth2Client.BasePath = oauth2BasePath + } + + jwks, err := oauth2Client.GetCertForOpenIdConnect().Do() + if err != nil { + return nil, err + } + + for _, key := range jwks.Keys { + if key.Kid == kid && jose.SignatureAlgorithm(key.Alg) == jose.RS256 { + // Trim extra '=' from key so it can be parsed. + key.N = strings.TrimRight(key.N, "=") + js, err := key.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("unable to marshal json %v", err) + } + key := &jose.JSONWebKey{} + if err := key.UnmarshalJSON(js); err != nil { + return nil, fmt.Errorf("unable to unmarshal json %v", err) + } + + return key.Key, nil + } + } + + return nil, fmt.Errorf("could not find public key with kid '%s'", kid) +} diff --git a/vendor/github.com/hashicorp/go-gcp-common/gcputil/iam_admin.go b/vendor/github.com/hashicorp/go-gcp-common/gcputil/iam_admin.go new file mode 100644 index 0000000000..59baf38668 --- /dev/null +++ b/vendor/github.com/hashicorp/go-gcp-common/gcputil/iam_admin.go @@ -0,0 +1,51 @@ +package gcputil + +import ( + "fmt" + "google.golang.org/api/iam/v1" +) + +const ( + ServiceAccountTemplate = "projects/%s/serviceAccounts/%s" + ServiceAccountKeyTemplate = "projects/%s/serviceAccounts/%s/keys/%s" + ServiceAccountKeyFileType = "TYPE_X509_PEM_FILE" +) + +type ServiceAccountId struct { + Project string + EmailOrId string +} + +func (id *ServiceAccountId) ResourceName() string { + return fmt.Sprintf(ServiceAccountTemplate, id.Project, id.EmailOrId) +} + +type ServiceAccountKeyId struct { + Project string + EmailOrId string + Key string +} + +func (id *ServiceAccountKeyId) ResourceName() string { + return fmt.Sprintf(ServiceAccountKeyTemplate, id.Project, id.EmailOrId, id.Key) +} + +// ServiceAccount wraps a call to the GCP IAM API to get a service account. +func ServiceAccount(iamClient *iam.Service, accountId *ServiceAccountId) (*iam.ServiceAccount, error) { + account, err := iamClient.Projects.ServiceAccounts.Get(accountId.ResourceName()).Do() + if err != nil { + return nil, fmt.Errorf("could not find service account '%s': %v", accountId.ResourceName(), err) + } + + return account, nil +} + +// ServiceAccountKey wraps a call to the GCP IAM API to get a service account key. +func ServiceAccountKey(iamClient *iam.Service, keyId *ServiceAccountKeyId) (*iam.ServiceAccountKey, error) { + keyResource := keyId.ResourceName() + key, err := iamClient.Projects.ServiceAccounts.Keys.Get(keyId.ResourceName()).PublicKeyType(ServiceAccountKeyFileType).Do() + if err != nil { + return nil, fmt.Errorf("could not find service account key '%s': %v", keyResource, err) + } + return key, nil +} diff --git a/vendor/github.com/hashicorp/go-gcp-common/gcputil/resource_name.go b/vendor/github.com/hashicorp/go-gcp-common/gcputil/resource_name.go new file mode 100644 index 0000000000..344b601594 --- /dev/null +++ b/vendor/github.com/hashicorp/go-gcp-common/gcputil/resource_name.go @@ -0,0 +1,124 @@ +package gcputil + +import ( + "fmt" + "net/url" + "regexp" + "strings" +) + +const ( + resourceIdRegex = "^[^\t\n\f\r]+$" + collectionIdRegex = "^[a-z][a-zA-Z]*$" + + fullResourceNameRegex = "^//([a-z]+).googleapis.com/(.+)$" + selfLinkMarker = "projects/" +) + +var singleCollectionIds = map[string]struct{}{ + "global": {}, +} + +type RelativeResourceName struct { + Name string + TypeKey string + IdTuples map[string]string +} + +func ParseRelativeName(resource string) (*RelativeResourceName, error) { + resourceRe := regexp.MustCompile(resourceIdRegex) + collectionRe := regexp.MustCompile(collectionIdRegex) + + tokens := strings.Split(resource, "/") + if len(tokens) < 2 { + return nil, fmt.Errorf("invalid relative resource name %s (too few tokens)", resource) + } + + ids := map[string]string{} + typeKey := "" + currColId := "" + for idx, v := range tokens { + if len(currColId) == 0 { + if _, ok := singleCollectionIds[v]; ok { + // Ignore 'single' collectionIds like Global, but error if they are the last ID + if idx == len(tokens)-1 { + return nil, fmt.Errorf("invalid relative resource name %s (last collection '%s' has no ID)", resource, currColId) + } + continue + } + if len(collectionRe.FindAllString(v, 1)) == 0 { + return nil, fmt.Errorf("invalid relative resource name %s (invalid collection ID %s)", resource, v) + } + currColId = v + typeKey += currColId + "/" + } else { + if len(resourceRe.FindAllString(v, 1)) == 0 { + return nil, fmt.Errorf("invalid relative resource name %s (invalid resource sub-ID %s)", resource, v) + } + ids[currColId] = v + currColId = "" + } + } + + resourceName := tokens[len(tokens)-2] + return &RelativeResourceName{ + Name: resourceName, + TypeKey: typeKey[:len(typeKey)-1], + IdTuples: ids, + }, nil +} + +type FullResourceName struct { + Service string + *RelativeResourceName +} + +func ParseFullResourceName(name string) (*FullResourceName, error) { + fullRe := regexp.MustCompile(fullResourceNameRegex) + matches := fullRe.FindAllStringSubmatch(name, 1) + if len(matches) == 0 { + return nil, fmt.Errorf("invalid full name '%s'", name) + } + + if len(matches[0]) != 3 { + return nil, fmt.Errorf("invalid full name '%s'", name) + } + + serviceName := matches[0][1] + relName, err := ParseRelativeName(strings.Trim(matches[0][2], "/")) + if err != nil { + return nil, fmt.Errorf("error parsing relative resource path in full resource name '%s': %v", name, err) + } + + return &FullResourceName{ + Service: serviceName, + RelativeResourceName: relName, + }, nil +} + +type SelfLink struct { + Prefix string + *RelativeResourceName +} + +func ParseProjectResourceSelfLink(link string) (*SelfLink, error) { + u, err := url.Parse(link) + if err != nil || u.Scheme == "" || u.Host == "" { + return nil, fmt.Errorf("invalid self link '%s' must have scheme/host", link) + } + + split := strings.SplitAfterN(link, selfLinkMarker, 2) + if len(split) != 2 { + return nil, fmt.Errorf("self link '%s' is not for project-level resource, must contain '%s')", link, selfLinkMarker) + } + + relName, err := ParseRelativeName(selfLinkMarker + split[1]) + if err != nil { + return nil, fmt.Errorf("error parsing relative resource path in self-link '%s': %v", link, err) + } + + return &SelfLink{ + Prefix: strings.TrimSuffix(split[0], selfLinkMarker), + RelativeResourceName: relName, + }, nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/LICENSE b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/LICENSE new file mode 100644 index 0000000000..e87a115e46 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/LICENSE @@ -0,0 +1,363 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/backend.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/backend.go new file mode 100644 index 0000000000..fd1b9af1c5 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/backend.go @@ -0,0 +1,114 @@ +package gcpsecrets + +import ( + "context" + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-gcp-common/gcputil" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "golang.org/x/oauth2" + "google.golang.org/api/iam/v1" + "net/http" + "strings" + "sync" + "time" +) + +type backend struct { + *framework.Backend + + enabledIamResources iamutil.EnabledResources + + rolesetLock sync.Mutex +} + +func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { + b := Backend() + if err := b.Setup(ctx, conf); err != nil { + return nil, err + } + return b, nil +} + +func Backend() *backend { + var b = backend{ + enabledIamResources: iamutil.GetEnabledIamResources(), + } + + b.Backend = &framework.Backend{ + Help: strings.TrimSpace(backendHelp), + PathsSpecial: &logical.Paths{ + LocalStorage: []string{ + framework.WALPrefix, + }, + SealWrapStorage: []string{ + "config", + }, + }, + + Paths: framework.PathAppend( + pathsRoleSet(&b), + []*framework.Path{ + pathConfig(&b), + pathSecretAccessToken(&b), + pathSecretServiceAccountKey(&b), + }, + ), + Secrets: []*framework.Secret{ + secretAccessToken(&b), + secretServiceAccountKey(&b), + }, + + BackendType: logical.TypeLogical, + WALRollback: b.walRollback, + WALRollbackMinAge: 5 * time.Minute, + } + + return &b +} + +func newHttpClient(ctx context.Context, s logical.Storage, scopes ...string) (*http.Client, error) { + if len(scopes) == 0 { + scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} + } + + cfg, err := getConfig(ctx, s) + if err != nil { + return nil, err + } + credsJSON := "" + if cfg != nil { + credsJSON = cfg.CredentialsRaw + } + + _, tokenSource, err := gcputil.FindCredentials(credsJSON, ctx, scopes...) + if err != nil { + return nil, err + } + + tc := cleanhttp.DefaultClient() + return oauth2.NewClient( + context.WithValue(ctx, oauth2.HTTPClient, tc), + tokenSource), nil +} + +func newIamAdmin(ctx context.Context, s logical.Storage) (*iam.Service, error) { + c, err := newHttpClient(ctx, s, iam.CloudPlatformScope) + if err != nil { + return nil, err + } + + return iam.New(c) +} + +const backendHelp = ` +The GCP secrets backend dynamically generates GCP IAM service +account keys with a given set of IAM policies. The service +account keys have a configurable lease set and are automatically +revoked at the end of the lease. + +After mounting this backend, credentials to generate IAM keys must +be configured with the "config/" endpoints and policies must be +written using the "roles/" endpoints before any keys can be generated. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_handle.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_handle.go new file mode 100644 index 0000000000..1da4a68969 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_handle.go @@ -0,0 +1,77 @@ +package iamutil + +import ( + "context" + "encoding/json" + "github.com/hashicorp/errwrap" + "google.golang.org/api/gensupport" + "google.golang.org/api/googleapi" + "net/http" +) + +type IamHandle struct { + c *http.Client + userAgent string +} + +func GetIamHandle(client *http.Client, userAgent string) *IamHandle { + return &IamHandle{ + c: client, + userAgent: userAgent, + } +} + +func (h *IamHandle) GetIamPolicy(ctx context.Context, r IamResource) (*Policy, error) { + req, err := r.GetIamPolicyRequest() + if err != nil { + return nil, errwrap.Wrapf("unable to construct GetIamPolicy request: {{err}}", err) + } + var p Policy + if err := h.doRequest(ctx, req, &p); err != nil { + return nil, errwrap.Wrapf("unable to get policy: {{err}}", err) + } + return &p, nil +} + +func (h *IamHandle) SetIamPolicy(ctx context.Context, r IamResource, p *Policy) (*Policy, error) { + req, err := r.SetIamPolicyRequest(p) + if err != nil { + return nil, errwrap.Wrapf("unable to construct SetIamPolicy request: {{err}}", err) + } + var out Policy + if err := h.doRequest(ctx, req, &out); err != nil { + return nil, errwrap.Wrapf("unable to set policy: {{err}}", err) + } + return &out, nil +} + +func (h *IamHandle) doRequest(ctx context.Context, req *http.Request, out interface{}) error { + if req.Header == nil { + req.Header = make(http.Header) + } + if h.userAgent != "" { + req.Header.Set("User-Agent", h.userAgent) + } + + resp, err := gensupport.SendRequest(ctx, h.c, req) + defer googleapi.CloseBody(resp) + + if resp != nil && resp.StatusCode == http.StatusNotModified { + return &googleapi.Error{ + Code: resp.StatusCode, + Header: resp.Header, + } + } + if err != nil { + return err + } + + if err := googleapi.CheckResponse(resp); err != nil { + return err + } + + if err := json.NewDecoder(resp.Body).Decode(out); err != nil { + return errwrap.Wrapf("unable to decode JSON resp to output interface: {{err}}", err) + } + return nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_policy.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_policy.go new file mode 100644 index 0000000000..b70fdb6e7e --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_policy.go @@ -0,0 +1,100 @@ +package iamutil + +import ( + "fmt" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util" +) + +const ( + ServiceAccountMemberTmpl = "serviceAccount:%s" +) + +type Policy struct { + Bindings []*Binding `json:"bindings,omitempty"` + Etag string `json:"etag,omitempty"` +} + +type Binding struct { + Members []string `json:"members,omitempty"` + Role string `json:"role,omitempty"` +} + +type PolicyDelta struct { + Roles util.StringSet + Email string +} + +func (p *Policy) AddBindings(toAdd *PolicyDelta) (changed bool, updated *Policy) { + return p.ChangedBindings(toAdd, nil) +} + +func (p *Policy) RemoveBindings(toRemove *PolicyDelta) (changed bool, updated *Policy) { + return p.ChangedBindings(nil, toRemove) +} + +func (p *Policy) ChangedBindings(toAdd *PolicyDelta, toRemove *PolicyDelta) (changed bool, updated *Policy) { + if toAdd == nil && toRemove == nil { + return false, p + } + + var toAddMem, toRemoveMem string + if toAdd != nil { + toAddMem = fmt.Sprintf(ServiceAccountMemberTmpl, toAdd.Email) + } + if toRemove != nil { + toRemoveMem = fmt.Sprintf(ServiceAccountMemberTmpl, toRemove.Email) + } + + changed = false + + newBindings := make([]*Binding, 0, len(p.Bindings)) + alreadyAdded := make(util.StringSet) + + for _, bind := range p.Bindings { + memberSet := util.ToSet(bind.Members) + + if toAdd != nil { + if toAdd.Roles.Includes(bind.Role) { + changed = true + alreadyAdded.Add(bind.Role) + memberSet.Add(toAddMem) + } + } + + if toRemove != nil { + if toRemove.Roles.Includes(bind.Role) { + if memberSet.Includes(toRemoveMem) { + changed = true + delete(memberSet, toRemoveMem) + } + } + } + + if len(memberSet) > 0 { + newBindings = append(newBindings, &Binding{ + Role: bind.Role, + Members: memberSet.ToSlice(), + }) + } + } + + if toAdd != nil { + for r := range toAdd.Roles { + if !alreadyAdded.Includes(r) { + changed = true + newBindings = append(newBindings, &Binding{ + Role: r, + Members: []string{toAddMem}, + }) + } + } + } + + if changed { + return true, &Policy{ + Bindings: newBindings, + Etag: p.Etag, + } + } + return false, p +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources.go new file mode 100644 index 0000000000..d680a26ae9 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources.go @@ -0,0 +1,253 @@ +//go:generate go run internal/generate_iam_resources.go + +package iamutil + +import ( + "errors" + "fmt" + "github.com/hashicorp/go-gcp-common/gcputil" + "google.golang.org/api/googleapi" + "io" + "net/http" + "net/url" + "strings" +) + +const ( + resourceParsingErrorTmpl = `invalid resource "%s": %v` + resourceMultipleServicesTmpl = `please provide a self-link or full resource name for non-service-unique resource type '%s' (supported services: %s)` + resourceMultipleVersions = `please provide a self-link with version instead; IAM support for this resource is for multiple non-preferred service versions` +) + +type EnabledResources interface { + Resource(resource string) (IamResource, error) +} + +type iamResourceMap map[string]map[string]map[string]*IamResourceConfig + +type generatedIamResources struct { + resources iamResourceMap +} + +func (apis *generatedIamResources) parseResource(name string) (*gcputil.RelativeResourceName, *IamResourceConfig, error) { + rUrl, err := url.Parse(name) + if err != nil { + return nil, nil, fmt.Errorf(`resource "%s" is invalid URI`, name) + } + + var relName *gcputil.RelativeResourceName + var hasServiceVersion bool + var serviceName string + + if rUrl.Scheme != "" { + selfLink, err := gcputil.ParseProjectResourceSelfLink(name) + if err != nil { + return nil, nil, err + } + hasServiceVersion = true + relName = selfLink.RelativeResourceName + } else if rUrl.Host != "" { + fullName, err := gcputil.ParseFullResourceName(name) + if err != nil { + return nil, nil, err + } + relName = fullName.RelativeResourceName + serviceName = fullName.Service + } else { + relName, err = gcputil.ParseRelativeName(name) + if err != nil { + return nil, nil, err + } + } + + if relName == nil { + return nil, nil, fmt.Errorf(resourceParsingErrorTmpl, name, "unable to parse relative name") + } + + serviceMap, ok := apis.resources[relName.TypeKey] + if !ok { + return nil, nil, fmt.Errorf(resourceParsingErrorTmpl, name, fmt.Errorf("unsupported resource type: %s", relName.TypeKey)) + } + + var resConfig *IamResourceConfig + if hasServiceVersion { + resConfig, err = tryGetConfigForSelfLink(name, relName.TypeKey, serviceMap) + } else { + resConfig, err = tryGetUniqueVersion(serviceName, relName.TypeKey, serviceMap) + } + if err != nil { + return nil, nil, err + } + if resConfig == nil { + return nil, nil, fmt.Errorf(resourceParsingErrorTmpl, name, "unable to get IAM resource config") + } + + return relName, resConfig, nil +} + +type IamResource interface { + GetIamPolicyRequest() (*http.Request, error) + SetIamPolicyRequest(*Policy) (*http.Request, error) +} + +func (apis *generatedIamResources) Resource(name string) (IamResource, error) { + relName, cfg, err := apis.parseResource(name) + if err != nil { + return nil, err + } + + return &iamResourceImpl{ + relativeId: relName, + config: cfg, + }, nil +} + +func tryGetConfigForSelfLink(link, typeKey string, resourceServices map[string]map[string]*IamResourceConfig) (*IamResourceConfig, error) { + for _, verMap := range resourceServices { + for _, resourceCfg := range verMap { + prefix := resourceCfg.Service.RootUrl + resourceCfg.Service.ServicePath + if strings.HasPrefix(link, prefix) { + return resourceCfg, nil + } + } + } + return nil, fmt.Errorf("could not find service/version given in self-link for resource type %s", typeKey) +} + +func tryGetUniqueVersion(serviceName, typeKey string, resourceServices map[string]map[string]*IamResourceConfig) (*IamResourceConfig, error) { + if serviceName == "" { + return tryGetUniqueServiceAndVersion(typeKey, resourceServices) + } + if resourceServices == nil { + return nil, fmt.Errorf("no supported services for %s", typeKey) + } + verMap, hasService := resourceServices[serviceName] + if !hasService { + return nil, fmt.Errorf("unsupported service '%s' for resource type: %s", serviceName, typeKey) + } + return getResourceFromVersions(verMap) +} + +func tryGetUniqueServiceAndVersion(typeKey string, resourceServices map[string]map[string]*IamResourceConfig) (*IamResourceConfig, error) { + if resourceServices == nil || len(resourceServices) < 1 { + return nil, fmt.Errorf("no supported services for %s", typeKey) + } + isUnique := len(resourceServices) == 1 + supported := "" + for serviceName, verMap := range resourceServices { + supported += serviceName + ", " + if isUnique { + return getResourceFromVersions(verMap) + } + } + + return nil, fmt.Errorf(resourceMultipleServicesTmpl, typeKey, strings.Trim(supported, ", ")) +} + +func getResourceFromVersions(versionsMap map[string]*IamResourceConfig) (*IamResourceConfig, error) { + var preferredVer *IamResourceConfig + var onlyCfg *IamResourceConfig + + for _, onlyCfg = range versionsMap { + if onlyCfg.Service.IsPreferredVersion { + preferredVer = onlyCfg + break + } + } + + if preferredVer != nil { + return preferredVer, nil + } else if len(versionsMap) == 1 { + return onlyCfg, nil + } else { + return nil, errors.New(resourceMultipleVersions) + } +} + +type iamResourceImpl struct { + relativeId *gcputil.RelativeResourceName + config *IamResourceConfig +} + +type IamResourceConfig struct { + // Service this resource belongs to + Service *ServiceConfig + + // Config for IAM Methods + SetIamPolicy *HttpMethodCfg + GetIamPolicy *HttpMethodCfg +} + +type HttpMethodCfg struct { + // HTTP method, e.g. GET/PUT/POST + HttpMethod string `json:"httpMethod"` + + // Path is the API method's path with replacement keys, e.g. + // v1/projects/{project}:getIamPolicy + Path string `json:"flatPath"` + + // ReplacementKeys maps collectionIds in the expected resource format to the key for googleapis.Expand + // For example, given input of: + // Resource: "projects/my-project/zones/my-zone/instances/someInstance" + // Method Path: "p/{projectId}/z/{zoneId}/i/{resource}" + // + // This would be: + // map[string]string{ + // "projects": "projectId" , + // "zones": "zoneId", + // "instances": "resource" + // } + ReplacementKeys map[string]string +} + +type ServiceConfig struct { + // API service Name (e.g. "compute", "iam", "pubsub") + Name string + + // API service Version. + Version string + + // IsPreferredVersion is + IsPreferredVersion bool + + // Root URL + Service Path is the prefix for all calls using this service. + RootUrl string + ServicePath string +} + +func (r *iamResourceImpl) SetIamPolicyRequest(p *Policy) (*http.Request, error) { + data := struct { + Policy *Policy `json:"policy,omitempty"` + }{Policy: p} + + buf, err := googleapi.WithoutDataWrapper.JSONReader(data) + if err != nil { + return nil, err + } + + return r.constructRequest(r.config.SetIamPolicy, buf) +} + +func (r *iamResourceImpl) GetIamPolicyRequest() (*http.Request, error) { + return r.constructRequest(r.config.GetIamPolicy, nil) +} + +func (r *iamResourceImpl) constructRequest(httpMtd *HttpMethodCfg, data io.Reader) (*http.Request, error) { + reqUrl := googleapi.ResolveRelative(r.config.Service.RootUrl+r.config.Service.ServicePath, httpMtd.Path) + req, err := http.NewRequest(httpMtd.HttpMethod, reqUrl, data) + if err != nil { + return nil, err + } + + replacementMap := make(map[string]string) + for cId, replaceK := range httpMtd.ReplacementKeys { + rId, ok := r.relativeId.IdTuples[cId] + if !ok { + return nil, fmt.Errorf("expected value for collection id %s", cId) + } + replacementMap[replaceK] = rId + } + + googleapi.Expand(req.URL, replacementMap) + return req, nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources_generated.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources_generated.go new file mode 100644 index 0000000000..4096e5bd1b --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil/iam_resources_generated.go @@ -0,0 +1,1365 @@ +// THIS FILE IS AUTOGENERATED USING go generate. DO NOT EDIT. +package iamutil + +func GetEnabledIamResources() EnabledResources { + return &generatedIamResources{ + resources: generatedResources, + } +} + +var generatedServices = map[string]map[string]*ServiceConfig{ + "cloudiot": { + "v1": &ServiceConfig{ + Name: "cloudiot", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://cloudiot.googleapis.com/", + ServicePath: "", + }, + }, + "cloudkms": { + "v1": &ServiceConfig{ + Name: "cloudkms", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://cloudkms.googleapis.com/", + ServicePath: "", + }, + }, + "cloudresourcemanager": { + "v1": &ServiceConfig{ + Name: "cloudresourcemanager", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://cloudresourcemanager.googleapis.com/", + ServicePath: "", + }, + "v1beta1": &ServiceConfig{ + Name: "cloudresourcemanager", + Version: "v1beta1", + IsPreferredVersion: false, + RootUrl: "https://cloudresourcemanager.googleapis.com/", + ServicePath: "", + }, + "v2beta1": &ServiceConfig{ + Name: "cloudresourcemanager", + Version: "v2beta1", + IsPreferredVersion: false, + RootUrl: "https://cloudresourcemanager.googleapis.com/", + ServicePath: "", + }, + }, + "cloudtasks": { + "v2beta2": &ServiceConfig{ + Name: "cloudtasks", + Version: "v2beta2", + IsPreferredVersion: true, + RootUrl: "https://cloudtasks.googleapis.com/", + ServicePath: "", + }, + }, + "clouduseraccounts": { + "alpha": &ServiceConfig{ + Name: "clouduseraccounts", + Version: "alpha", + IsPreferredVersion: false, + RootUrl: "https://www.googleapis.com/", + ServicePath: "clouduseraccounts/alpha/projects/", + }, + "vm_alpha": &ServiceConfig{ + Name: "clouduseraccounts", + Version: "vm_alpha", + IsPreferredVersion: true, + RootUrl: "https://www.googleapis.com/", + ServicePath: "clouduseraccounts/vm_alpha/projects/", + }, + }, + "compute": { + "alpha": &ServiceConfig{ + Name: "compute", + Version: "alpha", + IsPreferredVersion: false, + RootUrl: "https://www.googleapis.com/", + ServicePath: "compute/alpha/projects/", + }, + "beta": &ServiceConfig{ + Name: "compute", + Version: "beta", + IsPreferredVersion: false, + RootUrl: "https://www.googleapis.com/", + ServicePath: "compute/beta/projects/", + }, + }, + "dataproc": { + "v1beta2": &ServiceConfig{ + Name: "dataproc", + Version: "v1beta2", + IsPreferredVersion: false, + RootUrl: "https://dataproc.googleapis.com/", + ServicePath: "", + }, + }, + "deploymentmanager": { + "alpha": &ServiceConfig{ + Name: "deploymentmanager", + Version: "alpha", + IsPreferredVersion: false, + RootUrl: "https://www.googleapis.com/", + ServicePath: "deploymentmanager/alpha/projects/", + }, + "v2": &ServiceConfig{ + Name: "deploymentmanager", + Version: "v2", + IsPreferredVersion: true, + RootUrl: "https://www.googleapis.com/", + ServicePath: "deploymentmanager/v2/projects/", + }, + "v2beta": &ServiceConfig{ + Name: "deploymentmanager", + Version: "v2beta", + IsPreferredVersion: false, + RootUrl: "https://www.googleapis.com/", + ServicePath: "deploymentmanager/v2beta/projects/", + }, + }, + "genomics": { + "v1": &ServiceConfig{ + Name: "genomics", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://genomics.googleapis.com/", + ServicePath: "", + }, + }, + "iam": { + "v1": &ServiceConfig{ + Name: "iam", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://iam.googleapis.com/", + ServicePath: "", + }, + }, + "ml": { + "v1": &ServiceConfig{ + Name: "ml", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://ml.googleapis.com/", + ServicePath: "", + }, + }, + "pubsub": { + "v1": &ServiceConfig{ + Name: "pubsub", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://pubsub.googleapis.com/", + ServicePath: "", + }, + "v1beta2": &ServiceConfig{ + Name: "pubsub", + Version: "v1beta2", + IsPreferredVersion: false, + RootUrl: "https://pubsub.googleapis.com/", + ServicePath: "", + }, + }, + "runtimeconfig": { + "v1beta1": &ServiceConfig{ + Name: "runtimeconfig", + Version: "v1beta1", + IsPreferredVersion: false, + RootUrl: "https://runtimeconfig.googleapis.com/", + ServicePath: "", + }, + }, + "servicemanagement": { + "v1": &ServiceConfig{ + Name: "servicemanagement", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://servicemanagement.googleapis.com/", + ServicePath: "", + }, + }, + "sourcerepo": { + "v1": &ServiceConfig{ + Name: "sourcerepo", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://sourcerepo.googleapis.com/", + ServicePath: "", + }, + }, + "spanner": { + "v1": &ServiceConfig{ + Name: "spanner", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://spanner.googleapis.com/", + ServicePath: "", + }, + }, + "storage": { + "v1": &ServiceConfig{ + Name: "storage", + Version: "v1", + IsPreferredVersion: true, + RootUrl: "https://www.googleapis.com/", + ServicePath: "storage/v1/", + }, + }, +} + +var generatedResources = map[string]map[string]map[string]*IamResourceConfig{ + "b": { + "storage": { + "v1": &IamResourceConfig{ + Service: generatedServices["storage"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "PUT", + Path: "b/{bucket}/iam", + ReplacementKeys: map[string]string{ + "b": "bucket", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "b/{bucket}/iam", + ReplacementKeys: map[string]string{ + "b": "bucket", + }, + }, + }, + }, + }, + "b/o": { + "storage": { + "v1": &IamResourceConfig{ + Service: generatedServices["storage"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "PUT", + Path: "b/{bucket}/o/{object}/iam", + ReplacementKeys: map[string]string{ + "b": "bucket", + "o": "object", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "b/{bucket}/o/{object}/iam", + ReplacementKeys: map[string]string{ + "b": "bucket", + "o": "object", + }, + }, + }, + }, + }, + "buckets": { + "storage": { + "v1": &IamResourceConfig{ + Service: generatedServices["storage"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "PUT", + Path: "b/{bucket}/iam", + ReplacementKeys: map[string]string{ + "buckets": "bucket", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "b/{bucket}/iam", + ReplacementKeys: map[string]string{ + "buckets": "bucket", + }, + }, + }, + }, + }, + "buckets/objects": { + "storage": { + "v1": &IamResourceConfig{ + Service: generatedServices["storage"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "PUT", + Path: "b/{bucket}/o/{object}/iam", + ReplacementKeys: map[string]string{ + "buckets": "bucket", + "objects": "object", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "b/{bucket}/o/{object}/iam", + ReplacementKeys: map[string]string{ + "buckets": "bucket", + "objects": "object", + }, + }, + }, + }, + }, + "datasets": { + "genomics": { + "v1": &IamResourceConfig{ + Service: generatedServices["genomics"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/datasets/{datasetsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "datasets": "datasetsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/datasets/{datasetsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "datasets": "datasetsId", + }, + }, + }, + }, + }, + "folders": { + "cloudresourcemanager": { + "v2beta1": &IamResourceConfig{ + Service: generatedServices["cloudresourcemanager"]["v2beta1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v2beta1/folders/{foldersId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "folders": "foldersId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v2beta1/folders/{foldersId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "folders": "foldersId", + }, + }, + }, + }, + }, + "organizations": { + "cloudresourcemanager": { + "v1": &IamResourceConfig{ + Service: generatedServices["cloudresourcemanager"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/organizations/{organizationsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "organizations": "organizationsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/organizations/{organizationsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "organizations": "organizationsId", + }, + }, + }, + "v1beta1": &IamResourceConfig{ + Service: generatedServices["cloudresourcemanager"]["v1beta1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta1/organizations/{organizationsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "organizations": "organizationsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta1/organizations/{organizationsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "organizations": "organizationsId", + }, + }, + }, + }, + }, + "projects": { + "cloudresourcemanager": { + "v1": &IamResourceConfig{ + Service: generatedServices["cloudresourcemanager"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{resource}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "resource", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{resource}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "resource", + }, + }, + }, + "v1beta1": &IamResourceConfig{ + Service: generatedServices["cloudresourcemanager"]["v1beta1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta1/projects/{resource}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "resource", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta1/projects/{resource}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "resource", + }, + }, + }, + }, + }, + "projects/backendBuckets": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/backendBuckets/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "backendBuckets": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/backendBuckets/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "backendBuckets": "resource", + "projects": "project", + }, + }, + }, + }, + }, + "projects/configs": { + "runtimeconfig": { + "v1beta1": &IamResourceConfig{ + Service: generatedServices["runtimeconfig"]["v1beta1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta1/projects/{projectsId}/configs/{configsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "configs": "configsId", + "projects": "projectsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1beta1/projects/{projectsId}/configs/{configsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "configs": "configsId", + "projects": "projectsId", + }, + }, + }, + }, + }, + "projects/deployments": { + "deploymentmanager": { + "alpha": &IamResourceConfig{ + Service: generatedServices["deploymentmanager"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/deployments/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "deployments": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/deployments/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "deployments": "resource", + "projects": "project", + }, + }, + }, + "v2": &IamResourceConfig{ + Service: generatedServices["deploymentmanager"]["v2"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/deployments/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "deployments": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/deployments/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "deployments": "resource", + "projects": "project", + }, + }, + }, + "v2beta": &IamResourceConfig{ + Service: generatedServices["deploymentmanager"]["v2beta"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/deployments/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "deployments": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/deployments/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "deployments": "resource", + "projects": "project", + }, + }, + }, + }, + }, + "projects/groups": { + "clouduseraccounts": { + "alpha": &IamResourceConfig{ + Service: generatedServices["clouduseraccounts"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/groups/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "groups": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/groups/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "groups": "resource", + "projects": "project", + }, + }, + }, + "vm_alpha": &IamResourceConfig{ + Service: generatedServices["clouduseraccounts"]["vm_alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/groups/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "groups": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/groups/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "groups": "resource", + "projects": "project", + }, + }, + }, + }, + }, + "projects/images": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/images/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "images": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/images/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "images": "resource", + "projects": "project", + }, + }, + }, + }, + }, + "projects/instances": { + "spanner": { + "v1": &IamResourceConfig{ + Service: generatedServices["spanner"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/instances/{instancesId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "instances": "instancesId", + "projects": "projectsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/instances/{instancesId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "instances": "instancesId", + "projects": "projectsId", + }, + }, + }, + }, + }, + "projects/instances/databases": { + "spanner": { + "v1": &IamResourceConfig{ + Service: generatedServices["spanner"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/instances/{instancesId}/databases/{databasesId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "databases": "databasesId", + "instances": "instancesId", + "projects": "projectsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/instances/{instancesId}/databases/{databasesId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "databases": "databasesId", + "instances": "instancesId", + "projects": "projectsId", + }, + }, + }, + }, + }, + "projects/interconnects": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/interconnects/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "interconnects": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/interconnects/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "interconnects": "resource", + "projects": "project", + }, + }, + }, + }, + }, + "projects/jobs": { + "ml": { + "v1": &IamResourceConfig{ + Service: generatedServices["ml"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/jobs/{jobsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "jobs": "jobsId", + "projects": "projectsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/jobs/{jobsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "jobs": "jobsId", + "projects": "projectsId", + }, + }, + }, + }, + }, + "projects/licenseCodes": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/licenseCodes/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "licenseCodes": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/licenseCodes/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "licenseCodes": "resource", + "projects": "project", + }, + }, + }, + }, + }, + "projects/licenses": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/licenses/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "licenses": "resource", + "projects": "project", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/licenses/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "licenses": "resource", + "projects": "project", + }, + }, + }, + }, + }, + "projects/locations/keyRings": { + "cloudkms": { + "v1": &IamResourceConfig{ + Service: generatedServices["cloudkms"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/locations/{locationsId}/keyRings/{keyRingsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "keyRings": "keyRingsId", + "locations": "locationsId", + "projects": "projectsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/locations/{locationsId}/keyRings/{keyRingsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "keyRings": "keyRingsId", + "locations": "locationsId", + "projects": "projectsId", + }, + }, + }, + }, + }, + "projects/locations/keyRings/cryptoKeys": { + "cloudkms": { + "v1": &IamResourceConfig{ + Service: generatedServices["cloudkms"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/locations/{locationsId}/keyRings/{keyRingsId}/cryptoKeys/{cryptoKeysId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "cryptoKeys": "cryptoKeysId", + "keyRings": "keyRingsId", + "locations": "locationsId", + "projects": "projectsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/locations/{locationsId}/keyRings/{keyRingsId}/cryptoKeys/{cryptoKeysId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "cryptoKeys": "cryptoKeysId", + "keyRings": "keyRingsId", + "locations": "locationsId", + "projects": "projectsId", + }, + }, + }, + }, + }, + "projects/locations/queues": { + "cloudtasks": { + "v2beta2": &IamResourceConfig{ + Service: generatedServices["cloudtasks"]["v2beta2"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v2beta2/projects/{projectsId}/locations/{locationsId}/queues/{queuesId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "locations": "locationsId", + "projects": "projectsId", + "queues": "queuesId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v2beta2/projects/{projectsId}/locations/{locationsId}/queues/{queuesId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "locations": "locationsId", + "projects": "projectsId", + "queues": "queuesId", + }, + }, + }, + }, + }, + "projects/locations/registries": { + "cloudiot": { + "v1": &IamResourceConfig{ + Service: generatedServices["cloudiot"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/locations/{locationsId}/registries/{registriesId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "locations": "locationsId", + "projects": "projectsId", + "registries": "registriesId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/locations/{locationsId}/registries/{registriesId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "locations": "locationsId", + "projects": "projectsId", + "registries": "registriesId", + }, + }, + }, + }, + }, + "projects/models": { + "ml": { + "v1": &IamResourceConfig{ + Service: generatedServices["ml"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/models/{modelsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "models": "modelsId", + "projects": "projectsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/models/{modelsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "models": "modelsId", + "projects": "projectsId", + }, + }, + }, + }, + }, + "projects/regions/clusters": { + "dataproc": { + "v1beta2": &IamResourceConfig{ + Service: generatedServices["dataproc"]["v1beta2"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta2/projects/{projectsId}/regions/{regionsId}/clusters/{clustersId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "clusters": "clustersId", + "projects": "projectsId", + "regions": "regionsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1beta2/projects/{projectsId}/regions/{regionsId}/clusters/{clustersId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "clusters": "clustersId", + "projects": "projectsId", + "regions": "regionsId", + }, + }, + }, + }, + }, + "projects/regions/interconnectAttachments": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/regions/{region}/interconnectAttachments/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "interconnectAttachments": "resource", + "projects": "project", + "regions": "region", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/regions/{region}/interconnectAttachments/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "interconnectAttachments": "resource", + "projects": "project", + "regions": "region", + }, + }, + }, + }, + }, + "projects/regions/maintenancePolicies": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/regions/{region}/maintenancePolicies/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "maintenancePolicies": "resource", + "projects": "project", + "regions": "region", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/regions/{region}/maintenancePolicies/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "maintenancePolicies": "resource", + "projects": "project", + "regions": "region", + }, + }, + }, + }, + }, + "projects/regions/nodeTemplates": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/regions/{region}/nodeTemplates/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "nodeTemplates": "resource", + "projects": "project", + "regions": "region", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/regions/{region}/nodeTemplates/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "nodeTemplates": "resource", + "projects": "project", + "regions": "region", + }, + }, + }, + }, + }, + "projects/regions/subnetworks": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/regions/{region}/subnetworks/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "regions": "region", + "subnetworks": "resource", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/regions/{region}/subnetworks/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "regions": "region", + "subnetworks": "resource", + }, + }, + }, + "beta": &IamResourceConfig{ + Service: generatedServices["compute"]["beta"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/regions/{region}/subnetworks/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "regions": "region", + "subnetworks": "resource", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/regions/{region}/subnetworks/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "regions": "region", + "subnetworks": "resource", + }, + }, + }, + }, + }, + "projects/repos": { + "sourcerepo": { + "v1": &IamResourceConfig{ + Service: generatedServices["sourcerepo"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/repos/{reposId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "repos": "reposId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/repos/{reposId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "repos": "reposId", + }, + }, + }, + }, + }, + "projects/serviceAccounts": { + "iam": { + "v1": &IamResourceConfig{ + Service: generatedServices["iam"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/serviceAccounts/{serviceAccountsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "serviceAccounts": "serviceAccountsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/serviceAccounts/{serviceAccountsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "serviceAccounts": "serviceAccountsId", + }, + }, + }, + }, + }, + "projects/snapshots": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/snapshots/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "snapshots": "resource", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/snapshots/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "snapshots": "resource", + }, + }, + }, + }, + "pubsub": { + "v1": &IamResourceConfig{ + Service: generatedServices["pubsub"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/snapshots/{snapshotsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "snapshots": "snapshotsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/snapshots/{snapshotsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "snapshots": "snapshotsId", + }, + }, + }, + }, + }, + "projects/subscriptions": { + "pubsub": { + "v1": &IamResourceConfig{ + Service: generatedServices["pubsub"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/subscriptions/{subscriptionsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "subscriptions": "subscriptionsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/subscriptions/{subscriptionsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "subscriptions": "subscriptionsId", + }, + }, + }, + "v1beta2": &IamResourceConfig{ + Service: generatedServices["pubsub"]["v1beta2"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta2/projects/{projectsId}/subscriptions/{subscriptionsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "subscriptions": "subscriptionsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1beta2/projects/{projectsId}/subscriptions/{subscriptionsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "subscriptions": "subscriptionsId", + }, + }, + }, + }, + }, + "projects/topics": { + "pubsub": { + "v1": &IamResourceConfig{ + Service: generatedServices["pubsub"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/projects/{projectsId}/topics/{topicsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "topics": "topicsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1/projects/{projectsId}/topics/{topicsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "topics": "topicsId", + }, + }, + }, + "v1beta2": &IamResourceConfig{ + Service: generatedServices["pubsub"]["v1beta2"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1beta2/projects/{projectsId}/topics/{topicsId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "topics": "topicsId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "v1beta2/projects/{projectsId}/topics/{topicsId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "projectsId", + "topics": "topicsId", + }, + }, + }, + }, + }, + "projects/users": { + "clouduseraccounts": { + "alpha": &IamResourceConfig{ + Service: generatedServices["clouduseraccounts"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/users/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "users": "resource", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/users/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "users": "resource", + }, + }, + }, + "vm_alpha": &IamResourceConfig{ + Service: generatedServices["clouduseraccounts"]["vm_alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/global/users/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "users": "resource", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/global/users/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "projects": "project", + "users": "resource", + }, + }, + }, + }, + }, + "projects/zones/disks": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/zones/{zone}/disks/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "disks": "resource", + "projects": "project", + "zones": "zone", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/zones/{zone}/disks/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "disks": "resource", + "projects": "project", + "zones": "zone", + }, + }, + }, + }, + }, + "projects/zones/hosts": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/zones/{zone}/hosts/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "hosts": "resource", + "projects": "project", + "zones": "zone", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/zones/{zone}/hosts/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "hosts": "resource", + "projects": "project", + "zones": "zone", + }, + }, + }, + }, + }, + "projects/zones/instances": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/zones/{zone}/instances/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "instances": "resource", + "projects": "project", + "zones": "zone", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/zones/{zone}/instances/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "instances": "resource", + "projects": "project", + "zones": "zone", + }, + }, + }, + }, + }, + "projects/zones/nodeGroups": { + "compute": { + "alpha": &IamResourceConfig{ + Service: generatedServices["compute"]["alpha"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "{project}/zones/{zone}/nodeGroups/{resource}/setIamPolicy", + ReplacementKeys: map[string]string{ + "nodeGroups": "resource", + "projects": "project", + "zones": "zone", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "GET", + Path: "{project}/zones/{zone}/nodeGroups/{resource}/getIamPolicy", + ReplacementKeys: map[string]string{ + "nodeGroups": "resource", + "projects": "project", + "zones": "zone", + }, + }, + }, + }, + }, + "services": { + "servicemanagement": { + "v1": &IamResourceConfig{ + Service: generatedServices["servicemanagement"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/services/{servicesId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "services": "servicesId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/services/{servicesId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "services": "servicesId", + }, + }, + }, + }, + }, + "services/consumers": { + "servicemanagement": { + "v1": &IamResourceConfig{ + Service: generatedServices["servicemanagement"]["v1"], + SetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/services/{servicesId}/consumers/{consumersId}:setIamPolicy", + ReplacementKeys: map[string]string{ + "consumers": "consumersId", + "services": "servicesId", + }, + }, + GetIamPolicy: &HttpMethodCfg{ + HttpMethod: "POST", + Path: "v1/services/{servicesId}/consumers/{consumersId}:getIamPolicy", + ReplacementKeys: map[string]string{ + "consumers": "consumersId", + "services": "servicesId", + }, + }, + }, + }, + }, +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_config.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_config.go new file mode 100644 index 0000000000..112915e7de --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_config.go @@ -0,0 +1,136 @@ +package gcpsecrets + +import ( + "context" + "fmt" + "github.com/hashicorp/go-gcp-common/gcputil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "time" +) + +const ( + cfgReadWarning = "omitted sensitive credentials from read output" +) + +func pathConfig(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config", + Fields: map[string]*framework.FieldSchema{ + "credentials": { + Type: framework.TypeString, + Description: `GCP IAM service account credentials JSON with permissions to create new service accounts and set IAM policies`, + }, + "ttl": { + Type: framework.TypeDurationSecond, + Description: "Default lease for generated keys. If <= 0, will use system default.", + }, + "max_ttl": { + Type: framework.TypeDurationSecond, + Description: "Maximum time a service account key is valid for. If <= 0, will use system default.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathConfigRead, + logical.UpdateOperation: b.pathConfigWrite, + }, + + HelpSynopsis: pathConfigHelpSyn, + HelpDescription: pathConfigHelpDesc, + } +} + +func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + cfg, err := getConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if cfg == nil { + return nil, nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "ttl": int64(cfg.TTL / time.Second), + "max_ttl": int64(cfg.MaxTTL / time.Second), + }, + Warnings: []string{cfgReadWarning}, + }, nil +} + +func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + cfg, err := getConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if cfg == nil { + cfg = &config{} + } + + credentialsRaw, ok := data.GetOk("credentials") + if ok { + _, err := gcputil.Credentials(credentialsRaw.(string)) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("invalid credentials JSON file: %v", err)), nil + } + cfg.CredentialsRaw = credentialsRaw.(string) + } + + // Update token TTL. + ttlRaw, ok := data.GetOk("ttl") + if ok { + cfg.TTL = time.Duration(ttlRaw.(int)) * time.Second + } + + // Update token Max TTL. + maxTTLRaw, ok := data.GetOk("max_ttl") + if ok { + cfg.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second + } + + entry, err := logical.StorageEntryJSON("config", cfg) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + return nil, nil +} + +type config struct { + CredentialsRaw string + + TTL time.Duration + MaxTTL time.Duration +} + +func getConfig(ctx context.Context, s logical.Storage) (*config, error) { + var cfg config + cfgRaw, err := s.Get(ctx, "config") + if err != nil { + return nil, err + } + if cfgRaw == nil { + return nil, nil + } + + if err := cfgRaw.DecodeJSON(&cfg); err != nil { + return nil, err + } + + return &cfg, err +} + +const pathConfigHelpSyn = ` +Configure the GCP backend. +` + +const pathConfigHelpDesc = ` +The GCP backend requires credentials for managing IAM service accounts and keys +and IAM policies on various GCP resources. This endpoint is used to configure +those credentials as well as default values for the backend in general. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_role_set.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_role_set.go new file mode 100644 index 0000000000..79b0ca1129 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/path_role_set.go @@ -0,0 +1,520 @@ +package gcpsecrets + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util" + "github.com/hashicorp/vault/helper/useragent" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "google.golang.org/api/iam/v1" +) + +const ( + rolesetStoragePrefix = "roleset" +) + +func pathsRoleSet(b *backend) []*framework.Path { + return []*framework.Path{ + { + Pattern: fmt.Sprintf("roleset/%s", framework.GenericNameRegex("name")), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Required. Name of the role.", + }, + "secret_type": { + Type: framework.TypeString, + Description: fmt.Sprintf("Type of secret generated for this role set. Defaults to '%s'", SecretTypeAccessToken), + Default: SecretTypeAccessToken, + }, + "project": { + Type: framework.TypeString, + Description: "Name of the GCP project that this roleset's service account will belong to.", + }, + "bindings": { + Type: framework.TypeString, + Description: "Bindings configuration string.", + }, + "token_scopes": { + Type: framework.TypeCommaStringSlice, + Description: `List of OAuth scopes to assign to credentials generated under this role set`, + }, + }, + ExistenceCheck: b.pathRoleSetExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.DeleteOperation: b.pathRoleSetDelete, + logical.ReadOperation: b.pathRoleSetRead, + logical.CreateOperation: b.pathRoleSetCreateUpdate, + logical.UpdateOperation: b.pathRoleSetCreateUpdate, + }, + HelpSynopsis: pathRoleSetHelpSyn, + HelpDescription: pathRoleSetHelpDesc, + }, + // Path to rotate role set service accounts + { + Pattern: fmt.Sprintf("roleset/%s/rotate", framework.GenericNameRegex("name")), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the role.", + }, + }, + ExistenceCheck: b.pathRoleSetExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathRoleSetRotateAccount, + }, + HelpSynopsis: pathRoleSetRotateHelpSyn, + HelpDescription: pathRoleSetRotateHelpDesc, + }, + // Path to rotating role set service account key used to generate access tokens + { + Pattern: fmt.Sprintf("roleset/%s/rotate-key", framework.GenericNameRegex("name")), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the role.", + }, + }, + ExistenceCheck: b.pathRoleSetExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathRoleSetRotateKey, + }, + HelpSynopsis: pathRoleSetRotateKeyHelpSyn, + HelpDescription: pathRoleSetRotateKeyHelpDesc, + }, + // Paths for listing role sets + { + Pattern: "rolesets/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoleSetList, + }, + + HelpSynopsis: pathListRoleSetHelpSyn, + HelpDescription: pathListRoleSetHelpDesc, + }, + { + Pattern: "roleset/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoleSetList, + }, + + HelpSynopsis: pathListRoleSetHelpSyn, + HelpDescription: pathListRoleSetHelpDesc, + }, + } +} + +func (b *backend) pathRoleSetExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) { + nameRaw, ok := d.GetOk("name") + if !ok { + return false, errors.New("roleset name is required") + } + + rs, err := getRoleSet(nameRaw.(string), ctx, req.Storage) + if err != nil { + return false, err + } + + return rs != nil, nil +} + +func (b *backend) pathRoleSetRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + nameRaw, ok := d.GetOk("name") + if !ok { + return logical.ErrorResponse("name is required"), nil + } + + rs, err := getRoleSet(nameRaw.(string), ctx, req.Storage) + if err != nil { + return nil, err + } + if rs == nil { + return nil, nil + } + + data := map[string]interface{}{ + "secret_type": rs.SecretType, + "bindings": rs.Bindings.asOutput(), + } + + if rs.AccountId != nil { + data["service_account_email"] = rs.AccountId.EmailOrId + data["service_account_project"] = rs.AccountId.Project + } + + if rs.TokenGen != nil && rs.SecretType == SecretTypeAccessToken { + data["token_scopes"] = rs.TokenGen.Scopes + } + + return &logical.Response{ + Data: data, + }, nil +} + +func (b *backend) pathRoleSetDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + nameRaw, ok := d.GetOk("name") + if !ok { + return logical.ErrorResponse("name is required"), nil + } + rsName := nameRaw.(string) + + rs, err := getRoleSet(rsName, ctx, req.Storage) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("unable to get role set %s: {{err}}", rsName), err) + } + if rs == nil { + return nil, nil + } + + b.rolesetLock.Lock() + defer b.rolesetLock.Unlock() + + if rs.AccountId != nil { + _, err := framework.PutWAL(ctx, req.Storage, walTypeAccount, &walAccount{ + RoleSet: rsName, + Id: *rs.AccountId, + }) + if err != nil { + return nil, errwrap.Wrapf("unable to create WAL entry to clean up service account: {{err}}", err) + } + + for resName, roleSet := range rs.Bindings { + _, err := framework.PutWAL(ctx, req.Storage, walTypeIamPolicy, &walIamPolicy{ + RoleSet: rsName, + AccountId: *rs.AccountId, + Resource: resName, + Roles: roleSet.ToSlice(), + }) + if err != nil { + return nil, errwrap.Wrapf("unable to create WAL entry to clean up service account bindings: {{err}}", err) + } + } + + if rs.TokenGen != nil { + _, err := framework.PutWAL(ctx, req.Storage, walTypeAccount, &walAccountKey{ + RoleSet: rsName, + ServiceAccountName: rs.AccountId.ResourceName(), + KeyName: rs.TokenGen.KeyName, + }) + if err != nil { + return nil, errwrap.Wrapf("unable to create WAL entry to clean up service account key: {{err}}", err) + } + } + } + + if err := req.Storage.Delete(ctx, fmt.Sprintf("roleset/%s", nameRaw)); err != nil { + return nil, err + } + + // Clean up resources: + httpC, err := newHttpClient(ctx, req.Storage) + if err != nil { + return nil, err + } + + iamAdmin, err := iam.New(httpC) + if err != nil { + return nil, err + } + + iamHandle := iamutil.GetIamHandle(httpC, useragent.String()) + + warnings := make([]string, 0) + if rs.AccountId != nil { + if err := b.deleteServiceAccount(ctx, iamAdmin, rs.AccountId); err != nil { + w := fmt.Sprintf("unable to delete service account '%s' (WAL entry to clean-up later has been added): %v", rs.AccountId.ResourceName(), err) + warnings = append(warnings, w) + } + if err := b.deleteTokenGenKey(ctx, iamAdmin, rs.TokenGen); err != nil { + w := fmt.Sprintf("unable to delete key for service account '%s' (WAL entry to clean-up later has been added): %v", rs.AccountId.ResourceName(), err) + warnings = append(warnings, w) + } + if merr := b.removeBindings(ctx, iamHandle, rs.AccountId.EmailOrId, rs.Bindings); merr != nil { + for _, err := range merr.Errors { + w := fmt.Sprintf("unable to delete IAM policy bindings for service account '%s' (WAL entry to clean-up later has been added): %v", rs.AccountId.EmailOrId, err) + warnings = append(warnings, w) + } + } + } + + if len(warnings) > 0 { + return &logical.Response{Warnings: warnings}, nil + } + + return nil, nil +} + +func (b *backend) pathRoleSetCreateUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + var warnings []string + nameRaw, ok := d.GetOk("name") + if !ok { + return logical.ErrorResponse("name is required"), nil + } + name := nameRaw.(string) + + rs, err := getRoleSet(name, ctx, req.Storage) + if err != nil { + return nil, err + } + + if rs == nil { + rs = &RoleSet{ + Name: name, + } + } + + isCreate := req.Operation == logical.CreateOperation + + // Secret type + if isCreate { + secretType := d.Get("secret_type").(string) + switch secretType { + case SecretTypeKey, SecretTypeAccessToken: + rs.SecretType = secretType + default: + return logical.ErrorResponse(fmt.Sprintf(`invalid "secret_type" value: "%s"`, secretType)), nil + } + } else { + secretTypeRaw, ok := d.GetOk("secret_type") + if ok && rs.SecretType != secretTypeRaw.(string) { + return logical.ErrorResponse("cannot change secret_type after roleset creation"), nil + } + } + + // Project + var project string + projectRaw, ok := d.GetOk("project") + if ok { + project = projectRaw.(string) + if !isCreate && rs.AccountId.Project != project { + return logical.ErrorResponse(fmt.Sprintf("cannot change project for existing role set (old: %s, new: %s)", rs.AccountId.Project, project)), nil + } + if len(project) == 0 { + return logical.ErrorResponse("given empty project"), nil + } + } else { + if isCreate { + return logical.ErrorResponse("project argument is required for new role set"), nil + } + project = rs.AccountId.Project + } + + // Default scopes + var scopes []string + scopesRaw, ok := d.GetOk("token_scopes") + if ok { + if rs.SecretType != SecretTypeAccessToken { + warnings = []string{ + fmt.Sprintf("ignoring token_scopes, only valid for '%s' secret type role set", SecretTypeAccessToken), + } + } + scopes = scopesRaw.([]string) + if len(scopes) == 0 { + return logical.ErrorResponse("cannot provide empty token_scopes"), nil + } + } else if rs.SecretType == SecretTypeAccessToken { + if isCreate { + return logical.ErrorResponse("token_scopes must be provided for creating access token role set"), nil + } + if rs.TokenGen != nil { + scopes = rs.TokenGen.Scopes + } + } + + // Bindings + bRaw, newBindings := d.GetOk("bindings") + if len(bRaw.(string)) == 0 { + return logical.ErrorResponse("given empty bindings string"), nil + } + + if isCreate && newBindings == false { + return logical.ErrorResponse("bindings are required for new role set"), nil + } + + if !newBindings { + // Just save role with updated metadata: + if err := rs.save(ctx, req.Storage); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + return nil, nil + } + + // If new bindings, update service account. + var bindings ResourceBindings + bindings, err = util.ParseBindings(bRaw.(string)) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("unable to parse bindings: %v", err)), nil + } + if len(bindings) == 0 { + return logical.ErrorResponse("unable to parse any bindings from given bindings HCL"), nil + } + rs.RawBindings = bRaw.(string) + updateWarns, err := b.saveRoleSetWithNewAccount(ctx, req.Storage, rs, project, bindings, scopes) + if updateWarns != nil { + warnings = append(warnings, updateWarns...) + } + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } else if warnings != nil && len(warnings) > 0 { + return &logical.Response{Warnings: warnings}, nil + } + return nil, nil +} + +func (b *backend) pathRoleSetList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + rolesets, err := req.Storage.List(ctx, "roleset/") + if err != nil { + return nil, err + } + return logical.ListResponse(rolesets), nil +} + +func (b *backend) pathRoleSetRotateAccount(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + nameRaw, ok := d.GetOk("name") + if !ok { + return logical.ErrorResponse("name is required"), nil + } + name := nameRaw.(string) + + rs, err := getRoleSet(name, ctx, req.Storage) + if err != nil { + return nil, err + } + if rs == nil { + return logical.ErrorResponse(fmt.Sprintf("roleset '%s' not found", name)), nil + } + + var scopes []string + if rs.TokenGen != nil { + scopes = rs.TokenGen.Scopes + } + + warnings, err := b.saveRoleSetWithNewAccount(ctx, req.Storage, rs, rs.AccountId.Project, nil, scopes) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } else if warnings != nil && len(warnings) > 0 { + return &logical.Response{Warnings: warnings}, nil + } + return nil, nil +} + +func (b *backend) pathRoleSetRotateKey(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + nameRaw, ok := d.GetOk("name") + if !ok { + return logical.ErrorResponse("name is required"), nil + } + name := nameRaw.(string) + + rs, err := getRoleSet(name, ctx, req.Storage) + if err != nil { + return nil, err + } + if rs == nil { + return logical.ErrorResponse(fmt.Sprintf("roleset '%s' not found", name)), nil + } + + if rs.SecretType != SecretTypeAccessToken { + return logical.ErrorResponse("cannot rotate key for non-access-token role set"), nil + } + var scopes []string + if rs.TokenGen != nil { + scopes = rs.TokenGen.Scopes + } + warn, err := b.saveRoleSetWithNewTokenKey(ctx, req.Storage, rs, scopes) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + if warn != "" { + return &logical.Response{Warnings: []string{warn}}, nil + } + return nil, nil +} + +func getRoleSet(name string, ctx context.Context, s logical.Storage) (*RoleSet, error) { + entry, err := s.Get(ctx, fmt.Sprintf("%s/%s", rolesetStoragePrefix, name)) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + rs := &RoleSet{} + if err := entry.DecodeJSON(rs); err != nil { + return nil, err + } + return rs, nil +} + +const pathRoleSetHelpSyn = `Read/write sets of IAM roles to be given to generated credentials for specified GCP resources.` +const pathListRoleSetHelpSyn = `List existing rolesets.` +const pathRoleSetRotateHelpSyn = `Rotate the service account (and key for access token roleset) created and used to generate secrets` +const pathRoleSetRotateKeyHelpSyn = `Rotate only the service account key used by an access token roleset to generate tokens` + +const pathRoleSetRotateHelpDesc = ` +This path allows you to rotate (i.e. recreate) the service account used to +generate secrets for a given role set.` +const pathRoleSetRotateKeyHelpDesc = ` +This path allows you to rotate (i.e. recreate) the service account +key used to generate access tokens under a given role set. This +path only applies to role sets that generate access tokens ` + +const pathRoleSetHelpDesc = ` +This path allows you create role sets, which bind sets of IAM roles +to specific GCP resources. Secrets (either service account keys or +access tokens) are generated under a role set and will have the +given set of roles on resources. + +The specified binding file accepts an HCL (or JSON) string +with the following format: + +resource "some/gcp/resource/uri" { + roles = [ + "roles/role1", + "roles/role2", + "roles/role3", + ... + ] +} + +The given resource can have the following + +* Project-level self link + Self-link for a resource under a given project + (i.e. resource name starts with 'projects/...') + Use if you need to provide a versioned object or + are directly using resource.self_link. + + Example (Compute instance): + http://www.googleapis.com/compute/v1/projects/$PROJECT/zones/$ZONE/instances/$INSTANCE_NAME + +* Full Resource Name + A scheme-less URI consisting of a DNS-compatible + API service name and a resource path (i.e. the + relative resource name). Useful if you need to + specify what service this resource is under + but just want the preferred supported API version. + Note that if the resource you are using is for + a non-preferred API with multiple service versions, + you MUST specify the version. + + Example (IAM service account): + //$SERVICE.googleapis.com/projects/my-project/serviceAccounts/myserviceaccount@... + +* Relative Resource Name: + A URI path (path-noscheme) without the leading "/". + It identifies a resource within the API service. + Use if there is only one service that your + resource could belong to. If there are multiple + API versions that support the resource, we will + attempt to use the preferred version and ask + for more specific format otherwise. + + Example (Pubsub subscription): + projects/myproject/subscriptions/mysub +` +const pathListRoleSetHelpDesc = `List role sets by role set name` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/role_set.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/role_set.go new file mode 100644 index 0000000000..522639cdef --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/role_set.go @@ -0,0 +1,412 @@ +package gcpsecrets + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/go-gcp-common/gcputil" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util" + "github.com/hashicorp/vault/helper/useragent" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "google.golang.org/api/iam/v1" + "time" +) + +const ( + serviceAccountMaxLen = 30 + serviceAccountDisplayNameTmpl = "Service account for Vault secrets backend role set %s" + defaultCloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform" +) + +type RoleSet struct { + Name string + SecretType string + + RawBindings string + Bindings ResourceBindings + + AccountId *gcputil.ServiceAccountId + TokenGen *TokenGenerator +} + +func (rs *RoleSet) validate() error { + var err *multierror.Error + if rs.Name == "" { + err = multierror.Append(err, errors.New("role set name is empty")) + } + + if rs.SecretType == "" { + err = multierror.Append(err, errors.New("role set secret type is empty")) + } + + if rs.AccountId == nil { + err = multierror.Append(err, fmt.Errorf("role set should have account associated")) + } + + if len(rs.Bindings) == 0 { + err = multierror.Append(err, fmt.Errorf("role set bindings cannot be empty")) + } + + if len(rs.RawBindings) == 0 { + err = multierror.Append(err, fmt.Errorf("role set raw bindings cannot be empty string")) + } + + switch rs.SecretType { + case SecretTypeAccessToken: + if rs.TokenGen == nil { + err = multierror.Append(err, fmt.Errorf("access token role set should have initialized token generator")) + } else if len(rs.TokenGen.Scopes) == 0 { + err = multierror.Append(err, fmt.Errorf("access token role set should have defined scopes")) + } + case SecretTypeKey: + break + default: + err = multierror.Append(err, fmt.Errorf("unknown secret type: %s", rs.SecretType)) + } + return err.ErrorOrNil() +} + +func (rs *RoleSet) save(ctx context.Context, s logical.Storage) error { + if err := rs.validate(); err != nil { + return err + } + + entry, err := logical.StorageEntryJSON(fmt.Sprintf("%s/%s", rolesetStoragePrefix, rs.Name), rs) + if err != nil { + return err + } + + return s.Put(ctx, entry) +} + +func (rs *RoleSet) bindingHash() string { + ssum := sha256.Sum256([]byte(rs.RawBindings)[:]) + return base64.StdEncoding.EncodeToString(ssum[:]) +} + +func (rs *RoleSet) getServiceAccount(iamAdmin *iam.Service) (*iam.ServiceAccount, error) { + if rs.AccountId == nil { + return nil, fmt.Errorf("role set '%s' is invalid, has no associated service account", rs.Name) + } + + account, err := iamAdmin.Projects.ServiceAccounts.Get(rs.AccountId.ResourceName()).Do() + if err != nil { + return nil, fmt.Errorf("could not find service account: %v. If account was deleted, role set must be updated (write to roleset/%s/rotate) before generating new secrets", err, rs.Name) + } else if account == nil { + return nil, fmt.Errorf("roleset service account was removed - role set must be updated (path roleset/%s/rotate) before generating new secrets", rs.Name) + } + + return account, nil +} + +type ResourceBindings map[string]util.StringSet + +func (rb ResourceBindings) asOutput() map[string][]string { + out := make(map[string][]string) + for k, v := range rb { + out[k] = v.ToSlice() + } + return out +} + +type TokenGenerator struct { + KeyName string + B64KeyJSON string + + Scopes []string +} + +func (b *backend) saveRoleSetWithNewAccount(ctx context.Context, s logical.Storage, rs *RoleSet, project string, newBinds ResourceBindings, scopes []string) (warning []string, err error) { + b.rolesetLock.Lock() + defer b.rolesetLock.Unlock() + + httpC, err := newHttpClient(ctx, s, defaultCloudPlatformScope) + if err != nil { + return nil, err + } + + iamAdmin, err := newIamAdmin(ctx, s) + if err != nil { + return nil, err + } + + iamHandle := iamutil.GetIamHandle(httpC, useragent.String()) + + oldAccount := rs.AccountId + oldBindings := rs.Bindings + oldTokenKey := rs.TokenGen + + oldWals, err := rs.addWALsForCurrentAccount(ctx, s) + if err != nil { + tryDeleteWALs(ctx, s, oldWals...) + return nil, errwrap.Wrapf("failed to create WAL for cleaning up old account: {{err}}", err) + } + + newWals := make([]string, 0, len(newBinds)+2) + walId, err := rs.newServiceAccount(ctx, s, iamAdmin, project) + if err != nil { + tryDeleteWALs(ctx, s, oldWals...) + return nil, err + } + newWals = append(newWals, walId) + + binds := rs.Bindings + if newBinds != nil { + binds = newBinds + rs.Bindings = newBinds + } + walIds, err := rs.updateIamPolicies(ctx, s, b.enabledIamResources, iamHandle, binds) + if err != nil { + tryDeleteWALs(ctx, s, oldWals...) + return nil, err + } + newWals = append(newWals, walIds...) + + if rs.SecretType == SecretTypeAccessToken { + walId, err := rs.newKeyForTokenGen(ctx, s, iamAdmin, scopes) + if err != nil { + tryDeleteWALs(ctx, s, oldWals...) + return nil, err + } + newWals = append(newWals, walId) + } + + if err := rs.save(ctx, s); err != nil { + tryDeleteWALs(ctx, s, oldWals...) + return nil, err + } + + // Delete WALs for cleaning up new resources now that they have been saved. + tryDeleteWALs(ctx, s, newWals...) + + // Try deleting old resources (WALs exist so we can ignore failures) + if oldAccount == nil || oldAccount.EmailOrId == "" { + // nothing to clean up + return nil, nil + } + + // Return any errors as warnings so user knows immediate cleanup failed + warnings := make([]string, 0) + if errs := b.removeBindings(ctx, iamHandle, oldAccount.EmailOrId, oldBindings); errs != nil { + warnings = make([]string, len(errs.Errors), len(errs.Errors)+2) + for idx, err := range errs.Errors { + warnings[idx] = fmt.Sprintf("unable to immediately delete old binding (WAL cleanup entry has been added): %v", err) + } + } + if err := b.deleteServiceAccount(ctx, iamAdmin, oldAccount); err != nil { + warnings = append(warnings, fmt.Sprintf("unable to immediately delete old account (WAL cleanup entry has been added): %v", err)) + } + if err := b.deleteTokenGenKey(ctx, iamAdmin, oldTokenKey); err != nil { + warnings = append(warnings, fmt.Sprintf("unable to immediately delete old key (WAL cleanup entry has been added): %v", err)) + } + return warnings, nil +} + +func (b *backend) saveRoleSetWithNewTokenKey(ctx context.Context, s logical.Storage, rs *RoleSet, scopes []string) (warning string, err error) { + b.rolesetLock.Lock() + defer b.rolesetLock.Unlock() + + if rs.SecretType != SecretTypeAccessToken { + return "", fmt.Errorf("a key is not saved or used for non-access-token role set '%s'", rs.Name) + } + + iamAdmin, err := newIamAdmin(ctx, s) + if err != nil { + return "", err + } + + oldKeyWalId := "" + if rs.TokenGen != nil { + if oldKeyWalId, err = framework.PutWAL(ctx, s, walTypeAccountKey, &walAccountKey{ + RoleSet: rs.Name, + KeyName: rs.TokenGen.KeyName, + ServiceAccountName: rs.AccountId.ResourceName(), + }); err != nil { + return "", errwrap.Wrapf("unable to create WAL for deleting old key: {{err}}", err) + } + } + oldKeyGen := rs.TokenGen + + newKeyWalId, err := rs.newKeyForTokenGen(ctx, s, iamAdmin, scopes) + if err != nil { + tryDeleteWALs(ctx, s, oldKeyWalId) + return "", err + } + + if err := rs.save(ctx, s); err != nil { + tryDeleteWALs(ctx, s, oldKeyWalId) + return "", err + } + + // Delete WALs for cleaning up new key now that it's been saved. + tryDeleteWALs(ctx, s, newKeyWalId) + if err := b.deleteTokenGenKey(ctx, iamAdmin, oldKeyGen); err != nil { + return errwrap.Wrapf("unable to delete old key (delayed cleaned up WAL entry added): {{err}}", err).Error(), nil + } + + return "", nil +} + +func (rs *RoleSet) addWALsForCurrentAccount(ctx context.Context, s logical.Storage) ([]string, error) { + if rs.AccountId == nil { + return nil, nil + } + wals := make([]string, 0, len(rs.Bindings)+2) + walId, err := framework.PutWAL(ctx, s, walTypeAccount, &walAccount{ + RoleSet: rs.Name, + Id: gcputil.ServiceAccountId{ + Project: rs.AccountId.Project, + EmailOrId: rs.AccountId.EmailOrId, + }, + }) + if err != nil { + return nil, err + } + wals = append(wals, walId) + for resource, roles := range rs.Bindings { + var walId string + walId, err = framework.PutWAL(ctx, s, walTypeIamPolicy, &walIamPolicy{ + RoleSet: rs.Name, + AccountId: gcputil.ServiceAccountId{ + Project: rs.AccountId.Project, + EmailOrId: rs.AccountId.EmailOrId, + }, + Resource: resource, + Roles: roles.ToSlice(), + }) + if err != nil { + return nil, err + } + wals = append(wals, walId) + } + + if rs.SecretType == SecretTypeAccessToken && rs.TokenGen != nil { + walId, err := framework.PutWAL(ctx, s, walTypeAccountKey, &walAccountKey{ + RoleSet: rs.Name, + KeyName: rs.TokenGen.KeyName, + ServiceAccountName: rs.AccountId.ResourceName(), + }) + if err != nil { + return nil, err + } + wals = append(wals, walId) + } + return wals, nil +} + +func (rs *RoleSet) newServiceAccount(ctx context.Context, s logical.Storage, iamAdmin *iam.Service, project string) (string, error) { + saEmailPrefix := roleSetServiceAccountName(rs.Name) + projectName := fmt.Sprintf("projects/%s", project) + displayName := fmt.Sprintf(serviceAccountDisplayNameTmpl, rs.Name) + + walId, err := framework.PutWAL(ctx, s, walTypeAccount, &walAccount{ + RoleSet: rs.Name, + Id: gcputil.ServiceAccountId{ + Project: project, + EmailOrId: fmt.Sprintf("%s@%s.iam.gserviceaccount.com", saEmailPrefix, project), + }, + }) + if err != nil { + return "", errwrap.Wrapf("unable to create WAL entry for generating new service account: {{err}}", err) + } + + sa, err := iamAdmin.Projects.ServiceAccounts.Create( + projectName, &iam.CreateServiceAccountRequest{ + AccountId: saEmailPrefix, + ServiceAccount: &iam.ServiceAccount{DisplayName: displayName}, + }).Do() + if err != nil { + return walId, errwrap.Wrapf(fmt.Sprintf("unable to create new service account under project '%s': {{err}}", projectName), err) + } + rs.AccountId = &gcputil.ServiceAccountId{ + Project: project, + EmailOrId: sa.Email, + } + return walId, nil +} + +func (rs *RoleSet) newKeyForTokenGen(ctx context.Context, s logical.Storage, iamAdmin *iam.Service, scopes []string) (string, error) { + walId, err := framework.PutWAL(ctx, s, walTypeAccountKey, &walAccountKey{ + RoleSet: rs.Name, + KeyName: "", + ServiceAccountName: rs.AccountId.ResourceName(), + }) + if err != nil { + return "", err + } + + key, err := iamAdmin.Projects.ServiceAccounts.Keys.Create(rs.AccountId.ResourceName(), + &iam.CreateServiceAccountKeyRequest{ + PrivateKeyType: privateKeyTypeJson, + }).Do() + if err != nil { + framework.DeleteWAL(ctx, s, walId) + return "", err + } + rs.TokenGen = &TokenGenerator{ + KeyName: key.Name, + B64KeyJSON: key.PrivateKeyData, + Scopes: scopes, + } + return walId, nil +} + +func (rs *RoleSet) updateIamPolicies(ctx context.Context, s logical.Storage, enabledIamResources iamutil.EnabledResources, iamHandle *iamutil.IamHandle, rb ResourceBindings) ([]string, error) { + wals := make([]string, 0, len(rb)) + for rName, roles := range rb { + walId, err := framework.PutWAL(ctx, s, walTypeIamPolicy, &walIamPolicy{ + RoleSet: rs.Name, + AccountId: gcputil.ServiceAccountId{ + Project: rs.AccountId.Project, + EmailOrId: rs.AccountId.EmailOrId, + }, + Resource: rName, + Roles: roles.ToSlice(), + }) + if err != nil { + return wals, err + } + + resource, err := enabledIamResources.Resource(rName) + if err != nil { + return wals, err + } + + p, err := iamHandle.GetIamPolicy(ctx, resource) + if err != nil { + return wals, err + } + + changed, newP := p.AddBindings(&iamutil.PolicyDelta{ + Roles: roles, + Email: rs.AccountId.EmailOrId, + }) + if !changed || newP == nil { + continue + } + + if _, err := iamHandle.SetIamPolicy(ctx, resource, newP); err != nil { + return wals, err + } + wals = append(wals, walId) + } + return wals, nil +} + +func roleSetServiceAccountName(rsName string) (name string) { + intSuffix := fmt.Sprintf("%d", time.Now().Unix()) + fullName := fmt.Sprintf("vault%s-%s", rsName, intSuffix) + + name = fullName + if len(fullName) > serviceAccountMaxLen { + toTrunc := len(fullName) - serviceAccountMaxLen + name = fmt.Sprintf("vault%s-%s", rsName[:len(rsName)-toTrunc], intSuffix) + } + return name +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/rollback.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/rollback.go new file mode 100644 index 0000000000..bb56e4653d --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/rollback.go @@ -0,0 +1,276 @@ +package gcpsecrets + +import ( + "context" + "fmt" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/go-gcp-common/gcputil" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil" + "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util" + "github.com/hashicorp/vault/helper/useragent" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "github.com/mitchellh/mapstructure" + "google.golang.org/api/googleapi" + "google.golang.org/api/iam/v1" +) + +const ( + walTypeAccount = "account" + walTypeAccountKey = "account_key" + walTypeIamPolicy = "iam_policy" +) + +func (b *backend) walRollback(ctx context.Context, req *logical.Request, kind string, data interface{}) error { + switch kind { + case walTypeAccount: + return b.serviceAccountRollback(ctx, req, data) + case walTypeAccountKey: + return b.serviceAccountKeyRollback(ctx, req, data) + case walTypeIamPolicy: + return b.serviceAccountPolicyRollback(ctx, req, data) + default: + return fmt.Errorf("unknown type to rollback") + } +} + +type walAccount struct { + RoleSet string + Id gcputil.ServiceAccountId +} + +type walAccountKey struct { + RoleSet string + ServiceAccountName string + KeyName string +} + +type walIamPolicy struct { + RoleSet string + AccountId gcputil.ServiceAccountId + Resource string + Roles []string +} + +func (b *backend) serviceAccountRollback(ctx context.Context, req *logical.Request, data interface{}) error { + b.rolesetLock.Lock() + defer b.rolesetLock.Unlock() + + var entry walAccount + if err := mapstructure.Decode(data, &entry); err != nil { + return err + } + + // If account is still being used, WAL entry was not + // deleted properly after a successful operation. + // Remove WAL entry. + rs, err := getRoleSet(entry.RoleSet, ctx, req.Storage) + if err != nil { + return err + } + if rs != nil && entry.Id.ResourceName() == rs.AccountId.ResourceName() { + // Still being used - don't delete this service account. + return nil + } + + // Delete service account. + iamC, err := newIamAdmin(ctx, req.Storage) + if err != nil { + return err + } + + return b.deleteServiceAccount(ctx, iamC, &entry.Id) +} + +func (b *backend) serviceAccountKeyRollback(ctx context.Context, req *logical.Request, data interface{}) error { + b.rolesetLock.Lock() + defer b.rolesetLock.Unlock() + + var entry walAccountKey + if err := mapstructure.Decode(data, &entry); err != nil { + return err + } + + // If key is still being used, WAL entry was not + // deleted properly after a successful operation. + // Remove WAL entry. + rs, err := getRoleSet(entry.RoleSet, ctx, req.Storage) + if err != nil { + return err + } + if rs != nil && rs.TokenGen != nil && entry.KeyName == rs.TokenGen.KeyName { + return nil + } + + iamC, err := newIamAdmin(ctx, req.Storage) + if err != nil { + return err + } + + if entry.KeyName == "" { + if rs.SecretType != SecretTypeAccessToken { + // Do not clean up non-access-token role set keys. + return nil + } + + // delete all keys not in use by role set + keys, err := iamC.Projects.ServiceAccounts.Keys.List(rs.AccountId.ResourceName()).KeyTypes("USER_MANAGED").Do() + if err != nil && !isGoogleApi404Error(err) { + return err + } else if err != nil || keys == nil { + return nil + } + + for _, k := range keys.Keys { + if rs.TokenGen != nil && rs.TokenGen.KeyName == k.Name { + continue + } + // Delete all keys not being used by role set + _, err = iamC.Projects.ServiceAccounts.Keys.Delete(entry.KeyName).Do() + if err != nil && !isGoogleApi404Error(err) { + return err + } + } + } else { + _, err = iamC.Projects.ServiceAccounts.Keys.Delete(entry.KeyName).Do() + if err != nil && !isGoogleApi404Error(err) { + return err + } + } + return nil +} + +func (b *backend) serviceAccountPolicyRollback(ctx context.Context, req *logical.Request, data interface{}) error { + b.rolesetLock.Lock() + defer b.rolesetLock.Unlock() + + var entry walIamPolicy + if err := mapstructure.Decode(data, &entry); err != nil { + return err + } + + // Try to verify service account not being used by roleset + rs, err := getRoleSet(entry.RoleSet, ctx, req.Storage) + if err != nil { + return err + } + + // Take our any bindings still being used by this role set from roles being removed. + rolesToRemove := util.ToSet(entry.Roles) + if rs.AccountId.ResourceName() == entry.AccountId.ResourceName() { + currRoles, ok := rs.Bindings[entry.Resource] + if ok { + rolesToRemove = rolesToRemove.Sub(currRoles) + } + } + + r, err := b.enabledIamResources.Resource(entry.Resource) + if err != nil { + return err + } + + httpC, err := newHttpClient(ctx, req.Storage) + if err != nil { + return err + } + + iamHandle := iamutil.GetIamHandle(httpC, useragent.String()) + if err != nil { + return err + } + + p, err := iamHandle.GetIamPolicy(ctx, r) + if err != nil { + return err + } + + changed, newP := p.RemoveBindings( + &iamutil.PolicyDelta{ + Email: entry.AccountId.EmailOrId, + Roles: rolesToRemove, + }) + + if !changed { + return nil + } + + _, err = iamHandle.SetIamPolicy(ctx, r, newP) + return err +} + +func (b *backend) deleteServiceAccount(ctx context.Context, iamAdmin *iam.Service, account *gcputil.ServiceAccountId) error { + if account == nil || account.EmailOrId == "" { + return nil + } + + _, err := iamAdmin.Projects.ServiceAccounts.Delete(account.ResourceName()).Do() + if err != nil && !isGoogleApi404Error(err) { + return errwrap.Wrapf("unable to delete service account: {{err}}", err) + } + return nil +} + +func (b *backend) deleteTokenGenKey(ctx context.Context, iamAdmin *iam.Service, tgen *TokenGenerator) error { + if tgen == nil || tgen.KeyName == "" { + return nil + } + + _, err := iamAdmin.Projects.ServiceAccounts.Keys.Delete(tgen.KeyName).Do() + if err != nil && !isGoogleApi404Error(err) { + return errwrap.Wrapf("unable to delete service account key: {{err}}", err) + } + return nil +} + +func (b *backend) removeBindings(ctx context.Context, iamHandle *iamutil.IamHandle, email string, bindings ResourceBindings) (allErr *multierror.Error) { + for resName, roles := range bindings { + resource, err := b.enabledIamResources.Resource(resName) + if err != nil { + allErr = multierror.Append(allErr, errwrap.Wrapf(fmt.Sprintf("unable to delete role binding for resource '%s': {{err}}", resName), err)) + continue + } + + p, err := iamHandle.GetIamPolicy(ctx, resource) + if err != nil { + allErr = multierror.Append(allErr, errwrap.Wrapf(fmt.Sprintf("unable to delete role binding for resource '%s': {{err}}", resName), err)) + continue + } + + changed, newP := p.RemoveBindings(&iamutil.PolicyDelta{ + Email: email, + Roles: roles, + }) + if !changed { + continue + } + if _, err = iamHandle.SetIamPolicy(ctx, resource, newP); err != nil { + allErr = multierror.Append(allErr, errwrap.Wrapf(fmt.Sprintf("unable to delete role binding for resource '%s': {{err}}", resName), err)) + continue + } + } + return +} + +// This tries to clean up WALs that are no longer needed. +// We can ignore errors if deletion fails as WAL rollback +// will not be done if the object is still in use in the roleset +// or was not actually created. +func tryDeleteWALs(ctx context.Context, s logical.Storage, walIds ...string) { + for _, walId := range walIds { + // ignore errors - if not deleted and still used by + // roleset, will be ignored + framework.DeleteWAL(ctx, s, walId) + } +} + +func isGoogleApi404Error(err error) bool { + if err == nil { + return false + } + gErr, ok := err.(*googleapi.Error) + if ok && gErr.Code == 404 { + return true + } + return false +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_access_token.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_access_token.go new file mode 100644 index 0000000000..7123c2cd6e --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_access_token.go @@ -0,0 +1,162 @@ +package gcpsecrets + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/googleapi" + "google.golang.org/api/iam/v1" + "net/http" + "net/url" + "time" +) + +const ( + SecretTypeAccessToken = "access_token" + revokeAccessTokenEndpoint = "https://accounts.google.com/o/oauth2/revoke" + revokeTokenWarning = `revocation request was successful; however, due to how OAuth access propagation works, the OAuth token might still be valid until it expires` +) + +func secretAccessToken(b *backend) *framework.Secret { + return &framework.Secret{ + Type: SecretTypeAccessToken, + Fields: map[string]*framework.FieldSchema{ + "token": { + Type: framework.TypeString, + Description: "OAuth2 token", + }, + }, + Renew: b.secretAccessTokenRenew, + Revoke: secretAccessTokenRevoke, + } +} + +func pathSecretAccessToken(b *backend) *framework.Path { + return &framework.Path{ + Pattern: fmt.Sprintf("token/%s", framework.GenericNameRegex("roleset")), + Fields: map[string]*framework.FieldSchema{ + "roleset": { + Type: framework.TypeString, + Description: "Required. Name of the role set.", + }, + }, + ExistenceCheck: b.pathRoleSetExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathAccessToken, + logical.UpdateOperation: b.pathAccessToken, + }, + HelpSynopsis: pathTokenHelpSyn, + HelpDescription: pathTokenHelpDesc, + } +} + +func (b *backend) pathAccessToken(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + rsName := d.Get("roleset").(string) + + rs, err := getRoleSet(rsName, ctx, req.Storage) + if err != nil { + return nil, err + } + if rs == nil { + return logical.ErrorResponse(fmt.Sprintf("role set '%s' does not exists", rsName)), nil + } + + if rs.SecretType != SecretTypeAccessToken { + return logical.ErrorResponse(fmt.Sprintf("role set '%s' cannot generate access tokens (has secret type %s)", rsName, rs.SecretType)), nil + } + + return b.getSecretAccessToken(ctx, req.Storage, rs) +} + +func (b *backend) secretAccessTokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + // Renewal not allowed + return logical.ErrorResponse("short-term access tokens cannot be renewed - request new access token instead"), nil +} + +func secretAccessTokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + tokenRaw, ok := req.Secret.InternalData["access_token"] + if !ok { + return nil, fmt.Errorf("secret is missing token internal data") + } + + resp, err := http.Get(revokeAccessTokenEndpoint + fmt.Sprintf("?token=%s", url.QueryEscape(tokenRaw.(string)))) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("revoke returned error: %v", err)), nil + } + if err := googleapi.CheckResponse(resp); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + return &logical.Response{ + Warnings: []string{revokeTokenWarning}, + }, nil +} + +func (b *backend) getSecretAccessToken(ctx context.Context, s logical.Storage, rs *RoleSet) (*logical.Response, error) { + iamC, err := newIamAdmin(ctx, s) + if err != nil { + return nil, errwrap.Wrapf("could not create IAM Admin client: {{err}}", err) + } + + // Verify account still exists + _, err = rs.getServiceAccount(iamC) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("could not get role set service account: %v", err)), nil + } + + if rs.TokenGen == nil || rs.TokenGen.KeyName == "" { + return logical.ErrorResponse(fmt.Sprintf("invalid role set has no service account key, must be updated (path roleset/%s/rotate-key) before generating new secrets", rs.Name)), nil + } + + token, err := rs.TokenGen.getAccessToken(ctx, iamC) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("could not generate token: %v", err)), nil + } + + secretD := map[string]interface{}{ + "token": token.AccessToken, + } + internalD := map[string]interface{}{ + "access_token": token.AccessToken, + "key_name": rs.TokenGen.KeyName, + "role_set": rs.Name, + "role_set_bindings": rs.bindingHash(), + } + resp := b.Secret(SecretTypeAccessToken).Response(secretD, internalD) + resp.Secret.LeaseOptions.TTL = token.Expiry.Sub(time.Now()) + resp.Secret.LeaseOptions.Renewable = false + + return resp, err +} + +func (tg *TokenGenerator) getAccessToken(ctx context.Context, iamAdmin *iam.Service) (*oauth2.Token, error) { + key, err := iamAdmin.Projects.ServiceAccounts.Keys.Get(tg.KeyName).Do() + if err != nil { + return nil, errwrap.Wrapf("could not verify key used to generate tokens: {{err}}", err) + } + if key == nil { + return nil, errors.New("could not find key used to generate tokens, must update role set") + } + + jsonBytes, err := base64.StdEncoding.DecodeString(tg.B64KeyJSON) + if err != nil { + return nil, errwrap.Wrapf("could not b64-decode key data: {{err}}", err) + } + + cfg, err := google.JWTConfigFromJSON(jsonBytes, tg.Scopes...) + if err != nil { + return nil, errwrap.Wrapf("could not generate token JWT config: {{err}}", err) + } + + tkn, err := cfg.TokenSource(ctx).Token() + if err != nil { + return nil, errwrap.Wrapf("could not generate token: {{err}}", err) + } + return tkn, err +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_service_account_key.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_service_account_key.go new file mode 100644 index 0000000000..422fb78312 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/secrets_service_account_key.go @@ -0,0 +1,245 @@ +package gcpsecrets + +import ( + "context" + "fmt" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "google.golang.org/api/iam/v1" + "time" +) + +const ( + SecretTypeKey = "service_account_key" + keyAlgorithmRSA2k = "KEY_ALG_RSA_2048" + privateKeyTypeJson = "TYPE_GOOGLE_CREDENTIALS_FILE" +) + +func secretServiceAccountKey(b *backend) *framework.Secret { + return &framework.Secret{ + Type: SecretTypeKey, + Fields: map[string]*framework.FieldSchema{ + "private_key_data": { + Type: framework.TypeString, + Description: "Base-64 encoded string. Private key data for a service account key", + }, + "key_algorithm": { + Type: framework.TypeString, + Description: "Which type of key and algorithm to use for the key (defaults to 2K RSA). Valid values are GCP enum(ServiceAccountKeyAlgorithm)", + }, + "key_type": { + Type: framework.TypeString, + Description: "Type of the private key (i.e. whether it is JSON or P12). Valid values are GCP enum(ServiceAccountPrivateKeyType)", + }, + }, + + Renew: b.secretKeyRenew, + Revoke: secretKeyRevoke, + } +} + +func pathSecretServiceAccountKey(b *backend) *framework.Path { + return &framework.Path{ + Pattern: fmt.Sprintf("key/%s", framework.GenericNameRegex("roleset")), + Fields: map[string]*framework.FieldSchema{ + "roleset": { + Type: framework.TypeString, + Description: "Required. Name of the role set.", + }, + "key_algorithm": { + Type: framework.TypeString, + Description: fmt.Sprintf(`Private key algorithm for service account key - defaults to %s"`, keyAlgorithmRSA2k), + Default: keyAlgorithmRSA2k, + }, + "key_type": { + Type: framework.TypeString, + Description: fmt.Sprintf(`Private key type for service account key - defaults to %s"`, privateKeyTypeJson), + Default: privateKeyTypeJson, + }, + }, + ExistenceCheck: b.pathRoleSetExistenceCheck, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathServiceAccountKey, + logical.UpdateOperation: b.pathServiceAccountKey, + }, + HelpSynopsis: pathServiceAccountKeySyn, + HelpDescription: pathServiceAccountKeyDesc, + } +} + +func (b *backend) pathServiceAccountKey(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + rsName := d.Get("roleset").(string) + keyType := d.Get("key_type").(string) + keyAlg := d.Get("key_algorithm").(string) + + rs, err := getRoleSet(rsName, ctx, req.Storage) + if err != nil { + return nil, err + } + if rs == nil { + return logical.ErrorResponse(fmt.Sprintf("role set '%s' does not exists", rsName)), nil + } + + if rs.SecretType != SecretTypeKey { + return logical.ErrorResponse(fmt.Sprintf("role set '%s' cannot generate service account keys (has secret type %s)", rsName, rs.SecretType)), nil + } + + return b.getSecretKey(ctx, req.Storage, rs, keyType, keyAlg) +} + +func (b *backend) secretKeyRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + resp, err := b.verifySecretServiceKeyExists(ctx, req) + if err != nil || resp != nil { + return resp, err + } + + cfg, err := getConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if cfg == nil { + cfg = &config{} + } + + f := framework.LeaseExtend(cfg.TTL, cfg.MaxTTL, b.System()) + return f(ctx, req, d) +} + +func (b *backend) verifySecretServiceKeyExists(ctx context.Context, req *logical.Request) (*logical.Response, error) { + keyName, ok := req.Secret.InternalData["key_name"] + if !ok { + return nil, fmt.Errorf("invalid secret, internal data is missing key name") + } + + rsName, ok := req.Secret.InternalData["role_set"] + if !ok { + return nil, fmt.Errorf("invalid secret, internal data is missing role set name") + } + + bindingSum, ok := req.Secret.InternalData["role_set_bindings"] + if !ok { + return nil, fmt.Errorf("invalid secret, internal data is missing role set checksum") + } + + // Verify role set was not deleted. + rs, err := getRoleSet(rsName.(string), ctx, req.Storage) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("could not find role set '%v' for secret", rsName)), nil + } + + // Verify role set bindings have not changed since secret was generated. + if rs.bindingHash() != bindingSum.(string) { + return logical.ErrorResponse(fmt.Sprintf("role set '%v' bindings were updated since secret was generated, cannot renew", rsName)), nil + } + + // Verify service account key still exists. + iamAdmin, err := newIamAdmin(ctx, req.Storage) + if err != nil { + return logical.ErrorResponse("could not confirm key still exists in GCP"), nil + } + if k, err := iamAdmin.Projects.ServiceAccounts.Keys.Get(keyName.(string)).Do(); err != nil || k == nil { + return logical.ErrorResponse(fmt.Sprintf("could not confirm key still exists in GCP: %v", err)), nil + } + return nil, nil +} + +func secretKeyRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + keyNameRaw, ok := req.Secret.InternalData["key_name"] + if !ok { + return nil, fmt.Errorf("secret is missing key_name internal data") + } + + iamAdmin, err := newIamAdmin(ctx, req.Storage) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + _, err = iamAdmin.Projects.ServiceAccounts.Keys.Delete(keyNameRaw.(string)).Do() + if err != nil && !isGoogleApi404Error(err) { + return logical.ErrorResponse(fmt.Sprintf("unable to delete service account key: %v", err)), nil + } + + return nil, nil +} + +func (b *backend) getSecretKey(ctx context.Context, s logical.Storage, rs *RoleSet, keyType, keyAlgorithm string) (*logical.Response, error) { + var ttl time.Duration + cfg, err := getConfig(ctx, s) + if err != nil { + return nil, errwrap.Wrapf("could not read backend config: {{err}}", err) + } + max := b.System().MaxLeaseTTL() + if cfg == nil { + ttl = b.System().DefaultLeaseTTL() + } else { + if cfg.MaxTTL != 0 && cfg.MaxTTL < max { + max = cfg.MaxTTL + } + if cfg.TTL > 0 { + ttl = cfg.TTL + } + } + + if ttl > max { + ttl = max + } + + iamC, err := newIamAdmin(ctx, s) + if err != nil { + return nil, errwrap.Wrapf("could not create IAM Admin client: {{err}}", err) + } + + account, err := rs.getServiceAccount(iamC) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("roleset service account was removed - role set must be updated (write to roleset/%s/rotate) before generating new secrets", rs.Name)), nil + } + + key, err := iamC.Projects.ServiceAccounts.Keys.Create( + account.Name, &iam.CreateServiceAccountKeyRequest{ + KeyAlgorithm: keyAlgorithm, + PrivateKeyType: keyType, + }).Do() + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + secretD := map[string]interface{}{ + "private_key_data": key.PrivateKeyData, + "key_algorithm": key.KeyAlgorithm, + "key_type": key.PrivateKeyType, + } + internalD := map[string]interface{}{ + "key_name": key.Name, + "role_set": rs.Name, + "role_set_bindings": rs.bindingHash(), + } + + resp := b.Secret(SecretTypeKey).Response(secretD, internalD) + resp.Secret.LeaseOptions.TTL = ttl + resp.Secret.LeaseOptions.Renewable = true + return resp, nil +} + +const pathTokenHelpSyn = `Generate an OAuth2 access token under a specific role set.` +const pathTokenHelpDesc = ` +This path will generate a new OAuth2 access token for accessing GCP APIs. +A role set, binding IAM roles to specific GCP resources, will be specified +by name - for example, if this backend is mounted at "gcp", +then "gcp/token/deploy" would generate tokens for the "deploy" role set. + +On the backend, each roleset is associated with a service account. +The token will be associated with this service account. Tokens have a +short-term lease (1-hour) associated with them but cannot be renewed. +` + +const pathServiceAccountKeySyn = `Generate an service account private key under a specific role set.` +const pathServiceAccountKeyDesc = ` +This path will generate a new service account private key for accessing GCP APIs. +A role set, binding IAM roles to specific GCP resources, will be specified +by name - for example, if this backend is mounted at "gcp", then "gcp/key/deploy" +would generate service account keys for the "deploy" role set. + +On the backend, each roleset is associated with a service account under +which secrets/keys are created. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/bindings_template b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/bindings_template new file mode 100644 index 0000000000..cc8ace02f4 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/bindings_template @@ -0,0 +1,12 @@ +{{define "bindings" -}} +{{ range $resource,$roleStringSet := . -}} +resource "{{$resource}}" { + roles = [ + {{- range $role, $v := $roleStringSet -}} + "{{ $role }}", + {{- end -}} + ], +} + +{{ end -}} +{{- end }} \ No newline at end of file diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/parse_bindings.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/parse_bindings.go new file mode 100644 index 0000000000..d0d912185d --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/parse_bindings.go @@ -0,0 +1,135 @@ +package util + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/hcl" + "github.com/hashicorp/hcl/hcl/ast" + "io/ioutil" + "strings" + "text/template" +) + +const bindingTemplate = "util/bindings_template" + +func BindingsHCL(bindings map[string]StringSet) (string, error) { + tpl, err := template.ParseFiles(bindingTemplate) + if err != nil { + return "", err + } + + var buf bytes.Buffer + if err := tpl.ExecuteTemplate(&buf, "bindings", bindings); err != nil { + return "", err + } + return buf.String(), nil +} + +func ParseBindings(bindingsStr string) (map[string]StringSet, error) { + // Try to base64 decode + decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(bindingsStr)) + decoded, b64err := ioutil.ReadAll(decoder) + + var bindsString string + if b64err != nil { + bindsString = bindingsStr + } else { + bindsString = string(decoded) + } + + root, err := hcl.Parse(bindsString) + if err != nil { + if b64err == nil { + return nil, errwrap.Wrapf("unable to parse base64-encoded bindings as valid HCL: {{err}}", err) + } else { + return nil, errwrap.Wrapf("unable to parse raw string bindings as valid HCL: {{err}}", err) + } + } + + bindingLst, ok := root.Node.(*ast.ObjectList) + if !ok { + return nil, errors.New("unable to parse bindings: does not contain a root object") + } + + bindingsMap, err := parseBindingObjList(bindingLst) + if err != nil { + return nil, errwrap.Wrapf("unable to parse bindings: {{err}}", err) + } + return bindingsMap, nil +} + +func parseBindingObjList(topList *ast.ObjectList) (map[string]StringSet, error) { + var merr *multierror.Error + + bindings := make(map[string]StringSet) + + for _, item := range topList.Items { + if len(item.Keys) != 2 { + merr = multierror.Append(merr, fmt.Errorf("invalid resource item does not have ID on line %d", item.Assign.Line)) + continue + } + + key := item.Keys[0].Token.Value().(string) + if key != "resource" { + merr = multierror.Append(merr, fmt.Errorf("invalid key '%s' (line %d)", key, item.Assign.Line)) + continue + } + + resourceName := item.Keys[1].Token.Value().(string) + _, ok := bindings[resourceName] + if !ok { + bindings[resourceName] = make(StringSet) + } + + resourceList := item.Val.(*ast.ObjectType).List + for _, rolesItem := range resourceList.Items { + key := rolesItem.Keys[0].Token.Text + switch key { + case "roles": + parseRoles(rolesItem, bindings[resourceName], merr) + default: + merr = multierror.Append(merr, fmt.Errorf("invalid key '%s' in resource '%s' (line %d)", key, resourceName, item.Assign.Line)) + continue + } + } + } + err := merr.ErrorOrNil() + if err != nil { + return nil, err + } + return bindings, nil +} + +func parseRoles(item *ast.ObjectItem, roleSet StringSet, merr *multierror.Error) { + lst, ok := item.Val.(*ast.ListType) + if !ok { + merr = multierror.Append(merr, fmt.Errorf("roles must be a list (line %d)", item.Assign.Line)) + return + } + + for _, roleItem := range lst.List { + role := roleItem.(*ast.LiteralType).Token.Value().(string) + + tkns := strings.Split(role, "/") + switch len(tkns) { + case 2: + // "roles/X" + if tkns[0] == "roles" { + roleSet.Add(role) + continue + } + case 4: + // "projects/X/roles/Y" or "organizations/X/roles/Y" + if (tkns[0] == "projects" || tkns[0] == "organizations") && tkns[2] == "roles" { + roleSet.Add(role) + continue + } + } + + merr = multierror.Append(merr, fmt.Errorf("invalid role: %s (line %d): must be project-level, organization-level, or global role", role, roleItem.Pos().Line)) + } +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/string_set.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/string_set.go new file mode 100644 index 0000000000..d2e408b42d --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/string_set.go @@ -0,0 +1,80 @@ +package util + +// A set of strings +type StringSet map[string]struct{} + +func ToSet(values []string) StringSet { + s := make(StringSet) + for _, v := range values { + s[v] = struct{}{} + } + return s +} + +func (ss StringSet) Add(v string) { + ss[v] = struct{}{} +} + +func (ss StringSet) ToSlice() []string { + ls := make([]string, len(ss)) + i := 0 + for r := range ss { + ls[i] = r + i++ + } + return ls +} + +func (ss StringSet) Includes(v string) bool { + _, ok := ss[v] + return ok +} + +func (ss StringSet) Update(members ...string) { + for _, v := range members { + ss[v] = struct{}{} + } +} + +func (ss StringSet) Union(other StringSet) StringSet { + un := make(StringSet) + for v := range ss { + un[v] = struct{}{} + } + for v := range other { + un[v] = struct{}{} + } + return un +} + +func (ss StringSet) Intersection(other StringSet) StringSet { + inter := make(StringSet) + + var s StringSet + if len(ss) > len(other) { + s = other + } else { + s = ss + } + + for v := range s { + if other.Includes(v) { + inter[v] = struct{}{} + } + } + return inter +} + +func (ss StringSet) Sub(other StringSet) StringSet { + sub := make(StringSet) + for v := range ss { + if !other.Includes(v) { + sub[v] = struct{}{} + } + } + return sub +} + +func (ss StringSet) Equals(other StringSet) bool { + return len(ss.Intersection(other)) == len(ss) +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/testing.go b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/testing.go new file mode 100644 index 0000000000..a69e3d3225 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util/testing.go @@ -0,0 +1,31 @@ +package util + +import ( + "github.com/hashicorp/go-gcp-common/gcputil" + "os" + "testing" +) + +const googleCredentialsEnv = "TEST_GOOGLE_CREDENTIALS" +const googleProjectEnv = "TEST_GOOGLE_PROJECT" + +func GetTestCredentials(t *testing.T) (string, *gcputil.GcpCredentials) { + credentialsJSON := os.Getenv(googleCredentialsEnv) + if credentialsJSON == "" { + t.Fatalf("%s must be set to JSON string of valid Google credentials file", googleCredentialsEnv) + } + + credentials, err := gcputil.Credentials(credentialsJSON) + if err != nil { + t.Fatalf("valid Google credentials JSON could not be read from %s env variable: %v", googleCredentialsEnv, err) + } + return credentialsJSON, credentials +} + +func GetTestProject(t *testing.T) string { + project := os.Getenv(googleProjectEnv) + if project == "" { + t.Fatalf("%s must be set to JSON string of valid Google credentials file", googleProjectEnv) + } + return project +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 39e3122396..45ac28e75e 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1080,6 +1080,12 @@ "revision": "d5fe4b57a186c716b0e00b8c301cbd9b4182694d", "revisionTime": "2017-12-18T14:54:08Z" }, + { + "checksumSHA1": "mxGkRISfv2EldeYihuciIYrKNjI=", + "path": "github.com/hashicorp/go-gcp-common/gcputil", + "revision": "4b38f46ac60aebc9c67f2548aec1ff5bc1e9a4eb", + "revisionTime": "2018-02-23T19:15:17Z" + }, { "checksumSHA1": "xTdKLI1gAJOTqswvr15a/fI8ucA=", "path": "github.com/hashicorp/go-hclog", @@ -1302,6 +1308,24 @@ "revision": "6c7dd3f219b06c4fa249bff2b859733a2f262e3b", "revisionTime": "2018-03-20T18:05:36Z" }, + { + "checksumSHA1": "w+bhfXfIBMWNMuH2SJqw6Y5c6mk=", + "path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin", + "revision": "7633b05ac6d9a8f77f9255ef5aea09f35a145b0b", + "revisionTime": "2018-03-22T03:06:48Z" + }, + { + "checksumSHA1": "0bNwTVNqwHHUJ+r1usgdtGsiSJs=", + "path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil", + "revision": "69b9785fc3a7b4e7ca40c13f1d5b6284d43bf273", + "revisionTime": "2018-03-21T19:18:39Z" + }, + { + "checksumSHA1": "qF5wbamqBc44thWKDSmF8ayi6nI=", + "path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util", + "revision": "69b9785fc3a7b4e7ca40c13f1d5b6284d43bf273", + "revisionTime": "2018-03-21T19:18:39Z" + }, { "checksumSHA1": "ZYuIUFGjAZ2rgy/zwdjfANFZc/U=", "path": "github.com/hashicorp/vault-plugin-secrets-kv",