1package gcp
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io/ioutil"
9	"net/http"
10	"time"
11
12	"github.com/hashicorp/errwrap"
13	cleanhttp "github.com/hashicorp/go-cleanhttp"
14	"github.com/hashicorp/go-gcp-common/gcputil"
15	hclog "github.com/hashicorp/go-hclog"
16	"github.com/hashicorp/vault/api"
17	"github.com/hashicorp/vault/command/agent/auth"
18	"github.com/hashicorp/vault/sdk/helper/parseutil"
19	"golang.org/x/oauth2"
20	iam "google.golang.org/api/iam/v1"
21)
22
23const (
24	typeGCE                    = "gce"
25	typeIAM                    = "iam"
26	identityEndpoint           = "http://metadata/computeMetadata/v1/instance/service-accounts/%s/identity"
27	defaultIamMaxJwtExpMinutes = 15
28)
29
30type gcpMethod struct {
31	logger         hclog.Logger
32	authType       string
33	mountPath      string
34	role           string
35	credentials    string
36	serviceAccount string
37	project        string
38	jwtExp         int64
39}
40
41func NewGCPAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
42	if conf == nil {
43		return nil, errors.New("empty config")
44	}
45	if conf.Config == nil {
46		return nil, errors.New("empty config data")
47	}
48
49	var err error
50
51	g := &gcpMethod{
52		logger:         conf.Logger,
53		mountPath:      conf.MountPath,
54		serviceAccount: "default",
55	}
56
57	typeRaw, ok := conf.Config["type"]
58	if !ok {
59		return nil, errors.New("missing 'type' value")
60	}
61	g.authType, ok = typeRaw.(string)
62	if !ok {
63		return nil, errors.New("could not convert 'type' config value to string")
64	}
65
66	roleRaw, ok := conf.Config["role"]
67	if !ok {
68		return nil, errors.New("missing 'role' value")
69	}
70	g.role, ok = roleRaw.(string)
71	if !ok {
72		return nil, errors.New("could not convert 'role' config value to string")
73	}
74
75	switch {
76	case g.role == "":
77		return nil, errors.New("'role' value is empty")
78	case g.authType == "":
79		return nil, errors.New("'type' value is empty")
80	case g.authType != typeGCE && g.authType != typeIAM:
81		return nil, errors.New("'type' value is invalid")
82	}
83
84	credentialsRaw, ok := conf.Config["credentials"]
85	if ok {
86		g.credentials, ok = credentialsRaw.(string)
87		if !ok {
88			return nil, errors.New("could not convert 'credentials' value into string")
89		}
90	}
91
92	serviceAccountRaw, ok := conf.Config["service_account"]
93	if ok {
94		g.serviceAccount, ok = serviceAccountRaw.(string)
95		if !ok {
96			return nil, errors.New("could not convert 'service_account' value into string")
97		}
98	}
99
100	projectRaw, ok := conf.Config["project"]
101	if ok {
102		g.project, ok = projectRaw.(string)
103		if !ok {
104			return nil, errors.New("could not convert 'project' value into string")
105		}
106	}
107
108	jwtExpRaw, ok := conf.Config["jwt_exp"]
109	if ok {
110		g.jwtExp, err = parseutil.ParseInt(jwtExpRaw)
111		if err != nil {
112			return nil, errwrap.Wrapf("error parsing 'jwt_raw' into integer: {{err}}", err)
113		}
114	}
115
116	return g, nil
117}
118
119func (g *gcpMethod) Authenticate(ctx context.Context, client *api.Client) (retPath string, retData map[string]interface{}, retErr error) {
120	g.logger.Trace("beginning authentication")
121
122	data := make(map[string]interface{})
123	var jwt string
124
125	switch g.authType {
126	case typeGCE:
127		httpClient := cleanhttp.DefaultClient()
128
129		// Fetch token
130		{
131			req, err := http.NewRequest("GET", fmt.Sprintf(identityEndpoint, g.serviceAccount), nil)
132			if err != nil {
133				retErr = errwrap.Wrapf("error creating request: {{err}}", err)
134				return
135			}
136			req = req.WithContext(ctx)
137			req.Header.Add("Metadata-Flavor", "Google")
138			q := req.URL.Query()
139			q.Add("audience", fmt.Sprintf("%s/vault/%s", client.Address(), g.role))
140			q.Add("format", "full")
141			req.URL.RawQuery = q.Encode()
142			resp, err := httpClient.Do(req)
143			if err != nil {
144				retErr = errwrap.Wrapf("error fetching instance token: {{err}}", err)
145				return
146			}
147			if resp == nil {
148				retErr = errors.New("empty response fetching instance toke")
149				return
150			}
151			defer resp.Body.Close()
152			jwtBytes, err := ioutil.ReadAll(resp.Body)
153			if err != nil {
154				retErr = errwrap.Wrapf("error reading instance token response body: {{err}}", err)
155				return
156			}
157
158			jwt = string(jwtBytes)
159		}
160
161	default:
162		ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient())
163
164		credentials, tokenSource, err := gcputil.FindCredentials(g.credentials, ctx, iam.CloudPlatformScope)
165		if err != nil {
166			retErr = errwrap.Wrapf("could not obtain credentials: {{err}}", err)
167			return
168		}
169
170		httpClient := oauth2.NewClient(ctx, tokenSource)
171
172		var serviceAccount string
173		if g.serviceAccount == "" && credentials != nil {
174			serviceAccount = credentials.ClientEmail
175		} else {
176			serviceAccount = g.serviceAccount
177		}
178		if serviceAccount == "" {
179			retErr = errors.New("could not obtain service account from credentials (possibly Application Default Credentials are being used); a service account to authenticate as must be provided")
180			return
181		}
182
183		project := "-"
184		if g.project != "" {
185			project = g.project
186		} else if credentials != nil {
187			project = credentials.ProjectId
188		}
189
190		ttlMin := int64(defaultIamMaxJwtExpMinutes)
191		if g.jwtExp != 0 {
192			ttlMin = g.jwtExp
193		}
194		ttl := time.Minute * time.Duration(ttlMin)
195
196		jwtPayload := map[string]interface{}{
197			"aud": fmt.Sprintf("http://vault/%s", g.role),
198			"sub": serviceAccount,
199			"exp": time.Now().Add(ttl).Unix(),
200		}
201		payloadBytes, err := json.Marshal(jwtPayload)
202		if err != nil {
203			retErr = errwrap.Wrapf("could not convert JWT payload to JSON string: {{err}}", err)
204			return
205		}
206
207		jwtReq := &iam.SignJwtRequest{
208			Payload: string(payloadBytes),
209		}
210
211		iamClient, err := iam.New(httpClient)
212		if err != nil {
213			retErr = errwrap.Wrapf("could not create IAM client: {{err}}", err)
214			return
215		}
216
217		resourceName := fmt.Sprintf("projects/%s/serviceAccounts/%s", project, serviceAccount)
218		resp, err := iamClient.Projects.ServiceAccounts.SignJwt(resourceName, jwtReq).Do()
219		if err != nil {
220			retErr = errwrap.Wrapf(fmt.Sprintf("unable to sign JWT for %s using given Vault credentials: {{err}}", resourceName), err)
221			return
222		}
223
224		jwt = resp.SignedJwt
225	}
226
227	data["role"] = g.role
228	data["jwt"] = jwt
229
230	return fmt.Sprintf("%s/login", g.mountPath), data, nil
231}
232
233func (g *gcpMethod) NewCreds() chan struct{} {
234	return nil
235}
236
237func (g *gcpMethod) CredSuccess() {
238}
239
240func (g *gcpMethod) Shutdown() {
241}
242