Skip to content

Commit

Permalink
Merge pull request #4251 from rancher-sandbox/4193-registry-profile-i…
Browse files Browse the repository at this point in the history
…mprovements

Finish reading deployment profiles from the Windows registry
  • Loading branch information
ericpromislow authored May 12, 2023
2 parents 4d45567 + 7dc893d commit 7a2d611
Show file tree
Hide file tree
Showing 2 changed files with 585 additions and 100 deletions.
317 changes: 317 additions & 0 deletions pkg/rancher-desktop/main/__tests__/deploymentProfiles.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
/* eslint object-curly-newline: ["error", {"consistent": true}] */

import fs from 'fs';
import os from 'os';
import path from 'path';

import * as settings from '@pkg/config/settings';
import { readDeploymentProfiles } from '@pkg/main/deploymentProfiles';
import { spawnFile } from '@pkg/utils/childProcess';
import Logging from '@pkg/utils/logging';
import { RecursivePartial } from '@pkg/utils/typeUtils';

// for some reason `import nativeReg...` => undefined when run in jest on Windows
// import nativeReg from 'native-reg';
const nativeReg = require('native-reg');

const console = Logging.deploymentProfile;

// Note that we can't modify the HKLM hive without admin privileges,
// so this whole test will just work with the user's HKCU hive.
const REG_PATH_START = ['SOFTWARE', 'Rancher Desktop'];
const FULL_REG_PATH_START = ['HKEY_CURRENT_USER'].concat(REG_PATH_START);
const REGISTRY_PATH_PROFILE = REG_PATH_START.concat('TestProfile');

const NON_PROFILE_PATH = FULL_REG_PATH_START.join('\\');
const FULL_PROFILE_PATH = FULL_REG_PATH_START.concat('TestProfile').join('\\');

const describeWindows = process.platform === 'win32' ? describe : describe.skip;

let testDir = '';
let regFilePath = '';

async function clearRegistry() {
try {
await spawnFile('reg', ['DELETE', `HKCU\\${ REGISTRY_PATH_PROFILE.join('\\') }`, '/f']);
} catch {
// Ignore any errors
}
}

async function installInRegistry(regFileContents: string) {
await fs.promises.writeFile(regFilePath, regFileContents, { encoding: 'ascii' });
try {
await spawnFile('reg', ['IMPORT', regFilePath]);
} catch (ex: any) {
// Use expect to display the error message
expect(ex).toBeNull();
throw ex;
}
}

// Registry multi-stringSZ settings in a reg file are hard to read, so expand them here.
// e.g.=> ["abc", "def"] would be ucs-2-encoded as '61,00,62,00,63,00,00,00,64,00,65,00,66,00,00,00,00,00'
// where null dwords (so two 00 bytes) separate each pair of words and
// two null dwords ("00 00 00 00") indicate the end of the list
function stringToMultiStringHexBytes(s: string[]): string {
const hexBytes = Buffer.from(s.join('\x00'), 'ucs2')
.toString('hex')
.split(/(..)/)
.filter(x => x)
.join(',');

return `${ hexBytes },00,00,00,00`;
}

// We *could* write a routine that converts json to reg files, but that's not the point of this test.
// Better to just hard-wire a few regfiles here.

const defaultsUserRegFile = `Windows Registry Editor Version 5.00
[${ NON_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }\\Defaults]
[${ FULL_PROFILE_PATH }\\Defaults\\application]
[${ FULL_PROFILE_PATH }\\Defaults\\application]
"Debug"=dword:1
"adminAccess"=dword:0
[${ FULL_PROFILE_PATH }\\Defaults\\application\\Telemetry]
"ENABLED"=dword:1
[${ FULL_PROFILE_PATH }\\Defaults\\CONTAINERENGINE]
"name"="moby"
[${ FULL_PROFILE_PATH }\\Defaults\\containerEngine\\allowedImages]
"patterns"=hex(7):${ stringToMultiStringHexBytes(['edmonton', 'calgary', 'red deer', 'bassano']) }
"enabled"=dword:00000000
[${ FULL_PROFILE_PATH }\\Defaults\\wsl]
[${ FULL_PROFILE_PATH }\\Defaults\\wsl\\integrations]
"kingston"=dword:0
"napanee"=dword:0
"yarker"=dword:1
"weed"=dword:1
[${ FULL_PROFILE_PATH }\\Defaults\\kubernetes]
"version"="867-5309"
[${ FULL_PROFILE_PATH }\\Defaults\\diagnostics]
"showmuted"=dword:1
[${ FULL_PROFILE_PATH }\\Defaults\\diagnostics\\mutedChecks]
"montreal"=dword:1
"riviere du loup"=dword:0
"magog"=dword:0
[${ FULL_PROFILE_PATH }\\Defaults\\extensions]
"bellingham"="WA"
"portland"="OR"
"shasta"="CA"
"elko"="NV"
`;

