feat: add ability to add insecure registry / skipping cert verify (#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 <charlie@charliedrage.com>

* renaming and refactor getOptions

Signed-off-by: Charlie Drage <charlie@charliedrage.com>

---------

Signed-off-by: Charlie Drage <charlie@charliedrage.com>
This commit is contained in:
Charlie Drage 2023-06-23 12:06:08 -04:00 committed by GitHub
parent fe18540ff9
commit afcc91effc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 79 additions and 8 deletions

0
kind Executable file
View file

View file

@ -445,6 +445,7 @@ declare module '@podman-desktop/api' {
serverUrl: string;
username: string;
secret: string;
insecure?: boolean;
}
export interface RegistryProvider {

View file

@ -385,6 +385,21 @@ describe('expect checkCredentials', async () => {
);
});
test('expect checkCredentials works with ignoring the certificate', async () => {
const spyGetAuthInfo = vi.spyOn(imageRegistry, 'getAuthInfo');
spyGetAuthInfo.mockResolvedValue({ authUrl: 'foo', scheme: 'bearer' });
const spydoCheckCredentials = vi.spyOn(imageRegistry, 'doCheckCredentials');
spydoCheckCredentials.mockResolvedValue();
await imageRegistry.checkCredentials(
'my-podman-desktop-fake-registry.io/my/extension',
'my-username',
'my-password',
true,
);
});
test('expect checkCredentials fails', async () => {
const spyGetAuthInfo = vi.spyOn(imageRegistry, 'getAuthInfo');
spyGetAuthInfo.mockResolvedValue({ authUrl: 'foo', scheme: 'bearer' });

View file

@ -242,10 +242,12 @@ export class ImageRegistry {
if (exists) {
throw new Error(`Registry ${registryCreateOptions.serverUrl} already exists`);
}
await this.checkCredentials(
registryCreateOptions.serverUrl,
registryCreateOptions.username,
registryCreateOptions.secret,
registryCreateOptions.insecure,
);
const registry = provider.create(registryCreateOptions);
return this.registerRegistry(registry);
@ -308,7 +310,7 @@ export class ImageRegistry {
return undefined;
}
getOptions(): OptionsOfTextResponseBody {
getOptions(insecure?: boolean): OptionsOfTextResponseBody {
const httpsOptions: HttpsOptions = {};
const options: OptionsOfTextResponseBody = {
https: httpsOptions,
@ -316,6 +318,9 @@ export class ImageRegistry {
if (options.https) {
options.https.certificateAuthority = this.certificates.getAllCertificates();
if (insecure) {
options.https.rejectUnauthorized = false;
}
}
if (this.proxyEnabled) {
@ -650,8 +655,9 @@ export class ImageRegistry {
return this.getManifestFromURL(manifestURL, imageData, token);
}
async getAuthInfo(serviceUrl: string): Promise<{ authUrl: string; scheme: string }> {
async getAuthInfo(serviceUrl: string, insecure?: boolean): Promise<{ authUrl: string; scheme: string }> {
let registryUrl: string;
const options = this.getOptions(insecure);
if (serviceUrl.includes('docker.io')) {
registryUrl = 'https://index.docker.io/v2/';
@ -667,7 +673,7 @@ export class ImageRegistry {
let scheme = '';
try {
await got.get(registryUrl, this.getOptions());
await got.get(registryUrl, options);
} catch (requestErr) {
if (requestErr instanceof HTTPError) {
const wwwAuthenticate = requestErr.response?.headers['www-authenticate'];
@ -703,7 +709,7 @@ export class ImageRegistry {
return { authUrl, scheme };
}
async checkCredentials(serviceUrl: string, username: string, password: string): Promise<void> {
async checkCredentials(serviceUrl: string, username: string, password: string, insecure?: boolean): Promise<void> {
if (serviceUrl === undefined || !validator.isURL(serviceUrl)) {
throw Error(
'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 {
throw Error('Password should not be empty.');
}
const { authUrl, scheme } = await this.getAuthInfo(serviceUrl);
const { authUrl, scheme } = await this.getAuthInfo(serviceUrl, insecure);
if (authUrl !== undefined) {
await this.doCheckCredentials(scheme, authUrl, username, password);
await this.doCheckCredentials(scheme, authUrl, username, password, insecure);
}
}
@ -758,12 +764,19 @@ export class ImageRegistry {
return response.token;
}
async doCheckCredentials(scheme: string, authUrl: string, username: string, password: string): Promise<void> {
async doCheckCredentials(
scheme: string,
authUrl: string,
username: string,
password: string,
insecure?: boolean,
): Promise<void> {
const options = this.getOptions(insecure);
let rawResponse: string | undefined;
// add credentials in the header
// encode username:password in base64
const token = Buffer.from(`${username}:${password}`).toString('base64');
const options = this.getOptions();
options.headers = {
Authorization: `Basic ${token}`,
};

View file

@ -1195,6 +1195,18 @@ export class PluginSystem {
},
);
// Check credentials for a registry
this.ipcHandle(
'image-registry:checkCredentials',
async (_listener, registryCreateOptions: containerDesktopAPI.RegistryCreateOptions): Promise<void> => {
return imageRegistry.checkCredentials(
registryCreateOptions.serverUrl,
registryCreateOptions.username,
registryCreateOptions.secret,
);
},
);
this.ipcHandle(
'image-registry:createRegistry',
async (

View file

@ -821,6 +821,13 @@ function initExposure(): void {
},
);
contextBridge.exposeInMainWorld(
'checkImageCredentials',
async (registryCreateOptions: containerDesktopAPI.RegistryCreateOptions): Promise<void> => {
return ipcInvoke('image-registry:checkCredentials', registryCreateOptions);
},
);
contextBridge.exposeInMainWorld(
'updateImageRegistry',
async (registry: containerDesktopAPI.Registry): Promise<void> => {

View file

@ -182,6 +182,29 @@ async function loginToRegistry(registry: containerDesktopAPI.Registry) {
const newRegistry = registry === newRegistryRequest;
// Always check credentials before creating image / updating to see if they pass.
// if we happen to get a certificate verification issue, as the user if they would like to
// continue with the registry anyway.
try {
await window.checkImageCredentials(registry);
} catch (error) {
if (error instanceof Error && error.message.includes('unable to verify the first certificate')) {
const result = await window.showMessageBox({
title: 'Invalid Certificate',
type: 'warning',
message: 'The certificate for this registry is not trusted / verifiable. Would you like to still add it?',
buttons: ['Yes', 'No'],
});
if (result && result.response === 0) {
registry.insecure = true;
} else {
setErrorResponse(registry.serverUrl, error.message);
loggingIn = false;
return;
}
}
}
try {
if (newRegistry) {
await window.createImageRegistry(registry.source, registry);