Skip to content

Commit 6f3319e

Browse files
kylos101Copilotgeropl
authored
[gitpod-protocol] handle host:port:token for getGitpodImageAuth (#20806)
* [gitpod-protocol] handle host:token and host:port:token for getGitpodImageAuth * Cleanup * Improve readability * Code review feedback * Update components/gitpod-protocol/src/protocol.ts Co-authored-by: Copilot <[email protected]> * [supervisor] tests to cover insertCredentialsIntoConfig * Fix tests * [supervisor] handle auth token like `host:port:token` * [image-builder-mk3] handle host:port:token for auth * Fix * Cleanup * Cleanup * Cleanup * Cleanup * [image-builder-bob] tolerate host:port:token Special case is to strip port for 443 * Handle center values & code review feedback * Remove extra/unnecessary trim * [bob] proxy: explicit and implicit fallback for exact header matching for auth proxy --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Gero Posmyk-Leinemann <[email protected]>
1 parent ffe6bb1 commit 6f3319e

File tree

8 files changed

+485
-20
lines changed

8 files changed

+485
-20
lines changed

components/gitpod-protocol/src/protocol.spec.ts

+118-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { suite, test } from "@testdeck/mocha";
88
import * as chai from "chai";
9-
import { SSHPublicKeyValue } from ".";
9+
import { SSHPublicKeyValue, EnvVar, EnvVarWithValue } from ".";
1010

1111
const expect = chai.expect;
1212

@@ -94,4 +94,120 @@ class TestSSHPublicKeyValue {
9494
).to.throw("Key is invalid");
9595
}
9696
}
97-
module.exports = new TestSSHPublicKeyValue(); // Only to circumvent no usage warning :-/
97+
98+
@suite
99+
class TestEnvVar {
100+
@test
101+
public testGetGitpodImageAuth_empty() {
102+
const result = EnvVar.getGitpodImageAuth([]);
103+
expect(result.size).to.equal(0);
104+
}
105+
106+
@test
107+
public testGetGitpodImageAuth_noRelevantVar() {
108+
const envVars: EnvVarWithValue[] = [{ name: "OTHER_VAR", value: "some_value" }];
109+
const result = EnvVar.getGitpodImageAuth(envVars);
110+
expect(result.size).to.equal(0);
111+
}
112+
113+
@test
114+
public testGetGitpodImageAuth_singleEntryNoPort() {
115+
const envVars: EnvVarWithValue[] = [
116+
{
117+
name: EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME,
118+
value: "my-registry.foo.net:Zm9vOmJhcg==",
119+
},
120+
];
121+
const result = EnvVar.getGitpodImageAuth(envVars);
122+
expect(result.size).to.equal(1);
123+
expect(result.get("my-registry.foo.net")).to.equal("Zm9vOmJhcg==");
124+
}
125+
126+
@test
127+
public testGetGitpodImageAuth_singleEntryWithPort() {
128+
const envVars: EnvVarWithValue[] = [
129+
{
130+
name: EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME,
131+
value: "my-registry.foo.net:5000:Zm9vOmJhcg==",
132+
},
133+
];
134+
const result = EnvVar.getGitpodImageAuth(envVars);
135+
expect(result.size).to.equal(1);
136+
expect(result.get("my-registry.foo.net:5000")).to.equal("Zm9vOmJhcg==");
137+
}
138+
139+
@test
140+
public testGetGitpodImageAuth_multipleEntries() {
141+
const envVars: EnvVarWithValue[] = [
142+
{
143+
name: EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME,
144+
value: "my-registry.foo.net:Zm9vOmJhcg==,my-registry2.bar.com:YWJjOmRlZg==",
145+
},
146+
];
147+
const result = EnvVar.getGitpodImageAuth(envVars);
148+
expect(result.size).to.equal(2);
149+
expect(result.get("my-registry.foo.net")).to.equal("Zm9vOmJhcg==");
150+
expect(result.get("my-registry2.bar.com")).to.equal("YWJjOmRlZg==");
151+
}
152+
153+
@test
154+
public testGetGitpodImageAuth_multipleEntriesWithPortAndMalformed() {
155+
const envVars: EnvVarWithValue[] = [
156+
{
157+
name: EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME,
158+
value: "my-registry.foo.net:5000:Zm9vOmJhcg==,my-registry2.bar.com:YWJjOmRlZg==,invalidEntry,another.host:anothercred",
159+
},
160+
];
161+
const result = EnvVar.getGitpodImageAuth(envVars);
162+
expect(result.size).to.equal(3);
163+
expect(result.get("my-registry2.bar.com")).to.equal("YWJjOmRlZg==");
164+
expect(result.get("another.host")).to.equal("anothercred");
165+
expect(result.get("my-registry.foo.net:5000")).to.equal("Zm9vOmJhcg==");
166+
}
167+
168+
@test
169+
public testGetGitpodImageAuth_emptyValue() {
170+
const envVars: EnvVarWithValue[] = [
171+
{
172+
name: EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME,
173+
value: "",
174+
},
175+
];
176+
const result = EnvVar.getGitpodImageAuth(envVars);
177+
expect(result.size).to.equal(0);
178+
}
179+
180+
@test
181+
public testGetGitpodImageAuth_malformedEntries() {
182+
const envVars: EnvVarWithValue[] = [
183+
{
184+
name: EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME,
185+
value: "justhost,hostonly:,:credonly,:::,:,",
186+
},
187+
];
188+
const result = EnvVar.getGitpodImageAuth(envVars);
189+
expect(result.size).to.equal(0);
190+
}
191+
192+
@test
193+
public testGetGitpodImageAuth_entriesWithSpaces() {
194+
const envVars: EnvVarWithValue[] = [
195+
{
196+
name: EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME,
197+
value: " my-registry.foo.net : Zm9vOmJhcg== , my-registry2.bar.com:YWJjOmRlZg== ",
198+
},
199+
];
200+
const result = EnvVar.getGitpodImageAuth(envVars);
201+
expect(result.size).to.equal(2);
202+
expect(result.get("my-registry.foo.net")).to.equal("Zm9vOmJhcg==");
203+
expect(result.get("my-registry2.bar.com")).to.equal("YWJjOmRlZg==");
204+
}
205+
}
206+
207+
// Exporting both test suites
208+
const testSSHPublicKeyValue = new TestSSHPublicKeyValue();
209+
const testEnvVar = new TestEnvVar();
210+
module.exports = {
211+
testSSHPublicKeyValue,
212+
testEnvVar,
213+
};

