Skip to content

Commit afcc91e

Browse files
authored
feat: add ability to add insecure registry / skipping cert verify (podman-desktop#2896)
* draft: add ability to add insecure registry / skipping cert verify ### What does this PR do? Adds the ability to prompt the user that the certificate is unverifiable but they can add it / skip the verification process if they wish. ### Screenshot/screencast of this PR <!-- Please include a screenshot or a screencast explaining what is doing this PR --> ### What issues does this PR fix or reference? <!-- Please include any related issue from Podman Desktop repository (or from another issue tracker). --> ### How to test this PR? <!-- Please explain steps to reproduce --> 1. Add a test registry (registry.k8s.land) which has a self-signed certificate 2. Podman Desktop should prompt that it is unverifiable / cert does not work 3. PD should succesfully add the registry Signed-off-by: Charlie Drage <[email protected]> * renaming and refactor getOptions Signed-off-by: Charlie Drage <[email protected]> --------- Signed-off-by: Charlie Drage <[email protected]>
1 parent fe18540 commit afcc91e

File tree

7 files changed

+79
-8
lines changed

7 files changed

+79
-8
lines changed

kind

Whitespace-only changes.

packages/extension-api/src/extension-api.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ declare module '@podman-desktop/api' {
445445
serverUrl: string;
446446
username: string;
447447
secret: string;
448+
insecure?: boolean;
448449
}
449450

450451
export interface RegistryProvider {

packages/main/src/plugin/image-registry.spec.ts

+15
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,21 @@ describe('expect checkCredentials', async () => {
385385
);
386386
});
387387

388+
test('expect checkCredentials works with ignoring the certificate', async () => {
389+
const spyGetAuthInfo = vi.spyOn(imageRegistry, 'getAuthInfo');
390+
spyGetAuthInfo.mockResolvedValue({ authUrl: 'foo', scheme: 'bearer' });
391+
392+
const spydoCheckCredentials = vi.spyOn(imageRegistry, 'doCheckCredentials');
393+
spydoCheckCredentials.mockResolvedValue();
394+
395+
await imageRegistry.checkCredentials(
396+
'my-podman-desktop-fake-registry.io/my/extension',
397+
'my-username',
398+
'my-password',
399+
true,
400+
);
401+
});
402+
388403
test('expect checkCredentials fails', async () => {
389404
const spyGetAuthInfo = vi.spyOn(imageRegistry, 'getAuthInfo');
390405
spyGetAuthInfo.mockResolvedValue({ authUrl: 'foo', scheme: 'bearer' });

packages/main/src/plugin/image-registry.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,12 @@ export class ImageRegistry {
242242
if (exists) {
243243
throw new Error(`Registry ${registryCreateOptions.serverUrl} already exists`);
244244
}
245+
245246
await this.checkCredentials(
246247
registryCreateOptions.serverUrl,
247248
registryCreateOptions.username,
248249
registryCreateOptions.secret,
250+
registryCreateOptions.insecure,
249251
);
250252
const registry = provider.create(registryCreateOptions);
251253
return this.registerRegistry(registry);
@@ -308,14 +310,17 @@ export class ImageRegistry {
308310
return undefined;
309311
}
310312

311-
getOptions(): OptionsOfTextResponseBody {
313+
getOptions(insecure?: boolean): OptionsOfTextResponseBody {
312314
const httpsOptions: HttpsOptions = {};
313315
const options: OptionsOfTextResponseBody = {
314316
https: httpsOptions,
315317
};
316318

317319
if (options.https) {
318320
options.https.certificateAuthority = this.certificates.getAllCertificates();
321+
if (insecure) {
322+
options.https.rejectUnauthorized = false;
323+
}
319324
}
320325

321326
if (this.proxyEnabled) {
@@ -650,8 +655,9 @@ export class ImageRegistry {
650655
return this.getManifestFromURL(manifestURL, imageData, token);
651656
}
652657

653-
async getAuthInfo(serviceUrl: string): Promise<{ authUrl: string; scheme: string }> {
658+
async getAuthInfo(serviceUrl: string, insecure?: boolean): Promise<{ authUrl: string; scheme: string }> {
654659
let registryUrl: string;
660+
const options = this.getOptions(insecure);
655661

656662
if (serviceUrl.includes('docker.io')) {
657663
registryUrl = 'https://index.docker.io/v2/';
@@ -667,7 +673,7 @@ export class ImageRegistry {
667673
let scheme = '';
668674

669675
try {
670-
await got.get(registryUrl, this.getOptions());
676+
await got.get(registryUrl, options);
671677
} catch (requestErr) {
672678
if (requestErr instanceof HTTPError) {
673679
const wwwAuthenticate = requestErr.response?.headers['www-authenticate'];
@@ -703,7 +709,7 @@ export class ImageRegistry {
703709
return { authUrl, scheme };
704710
}
705711

706-
async checkCredentials(serviceUrl: string, username: string, password: string): Promise<void> {
712+
async checkCredentials(serviceUrl: string, username: string, password: string, insecure?: boolean): Promise<void> {
707713
if (serviceUrl === undefined || !validator.isURL(serviceUrl)) {
708714
throw Error(
709715
'The format of the Registry Location is incorrect.\nPlease use the format "registry.location.com" and try again.',
@@ -718,10 +724,10 @@ export class ImageRegistry {
718724
throw Error('Password should not be empty.');
719725
}
720726

721-
const { authUrl, scheme } = await this.getAuthInfo(serviceUrl);
727+
const { authUrl, scheme } = await this.getAuthInfo(serviceUrl, insecure);
722728

723729
if (authUrl !== undefined) {
724-
await this.doCheckCredentials(scheme, authUrl, username, password);
730+
await this.doCheckCredentials(scheme, authUrl, username, password, insecure);
725731
}
726732
}
727733

@@ -758,12 +764,19 @@ export class ImageRegistry {
758764
return response.token;
759765
}
760766

761-
async doCheckCredentials(scheme: string, authUrl: string, username: string, password: string): Promise<void> {
767+
async doCheckCredentials(
768+
scheme: string,
769+
authUrl: string,
770+
username: string,
771+
password: string,
772+
insecure?: boolean,
773+
): Promise<void> {
774+
const options = this.getOptions(insecure);
775+
762776
let rawResponse: string | undefined;
763777
// add credentials in the header
764778
// encode username:password in base64
765779
const token = Buffer.from(`${username}:${password}`).toString('base64');
766-
const options = this.getOptions();
767780
options.headers = {
768781
Authorization: `Basic ${token}`,
769782
};

packages/main/src/plugin/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,18 @@ export class PluginSystem {
11951195
},
11961196
);
11971197

1198+
// Check credentials for a registry
1199+
this.ipcHandle(
1200+
'image-registry:checkCredentials',
1201+
async (_listener, registryCreateOptions: containerDesktopAPI.RegistryCreateOptions): Promise<void> => {
1202+
return imageRegistry.checkCredentials(
1203+
registryCreateOptions.serverUrl,
1204+
registryCreateOptions.username,
1205+
registryCreateOptions.secret,
1206+
);
1207+
},
1208+
);
1209+
11981210
this.ipcHandle(
11991211
'image-registry:createRegistry',
12001212
async (

packages/preload/src/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,13 @@ function initExposure(): void {
821821
},
822822
);
823823

824+
contextBridge.exposeInMainWorld(
825+
'checkImageCredentials',
826+
async (registryCreateOptions: containerDesktopAPI.RegistryCreateOptions): Promise<void> => {
827+
return ipcInvoke('image-registry:checkCredentials', registryCreateOptions);
828+
},
829+
);
830+
824831
contextBridge.exposeInMainWorld(
825832
'updateImageRegistry',
826833
async (registry: containerDesktopAPI.Registry): Promise<void> => {

packages/renderer/src/lib/preferences/PreferencesRegistriesEditing.svelte

+23
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,29 @@ async function loginToRegistry(registry: containerDesktopAPI.Registry) {
182182
183183
const newRegistry = registry === newRegistryRequest;
184184
185+
// Always check credentials before creating image / updating to see if they pass.
186+
// if we happen to get a certificate verification issue, as the user if they would like to
187+
// continue with the registry anyway.
188+
try {
189+
await window.checkImageCredentials(registry);
190+
} catch (error) {
191+
if (error instanceof Error && error.message.includes('unable to verify the first certificate')) {
192+
const result = await window.showMessageBox({
193+
title: 'Invalid Certificate',
194+
type: 'warning',
195+
message: 'The certificate for this registry is not trusted / verifiable. Would you like to still add it?',
196+
buttons: ['Yes', 'No'],
197+
});
198+
if (result && result.response === 0) {
199+
registry.insecure = true;
200+
} else {
201+
setErrorResponse(registry.serverUrl, error.message);
202+
loggingIn = false;
203+
return;
204+
}
205+
}
206+
}
207+
185208
try {
186209
if (newRegistry) {
187210
await window.createImageRegistry(registry.source, registry);

0 commit comments

Comments
 (0)