const lockedUserRegFile = `Windows Registry Editor Version 5.00
[${ NON_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }\\Locked]
[${ FULL_PROFILE_PATH }\\Locked\\containerEngine]
[${ FULL_PROFILE_PATH }\\Locked\\containerEngine\\allowedImages]
"enabled"=dword:00000000
"patterns"=hex(7):${ stringToMultiStringHexBytes(['busybox', 'nginx']) }
`;

// Deliberate errors in defaults:
// * Specifying application/updater=1 instead of application/updater/enabled=1
// * Specifying application/adminAccess/debug=string when it should be 0 or 1
// * application/debug should be a number, not a string
// * containerEngine/name should be a string, not a number
// * containerEngine/allowedImages/patterns should be a list of strings, not a number
// * containerEngine/allowedImages/enabled should be a boolean, not a string
// * images/namespace should be a single string, not a multi-SZ string value
// * wsl/integrations should be a special-purpose object
// * diagnostics/mutedChecks should be a special-purpose object
// * kubernetes/version should be a string, not an object

const incorrectDefaultsUserRegFile = `Windows Registry Editor Version 5.00
[${ NON_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }\\Defaults]
[${ FULL_PROFILE_PATH }\\Defaults\\application]
[${ FULL_PROFILE_PATH }\\Defaults\\application]
"Debug"="should be a number"
"Updater"=dword:0
[${ FULL_PROFILE_PATH }\\Defaults\\application\\adminAccess]
"sudo"=dword:1
[${ FULL_PROFILE_PATH }\\Defaults\\application\\Telemetry]
"ENABLED"=dword:1
[${ FULL_PROFILE_PATH }\\Defaults\\CONTAINERENGINE]
"name"=dword:5
[${ FULL_PROFILE_PATH }\\Defaults\\containerEngine\\allowedImages]
"patterns"=DWORD:19
"enabled"="should be a boolean"
[${ FULL_PROFILE_PATH }\\Defaults\\images]
"namespace"=hex(7):${ stringToMultiStringHexBytes(['busybox', 'nginx']) }
[${ FULL_PROFILE_PATH }\\Defaults\\wsl]
"integrations"="should be a sub-object"
[${ FULL_PROFILE_PATH }\\Defaults\\kubernetes]
[${ FULL_PROFILE_PATH }\\Defaults\\kubernetes\\version]
[${ FULL_PROFILE_PATH }\\Defaults\\diagnostics]
"showmuted"=dword:1
"mutedChecks"=dword:42
`;

const arrayFromSingleStringDefaultsUserRegFile = `Windows Registry Editor Version 5.00
[${ NON_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }]
[${ FULL_PROFILE_PATH }\\Defaults]
[${ FULL_PROFILE_PATH }\\Defaults\\CONTAINERENGINE]
"name"="moby"
[${ FULL_PROFILE_PATH }\\Defaults\\containerEngine\\allowedImages]
"patterns"="hokey smoke!"
`;

describeWindows('windows deployment profiles', () => {
/* Mock console.error() to capture error messages. */
let consoleMock: jest.SpyInstance<void, [message?: any, ...optionalArgs: any[]]>;

beforeEach(async() => {
nativeReg.deleteTree(nativeReg.HKCU, path.join(...(REGISTRY_PATH_PROFILE)));
testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'regtest-'));
regFilePath = path.join(testDir, 'import.reg');
consoleMock = jest.spyOn(console, 'error');
});
afterEach(async() => {
await fs.promises.rm(testDir, { force: true, recursive: true });
consoleMock.mockReset();
});
// TODO: Add an `afterAll(clearRegistry)` when we're finished developing.