components/gitpod-protocol/src/protocol.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,28 @@ export namespace EnvVar {
293293
return res;
294294
}
295295

296+
const parse = (parts: string[]): { host: string; auth: string } | undefined => {
297+
if (parts.some((e) => e === "")) {
298+
return undefined;
299+
}
300+
if (parts.length === 2) {
301+
return { host: parts[0], auth: parts[1] };
302+
} else if (parts.length === 3) {
303+
return { host: `${parts[0]}:${parts[1]}`, auth: parts[2] };
304+
}
305+
return undefined;
306+
};
307+
296308
(imageAuth.value || "")
297309
.split(",")
298-
.map((e) => e.trim().split(":"))
299-
.filter((e) => e.length == 2)
300-
.forEach((e) => res.set(e[0], e[1]));
310+
.map((e) => e.split(":").map((part) => part.trim()))
311+
.forEach((parts) => {
312+
const parsed = parse(parts);
313+
if (parsed) {
314+
res.set(parsed.host, parsed.auth);
315+
}
316+
});
317+
301318
return res;
302319
}
303320
}

components/image-builder-bob/pkg/proxy/auth.go

+26-6
Original file line numberDiff line numberDiff line change
@@ -44,30 +44,50 @@ type authConfig struct {
4444

4545
type MapAuthorizer map[string]authConfig
4646

47-
func (a MapAuthorizer) Authorize(host string) (user, pass string, err error) {
47+
func (a MapAuthorizer) Authorize(hostHeader string) (user, pass string, err error) {
4848
defer func() {
4949
log.WithFields(logrus.Fields{
50-
"host": host,
50+
"host": hostHeader,
5151
"user": user,
5252
}).Info("authorizing registry access")
5353
}()
5454

55-
// Strip any port from the host if present
56-
host = strings.Split(host, ":")[0]
55+
parseHostHeader := func(hostHeader string) (string, string) {
56+
hostHeaderSlice := strings.Split(hostHeader, ":")
57+
hostname := strings.TrimSpace(hostHeaderSlice[0])
58+
var port string
59+
if len(hostHeaderSlice) > 1 {
60+
port = strings.TrimSpace(hostHeaderSlice[1])
61+
}
62+
return hostname, port
63+
}
64+
hostname, port := parseHostHeader(hostHeader)
65+
// gpl: Could be port 80 as well, but we don't know if we are servinc http or https, we assume https
66+
if port == "" {
67+
port = "443"
68+
}
69+
host := hostname + ":" + port
5770

5871
explicitHostMatcher := func() (authConfig, bool) {
72+
// 1. precise host match
5973
res, ok := a[host]
74+
if ok {
75+
return res, ok
76+
}
77+
78+
// 2. make sure we not have a hostname match
79+
res, ok = a[hostname]
6080
return res, ok
6181
}
6282
ecrHostMatcher := func() (authConfig, bool) {
63-
if isECRRegistry(host) {
83+
if isECRRegistry(hostname) {
6484
res, ok := a[DummyECRRegistryDomain]
6585
return res, ok
6686
}
6787
return authConfig{}, false
6888
}
6989
dockerHubHostMatcher := func() (authConfig, bool) {
70-
if isDockerHubRegistry(host) {
90+
if isDockerHubRegistry(hostname) {
7191
res, ok := a["docker.io"]
7292
return res, ok
7393
}

components/image-builder-bob/pkg/proxy/auth_test.go

+21-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestAuthorize(t *testing.T) {
3232
},
3333
},
3434
{
35-
name: "docker auth format - valid credentials - host with port",
35+
name: "docker auth format - valid credentials - host with :443 port",
3636
constructor: NewAuthorizerFromDockerEnvVar,
3737
input: `{"auths": {"registry.example.com": {"auth": "dXNlcjpwYXNz"}}}`, // base64(user:pass)
3838
testHost: "registry.example.com:443",
@@ -110,6 +110,26 @@ func TestAuthorize(t *testing.T) {
110110
pass: "",
111111
},
112112
},
113+
{
114+
name: "Docker Hub",
115+
constructor: NewAuthorizerFromEnvVar,
116+
input: `{"docker.io": {"auth": "dXNlcjpwYXNz"}}`,
117+
testHost: "registry-1.docker.io",
118+
expected: expectation{
119+
user: "user",
120+
pass: "pass",
121+
},
122+
},
123+
{
124+
name: "docker auth format - valid credentials - host with :5000 port",
125+
constructor: NewAuthorizerFromDockerEnvVar,
126+
input: `{"auths": {"registry.example.com:5000": {"auth": "dXNlcjpwYXNz"}}}`, // base64(user:pass)
127+
testHost: "registry.example.com:5000",
128+
expected: expectation{
129+
user: "user",
130+
pass: "pass",
131+
},
132+
},
113133
}
114134

115135
for _, tt := range tests {

components/image-builder-mk3/pkg/auth/auth.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,11 @@ func (a AllowedAuthFor) additionalAuth(domain string) *Authentication {
383383
dec, err := base64.StdEncoding.DecodeString(ath)
384384
if err == nil {
385385
segs := strings.Split(string(dec), ":")
386-
if len(segs) > 1 {
387-
res.Username = segs[0]
388-
res.Password = strings.Join(segs[1:], ":")
386+
numSegs := len(segs)
387+
388+
if numSegs > 1 {
389+
res.Username = strings.Join(segs[:numSegs-1], ":")
390+
res.Password = segs[numSegs-1]
389391
}
390392
} else {
391393
log.Errorf("failed getting additional auth")

components/image-builder-mk3/pkg/auth/auth_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package auth
66

77
import (
8+
"encoding/base64"
89
"testing"
910

1011
"github.com/google/go-cmp/cmp"
@@ -30,3 +31,98 @@ func TestIsECRRegistry(t *testing.T) {
3031
})
3132
}
3233
}
34+
35+
func TestAdditionalAuth(t *testing.T) {
36+
tests := []struct {
37+
name string
38+
domain string
39+
additionalMap map[string]string
40+
expectedAuth *Authentication
41+
}{
42+
{
43+
name: "standard host:token",
44+
domain: "myregistry.com",
45+
additionalMap: map[string]string{
46+
"myregistry.com": base64.StdEncoding.EncodeToString([]byte("myregistry.com:mytoken")),
47+
},
48+
expectedAuth: &Authentication{
49+
Username: "myregistry.com",
50+
Password: "mytoken",
51+
Auth: base64.StdEncoding.EncodeToString([]byte("myregistry.com:mytoken")),
52+
},
53+
},
54+
{
55+
name: "buggy host:port:token",
56+
domain: "myregistry.com:5000",
57+
additionalMap: map[string]string{
58+
"myregistry.com:5000": base64.StdEncoding.EncodeToString([]byte("myregistry.com:5000:mytoken")),
59+
},
60+
expectedAuth: &Authentication{
61+
Username: "myregistry.com:5000",
62+
Password: "mytoken",
63+
Auth: base64.StdEncoding.EncodeToString([]byte("myregistry.com:5000:mytoken")),
64+
},
65+
},
66+
{
67+
name: "only username, no password/token (single segment)",
68+
domain: "useronly.com",
69+
additionalMap: map[string]string{
70+
"useronly.com": base64.StdEncoding.EncodeToString([]byte("justauser")),
71+
},
72+
expectedAuth: &Authentication{
73+
Auth: base64.StdEncoding.EncodeToString([]byte("justauser")),
74+
},
75+
},
76+
{
77+
name: "empty auth string",
78+
domain: "emptyauth.com",
79+
additionalMap: map[string]string{
80+
"emptyauth.com": base64.StdEncoding.EncodeToString([]byte("")),
81+
},
82+
expectedAuth: &Authentication{
83+
Auth: base64.StdEncoding.EncodeToString([]byte("")),
84+
},
85+
},
86+
{
87+
name: "domain not in map",
88+
domain: "notfound.com",
89+
additionalMap: map[string]string{"someother.com": base64.StdEncoding.EncodeToString([]byte("someauth"))},
90+
expectedAuth: nil,
91+
},
92+
{
93+
name: "invalid base64 string",
94+
domain: "invalidbase64.com",
95+
additionalMap: map[string]string{
96+
"invalidbase64.com": "!!!INVALID_BASE64!!!",
97+
},
98+
expectedAuth: &Authentication{
99+
Auth: "!!!INVALID_BASE64!!!",
100+
},
101+
},
102+
{
103+
name: "standard host:token where username in cred is different from domain key",
104+
domain: "docker.io",
105+
additionalMap: map[string]string{
106+
"docker.io": base64.StdEncoding.EncodeToString([]byte("user1:pass1")),
107+
},
108+
expectedAuth: &Authentication{
109+
Username: "user1",
110+
Password: "pass1",
111+
Auth: base64.StdEncoding.EncodeToString([]byte("user1:pass1")),
112+
},
113+
},
114+
}
115+
116+
for _, tt := range tests {
117+
t.Run(tt.name, func(t *testing.T) {
118+
aaf := AllowedAuthFor{
119+
Additional: tt.additionalMap,
120+
}
121+
actualAuth := aaf.additionalAuth(tt.domain)
122+
123+
if diff := cmp.Diff(tt.expectedAuth, actualAuth); diff != "" {
124+
t.Errorf("additionalAuth() mismatch (-want +got):\n%s", diff)
125+
}
126+
})
127+
}
128+
}

0 commit comments

Comments
 (0)