-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
gcp.go
139 lines (118 loc) · 3.61 KB
/
gcp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package gcp
import (
"context"
"encoding/json"
"fmt"
"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
)
// NewGCPAuth initializes a new GCP auth method interface to be
// passed as a parameter to the client.Auth().Login method.
//
// Supported options: WithMountPath, WithIAMAuth
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) {
loginData := map[string]interface{}{
"role": a.roleName,
}
switch a.authType {
case gceType:
if !metadata.OnGCE() {
return nil, fmt.Errorf("GCE metadata service not available")
}
// loginData["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)
resp, err := client.Logical().Write(path, loginData)
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
}
}
// 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
}