Update plugins
This commit is contained in:
parent
6e3d8311a1
commit
2b2b69cf0b
|
@ -3,32 +3,26 @@ package gcpauth
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/go-gcp-common/gcputil"
|
||||
"github.com/hashicorp/vault/helper/useragent"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
"github.com/hashicorp/vault/version"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/cloudresourcemanager/v1"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/iam/v1"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const defaultCloudScope = "https://www.googleapis.com/auth/cloud-platform"
|
||||
|
||||
type GcpAuthBackend struct {
|
||||
*framework.Backend
|
||||
|
||||
// OAuth scopes for generating HTTP and GCP service clients.
|
||||
oauthScopes []string
|
||||
|
||||
// Locks for guarding service clients
|
||||
clientMutex sync.RWMutex
|
||||
|
||||
// -- GCP service clients --
|
||||
iamClient *iam.Service
|
||||
gceClient *compute.Service
|
||||
}
|
||||
|
||||
// Factory returns a new backend as logical.Backend.
|
||||
|
@ -42,16 +36,12 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
|
|||
|
||||
func Backend() *GcpAuthBackend {
|
||||
b := &GcpAuthBackend{
|
||||
oauthScopes: []string{
|
||||
iam.CloudPlatformScope,
|
||||
compute.ComputeReadonlyScope,
|
||||
},
|
||||
oauthScopes: []string{defaultCloudScope},
|
||||
}
|
||||
|
||||
b.Backend = &framework.Backend{
|
||||
AuthRenew: b.pathLoginRenew,
|
||||
BackendType: logical.TypeCredential,
|
||||
Invalidate: b.invalidate,
|
||||
Help: backendHelp,
|
||||
PathsSpecial: &logical.Paths{
|
||||
Unauthenticated: []string{
|
||||
|
@ -72,106 +62,77 @@ func Backend() *GcpAuthBackend {
|
|||
return b
|
||||
}
|
||||
|
||||
func (b *GcpAuthBackend) invalidate(_ context.Context, key string) {
|
||||
switch key {
|
||||
case "config":
|
||||
b.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close deletes created GCP clients in backend.
|
||||
func (b *GcpAuthBackend) Close() {
|
||||
b.clientMutex.Lock()
|
||||
defer b.clientMutex.Unlock()
|
||||
|
||||
b.iamClient = nil
|
||||
b.gceClient = nil
|
||||
}
|
||||
|
||||
func (b *GcpAuthBackend) IAM(ctx context.Context, s logical.Storage) (*iam.Service, error) {
|
||||
b.clientMutex.RLock()
|
||||
if b.iamClient != nil {
|
||||
defer b.clientMutex.RUnlock()
|
||||
return b.iamClient, nil
|
||||
}
|
||||
|
||||
b.clientMutex.RUnlock()
|
||||
b.clientMutex.Lock()
|
||||
defer b.clientMutex.Unlock()
|
||||
|
||||
// Check if client was created during lock switch.
|
||||
if b.iamClient == nil {
|
||||
err := b.initClients(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b.iamClient, nil
|
||||
}
|
||||
|
||||
func (b *GcpAuthBackend) GCE(ctx context.Context, s logical.Storage) (*compute.Service, error) {
|
||||
b.clientMutex.RLock()
|
||||
if b.gceClient != nil {
|
||||
defer b.clientMutex.RUnlock()
|
||||
return b.gceClient, nil
|
||||
}
|
||||
|
||||
b.clientMutex.RUnlock()
|
||||
b.clientMutex.Lock()
|
||||
defer b.clientMutex.Unlock()
|
||||
|
||||
// Check if client was created during lock switch.
|
||||
if b.gceClient == nil {
|
||||
err := b.initClients(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b.gceClient, nil
|
||||
}
|
||||
|
||||
// Initialize attempts to create GCP clients from stored config.
|
||||
// It does not attempt to claim the client lock.
|
||||
func (b *GcpAuthBackend) initClients(ctx context.Context, s logical.Storage) (err error) {
|
||||
func (b *GcpAuthBackend) httpClient(ctx context.Context, s logical.Storage) (*http.Client, error) {
|
||||
config, err := b.config(ctx, s)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, errwrap.Wrapf(
|
||||
"could not check to see if GCP credentials were configured, error"+
|
||||
"reading config: {{err}}", err)
|
||||
}
|
||||
|
||||
var httpClient *http.Client
|
||||
if config == nil || config.Credentials == nil {
|
||||
_, tknSrc, err := gcputil.FindCredentials("", ctx, b.oauthScopes...)
|
||||
credsBytes, err := config.formatAndMarshalCredentials()
|
||||
if err != nil {
|
||||
return fmt.Errorf("credentials were not configured and fallback to application default credentials failed: %v", err)
|
||||
return nil, errwrap.Wrapf(
|
||||
"unable to marshal given GCP credential JSON: {{err}}", err)
|
||||
}
|
||||
|
||||
var creds *google.Credentials
|
||||
if config != nil && config.Credentials != nil {
|
||||
creds, err = google.CredentialsFromJSON(ctx, credsBytes, b.oauthScopes...)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to parse credentials: {{err}}", err)
|
||||
}
|
||||
cleanCtx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient())
|
||||
httpClient = oauth2.NewClient(cleanCtx, tknSrc)
|
||||
} else {
|
||||
httpClient, err = gcputil.GetHttpClient(config.Credentials, b.oauthScopes...)
|
||||
creds, err = google.FindDefaultCredentials(ctx, b.oauthScopes...)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, errwrap.Wrapf(
|
||||
"credentials were not configured and Vault could not find "+
|
||||
"Application Default Credentials (ADC). Either set ADC or "+
|
||||
"configure this auth backend at auth/$MOUNT/config "+
|
||||
"(default auth/gcp/config). Error: {{err}}", err)
|
||||
}
|
||||
}
|
||||
|
||||
userAgentStr := fmt.Sprintf("(%s %s) Vault/%s", runtime.GOOS, runtime.GOARCH, version.GetVersion().FullVersionNumber(true))
|
||||
cleanCtx := context.WithValue(ctx, oauth2.HTTPClient, cleanhttp.DefaultClient())
|
||||
client := oauth2.NewClient(cleanCtx, creds.TokenSource)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
b.iamClient, err = iam.New(httpClient)
|
||||
func (b *GcpAuthBackend) newGcpClients(ctx context.Context, s logical.Storage) (*clientHandles, error) {
|
||||
httpC, err := b.httpClient(ctx, s)
|
||||
if err != nil {
|
||||
b.Close()
|
||||
return err
|
||||
return nil, errwrap.Wrapf("could not obtain HTTP client: {{err}}", err)
|
||||
}
|
||||
b.iamClient.UserAgent = userAgentStr
|
||||
|
||||
b.gceClient, err = compute.New(httpClient)
|
||||
iamClient, err := iam.New(httpC)
|
||||
if err != nil {
|
||||
b.Close()
|
||||
return err
|
||||
return nil, fmt.Errorf(clientErrorTemplate, "IAM", err)
|
||||
}
|
||||
b.gceClient.UserAgent = userAgentStr
|
||||
iamClient.UserAgent = useragent.String()
|
||||
|
||||
return nil
|
||||
gceClient, err := compute.New(httpC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(clientErrorTemplate, "Compute", err)
|
||||
}
|
||||
iamClient.UserAgent = useragent.String()
|
||||
|
||||
crmClient, err := cloudresourcemanager.New(httpC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(clientErrorTemplate, "Cloud Resource Manager", err)
|
||||
}
|
||||
crmClient.UserAgent = useragent.String()
|
||||
|
||||
return &clientHandles{
|
||||
iam: iamClient,
|
||||
gce: gceClient,
|
||||
resourceManager: crmClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type clientHandles struct {
|
||||
iam *iam.Service
|
||||
gce *compute.Service
|
||||
resourceManager *cloudresourcemanager.Service
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
|
|
|
@ -163,8 +163,8 @@ Configuration:
|
|||
permissions on this service account.
|
||||
|
||||
project=<string>
|
||||
Project the service account belongs to. Defaults to credentials
|
||||
"project_id" if "credentials" specified and this value is not.
|
||||
Project for the service account who will be authenticating to Vault.
|
||||
Defaults to the credential's "project_id" (if credentials are specified)."
|
||||
`
|
||||
|
||||
return strings.TrimSpace(help)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"encoding/json"
|
||||
"github.com/hashicorp/go-gcp-common/gcputil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
|
@ -23,8 +24,7 @@ If not specified, will use application default credentials`,
|
|||
"google_certs_endpoint": {
|
||||
Type: framework.TypeString,
|
||||
Description: `
|
||||
Base endpoint url that Vault will use to get Google certificates.
|
||||
If not specified, will use the OAuth2 library default. Useful for testing.`,
|
||||
Deprecated. This field does nothing and be removed in a future release`,
|
||||
},
|
||||
},
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
|
@ -65,9 +65,6 @@ func (b *GcpAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Invalidate exisitng clients so they read the new configuration
|
||||
b.Close()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -94,9 +91,6 @@ func (b *GcpAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
|
|||
if v := config.Credentials.ProjectId; v != "" {
|
||||
resp["project_id"] = v
|
||||
}
|
||||
if v := config.GoogleCertsEndpoint; v != "" {
|
||||
resp["google_certs_endpoint"] = v
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: resp,
|
||||
|
@ -116,7 +110,29 @@ iam AUTH:
|
|||
// gcpConfig contains all config required for the GCP backend.
|
||||
type gcpConfig struct {
|
||||
Credentials *gcputil.GcpCredentials `json:"credentials"`
|
||||
GoogleCertsEndpoint string `json:"google_certs_endpoint"`
|
||||
}
|
||||
|
||||
// standardizedCreds wraps gcputil.GcpCredentials with a type to allow
|
||||
// parsing through Google libraries, since the google libraries struct is not
|
||||
// exposed.
|
||||
type standardizedCreds struct {
|
||||
*gcputil.GcpCredentials
|
||||
CredType string `json:"type"`
|
||||
}
|
||||
|
||||
const serviceAccountCredsType = "service_account"
|
||||
|
||||
// formatAsCredentialJSON converts and marshals the config credentials
|
||||
// into a parsable format by Google libraries.
|
||||
func (config *gcpConfig) formatAndMarshalCredentials() ([]byte, error) {
|
||||
if config == nil || config.Credentials == nil {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
return json.Marshal(standardizedCreds{
|
||||
GcpCredentials: config.Credentials,
|
||||
CredType: serviceAccountCredsType,
|
||||
})
|
||||
}
|
||||
|
||||
// Update sets gcpConfig values parsed from the FieldData.
|
||||
|
@ -132,12 +148,6 @@ func (config *gcpConfig) Update(data *framework.FieldData) error {
|
|||
}
|
||||
config.Credentials = creds
|
||||
}
|
||||
|
||||
certsEndpoint := data.Get("google_certs_endpoint").(string)
|
||||
if len(certsEndpoint) > 0 {
|
||||
config.GoogleCertsEndpoint = certsEndpoint
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
"google.golang.org/api/cloudresourcemanager/v1"
|
||||
"google.golang.org/api/compute/v1"
|
||||
"google.golang.org/api/iam/v1"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
@ -118,7 +119,7 @@ type gcpLoginInfo struct {
|
|||
Role *gcpRole
|
||||
|
||||
// ID or email of an IAM service account or that inferred for a GCE VM.
|
||||
ServiceAccountId string
|
||||
EmailOrId string
|
||||
|
||||
// Base JWT Claims (registered claims such as 'exp', 'iss', etc)
|
||||
JWTClaims *jwt.Claims
|
||||
|
@ -177,7 +178,7 @@ func (b *GcpAuthBackend) parseAndValidateJwt(ctx context.Context, req *logical.R
|
|||
if len(baseClaims.Subject) == 0 {
|
||||
return nil, errors.New("expected JWT to have non-empty 'sub' claim")
|
||||
}
|
||||
loginInfo.ServiceAccountId = baseClaims.Subject
|
||||
loginInfo.EmailOrId = baseClaims.Subject
|
||||
|
||||
if customClaims.Google != nil && customClaims.Google.Compute != nil && len(customClaims.Google.Compute.InstanceId) > 0 {
|
||||
loginInfo.GceMetadata = customClaims.Google.Compute
|
||||
|
@ -199,24 +200,23 @@ func (b *GcpAuthBackend) getSigningKey(ctx context.Context, token *jwt.JSONWebTo
|
|||
|
||||
switch role.RoleType {
|
||||
case iamRoleType:
|
||||
iamClient, err := b.IAM(ctx, s)
|
||||
clients, err := b.newGcpClients(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceAccountId, err := parseServiceAccountFromIAMJWT(rawToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountKey, err := gcputil.ServiceAccountKey(iamClient, &gcputil.ServiceAccountKeyId{
|
||||
Project: role.ProjectId,
|
||||
accountKey, err := gcputil.ServiceAccountKey(clients.iam, &gcputil.ServiceAccountKeyId{
|
||||
Project: "-",
|
||||
EmailOrId: serviceAccountId,
|
||||
Key: keyId,
|
||||
})
|
||||
if err != nil {
|
||||
// Attempt to get a normal Google Oauth cert in case of GCE inferrence.
|
||||
key, err := b.getGoogleOauthCert(ctx, keyId, s)
|
||||
key, err := gcputil.OAuth2RSAPublicKey(keyId, "")
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(
|
||||
fmt.Sprintf("could not find service account key or Google Oauth cert with given 'kid' id %s: {{err}}", keyId),
|
||||
|
@ -226,7 +226,7 @@ func (b *GcpAuthBackend) getSigningKey(ctx context.Context, token *jwt.JSONWebTo
|
|||
}
|
||||
return gcputil.PublicKey(accountKey.PublicKeyData)
|
||||
case gceRoleType:
|
||||
return b.getGoogleOauthCert(ctx, keyId, s)
|
||||
return gcputil.OAuth2RSAPublicKey(keyId, "")
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected role type %s", role.RoleType)
|
||||
}
|
||||
|
@ -245,17 +245,8 @@ func parseServiceAccountFromIAMJWT(signedJwt string) (string, error) {
|
|||
return accountId, nil
|
||||
}
|
||||
|
||||
func (b *GcpAuthBackend) getGoogleOauthCert(ctx context.Context, keyId string, s logical.Storage) (interface{}, error) {
|
||||
var certsEndpoint string
|
||||
conf, err := b.config(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read config for backend: %v", err)
|
||||
}
|
||||
if conf != nil {
|
||||
certsEndpoint = conf.GoogleCertsEndpoint
|
||||
}
|
||||
|
||||
key, err := gcputil.OAuth2RSAPublicKey(keyId, certsEndpoint)
|
||||
func (b *GcpAuthBackend) getGoogleOauthCert(ctx context.Context, keyId string) (interface{}, error) {
|
||||
key, err := gcputil.OAuth2RSAPublicKey(keyId, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -288,9 +279,9 @@ func validateBaseJWTClaims(c *jwt.Claims, roleName string) error {
|
|||
// ---- IAM login domain ----
|
||||
// pathIamLogin attempts a login operation using the parsed login info.
|
||||
func (b *GcpAuthBackend) pathIamLogin(ctx context.Context, req *logical.Request, loginInfo *gcpLoginInfo) (*logical.Response, error) {
|
||||
iamClient, err := b.IAM(ctx, req.Storage)
|
||||
clients, err := b.newGcpClients(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(clientErrorTemplate, "IAM", err)), nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
role := loginInfo.Role
|
||||
|
@ -306,10 +297,10 @@ func (b *GcpAuthBackend) pathIamLogin(ctx context.Context, req *logical.Request,
|
|||
|
||||
// Get service account and make sure it still exists.
|
||||
accountId := &gcputil.ServiceAccountId{
|
||||
Project: role.ProjectId,
|
||||
EmailOrId: loginInfo.ServiceAccountId,
|
||||
Project: "-",
|
||||
EmailOrId: loginInfo.EmailOrId,
|
||||
}
|
||||
serviceAccount, err := gcputil.ServiceAccount(iamClient, accountId)
|
||||
serviceAccount, err := gcputil.ServiceAccount(clients.iam, accountId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -348,6 +339,18 @@ func (b *GcpAuthBackend) pathIamLogin(ctx context.Context, req *logical.Request,
|
|||
},
|
||||
},
|
||||
}
|
||||
if role.AddGroupAliases {
|
||||
clients, err := b.newGcpClients(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aliases, err := b.groupAliases(clients.resourceManager, ctx, serviceAccount.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Auth.GroupAliases = aliases
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
@ -355,9 +358,9 @@ func (b *GcpAuthBackend) pathIamLogin(ctx context.Context, req *logical.Request,
|
|||
// pathIamRenew returns an error if the service account referenced in the auth token metadata cannot renew the
|
||||
// auth token for the given role.
|
||||
func (b *GcpAuthBackend) pathIamRenew(ctx context.Context, req *logical.Request, roleName string, role *gcpRole) error {
|
||||
iamClient, err := b.IAM(ctx, req.Storage)
|
||||
clients, err := b.newGcpClients(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return fmt.Errorf(clientErrorTemplate, "IAM", err)
|
||||
return err
|
||||
}
|
||||
|
||||
serviceAccountId, ok := req.Auth.Metadata["service_account_id"]
|
||||
|
@ -365,8 +368,14 @@ func (b *GcpAuthBackend) pathIamRenew(ctx context.Context, req *logical.Request,
|
|||
return errors.New("service account id metadata not associated with auth token, invalid")
|
||||
}
|
||||
|
||||
serviceAccount, err := gcputil.ServiceAccount(iamClient, &gcputil.ServiceAccountId{
|
||||
Project: role.ProjectId,
|
||||
// This project is the service account's project.
|
||||
project, ok := req.Auth.Metadata["project_id"]
|
||||
if !ok {
|
||||
project = "-"
|
||||
}
|
||||
|
||||
serviceAccount, err := gcputil.ServiceAccount(clients.iam, &gcputil.ServiceAccountId{
|
||||
Project: project,
|
||||
EmailOrId: serviceAccountId,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -387,9 +396,8 @@ func (b *GcpAuthBackend) pathIamRenew(ctx context.Context, req *logical.Request,
|
|||
|
||||
// validateAgainstIAMRole returns an error if the given IAM service account is not authorized for the role.
|
||||
func (b *GcpAuthBackend) authorizeIAMServiceAccount(serviceAccount *iam.ServiceAccount, role *gcpRole) error {
|
||||
// This is just in case - project should already be used to retrieve service account.
|
||||
if role.ProjectId != serviceAccount.ProjectId {
|
||||
return fmt.Errorf("service account %s does not belong to project %s", serviceAccount.Email, role.ProjectId)
|
||||
if len(role.BoundProjects) > 0 && !strutil.StrListContains(role.BoundProjects, serviceAccount.ProjectId) {
|
||||
return fmt.Errorf("service account %q not in bound projects %+v", serviceAccount.Email, role.BoundProjects)
|
||||
}
|
||||
|
||||
// Check if role has the wildcard as the only service account.
|
||||
|
@ -416,42 +424,46 @@ func (b *GcpAuthBackend) pathGceLogin(ctx context.Context, req *logical.Request,
|
|||
return logical.ErrorResponse("could not get GCE metadata from given JWT"), nil
|
||||
}
|
||||
|
||||
if role.ProjectId != metadata.ProjectId {
|
||||
if len(role.BoundProjects) > 0 && !strutil.StrListContains(role.BoundProjects, metadata.ProjectId) {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"GCE instance must belong to project %s; metadata given has project %s",
|
||||
role.ProjectId, metadata.ProjectId)), nil
|
||||
"instance %q (project %q) not in bound projects %+v", metadata.InstanceId, metadata.ProjectId, role.BoundProjects)), nil
|
||||
}
|
||||
|
||||
// Verify instance exists.
|
||||
gceClient, err := b.GCE(ctx, req.Storage)
|
||||
clients, err := b.newGcpClients(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(clientErrorTemplate, "GCE", err)), nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instance, err := metadata.GetVerifiedInstance(gceClient)
|
||||
instance, err := metadata.GetVerifiedInstance(clients.gce)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"error when attempting to find instance (project %s, zone: %s, instance: %s) :%v",
|
||||
metadata.ProjectId, metadata.Zone, metadata.InstanceName, err)), nil
|
||||
}
|
||||
|
||||
if err := b.authorizeGCEInstance(ctx, instance, req.Storage, role, loginInfo.ServiceAccountId); err != nil {
|
||||
if err := b.authorizeGCEInstance(ctx, instance, req.Storage, role, loginInfo.EmailOrId); err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
iamClient, err := b.IAM(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(clientErrorTemplate, "IAM", err)), nil
|
||||
if req.Operation == logical.AliasLookaheadOperation {
|
||||
return &logical.Response{
|
||||
Auth: &logical.Auth{
|
||||
Alias: &logical.Alias{
|
||||
Name: fmt.Sprintf("gce-%s", strconv.FormatUint(instance.Id, 10)),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
serviceAccount, err := gcputil.ServiceAccount(iamClient, &gcputil.ServiceAccountId{
|
||||
Project: loginInfo.Role.ProjectId,
|
||||
EmailOrId: loginInfo.ServiceAccountId,
|
||||
serviceAccount, err := gcputil.ServiceAccount(clients.iam, &gcputil.ServiceAccountId{
|
||||
Project: "-",
|
||||
EmailOrId: loginInfo.EmailOrId,
|
||||
})
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Could not find service account '%s' used for GCE metadata token: %s",
|
||||
loginInfo.ServiceAccountId, err)), nil
|
||||
loginInfo.EmailOrId, err)), nil
|
||||
}
|
||||
|
||||
resp := &logical.Response{
|
||||
|
@ -472,14 +484,56 @@ func (b *GcpAuthBackend) pathGceLogin(ctx context.Context, req *logical.Request,
|
|||
},
|
||||
}
|
||||
|
||||
if role.AddGroupAliases {
|
||||
aliases, err := b.groupAliases(clients.resourceManager, ctx, metadata.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Auth.GroupAliases = aliases
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// groupAliases will add group aliases for an authenticating GCP entity
|
||||
// starting at project-level and going up the Cloud Resource Manager
|
||||
// hierarchy
|
||||
//
|
||||
// For example, given a project hierarchy of
|
||||
// "my-org" --> "my-folder" --> "my-subfolder" --> "my-project",
|
||||
// this returns the following group aliases:
|
||||
// [
|
||||
// "project-my-project"
|
||||
// "folder-my-subfolder"
|
||||
// "folder-my-project"
|
||||
// "organization-my-org"
|
||||
// ]
|
||||
func (b *GcpAuthBackend) groupAliases(crmClient *cloudresourcemanager.Service, ctx context.Context, projectId string) ([]*logical.Alias, error) {
|
||||
ancestry, err := crmClient.Projects.
|
||||
GetAncestry(projectId, &cloudresourcemanager.GetAncestryRequest{}).
|
||||
Context(ctx).
|
||||
Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aliases := make([]*logical.Alias, len(ancestry.Ancestor)+1)
|
||||
aliases[0] = &logical.Alias{
|
||||
Name: fmt.Sprintf("project-%s", projectId),
|
||||
}
|
||||
for i, parent := range ancestry.Ancestor {
|
||||
aliases[i+1] = &logical.Alias{
|
||||
Name: fmt.Sprintf("%s-%s", parent.ResourceId.Type, parent.ResourceId.Id),
|
||||
}
|
||||
}
|
||||
return aliases, nil
|
||||
}
|
||||
|
||||
func authMetadata(loginInfo *gcpLoginInfo, serviceAccount *iam.ServiceAccount) map[string]string {
|
||||
metadata := map[string]string{
|
||||
"role": loginInfo.RoleName,
|
||||
"service_account_id": serviceAccount.UniqueId,
|
||||
"service_account_email": serviceAccount.Email,
|
||||
"project_id": serviceAccount.ProjectId,
|
||||
}
|
||||
|
||||
if loginInfo.GceMetadata != nil {
|
||||
|
@ -497,7 +551,12 @@ func authMetadata(loginInfo *gcpLoginInfo, serviceAccount *iam.ServiceAccount) m
|
|||
// pathGceRenew returns an error if the instance referenced in the auth token metadata cannot renew the
|
||||
// auth token for the given role.
|
||||
func (b *GcpAuthBackend) pathGceRenew(ctx context.Context, req *logical.Request, roleName string, role *gcpRole) error {
|
||||
gceClient, err := b.GCE(ctx, req.Storage)
|
||||
httpC, err := b.httpClient(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gceClient, err := compute.New(httpC)
|
||||
if err != nil {
|
||||
return fmt.Errorf(clientErrorTemplate, "GCE", err)
|
||||
}
|
||||
|
@ -573,23 +632,26 @@ func getInstanceMetadataFromAuth(authMetadata map[string]string) (*gcputil.GCEId
|
|||
// authorizeGCEInstance returns an error if the given GCE instance is not
|
||||
// authorized for the role.
|
||||
func (b *GcpAuthBackend) authorizeGCEInstance(ctx context.Context, instance *compute.Instance, s logical.Storage, role *gcpRole, serviceAccountId string) error {
|
||||
computeSvc, err := b.GCE(ctx, s)
|
||||
httpC, err := b.httpClient(ctx, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iamSvc, err := b.IAM(ctx, s)
|
||||
iamClient, err := iam.New(httpC)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf(clientErrorTemplate, "IAM", err)
|
||||
}
|
||||
|
||||
gceClient, err := compute.New(httpC)
|
||||
if err != nil {
|
||||
return fmt.Errorf(clientErrorTemplate, "GCE", err)
|
||||
}
|
||||
|
||||
return AuthorizeGCE(ctx, &AuthorizeGCEInput{
|
||||
client: &gcpClient{
|
||||
computeSvc: computeSvc,
|
||||
iamSvc: iamSvc,
|
||||
computeSvc: gceClient,
|
||||
iamSvc: iamClient,
|
||||
},
|
||||
|
||||
project: role.ProjectId,
|
||||
serviceAccount: serviceAccountId,
|
||||
|
||||
instanceLabels: instance.Labels,
|
||||
|
|
|
@ -23,7 +23,6 @@ const (
|
|||
// Errors
|
||||
errEmptyRoleName = "role name is required"
|
||||
errEmptyRoleType = "role type cannot be empty"
|
||||
errEmptyProjectId = "project id cannot be empty"
|
||||
errEmptyIamServiceAccounts = "IAM role type must have at least one service account"
|
||||
|
||||
errTemplateEditListWrongType = "role is type '%s', cannot edit attribute '%s' (expected role type: '%s')"
|
||||
|
@ -73,9 +72,9 @@ var baseRoleFieldSchema = map[string]*framework.FieldSchema{
|
|||
duration specified by this value. At each renewal, the token's TTL will be set to the value of this parameter.`,
|
||||
},
|
||||
// -- GCP Information
|
||||
"project_id": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The id of the project that authorized instances must belong to for this role.`,
|
||||
"bound_projects": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `GCP Projects that authenticating entities must belong to.`,
|
||||
},
|
||||
"bound_service_accounts": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
|
@ -84,9 +83,13 @@ var baseRoleFieldSchema = map[string]*framework.FieldSchema{
|
|||
If the single value "*" is given, this is assumed to be all service accounts under the role's project. If this
|
||||
is set on a GCE role, the inferred service account from the instance metadata token will be used.`,
|
||||
},
|
||||
"service_accounts": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `Deprecated, use bound_service_accounts instead.`,
|
||||
"add_group_aliases": {
|
||||
Type: framework.TypeBool,
|
||||
Default: false,
|
||||
Description: "If true, will add group aliases to auth tokens generated under this role. " +
|
||||
"This will add the full list of ancestors (projects, folders, organizations) " +
|
||||
"for the given entity's project. Requires IAM permission `resourcemanager.projects.get` " +
|
||||
"on this project.",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -132,20 +135,6 @@ var gceOnlyFieldSchema = map[string]*framework.FieldSchema{
|
|||
"\"key:value\" strings that must be present on the GCE instance " +
|
||||
"in order to authenticate. This option only applies to \"gce\" roles.",
|
||||
},
|
||||
|
||||
// Deprecated roles
|
||||
"bound_zone": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Deprecated: use \"bound_zones\" instead.",
|
||||
},
|
||||
"bound_region": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Deprecated: use \"bound_regions\" instead.",
|
||||
},
|
||||
"bound_instance_group": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Deprecated: use \"bound_instance_groups\" instead.",
|
||||
},
|
||||
}
|
||||
|
||||
// pathsRole creates paths for listing roles and CRUD operations.
|
||||
|
@ -160,6 +149,9 @@ func pathsRole(b *GcpAuthBackend) []*framework.Path {
|
|||
for k, v := range gceOnlyFieldSchema {
|
||||
roleFieldSchema[k] = v
|
||||
}
|
||||
for k, v := range deprecatedFieldSchema {
|
||||
roleFieldSchema[k] = v
|
||||
}
|
||||
|
||||
paths := []*framework.Path{
|
||||
{
|
||||
|
@ -291,9 +283,6 @@ func (b *GcpAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request,
|
|||
if role.RoleType != "" {
|
||||
resp["type"] = role.RoleType
|
||||
}
|
||||
if role.ProjectId != "" {
|
||||
resp["project_id"] = role.ProjectId
|
||||
}
|
||||
if len(role.Policies) > 0 {
|
||||
resp["policies"] = role.Policies
|
||||
}
|
||||
|
@ -309,6 +298,10 @@ func (b *GcpAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request,
|
|||
if len(role.BoundServiceAccounts) > 0 {
|
||||
resp["bound_service_accounts"] = role.BoundServiceAccounts
|
||||
}
|
||||
if len(role.BoundProjects) > 0 {
|
||||
resp["bound_projects"] = role.BoundProjects
|
||||
}
|
||||
resp["add_group_aliases"] = role.AddGroupAliases
|
||||
|
||||
switch role.RoleType {
|
||||
case iamRoleType:
|
||||
|
@ -509,6 +502,12 @@ func (b *GcpAuthBackend) role(ctx context.Context, s logical.Storage, name strin
|
|||
modified := false
|
||||
|
||||
// Move old bindings to new fields.
|
||||
if role.ProjectId != "" && len(role.BoundProjects) == 0 {
|
||||
role.BoundProjects = []string{role.ProjectId}
|
||||
role.ProjectId = ""
|
||||
modified = true
|
||||
}
|
||||
|
||||
if role.BoundRegion != "" && len(role.BoundRegions) == 0 {
|
||||
role.BoundRegions = []string{role.BoundRegion}
|
||||
role.BoundRegion = ""
|
||||
|
@ -576,9 +575,6 @@ type gcpRole struct {
|
|||
// Type of this role. See path_role constants for currently supported types.
|
||||
RoleType string `json:"role_type,omitempty"`
|
||||
|
||||
// Project ID in GCP for authorized entities.
|
||||
ProjectId string `json:"project_id,omitempty"`
|
||||
|
||||
// Policies for Vault to assign to authorized entities.
|
||||
Policies []string `json:"policies,omitempty"`
|
||||
|
||||
|
@ -593,6 +589,9 @@ type gcpRole struct {
|
|||
// with TTL equal to this value.
|
||||
Period time.Duration `json:"period,omitempty"`
|
||||
|
||||
// Projects that entities must belong to
|
||||
BoundProjects []string `json:"bound_projects,omitempty"`
|
||||
|
||||
// Service accounts allowed to login under this role.
|
||||
BoundServiceAccounts []string `json:"bound_service_accounts,omitempty"`
|
||||
|
||||
|
@ -618,8 +617,11 @@ type gcpRole struct {
|
|||
// BoundLabels that instances must currently have set in order to login under this role.
|
||||
BoundLabels map[string]string `json:"bound_labels,omitempty"`
|
||||
|
||||
AddGroupAliases bool `json:"add_group_aliases,omitempty"`
|
||||
|
||||
// Deprecated fields
|
||||
// TODO: Remove in 0.5.0+
|
||||
ProjectId string `json:"project_id,omitempty"`
|
||||
BoundRegion string `json:"bound_region,omitempty"`
|
||||
BoundZone string `json:"bound_zone,omitempty"`
|
||||
BoundInstanceGroup string `json:"bound_instance_group,omitempty"`
|
||||
|
@ -650,13 +652,6 @@ func (role *gcpRole) updateRole(sys logical.SystemView, op logical.Operation, da
|
|||
role.Policies = policyutil.ParsePolicies(nil)
|
||||
}
|
||||
|
||||
// Update GCP project id.
|
||||
if projectId, ok := data.GetOk("project_id"); ok {
|
||||
role.ProjectId = projectId.(string)
|
||||
} else if op == logical.CreateOperation {
|
||||
role.ProjectId = data.Get("project_id").(string)
|
||||
}
|
||||
|
||||
// Update token TTL.
|
||||
if ttl, ok := data.GetOk("ttl"); ok {
|
||||
role.TTL = time.Duration(ttl.(int)) * time.Second
|
||||
|
@ -711,12 +706,37 @@ func (role *gcpRole) updateRole(sys logical.SystemView, op logical.Operation, da
|
|||
role.BoundServiceAccounts = sa.([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if len(role.BoundServiceAccounts) > 0 {
|
||||
role.BoundServiceAccounts = strutil.TrimStrings(role.BoundServiceAccounts)
|
||||
role.BoundServiceAccounts = strutil.RemoveDuplicates(role.BoundServiceAccounts, false)
|
||||
}
|
||||
|
||||
// Update bound GCP projects.
|
||||
boundProjects, givenBoundProj := data.GetOk("bound_projects")
|
||||
if givenBoundProj {
|
||||
role.BoundProjects = boundProjects.([]string)
|
||||
}
|
||||
if projectId, ok := data.GetOk("project_id"); ok {
|
||||
if givenBoundProj {
|
||||
return warnings, errors.New("only one of 'bound_projects' or 'project_id' can be given")
|
||||
}
|
||||
warnings = append(warnings,
|
||||
`The "project_id" (singular) field is deprecated. `+
|
||||
`Please use plural "bound_projects" instead to bind required GCP projects. `+
|
||||
`The "project_id" field will be removed in a later release, so please update accordingly.`)
|
||||
role.BoundProjects = []string{projectId.(string)}
|
||||
}
|
||||
if len(role.BoundProjects) > 0 {
|
||||
role.BoundProjects = strutil.TrimStrings(role.BoundProjects)
|
||||
role.BoundProjects = strutil.RemoveDuplicates(role.BoundProjects, false)
|
||||
}
|
||||
|
||||
// Update bound GCP projects.
|
||||
addGroupAliases, ok := data.GetOk("add_group_aliases")
|
||||
if ok {
|
||||
role.AddGroupAliases = addGroupAliases.(bool)
|
||||
}
|
||||
|
||||
// Update fields specific to this type
|
||||
switch role.RoleType {
|
||||
case iamRoleType:
|
||||
|
@ -756,10 +776,6 @@ func (role *gcpRole) validate(sys logical.SystemView) (warnings []string, err er
|
|||
return warnings, fmt.Errorf("role type '%s' is invalid", role.RoleType)
|
||||
}
|
||||
|
||||
if role.ProjectId == "" {
|
||||
return warnings, errors.New(errEmptyProjectId)
|
||||
}
|
||||
|
||||
defaultLeaseTTL := sys.DefaultLeaseTTL()
|
||||
if role.TTL > defaultLeaseTTL {
|
||||
warnings = append(warnings, fmt.Sprintf(
|
||||
|
@ -960,3 +976,27 @@ func checkInvalidRoleTypeArgs(data *framework.FieldData, invalidSchema map[strin
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deprecatedFieldSchema contains the deprecated role attributes
|
||||
var deprecatedFieldSchema = map[string]*framework.FieldSchema{
|
||||
"service_accounts": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Deprecated: use \"bound_service_accounts\" instead.",
|
||||
},
|
||||
"project_id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Deprecated: use \"bound_projects\" instead",
|
||||
},
|
||||
"bound_zone": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Deprecated: use \"bound_zones\" instead.",
|
||||
},
|
||||
"bound_region": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Deprecated: use \"bound_regions\" instead.",
|
||||
},
|
||||
"bound_instance_group": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Deprecated: use \"bound_instance_groups\" instead.",
|
||||
},
|
||||
}
|
||||
|
|
2054
vendor/google.golang.org/api/cloudresourcemanager/v1/cloudresourcemanager-api.json
generated
vendored
Normal file
2054
vendor/google.golang.org/api/cloudresourcemanager/v1/cloudresourcemanager-api.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
8294
vendor/google.golang.org/api/cloudresourcemanager/v1/cloudresourcemanager-gen.go
generated
vendored
Normal file
8294
vendor/google.golang.org/api/cloudresourcemanager/v1/cloudresourcemanager-gen.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -1425,10 +1425,10 @@
|
|||
"revisionTime": "2018-08-16T20:11:31Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "pJC3V+AR+Khd/TpvekLf1ZRU7rA=",
|
||||
"checksumSHA1": "h7m+nCb1vdvcy5cUkCarix08yJI=",
|
||||
"path": "github.com/hashicorp/vault-plugin-auth-gcp/plugin",
|
||||
"revision": "86f7837fd8102347f7a10e3f0ad275e5a256863c",
|
||||
"revisionTime": "2018-10-25T21:28:40Z"
|
||||
"revision": "0a3edf071747fcb84aa4f9d08a34d2c249e6228d",
|
||||
"revisionTime": "2018-11-15T01:46:55Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "tt3FtyjXgdBI9Mb43UL4LtOZmAk=",
|
||||
|
@ -2808,6 +2808,12 @@
|
|||
"revision": "cc9bd73d51b4c5610e35bcb01f15368185cfaab3",
|
||||
"revisionTime": "2018-10-16T15:55:58Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "9Gt9K/Z3m6q4vOJPIm1ynTvgecg=",
|
||||
"path": "google.golang.org/api/cloudresourcemanager/v1",
|
||||
"revision": "83a9d304b1e613fc253e1e2710778642fe81af53",
|
||||
"revisionTime": "2018-11-14T18:14:26Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "QkzppExyctFz029dvXzKzh6UHgU=",
|
||||
"path": "google.golang.org/api/compute/v1",
|
||||
|
|
Loading…
Reference in New Issue