2023-03-15 16:00:52 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2021-11-12 17:32:05 +00:00
package gcp
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"cloud.google.com/go/compute/metadata"
credentials "cloud.google.com/go/iam/credentials/apiv1"
"github.com/hashicorp/vault/api"
credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
)
type GCPAuth struct {
roleName string
mountPath string
authType string
serviceAccountEmail string
}
var _ api . AuthMethod = ( * GCPAuth ) ( nil )
type LoginOption func ( a * GCPAuth ) error
const (
iamType = "iam"
gceType = "gce"
defaultMountPath = "gcp"
defaultAuthType = gceType
identityMetadataURL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
)
// NewGCPAuth initializes a new GCP auth method interface to be
// passed as a parameter to the client.Auth().Login method.
//
// Supported options: WithMountPath, WithIAMAuth, WithGCEAuth
func NewGCPAuth ( roleName string , opts ... LoginOption ) ( * GCPAuth , error ) {
if roleName == "" {
return nil , fmt . Errorf ( "no role name provided for login" )
}
a := & GCPAuth {
mountPath : defaultMountPath ,
authType : defaultAuthType ,
roleName : roleName ,
}
// Loop through each option
for _ , opt := range opts {
// Call the option giving the instantiated
// *GCPAuth as the argument
err := opt ( a )
if err != nil {
return nil , fmt . Errorf ( "error with login option: %w" , err )
}
}
// return the modified auth struct instance
return a , nil
}
// Login sets up the required request body for the GCP auth method's /login
// endpoint, and performs a write to it. This method defaults to the "gce"
// auth type unless NewGCPAuth is called with WithIAMAuth().
func ( a * GCPAuth ) Login ( ctx context . Context , client * api . Client ) ( * api . Secret , error ) {
2022-04-06 15:20:34 +00:00
if ctx == nil {
ctx = context . Background ( )
}
2021-11-12 17:32:05 +00:00
loginData := map [ string ] interface { } {
"role" : a . roleName ,
}
switch a . authType {
case gceType :
jwt , err := a . getJWTFromMetadataService ( client . Address ( ) )
if err != nil {
return nil , fmt . Errorf ( "unable to retrieve JWT from GCE metadata service: %w" , err )
}
loginData [ "jwt" ] = jwt
case iamType :
jwtResp , err := a . signJWT ( )
if err != nil {
return nil , fmt . Errorf ( "unable to sign JWT for authenticating to GCP: %w" , err )
}
loginData [ "jwt" ] = jwtResp . SignedJwt
}
path := fmt . Sprintf ( "auth/%s/login" , a . mountPath )
2022-04-06 15:20:34 +00:00
resp , err := client . Logical ( ) . WriteWithContext ( ctx , path , loginData )
2021-11-12 17:32:05 +00:00
if err != nil {
return nil , fmt . Errorf ( "unable to log in with GCP auth: %w" , err )
}
return resp , nil
}
func WithMountPath ( mountPath string ) LoginOption {
return func ( a * GCPAuth ) error {
a . mountPath = mountPath
return nil
}
}
func WithIAMAuth ( serviceAccountEmail string ) LoginOption {
return func ( a * GCPAuth ) error {
a . serviceAccountEmail = serviceAccountEmail
a . authType = iamType
return nil
}
}
func WithGCEAuth ( ) LoginOption {
return func ( a * GCPAuth ) error {
a . authType = gceType
return nil
}
}
// generate signed JWT token from GCP IAM.
func ( a * GCPAuth ) signJWT ( ) ( * credentialspb . SignJwtResponse , error ) {
ctx := context . Background ( )
iamClient , err := credentials . NewIamCredentialsClient ( ctx ) // can pass option.WithCredentialsFile("path/to/creds.json") as second param if GOOGLE_APPLICATION_CREDENTIALS env var not set
if err != nil {
return nil , fmt . Errorf ( "unable to initialize IAM credentials client: %w" , err )
}
defer iamClient . Close ( )
resourceName := fmt . Sprintf ( "projects/-/serviceAccounts/%s" , a . serviceAccountEmail )
jwtPayload := map [ string ] interface { } {
"aud" : fmt . Sprintf ( "vault/%s" , a . roleName ) ,
"sub" : a . serviceAccountEmail ,
"exp" : time . Now ( ) . Add ( time . Minute * 10 ) . Unix ( ) ,
}
payloadBytes , err := json . Marshal ( jwtPayload )
if err != nil {
return nil , fmt . Errorf ( "unable to marshal jwt payload to json: %w" , err )
}
signJWTReq := & credentialspb . SignJwtRequest {
Name : resourceName ,
Payload : string ( payloadBytes ) ,
}
jwtResp , err := iamClient . SignJwt ( ctx , signJWTReq )
if err != nil {
return nil , fmt . Errorf ( "unable to sign JWT: %w" , err )
}
return jwtResp , nil
}
func ( a * GCPAuth ) getJWTFromMetadataService ( vaultAddress string ) ( string , error ) {
if ! metadata . OnGCE ( ) {
return "" , fmt . Errorf ( "GCE metadata service not available" )
}
// build request to metadata server
c := & http . Client { }
req , err := http . NewRequest ( http . MethodGet , identityMetadataURL , nil )
if err != nil {
return "" , fmt . Errorf ( "error creating http request: %w" , err )
}
req . Header . Add ( "Metadata-Flavor" , "Google" )
q := url . Values { }
q . Add ( "audience" , fmt . Sprintf ( "%s/vault/%s" , vaultAddress , a . roleName ) )
q . Add ( "format" , "full" )
req . URL . RawQuery = q . Encode ( )
resp , err := c . Do ( req )
if err != nil {
return "" , fmt . Errorf ( "error making request to metadata service: %w" , err )
}
defer resp . Body . Close ( )
// get jwt from response
body , err := ioutil . ReadAll ( resp . Body )
jwt := string ( body )
if err != nil {
return "" , fmt . Errorf ( "error reading response from metadata service: %w" , err )
}
return jwt , nil
}