-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcredentials_test.go
More file actions
359 lines (323 loc) · 14.4 KB
/
Copy pathcredentials_test.go
File metadata and controls
359 lines (323 loc) · 14.4 KB
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
// SPDX-License-Identifier: Apache-2.0
package git
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
configv1alpha3 "github.com/ConfigButler/gitops-reverser/api/v1alpha3"
)
func credTestSSHKey(t *testing.T) ([]byte, string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
privatePEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
pub, err := gossh.NewPublicKey(&key.PublicKey)
require.NoError(t, err)
return privatePEM, knownhosts.Line([]string{"github.com"}, pub)
}
func credTestClient(t *testing.T, objs ...client.Object) client.Client {
t.Helper()
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, configv1alpha3.AddToScheme(scheme))
return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build()
}
// AuthFromSecretData must read the SSH private key under any of the three ecosystem dialects
// (Kubernetes-native ssh-privatekey, Flux identity, Argo CD sshPrivateKey).
func TestAuthFromSecretData_SSHKeyDialects(t *testing.T) {
privateKey, knownHosts := credTestSSHKey(t)
c := credTestClient(t)
for _, keyName := range []string{"ssh-privatekey", "identity", "sshPrivateKey"} {
t.Run(keyName, func(t *testing.T) {
secret := &corev1.Secret{Data: map[string][]byte{
keyName: privateKey,
"known_hosts": []byte(knownHosts),
}}
auth, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.NoError(t, err)
assert.NotNil(t, auth.SSH)
})
}
}
// A password presented alongside an SSH key is its passphrase (Flux's shared-key convention), not
// HTTP basic auth — so the result is still SSH public-key auth, never BasicAuth.
func TestAuthFromSecretData_PasswordIsSSHPassphraseWhenKeyPresent(t *testing.T) {
privateKey, knownHosts := credTestSSHKey(t)
c := credTestClient(t)
secret := &corev1.Secret{Data: map[string][]byte{
"ssh-privatekey": privateKey,
"password": []byte(""), // unencrypted key: ignored, but must not divert to basic auth
"known_hosts": []byte(knownHosts),
}}
auth, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.NoError(t, err)
assert.NotNil(t, auth.SSH)
}
func TestAuthFromSecretData_HTTPBasicAndBearer(t *testing.T) {
c := credTestClient(t)
t.Run("basic", func(t *testing.T) {
secret := &corev1.Secret{Data: map[string][]byte{
"username": []byte("u"),
"password": []byte("p"),
}}
auth, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.NoError(t, err)
basic, ok := auth.Basic, auth.Basic != nil
require.True(t, ok)
assert.Equal(t, "u", basic.Username)
assert.Equal(t, "p", basic.Password)
})
t.Run("bearer token", func(t *testing.T) {
secret := &corev1.Secret{Data: map[string][]byte{"bearerToken": []byte("gho_token")}}
auth, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.NoError(t, err)
token, ok := auth.Bearer, auth.Bearer != nil
require.True(t, ok)
assert.Equal(t, "gho_token", token.Token)
})
t.Run("username without password", func(t *testing.T) {
secret := &corev1.Secret{Data: map[string][]byte{"username": []byte("u")}}
_, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "no password")
})
t.Run("no recognizable credentials", func(t *testing.T) {
secret := &corev1.Secret{Data: map[string][]byte{"random": []byte("x")}}
_, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "does not contain valid authentication data")
})
t.Run("nil secret is anonymous", func(t *testing.T) {
auth, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, nil, SSHHostKeyConfig{})
require.NoError(t, err)
assert.Equal(t, Credential{}, auth, "no secret means anonymous")
assert.Nil(t, auth.Options(), "anonymous renders no transport options")
})
}
// Host keys resolve in priority order: Secret → GitProvider.knownHostsRef → install-level default,
// then fail closed. These cases exercise each tier with a valid SSH key whose only variable is
// where the known_hosts come from.
func TestResolveKnownHosts_Priority(t *testing.T) {
privateKey, knownHosts := credTestSSHKey(t)
khConfigMap := func(name string, key string) *corev1.ConfigMap {
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "ns"},
Data: map[string]string{key: knownHosts},
}
}
t.Run("knownHostsRef ConfigMap (known_hosts key)", func(t *testing.T) {
c := credTestClient(t, khConfigMap("hosts", "known_hosts"))
provider := &configv1alpha3.GitProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns"},
Spec: configv1alpha3.GitProviderSpec{
KnownHostsRef: &configv1alpha3.KnownHostsReference{Name: "hosts"},
},
}
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
auth, err := CredentialFromSecretData(context.Background(), c, provider, secret, SSHHostKeyConfig{})
require.NoError(t, err)
assert.NotNil(t, auth.SSH)
})
t.Run("knownHostsRef ConfigMap (Argo ssh_known_hosts key)", func(t *testing.T) {
c := credTestClient(t, khConfigMap("hosts", "ssh_known_hosts"))
provider := &configv1alpha3.GitProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns"},
Spec: configv1alpha3.GitProviderSpec{
KnownHostsRef: &configv1alpha3.KnownHostsReference{Name: "hosts"},
},
}
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
auth, err := CredentialFromSecretData(context.Background(), c, provider, secret, SSHHostKeyConfig{})
require.NoError(t, err)
assert.NotNil(t, auth.SSH)
})
t.Run("knownHostsRef Secret", func(t *testing.T) {
refSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "host-secret", Namespace: "ns"},
Data: map[string][]byte{"known_hosts": []byte(knownHosts)},
}
c := credTestClient(t, refSecret)
provider := &configv1alpha3.GitProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns"},
Spec: configv1alpha3.GitProviderSpec{
KnownHostsRef: &configv1alpha3.KnownHostsReference{Kind: "Secret", Name: "host-secret"},
},
}
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
auth, err := CredentialFromSecretData(context.Background(), c, provider, secret, SSHHostKeyConfig{})
require.NoError(t, err)
assert.NotNil(t, auth.SSH)
})
t.Run("knownHostsRef missing object is an error", func(t *testing.T) {
c := credTestClient(t)
provider := &configv1alpha3.GitProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns"},
Spec: configv1alpha3.GitProviderSpec{
KnownHostsRef: &configv1alpha3.KnownHostsReference{Name: "absent"},
},
}
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
_, err := CredentialFromSecretData(context.Background(), c, provider, secret, SSHHostKeyConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "absent")
})
t.Run("install-level default ConfigMap", func(t *testing.T) {
c := credTestClient(t, khConfigMap("cluster-hosts", "known_hosts"))
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
hostKeys := SSHHostKeyConfig{ControllerNamespace: "ns", DefaultKnownHostsConfigMap: "cluster-hosts"}
auth, err := CredentialFromSecretData(context.Background(), c, &configv1alpha3.GitProvider{}, secret, hostKeys)
require.NoError(t, err)
assert.NotNil(t, auth.SSH)
})
t.Run("absent install-level default falls through to fail-closed", func(t *testing.T) {
c := credTestClient(t)
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
hostKeys := SSHHostKeyConfig{ControllerNamespace: "ns", DefaultKnownHostsConfigMap: "missing"}
_, err := CredentialFromSecretData(context.Background(), c, &configv1alpha3.GitProvider{}, secret, hostKeys)
require.Error(t, err)
assert.Contains(t, err.Error(), "known_hosts is required")
})
t.Run("no source and no opt-out fails closed", func(t *testing.T) {
c := credTestClient(t)
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
_, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "known_hosts is required")
})
t.Run("opt-out permits missing known_hosts", func(t *testing.T) {
c := credTestClient(t)
secret := &corev1.Secret{Data: map[string][]byte{"ssh-privatekey": privateKey}}
auth, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret,
SSHHostKeyConfig{AllowMissingKnownHosts: true})
require.NoError(t, err)
assert.NotNil(t, auth.SSH)
})
}
// getAuthFromSecret fetches the GitProvider's referenced Secret; a provider with no secretRef is
// anonymous, and a missing referenced Secret is an error.
func TestGetAuthFromSecret_FetchPaths(t *testing.T) {
t.Run("no secretRef is anonymous", func(t *testing.T) {
c := credTestClient(t)
provider := &configv1alpha3.GitProvider{ObjectMeta: metav1.ObjectMeta{Namespace: "ns"}}
auth, err := credentialFromSecret(context.Background(), c, provider, SSHHostKeyConfig{})
require.NoError(t, err)
assert.Equal(t, Credential{}, auth, "no secretRef means anonymous")
assert.Nil(t, auth.Options(), "anonymous renders no transport options")
})
t.Run("present secret resolves", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "creds", Namespace: "ns"},
Data: map[string][]byte{"username": []byte("u"), "password": []byte("p")},
}
c := credTestClient(t, secret)
provider := &configv1alpha3.GitProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns"},
Spec: configv1alpha3.GitProviderSpec{SecretRef: &configv1alpha3.LocalSecretReference{Name: "creds"}},
}
auth, err := credentialFromSecret(context.Background(), c, provider, SSHHostKeyConfig{})
require.NoError(t, err)
assert.NotNil(t, auth.Basic)
})
t.Run("missing referenced secret errors", func(t *testing.T) {
c := credTestClient(t)
provider := &configv1alpha3.GitProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns"},
Spec: configv1alpha3.GitProviderSpec{SecretRef: &configv1alpha3.LocalSecretReference{Name: "absent"}},
}
_, err := credentialFromSecret(context.Background(), c, provider, SSHHostKeyConfig{})
require.Error(t, err)
})
}
func TestCredentialFromSecretData_BearerToken(t *testing.T) {
c := credTestClient(t)
secret := &corev1.Secret{Data: map[string][]byte{"bearerToken": []byte("abc")}}
auth, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.NoError(t, err)
require.NotNil(t, auth.Bearer)
assert.Equal(t, "abc", auth.Bearer.Token)
assert.Len(t, auth.Options(), 1, "a bearer credential must render as one transport option")
empty := &corev1.Secret{Data: map[string][]byte{"bearerToken": []byte("")}}
_, err = CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, empty, SSHHostKeyConfig{})
require.Error(t, err)
}
// Azure DevOps documents its Personal Access Tokens as an empty username with the PAT as the
// password — the https://:PAT@dev.azure.com/... form. firstSecretValue treats an empty value as an
// absent key, so keying the basic-auth branch off the username refused exactly that Secret with
// "does not contain valid authentication data". The password is what carries the credential, so it
// is what we branch on.
func TestCredentialFromSecretData_AzureDevOpsPATForm(t *testing.T) {
c := credTestClient(t)
for _, tc := range []struct {
name string
data map[string][]byte
}{
{"empty username with a PAT", map[string][]byte{"username": []byte(""), "password": []byte("pat")}},
{"no username key at all", map[string][]byte{"password": []byte("pat")}},
} {
t.Run(tc.name, func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "ado", Namespace: "ns"},
Data: tc.data,
}
cred, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.NoError(t, err)
require.NotNil(t, cred.Basic, "an ADO PAT must resolve to HTTP basic auth")
assert.Empty(t, cred.Basic.Username)
assert.Equal(t, "pat", cred.Basic.Password)
assert.Len(t, cred.Options(), 1)
})
}
// The rule is general, not an Azure DevOps carve-out: a username is simply optional, and is passed
// through untouched when supplied. Azure DevOps ignores it server-side (measured), but nothing in
// the credential reader knows or cares which provider it is talking to.
t.Run("a supplied username is still honoured", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "basic", Namespace: "ns"},
Data: map[string][]byte{"username": []byte("alice"), "password": []byte("pw")},
}
cred, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.NoError(t, err)
require.NotNil(t, cred.Basic)
assert.Equal(t, "alice", cred.Basic.Username, "a username must not be dropped")
assert.Equal(t, "pw", cred.Basic.Password)
})
t.Run("a username with no password is still a mistake", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "half", Namespace: "ns"},
Data: map[string][]byte{"username": []byte("someone")},
}
_, err := CredentialFromSecretData(
context.Background(), c, &configv1alpha3.GitProvider{}, secret, SSHHostKeyConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "contains username but no password")
})
}