2022-03-03 15:57:19 -05:00
|
|
|
import bcrypt from 'bcryptjs';
|
2020-08-19 13:27:35 -05:00
|
|
|
import crypto from 'crypto';
|
|
|
|
import fs from 'fs';
|
2021-01-30 17:41:33 -05:00
|
|
|
import MockDate from 'mockdate';
|
2022-07-29 13:51:45 -05:00
|
|
|
import path from 'path';
|
2021-01-30 17:41:33 -05:00
|
|
|
|
2022-07-29 13:51:45 -05:00
|
|
|
import { Config, parseConfigFile } from '@verdaccio/config';
|
2023-04-22 13:55:45 -05:00
|
|
|
import { constants, pluginUtils } from '@verdaccio/core';
|
2022-03-03 15:57:19 -05:00
|
|
|
|
|
|
|
import HTPasswd, { DEFAULT_SLOW_VERIFY_MS, HTPasswdConfig } from '../src/htpasswd';
|
2020-08-19 13:27:35 -05:00
|
|
|
|
2022-03-03 15:57:19 -05:00
|
|
|
const options = {
|
2023-04-22 13:55:45 -05:00
|
|
|
logger: { warn: jest.fn(), info: jest.fn() },
|
2022-07-29 13:51:45 -05:00
|
|
|
config: new Config(parseConfigFile(path.join(__dirname, './__fixtures__/config.yaml'))),
|
2023-04-22 13:55:45 -05:00
|
|
|
} as any as pluginUtils.PluginOptions<HTPasswdConfig>;
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
const config = {
|
|
|
|
file: './htpasswd',
|
|
|
|
max_users: 1000,
|
2022-03-03 15:57:19 -05:00
|
|
|
} as HTPasswdConfig;
|
2021-01-30 17:41:33 -05:00
|
|
|
|
2020-08-19 13:27:35 -05:00
|
|
|
describe('HTPasswd', () => {
|
|
|
|
let wrapper;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2022-03-03 15:57:19 -05:00
|
|
|
wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
jest.resetModules();
|
2022-03-03 15:57:19 -05:00
|
|
|
jest.clearAllMocks();
|
2023-04-22 13:55:45 -05:00
|
|
|
|
|
|
|
// @ts-ignore
|
2020-08-19 13:27:35 -05:00
|
|
|
crypto.randomBytes = jest.fn(() => {
|
|
|
|
return {
|
|
|
|
toString: (): string => '$6',
|
|
|
|
};
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('constructor()', () => {
|
2023-04-22 13:55:45 -05:00
|
|
|
const error = jest.fn();
|
|
|
|
const warn = jest.fn();
|
|
|
|
const info = jest.fn();
|
|
|
|
const emptyPluginOptions = {
|
|
|
|
config: {
|
|
|
|
configPath: '',
|
|
|
|
},
|
|
|
|
logger: { warn, info, error },
|
|
|
|
} as any as pluginUtils.PluginOptions<HTPasswdConfig>;
|
2021-01-30 17:41:33 -05:00
|
|
|
|
2022-03-03 15:57:19 -05:00
|
|
|
test('should ensure file path configuration exists', () => {
|
2020-08-19 13:27:35 -05:00
|
|
|
expect(function () {
|
2022-03-03 15:57:19 -05:00
|
|
|
new HTPasswd({} as HTPasswdConfig, emptyPluginOptions);
|
2020-08-19 13:27:35 -05:00
|
|
|
}).toThrow(/should specify "file" in config/);
|
|
|
|
});
|
2021-01-30 17:41:33 -05:00
|
|
|
|
2023-04-22 13:55:45 -05:00
|
|
|
test('should switch to bcrypt if incorrect algorithm is set', () => {
|
|
|
|
let invalidConfig = { algorithm: 'invalid', ...config } as HTPasswdConfig;
|
|
|
|
new HTPasswd(invalidConfig, emptyPluginOptions);
|
|
|
|
expect(warn).toHaveBeenCalledWith(
|
|
|
|
'The algorithm selected %s is invalid, switching to to default one "bcrypt", password validation can be affected',
|
|
|
|
'invalid'
|
|
|
|
);
|
|
|
|
expect(info).toHaveBeenCalled();
|
2021-01-30 17:41:33 -05:00
|
|
|
});
|
2020-08-19 13:27:35 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('authenticate()', () => {
|
|
|
|
test('it should authenticate user with given credentials', (done) => {
|
2022-03-03 15:57:19 -05:00
|
|
|
const users = [
|
|
|
|
{ username: 'test', password: 'test' },
|
|
|
|
{ username: 'username', password: 'password' },
|
|
|
|
{ username: 'bcrypt', password: 'password' },
|
|
|
|
];
|
|
|
|
let usersAuthenticated = 0;
|
|
|
|
const generateCallback = (username) => (error, userGroups) => {
|
|
|
|
usersAuthenticated += 1;
|
|
|
|
expect(error).toBeNull();
|
|
|
|
expect(userGroups).toContain(username);
|
|
|
|
usersAuthenticated === users.length && done();
|
2020-08-19 13:27:35 -05:00
|
|
|
};
|
2022-03-03 15:57:19 -05:00
|
|
|
users.forEach(({ username, password }) =>
|
|
|
|
wrapper.authenticate(username, password, generateCallback(username))
|
|
|
|
);
|
2020-08-19 13:27:35 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
test('it should not authenticate user with given credentials', (done) => {
|
2022-03-03 15:57:19 -05:00
|
|
|
const users = ['test', 'username', 'bcrypt'];
|
|
|
|
let usersAuthenticated = 0;
|
|
|
|
const generateCallback = () => (error, userGroups) => {
|
|
|
|
usersAuthenticated += 1;
|
|
|
|
expect(error).toBeNull();
|
|
|
|
expect(userGroups).toBeFalsy();
|
|
|
|
usersAuthenticated === users.length && done();
|
|
|
|
};
|
|
|
|
users.forEach((username) =>
|
|
|
|
wrapper.authenticate(username, 'somerandompassword', generateCallback())
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('it should warn on slow password verification', (done) => {
|
2022-07-29 13:51:45 -05:00
|
|
|
// @ts-ignore
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2023-04-22 13:55:45 -05:00
|
|
|
bcrypt.compare = jest.fn((_passwd, _hash) => {
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, DEFAULT_SLOW_VERIFY_MS + 1)).then(
|
|
|
|
() => true
|
|
|
|
);
|
2022-03-03 15:57:19 -05:00
|
|
|
});
|
2020-08-19 13:27:35 -05:00
|
|
|
const callback = (a, b): void => {
|
|
|
|
expect(a).toBeNull();
|
2022-03-03 15:57:19 -05:00
|
|
|
expect(b).toContain('bcrypt');
|
2023-04-22 13:55:45 -05:00
|
|
|
const mockWarn = options.logger.warn as jest.MockedFn<jest.MockableFunction>;
|
|
|
|
expect(mockWarn.mock.calls.length).toBe(1);
|
|
|
|
const [{ user, durationMs }, message] = mockWarn.mock.calls[0];
|
|
|
|
expect(user).toEqual('bcrypt');
|
|
|
|
expect(durationMs).toBeGreaterThan(DEFAULT_SLOW_VERIFY_MS);
|
|
|
|
expect(message).toEqual('Password for user "@{user}" took @{durationMs}ms to verify');
|
2020-08-19 13:27:35 -05:00
|
|
|
done();
|
|
|
|
};
|
2022-03-03 15:57:19 -05:00
|
|
|
wrapper.authenticate('bcrypt', 'password', callback);
|
2022-07-29 13:51:45 -05:00
|
|
|
}, 18000);
|
2020-08-19 13:27:35 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('addUser()', () => {
|
|
|
|
test('it should not pass sanity check', (done) => {
|
|
|
|
const callback = (a): void => {
|
|
|
|
expect(a.message).toEqual('unauthorized access');
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
wrapper.adduser('test', 'somerandompassword', callback);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('it should add the user', (done) => {
|
|
|
|
let dataToWrite;
|
|
|
|
// @ts-ignore
|
2023-04-22 13:55:45 -05:00
|
|
|
fs.writeFile = jest.fn((name, data, callback) => {
|
2020-08-19 13:27:35 -05:00
|
|
|
dataToWrite = data;
|
|
|
|
callback();
|
|
|
|
});
|
2021-01-30 17:41:33 -05:00
|
|
|
|
|
|
|
MockDate.set('2018-01-14T11:17:40.712Z');
|
|
|
|
|
2020-08-19 13:27:35 -05:00
|
|
|
const callback = (a, b): void => {
|
|
|
|
expect(a).toBeNull();
|
|
|
|
expect(b).toBeTruthy();
|
|
|
|
expect(fs.writeFile).toHaveBeenCalled();
|
|
|
|
expect(dataToWrite.indexOf('usernotpresent')).not.toEqual(-1);
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
wrapper.adduser('usernotpresent', 'somerandompassword', callback);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('addUser() error handling', () => {
|
|
|
|
test('sanityCheck should return an Error', (done) => {
|
|
|
|
jest.doMock('../src/utils.ts', () => {
|
|
|
|
return {
|
|
|
|
sanityCheck: (): Error => Error('some error'),
|
2023-04-22 13:55:45 -05:00
|
|
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
2020-08-19 13:27:35 -05:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
const HTPasswd = require('../src/htpasswd.ts').default;
|
2022-03-03 15:57:19 -05:00
|
|
|
const wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
wrapper.adduser('sanityCheck', 'test', (sanity) => {
|
|
|
|
expect(sanity.message).toBeDefined();
|
|
|
|
expect(sanity.message).toMatch('some error');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('lockAndRead should return an Error', (done) => {
|
|
|
|
jest.doMock('../src/utils.ts', () => {
|
|
|
|
return {
|
|
|
|
sanityCheck: (): any => null,
|
|
|
|
lockAndRead: (_a, b): any => b(new Error('lock error')),
|
2023-04-22 13:55:45 -05:00
|
|
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
2020-08-19 13:27:35 -05:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
const HTPasswd = require('../src/htpasswd.ts').default;
|
2022-03-03 15:57:19 -05:00
|
|
|
const wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
wrapper.adduser('lockAndRead', 'test', (sanity) => {
|
|
|
|
expect(sanity.message).toBeDefined();
|
|
|
|
expect(sanity.message).toMatch('lock error');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('addUserToHTPasswd should return an Error', (done) => {
|
|
|
|
jest.doMock('../src/utils.ts', () => {
|
|
|
|
return {
|
|
|
|
sanityCheck: (): any => null,
|
|
|
|
parseHTPasswd: (): void => {},
|
|
|
|
lockAndRead: (_a, b): any => b(null, ''),
|
|
|
|
unlockFile: (_a, b): any => b(),
|
2023-04-22 13:55:45 -05:00
|
|
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
2020-08-19 13:27:35 -05:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
const HTPasswd = require('../src/htpasswd.ts').default;
|
2022-03-03 15:57:19 -05:00
|
|
|
const wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
wrapper.adduser('addUserToHTPasswd', 'test', () => {
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('writeFile should return an Error', (done) => {
|
|
|
|
jest.doMock('../src/utils.ts', () => {
|
|
|
|
return {
|
2023-04-22 13:55:45 -05:00
|
|
|
sanityCheck: () => Promise.resolve(null),
|
2020-08-19 13:27:35 -05:00
|
|
|
parseHTPasswd: (): void => {},
|
|
|
|
lockAndRead: (_a, b): any => b(null, ''),
|
|
|
|
addUserToHTPasswd: (): void => {},
|
2023-04-22 13:55:45 -05:00
|
|
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
2020-08-19 13:27:35 -05:00
|
|
|
};
|
|
|
|
});
|
|
|
|
jest.doMock('fs', () => {
|
|
|
|
const original = jest.requireActual('fs');
|
|
|
|
return {
|
|
|
|
...original,
|
|
|
|
writeFile: jest.fn((_name, _data, callback) => {
|
|
|
|
callback(new Error('write error'));
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
const HTPasswd = require('../src/htpasswd.ts').default;
|
2022-03-03 15:57:19 -05:00
|
|
|
const wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
wrapper.adduser('addUserToHTPasswd', 'test', (err) => {
|
|
|
|
expect(err).not.toBeNull();
|
|
|
|
expect(err.message).toMatch('write error');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('reload()', () => {
|
|
|
|
test('it should read the file and set the users', (done) => {
|
2022-03-03 15:57:19 -05:00
|
|
|
const output = {
|
|
|
|
test: '$6FrCaT/v0dwE',
|
|
|
|
username: '$66to3JK5RgZM',
|
|
|
|
bcrypt: '$2y$04$K2Cn3StiXx4CnLmcTW/ymekOrj7WlycZZF9xgmoJ/U0zGPqSLPVBe',
|
|
|
|
};
|
2020-08-19 13:27:35 -05:00
|
|
|
const callback = (): void => {
|
|
|
|
expect(wrapper.users).toEqual(output);
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
wrapper.reload(callback);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('reload should fails on check file', (done) => {
|
|
|
|
jest.doMock('fs', () => {
|
|
|
|
return {
|
|
|
|
stat: (_name, callback): void => {
|
|
|
|
callback(new Error('stat error'), null);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
|
|
|
const callback = (err): void => {
|
|
|
|
expect(err).not.toBeNull();
|
|
|
|
expect(err.message).toMatch('stat error');
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
|
|
|
|
const HTPasswd = require('../src/htpasswd.ts').default;
|
2022-03-03 15:57:19 -05:00
|
|
|
const wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
wrapper.reload(callback);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('reload times match', (done) => {
|
|
|
|
jest.doMock('fs', () => {
|
|
|
|
return {
|
|
|
|
stat: (_name, callback): void => {
|
|
|
|
callback(null, {
|
|
|
|
mtime: null,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
|
|
|
const callback = (err): void => {
|
|
|
|
expect(err).toBeUndefined();
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
|
|
|
|
const HTPasswd = require('../src/htpasswd.ts').default;
|
2022-03-03 15:57:19 -05:00
|
|
|
const wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
wrapper.reload(callback);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('reload should fails on read file', (done) => {
|
|
|
|
jest.doMock('fs', () => {
|
|
|
|
return {
|
|
|
|
stat: jest.requireActual('fs').stat,
|
|
|
|
readFile: (_name, _format, callback): void => {
|
|
|
|
callback(new Error('read error'), null);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
|
|
|
const callback = (err): void => {
|
|
|
|
expect(err).not.toBeNull();
|
|
|
|
expect(err.message).toMatch('read error');
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
|
|
|
|
const HTPasswd = require('../src/htpasswd.ts').default;
|
2022-03-03 15:57:19 -05:00
|
|
|
const wrapper = new HTPasswd(config, options);
|
2020-08-19 13:27:35 -05:00
|
|
|
wrapper.reload(callback);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('changePassword - it should throw an error for user not found', (done) => {
|
|
|
|
const callback = (error, isSuccess): void => {
|
|
|
|
expect(error).not.toBeNull();
|
2022-03-03 15:57:19 -05:00
|
|
|
expect(error.message).toBe(
|
|
|
|
`Unable to change password for user 'usernotpresent': user does not currently exist`
|
|
|
|
);
|
2020-08-19 13:27:35 -05:00
|
|
|
expect(isSuccess).toBeFalsy();
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
wrapper.changePassword('usernotpresent', 'oldPassword', 'newPassword', callback);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('changePassword - it should throw an error for wrong password', (done) => {
|
|
|
|
const callback = (error, isSuccess): void => {
|
|
|
|
expect(error).not.toBeNull();
|
2022-03-03 15:57:19 -05:00
|
|
|
expect(error.message).toBe(
|
|
|
|
`Unable to change password for user 'username': invalid old password`
|
|
|
|
);
|
2020-08-19 13:27:35 -05:00
|
|
|
expect(isSuccess).toBeFalsy();
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
wrapper.changePassword('username', 'wrongPassword', 'newPassword', callback);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('changePassword - it should change password', (done) => {
|
|
|
|
let dataToWrite;
|
|
|
|
// @ts-ignore
|
|
|
|
fs.writeFile = jest.fn((_name, data, callback) => {
|
|
|
|
dataToWrite = data;
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
const callback = (error, isSuccess): void => {
|
|
|
|
expect(error).toBeNull();
|
|
|
|
expect(isSuccess).toBeTruthy();
|
|
|
|
expect(fs.writeFile).toHaveBeenCalled();
|
|
|
|
expect(dataToWrite.indexOf('username')).not.toEqual(-1);
|
|
|
|
done();
|
|
|
|
};
|
|
|
|
wrapper.changePassword('username', 'password', 'newPassword', callback);
|
|
|
|
});
|
|
|
|
});
|