-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
approle.go
172 lines (146 loc) · 5.15 KB
/
approle.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package approle
import (
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/vault/api"
)
type AppRoleAuth struct {
mountPath string
roleID string
secretID string
unwrap bool
}
// SecretID is a struct that allows you to specify where your application is
// storing the secret ID required for login to the AppRole auth method.
type SecretID struct {
// Path on the file system where a trusted orchestrator has placed the
// application's secret ID. The recommended secure pattern is to use
// response-wrapping tokens rather than a plaintext value, by passing
// WithWrappingToken() to NewAppRoleAuth.
// https://learn.hashicorp.com/tutorials/vault/approle-best-practices?in=vault/auth-methods#secretid-delivery-best-practices
FromFile string
// The name of the environment variable containing the application's
// secret ID. Can be insecure if the environment variable is logged.
FromEnv string
// The secret ID as a plaintext string value. Insecure.
FromString string
}
type LoginOption func(a *AppRoleAuth) error
const (
defaultMountPath = "approle"
)
// NewAppRoleAuth initializes a new AppRole auth method interface to be
// passed as a parameter to the client.Auth().Login method.
//
// For a secret ID, the recommended secure pattern is to unwrap a one-time-use
// response-wrapping token that was placed here by a trusted orchestrator
// (https://learn.hashicorp.com/tutorials/vault/approle-best-practices?in=vault/auth-methods#secretid-delivery-best-practices)
// To indicate that the filepath points to this wrapping token and not just
// a plaintext secret ID, initialize NewAppRoleAuth with the
// WithWrappingToken LoginOption.
//
// Supported options: WithMountPath, WithWrappingToken
func NewAppRoleAuth(roleID string, secretID *SecretID, opts ...LoginOption) (*AppRoleAuth, error) {
var _ api.AuthMethod = (*AppRoleAuth)(nil)
if roleID == "" {
return nil, fmt.Errorf("no role ID provided for login")
}
if secretID == nil {
return nil, fmt.Errorf("no secret ID provided for login")
}
if secretID.FromFile == "" && secretID.FromEnv == "" && secretID.FromString == "" {
return nil, fmt.Errorf("secret ID for AppRole must be provided with a source file, environment variable, or plaintext string")
}
secretIDValue, err := readSecretID(secretID)
if err != nil {
return nil, fmt.Errorf("error reading secret ID: %w", err)
}
a := &AppRoleAuth{
mountPath: defaultMountPath,
roleID: roleID,
secretID: secretIDValue,
}
// Loop through each option
for _, opt := range opts {
// Call the option giving the instantiated
// *AppRoleAuth 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
}
func (a *AppRoleAuth) Login(client *api.Client) (*api.Secret, error) {
loginData := map[string]interface{}{
"role_id": a.roleID,
}
// if it was indicated that the value in the file was actually a wrapping
// token, unwrap it first
if a.unwrap {
unwrappedToken, err := client.Logical().Unwrap(a.secretID)
if err != nil {
return nil, fmt.Errorf("unable to unwrap token: %w. If the AppRoleAuth struct was initialized with the WithWrappingToken LoginOption, then the secret ID's filepath should be a path to a response-wrapping token", err)
}
loginData["secret_id"] = unwrappedToken.Data["secret_id"]
} else {
loginData["secret_id"] = a.secretID
}
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 app role auth: %w", err)
}
return resp, nil
}
func WithMountPath(mountPath string) LoginOption {
return func(a *AppRoleAuth) error {
a.mountPath = mountPath
return nil
}
}
func WithWrappingToken() LoginOption {
return func(a *AppRoleAuth) error {
a.unwrap = true
return nil
}
}
func readSecretID(secretID *SecretID) (string, error) {
var parsedSecretID string
if secretID.FromFile != "" {
if secretID.FromEnv != "" || secretID.FromString != "" {
return "", fmt.Errorf("only one location for the secret ID should be specified")
}
secretIDFile, err := os.Open(secretID.FromFile)
if err != nil {
return "", fmt.Errorf("unable to open file containing secret ID: %w", err)
}
defer secretIDFile.Close()
limitedReader := io.LimitReader(secretIDFile, 1000)
secretIDBytes, err := io.ReadAll(limitedReader)
if err != nil {
return "", fmt.Errorf("unable to read secret ID: %w", err)
}
parsedSecretID = string(secretIDBytes)
}
if secretID.FromEnv != "" {
if secretID.FromFile != "" || secretID.FromString != "" {
return "", fmt.Errorf("only one location for the secret ID should be specified")
}
parsedSecretID = os.Getenv(secretID.FromEnv)
if parsedSecretID == "" {
return "", fmt.Errorf("secret ID was specified with an environment variable with an empty value")
}
}
if secretID.FromString != "" {
if secretID.FromFile != "" || secretID.FromEnv != "" {
return "", fmt.Errorf("only one location for the secret ID should be specified")
}
parsedSecretID = secretID.FromString
}
secretIDValue := strings.TrimSuffix(parsedSecretID, "\n")
return secretIDValue, nil
}