describe('profile', () => {
describe('defaults', () => {
describe('happy paths', () => {
const defaultUserProfile: RecursivePartial<settings.Settings> = {
application: {
debug: true,
adminAccess: false,
telemetry: { enabled: true },
},
containerEngine: {
allowedImages: {
enabled: false,
patterns: ['edmonton', 'calgary', 'red deer', 'bassano'],
},
name: settings.ContainerEngine.MOBY,
},
WSL: { integrations: { kingston: false, napanee: false, yarker: true, weed: true } },
kubernetes: {
version: '867-5309',
},
diagnostics: {
showMuted: true,
mutedChecks: { montreal: true, 'riviere du loup': false, magog: false },
},
extensions: { bellingham: 'WA', portland: 'OR', shasta: 'CA', elko: 'NV' },
};
const lockedUserProfile = {
containerEngine: {
allowedImages: {
enabled: false,
patterns: ['busybox', 'nginx'],
},
},
};

describe('no system profiles, no user profiles', () => {
it('loads nothing', async() => {
const profile = await readDeploymentProfiles(REGISTRY_PATH_PROFILE);

expect(profile.defaults).toEqual({});
expect(profile.locked).toEqual({});
});
});

describe('no system profiles, both user profiles', () => {
it('loads both profiles', async() => {
await clearRegistry();
await installInRegistry(defaultsUserRegFile);
await installInRegistry(lockedUserRegFile);
const profile = await readDeploymentProfiles(REGISTRY_PATH_PROFILE);

expect(profile.defaults).toEqual(defaultUserProfile);
expect(profile.locked).toEqual(lockedUserProfile);
});
});

it('converts a single string into an array', async() => {
await clearRegistry();
await installInRegistry(arrayFromSingleStringDefaultsUserRegFile);
const profile = await readDeploymentProfiles(REGISTRY_PATH_PROFILE);

expect(profile.defaults).toEqual({
containerEngine: { allowedImages: { patterns: ['hokey smoke!'] }, name: 'moby' },
});
});
});
describe('error paths', () => {
const limitedUserProfile = {
application: {
telemetry: { enabled: true },
},
diagnostics: {
showMuted: true,
},
};

it('loads a bad profile, complains about all the errors, and keeps only valid entries', async() => {
await clearRegistry();
await installInRegistry(incorrectDefaultsUserRegFile);
const profile = await readDeploymentProfiles(REGISTRY_PATH_PROFILE);

expect(profile.defaults).toEqual(limitedUserProfile);
// Remember that sub-objects are processed before values
expect(consoleMock.mock.calls).toEqual([
[expect.stringMatching(/Expecting registry entry .*?application.adminAccess to be a boolean, but it's a registry object/)],
[expect.stringMatching(/Expecting registry entry .*?application.Debug to be a boolean, but it's a SZ/)],
[expect.stringMatching(/Expecting registry entry .*?application.Updater to be a registry object, but it's a DWORD, value: 0/)],
[expect.stringMatching(/Expecting registry entry .*?containerEngine.allowedImages.enabled to be a boolean, but it's a SZ, value: should be a boolean/)],
[expect.stringMatching(/Expecting registry entry .*?containerEngine.name to be a string, but it's a DWORD, value: 5/)],
[expect.stringMatching(/Expecting registry entry .*?diagnostics.mutedChecks to be a registry object, but it's a DWORD, value: 66/)],
[expect.stringMatching(/Expecting registry entry .*?images.namespace to be a single string, but it's an array of strings, value: busybox,nginx/)],
[expect.stringMatching(/Expecting registry entry .*?kubernetes.version to be a string, but it's a registry object/)],
[expect.stringMatching(/Expecting registry entry .*?WSL.integrations to be a registry object, but it's a SZ, value: should be a sub-object/)],
]);
});
});
});
});
});
Loading

0 comments on commit 7a2d611

Please sign in to comment.