mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-04-15 03:02:51 -05:00
feat: async bcrypt hash (#3694)
* +feat: async bcrypt hash * +feat: async bcrypt hash
This commit is contained in:
parent
4275b1894e
commit
c9d1af0e5b
9 changed files with 126 additions and 96 deletions
7
.changeset/tender-tigers-hammer.md
Normal file
7
.changeset/tender-tigers-hammer.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'@verdaccio/auth': minor
|
||||||
|
'@verdaccio/core': minor
|
||||||
|
'verdaccio-htpasswd': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: async bcrypt hash
|
|
@ -8,7 +8,7 @@ import { Config } from '@verdaccio/types';
|
||||||
import { Auth } from '../src';
|
import { Auth } from '../src';
|
||||||
import { authPluginFailureConf, authPluginPassThrougConf, authProfileConf } from './helper/plugin';
|
import { authPluginFailureConf, authPluginPassThrougConf, authProfileConf } from './helper/plugin';
|
||||||
|
|
||||||
setup({});
|
setup({ level: 'debug', type: 'stdout' });
|
||||||
|
|
||||||
describe('AuthTest', () => {
|
describe('AuthTest', () => {
|
||||||
test('should init correctly', async () => {
|
test('should init correctly', async () => {
|
||||||
|
@ -29,6 +29,18 @@ describe('AuthTest', () => {
|
||||||
expect(auth).toBeDefined();
|
expect(auth).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should load custom algorithm', async () => {
|
||||||
|
const config: Config = new AppConfig({
|
||||||
|
...authProfileConf,
|
||||||
|
auth: { htpasswd: { algorithm: 'sha1', file: './foo' } },
|
||||||
|
});
|
||||||
|
config.checkSecretKey('12345');
|
||||||
|
|
||||||
|
const auth: Auth = new Auth(config);
|
||||||
|
await auth.init();
|
||||||
|
expect(auth).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
describe('test authenticate method', () => {
|
describe('test authenticate method', () => {
|
||||||
describe('test authenticate states', () => {
|
describe('test authenticate states', () => {
|
||||||
test('should be a success login', async () => {
|
test('should be a success login', async () => {
|
||||||
|
|
|
@ -109,3 +109,10 @@ export const PACKAGE_ACCESS = {
|
||||||
SCOPE: '@*/*',
|
SCOPE: '@*/*',
|
||||||
ALL: '**',
|
ALL: '**',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum HtpasswdHashAlgorithm {
|
||||||
|
md5 = 'md5',
|
||||||
|
sha1 = 'sha1',
|
||||||
|
crypt = 'crypt',
|
||||||
|
bcrypt = 'bcrypt',
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ export {
|
||||||
DEFAULT_PASSWORD_VALIDATION,
|
DEFAULT_PASSWORD_VALIDATION,
|
||||||
DEFAULT_USER,
|
DEFAULT_USER,
|
||||||
USERS,
|
USERS,
|
||||||
|
HtpasswdHashAlgorithm,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
const validationUtils = validatioUtils;
|
const validationUtils = validatioUtils;
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -18,9 +18,6 @@ export type LoggerLevel = 'http' | 'fatal' | 'warn' | 'info' | 'debug' | 'trace'
|
||||||
|
|
||||||
export type LoggerConfigItem = {
|
export type LoggerConfigItem = {
|
||||||
type?: LoggerType;
|
type?: LoggerType;
|
||||||
/**
|
|
||||||
* The format
|
|
||||||
*/
|
|
||||||
format?: LoggerFormat;
|
format?: LoggerFormat;
|
||||||
path?: string;
|
path?: string;
|
||||||
level?: string;
|
level?: string;
|
||||||
|
|
|
@ -2,12 +2,12 @@ import buildDebug from 'debug';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { dirname, resolve } from 'path';
|
import { dirname, resolve } from 'path';
|
||||||
|
|
||||||
import { pluginUtils } from '@verdaccio/core';
|
import { constants, pluginUtils } from '@verdaccio/core';
|
||||||
import { unlockFile } from '@verdaccio/file-locking';
|
import { unlockFile } from '@verdaccio/file-locking';
|
||||||
import { Callback, Logger } from '@verdaccio/types';
|
import { Callback, Logger } from '@verdaccio/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HtpasswdHashAlgorithm,
|
DEFAULT_BCRYPT_ROUNDS,
|
||||||
HtpasswdHashConfig,
|
HtpasswdHashConfig,
|
||||||
addUserToHTPasswd,
|
addUserToHTPasswd,
|
||||||
changePasswordToHTPasswd,
|
changePasswordToHTPasswd,
|
||||||
|
@ -17,6 +17,8 @@ import {
|
||||||
verifyPassword,
|
verifyPassword,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
|
type HtpasswdHashAlgorithm = constants.HtpasswdHashAlgorithm;
|
||||||
|
|
||||||
const debug = buildDebug('verdaccio:plugin:htpasswd');
|
const debug = buildDebug('verdaccio:plugin:htpasswd');
|
||||||
|
|
||||||
export type HTPasswdConfig = {
|
export type HTPasswdConfig = {
|
||||||
|
@ -27,7 +29,6 @@ export type HTPasswdConfig = {
|
||||||
slow_verify_ms?: number;
|
slow_verify_ms?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_BCRYPT_ROUNDS = 10;
|
|
||||||
export const DEFAULT_SLOW_VERIFY_MS = 200;
|
export const DEFAULT_SLOW_VERIFY_MS = 200;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,15 +64,19 @@ export default class HTPasswd
|
||||||
let algorithm: HtpasswdHashAlgorithm;
|
let algorithm: HtpasswdHashAlgorithm;
|
||||||
let rounds: number | undefined;
|
let rounds: number | undefined;
|
||||||
|
|
||||||
if (config.algorithm === undefined) {
|
if (typeof config.algorithm === 'undefined') {
|
||||||
algorithm = HtpasswdHashAlgorithm.bcrypt;
|
algorithm = constants.HtpasswdHashAlgorithm.bcrypt;
|
||||||
} else if (HtpasswdHashAlgorithm[config.algorithm] !== undefined) {
|
} else if (constants.HtpasswdHashAlgorithm[config.algorithm] !== undefined) {
|
||||||
algorithm = HtpasswdHashAlgorithm[config.algorithm];
|
algorithm = constants.HtpasswdHashAlgorithm[config.algorithm];
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid algorithm "${config.algorithm}"`);
|
this.logger.warn(
|
||||||
|
`The algorithm selected %s is invalid, switching to to default one "bcrypt", password validation can be affected`,
|
||||||
|
config.algorithm
|
||||||
|
);
|
||||||
|
algorithm = constants.HtpasswdHashAlgorithm.bcrypt;
|
||||||
}
|
}
|
||||||
debug(`password hash algorithm: ${algorithm}`);
|
debug(`password hash algorithm: ${algorithm}`);
|
||||||
if (algorithm === HtpasswdHashAlgorithm.bcrypt) {
|
if (algorithm === constants.HtpasswdHashAlgorithm.bcrypt) {
|
||||||
rounds = config.rounds || DEFAULT_BCRYPT_ROUNDS;
|
rounds = config.rounds || DEFAULT_BCRYPT_ROUNDS;
|
||||||
} else if (config.rounds !== undefined) {
|
} else if (config.rounds !== undefined) {
|
||||||
this.logger.warn({ algo: algorithm }, 'Option "rounds" is not valid for "@{algo}" algorithm');
|
this.logger.warn({ algo: algorithm }, 'Option "rounds" is not valid for "@{algo}" algorithm');
|
||||||
|
@ -202,7 +207,7 @@ export default class HTPasswd
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._writeFile(addUserToHTPasswd(body, user, password, this.hashConfig), cb);
|
this._writeFile(await addUserToHTPasswd(body, user, password, this.hashConfig), cb);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,15 @@ import bcrypt from 'bcryptjs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import createError, { HttpError } from 'http-errors';
|
import createError, { HttpError } from 'http-errors';
|
||||||
|
|
||||||
import { API_ERROR, HTTP_STATUS } from '@verdaccio/core';
|
import { API_ERROR, HTTP_STATUS, constants } from '@verdaccio/core';
|
||||||
import { readFile } from '@verdaccio/file-locking';
|
import { readFile } from '@verdaccio/file-locking';
|
||||||
import { Callback } from '@verdaccio/types';
|
import { Callback } from '@verdaccio/types';
|
||||||
|
|
||||||
import crypt3 from './crypt3';
|
import crypt3 from './crypt3';
|
||||||
|
|
||||||
export enum HtpasswdHashAlgorithm {
|
export const DEFAULT_BCRYPT_ROUNDS = 10;
|
||||||
md5 = 'md5',
|
|
||||||
sha1 = 'sha1',
|
type HtpasswdHashAlgorithm = constants.HtpasswdHashAlgorithm;
|
||||||
crypt = 'crypt',
|
|
||||||
bcrypt = 'bcrypt',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HtpasswdHashConfig {
|
export interface HtpasswdHashConfig {
|
||||||
algorithm: HtpasswdHashAlgorithm;
|
algorithm: HtpasswdHashAlgorithm;
|
||||||
|
@ -80,24 +77,24 @@ export async function verifyPassword(passwd: string, hash: string): Promise<bool
|
||||||
* @param {HtpasswdHashConfig} hashConfig
|
* @param {HtpasswdHashConfig} hashConfig
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function generateHtpasswdLine(
|
export async function generateHtpasswdLine(
|
||||||
user: string,
|
user: string,
|
||||||
passwd: string,
|
passwd: string,
|
||||||
hashConfig: HtpasswdHashConfig
|
hashConfig: HtpasswdHashConfig
|
||||||
): string {
|
): Promise<string> {
|
||||||
let hash: string;
|
let hash: string;
|
||||||
|
|
||||||
switch (hashConfig.algorithm) {
|
switch (hashConfig.algorithm) {
|
||||||
case HtpasswdHashAlgorithm.bcrypt:
|
case constants.HtpasswdHashAlgorithm.bcrypt:
|
||||||
hash = bcrypt.hashSync(passwd, hashConfig.rounds);
|
hash = await bcrypt.hash(passwd, hashConfig.rounds || DEFAULT_BCRYPT_ROUNDS);
|
||||||
break;
|
break;
|
||||||
case HtpasswdHashAlgorithm.crypt:
|
case constants.HtpasswdHashAlgorithm.crypt:
|
||||||
hash = crypt3(passwd);
|
hash = crypt3(passwd);
|
||||||
break;
|
break;
|
||||||
case HtpasswdHashAlgorithm.md5:
|
case constants.HtpasswdHashAlgorithm.md5:
|
||||||
hash = md5(passwd);
|
hash = md5(passwd);
|
||||||
break;
|
break;
|
||||||
case HtpasswdHashAlgorithm.sha1:
|
case constants.HtpasswdHashAlgorithm.sha1:
|
||||||
hash = '{SHA}' + crypto.createHash('sha1').update(passwd, 'utf8').digest('base64');
|
hash = '{SHA}' + crypto.createHash('sha1').update(passwd, 'utf8').digest('base64');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -116,12 +113,12 @@ export function generateHtpasswdLine(
|
||||||
* @param {HtpasswdHashConfig} hashConfig
|
* @param {HtpasswdHashConfig} hashConfig
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function addUserToHTPasswd(
|
export async function addUserToHTPasswd(
|
||||||
body: string,
|
body: string,
|
||||||
user: string,
|
user: string,
|
||||||
passwd: string,
|
passwd: string,
|
||||||
hashConfig: HtpasswdHashConfig
|
hashConfig: HtpasswdHashConfig
|
||||||
): string {
|
): Promise<string> {
|
||||||
if (user !== encodeURIComponent(user)) {
|
if (user !== encodeURIComponent(user)) {
|
||||||
const err = createError('username should not contain non-uri-safe characters');
|
const err = createError('username should not contain non-uri-safe characters');
|
||||||
|
|
||||||
|
@ -129,7 +126,7 @@ export function addUserToHTPasswd(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newline = generateHtpasswdLine(user, passwd, hashConfig);
|
let newline = await generateHtpasswdLine(user, passwd, hashConfig);
|
||||||
|
|
||||||
if (body.length && body[body.length - 1] !== '\n') {
|
if (body.length && body[body.length - 1] !== '\n') {
|
||||||
newline = '\n' + newline;
|
newline = '\n' + newline;
|
||||||
|
@ -190,13 +187,14 @@ export async function sanityCheck(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* /**
|
||||||
* changePasswordToHTPasswd - change password for existing user
|
* changePasswordToHTPasswd - change password for existing user
|
||||||
* @param {string} body
|
* @param {string} body
|
||||||
* @param {string} user
|
* @param {string} user
|
||||||
* @param {string} passwd
|
* @param {string} passwd
|
||||||
* @param {string} newPasswd
|
* @param {string} newPasswd
|
||||||
* @param {HtpasswdHashConfig} hashConfig
|
* @param {HtpasswdHashConfig} hashConfig
|
||||||
* @returns {string}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
export async function changePasswordToHTPasswd(
|
export async function changePasswordToHTPasswd(
|
||||||
body: string,
|
body: string,
|
||||||
|
@ -215,7 +213,7 @@ export async function changePasswordToHTPasswd(
|
||||||
if (!passwordValid) {
|
if (!passwordValid) {
|
||||||
throw new Error(`Unable to change password for user '${user}': invalid old password`);
|
throw new Error(`Unable to change password for user '${user}': invalid old password`);
|
||||||
}
|
}
|
||||||
const updatedUserLine = generateHtpasswdLine(username, newPasswd, hashConfig);
|
const updatedUserLine = await generateHtpasswdLine(username, newPasswd, hashConfig);
|
||||||
lines.splice(userLineIndex, 1, updatedUserLine);
|
lines.splice(userLineIndex, 1, updatedUserLine);
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,18 @@
|
||||||
/* eslint-disable jest/no-mocks-import */
|
|
||||||
// @ts-ignore: Module has no default export
|
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
// @ts-ignore: Module has no default export
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
// @ts-ignore: Module has no default export
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import MockDate from 'mockdate';
|
import MockDate from 'mockdate';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Config, parseConfigFile } from '@verdaccio/config';
|
import { Config, parseConfigFile } from '@verdaccio/config';
|
||||||
import { logger, setup } from '@verdaccio/logger';
|
import { constants, pluginUtils } from '@verdaccio/core';
|
||||||
import { PluginOptions } from '@verdaccio/types';
|
|
||||||
|
|
||||||
import HTPasswd, { DEFAULT_SLOW_VERIFY_MS, HTPasswdConfig } from '../src/htpasswd';
|
import HTPasswd, { DEFAULT_SLOW_VERIFY_MS, HTPasswdConfig } from '../src/htpasswd';
|
||||||
import { HtpasswdHashAlgorithm } from '../src/utils';
|
|
||||||
|
|
||||||
setup();
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
logger,
|
logger: { warn: jest.fn(), info: jest.fn() },
|
||||||
config: new Config(parseConfigFile(path.join(__dirname, './__fixtures__/config.yaml'))),
|
config: new Config(parseConfigFile(path.join(__dirname, './__fixtures__/config.yaml'))),
|
||||||
} as any as PluginOptions<HTPasswdConfig>;
|
} as any as pluginUtils.PluginOptions<HTPasswdConfig>;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
file: './htpasswd',
|
file: './htpasswd',
|
||||||
|
@ -34,7 +26,8 @@ describe('HTPasswd', () => {
|
||||||
wrapper = new HTPasswd(config, options);
|
wrapper = new HTPasswd(config, options);
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
// @ts-ignore: Module has no default export
|
|
||||||
|
// @ts-ignore
|
||||||
crypto.randomBytes = jest.fn(() => {
|
crypto.randomBytes = jest.fn(() => {
|
||||||
return {
|
return {
|
||||||
toString: (): string => '$6',
|
toString: (): string => '$6',
|
||||||
|
@ -43,7 +36,15 @@ describe('HTPasswd', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('constructor()', () => {
|
describe('constructor()', () => {
|
||||||
const emptyPluginOptions = { config: {} } as any as PluginOptions<HTPasswdConfig>;
|
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>;
|
||||||
|
|
||||||
test('should ensure file path configuration exists', () => {
|
test('should ensure file path configuration exists', () => {
|
||||||
expect(function () {
|
expect(function () {
|
||||||
|
@ -51,11 +52,14 @@ describe('HTPasswd', () => {
|
||||||
}).toThrow(/should specify "file" in config/);
|
}).toThrow(/should specify "file" in config/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error about incorrect algorithm', () => {
|
test('should switch to bcrypt if incorrect algorithm is set', () => {
|
||||||
expect(function () {
|
let invalidConfig = { algorithm: 'invalid', ...config } as HTPasswdConfig;
|
||||||
let invalidConfig = { algorithm: 'invalid', ...config } as HTPasswdConfig;
|
new HTPasswd(invalidConfig, emptyPluginOptions);
|
||||||
new HTPasswd(invalidConfig, emptyPluginOptions);
|
expect(warn).toHaveBeenCalledWith(
|
||||||
}).toThrow(/Invalid algorithm "invalid"/);
|
'The algorithm selected %s is invalid, switching to to default one "bcrypt", password validation can be affected',
|
||||||
|
'invalid'
|
||||||
|
);
|
||||||
|
expect(info).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -95,21 +99,20 @@ describe('HTPasswd', () => {
|
||||||
test('it should warn on slow password verification', (done) => {
|
test('it should warn on slow password verification', (done) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
bcrypt.compare = jest.fn(async (_passwd, _hash) => {
|
bcrypt.compare = jest.fn((_passwd, _hash) => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, DEFAULT_SLOW_VERIFY_MS + 1));
|
return new Promise((resolve) => setTimeout(resolve, DEFAULT_SLOW_VERIFY_MS + 1)).then(
|
||||||
return true;
|
() => true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
const callback = (a, b): void => {
|
const callback = (a, b): void => {
|
||||||
expect(a).toBeNull();
|
expect(a).toBeNull();
|
||||||
expect(b).toContain('bcrypt');
|
expect(b).toContain('bcrypt');
|
||||||
// TODO: figure out how to test the warning properly without mocking the logger
|
const mockWarn = options.logger.warn as jest.MockedFn<jest.MockableFunction>;
|
||||||
// maybe mocking pino? not sure.
|
expect(mockWarn.mock.calls.length).toBe(1);
|
||||||
// const mockWarn = options.logger.warn as jest.MockedFn<jest.MockableFunction>;
|
const [{ user, durationMs }, message] = mockWarn.mock.calls[0];
|
||||||
// expect(mockWarn.mock.calls.length).toBe(1);
|
expect(user).toEqual('bcrypt');
|
||||||
// const [{ user, durationMs }, message] = mockWarn.mock.calls[0];
|
expect(durationMs).toBeGreaterThan(DEFAULT_SLOW_VERIFY_MS);
|
||||||
// expect(user).toEqual('bcrypt');
|
expect(message).toEqual('Password for user "@{user}" took @{durationMs}ms to verify');
|
||||||
// expect(durationMs).toBeGreaterThan(DEFAULT_SLOW_VERIFY_MS);
|
|
||||||
// expect(message).toEqual('Password for user "@{user}" took @{durationMs}ms to verify');
|
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
wrapper.authenticate('bcrypt', 'password', callback);
|
wrapper.authenticate('bcrypt', 'password', callback);
|
||||||
|
@ -128,7 +131,7 @@ describe('HTPasswd', () => {
|
||||||
test('it should add the user', (done) => {
|
test('it should add the user', (done) => {
|
||||||
let dataToWrite;
|
let dataToWrite;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fs.writeFile = jest.fn((_name, data, callback) => {
|
fs.writeFile = jest.fn((name, data, callback) => {
|
||||||
dataToWrite = data;
|
dataToWrite = data;
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
@ -150,7 +153,7 @@ describe('HTPasswd', () => {
|
||||||
jest.doMock('../src/utils.ts', () => {
|
jest.doMock('../src/utils.ts', () => {
|
||||||
return {
|
return {
|
||||||
sanityCheck: (): Error => Error('some error'),
|
sanityCheck: (): Error => Error('some error'),
|
||||||
HtpasswdHashAlgorithm,
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -168,7 +171,7 @@ describe('HTPasswd', () => {
|
||||||
return {
|
return {
|
||||||
sanityCheck: (): any => null,
|
sanityCheck: (): any => null,
|
||||||
lockAndRead: (_a, b): any => b(new Error('lock error')),
|
lockAndRead: (_a, b): any => b(new Error('lock error')),
|
||||||
HtpasswdHashAlgorithm,
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,7 +191,7 @@ describe('HTPasswd', () => {
|
||||||
parseHTPasswd: (): void => {},
|
parseHTPasswd: (): void => {},
|
||||||
lockAndRead: (_a, b): any => b(null, ''),
|
lockAndRead: (_a, b): any => b(null, ''),
|
||||||
unlockFile: (_a, b): any => b(),
|
unlockFile: (_a, b): any => b(),
|
||||||
HtpasswdHashAlgorithm,
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -202,11 +205,11 @@ describe('HTPasswd', () => {
|
||||||
test('writeFile should return an Error', (done) => {
|
test('writeFile should return an Error', (done) => {
|
||||||
jest.doMock('../src/utils.ts', () => {
|
jest.doMock('../src/utils.ts', () => {
|
||||||
return {
|
return {
|
||||||
sanityCheck: (): any => null,
|
sanityCheck: () => Promise.resolve(null),
|
||||||
parseHTPasswd: (): void => {},
|
parseHTPasswd: (): void => {},
|
||||||
lockAndRead: (_a, b): any => b(null, ''),
|
lockAndRead: (_a, b): any => b(null, ''),
|
||||||
addUserToHTPasswd: (): void => {},
|
addUserToHTPasswd: (): void => {},
|
||||||
HtpasswdHashAlgorithm,
|
HtpasswdHashAlgorithm: constants.HtpasswdHashAlgorithm,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
jest.doMock('fs', () => {
|
jest.doMock('fs', () => {
|
||||||
|
@ -246,9 +249,6 @@ describe('HTPasswd', () => {
|
||||||
test('reload should fails on check file', (done) => {
|
test('reload should fails on check file', (done) => {
|
||||||
jest.doMock('fs', () => {
|
jest.doMock('fs', () => {
|
||||||
return {
|
return {
|
||||||
readFile: (_name, callback): void => {
|
|
||||||
callback(new Error('stat error'), null);
|
|
||||||
},
|
|
||||||
stat: (_name, callback): void => {
|
stat: (_name, callback): void => {
|
||||||
callback(new Error('stat error'), null);
|
callback(new Error('stat error'), null);
|
||||||
},
|
},
|
||||||
|
@ -268,9 +268,6 @@ describe('HTPasswd', () => {
|
||||||
test('reload times match', (done) => {
|
test('reload times match', (done) => {
|
||||||
jest.doMock('fs', () => {
|
jest.doMock('fs', () => {
|
||||||
return {
|
return {
|
||||||
readFile: (_name, callback): void => {
|
|
||||||
callback(new Error('stat error'), null);
|
|
||||||
},
|
|
||||||
stat: (_name, callback): void => {
|
stat: (_name, callback): void => {
|
||||||
callback(null, {
|
callback(null, {
|
||||||
mtime: null,
|
mtime: null,
|
||||||
|
|
|
@ -3,9 +3,10 @@ import crypto from 'crypto';
|
||||||
import { HttpError } from 'http-errors';
|
import { HttpError } from 'http-errors';
|
||||||
import MockDate from 'mockdate';
|
import MockDate from 'mockdate';
|
||||||
|
|
||||||
import { DEFAULT_BCRYPT_ROUNDS } from '../src/htpasswd';
|
import { constants } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { DEFAULT_BCRYPT_ROUNDS } from '../src/utils';
|
||||||
import {
|
import {
|
||||||
HtpasswdHashAlgorithm,
|
|
||||||
addUserToHTPasswd,
|
addUserToHTPasswd,
|
||||||
changePasswordToHTPasswd,
|
changePasswordToHTPasswd,
|
||||||
generateHtpasswdLine,
|
generateHtpasswdLine,
|
||||||
|
@ -19,7 +20,7 @@ const mockReadFile = jest.fn();
|
||||||
const mockUnlockFile = jest.fn();
|
const mockUnlockFile = jest.fn();
|
||||||
|
|
||||||
const defaultHashConfig = {
|
const defaultHashConfig = {
|
||||||
algorithm: HtpasswdHashAlgorithm.bcrypt,
|
algorithm: constants.HtpasswdHashAlgorithm.bcrypt,
|
||||||
rounds: DEFAULT_BCRYPT_ROUNDS,
|
rounds: DEFAULT_BCRYPT_ROUNDS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,51 +112,56 @@ describe('generateHtpasswdLine', () => {
|
||||||
|
|
||||||
const [user, passwd] = ['username', 'password'];
|
const [user, passwd] = ['username', 'password'];
|
||||||
|
|
||||||
it('should correctly generate line for md5', () => {
|
it('should correctly generate line for md5', async () => {
|
||||||
const md5Conf = { algorithm: HtpasswdHashAlgorithm.md5 };
|
const md5Conf = { algorithm: constants.HtpasswdHashAlgorithm.md5 };
|
||||||
expect(generateHtpasswdLine(user, passwd, md5Conf)).toMatchSnapshot();
|
expect(await generateHtpasswdLine(user, passwd, md5Conf)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly generate line for sha1', () => {
|
it('should correctly generate line for sha1', async () => {
|
||||||
const sha1Conf = { algorithm: HtpasswdHashAlgorithm.sha1 };
|
const sha1Conf = { algorithm: constants.HtpasswdHashAlgorithm.sha1 };
|
||||||
expect(generateHtpasswdLine(user, passwd, sha1Conf)).toMatchSnapshot();
|
expect(await generateHtpasswdLine(user, passwd, sha1Conf)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly generate line for crypt', () => {
|
it('should correctly generate line for crypt', async () => {
|
||||||
const cryptConf = { algorithm: HtpasswdHashAlgorithm.crypt };
|
const cryptConf = { algorithm: constants.HtpasswdHashAlgorithm.crypt };
|
||||||
expect(generateHtpasswdLine(user, passwd, cryptConf)).toMatchSnapshot();
|
expect(await generateHtpasswdLine(user, passwd, cryptConf)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly generate line for bcrypt', () => {
|
it('should correctly generate line for bcrypt', async () => {
|
||||||
const bcryptAlgoConfig = {
|
const bcryptAlgoConfig = {
|
||||||
algorithm: HtpasswdHashAlgorithm.bcrypt,
|
algorithm: constants.HtpasswdHashAlgorithm.bcrypt,
|
||||||
rounds: 2,
|
rounds: 2,
|
||||||
};
|
};
|
||||||
expect(generateHtpasswdLine(user, passwd, bcryptAlgoConfig)).toMatchSnapshot();
|
expect(await generateHtpasswdLine(user, passwd, bcryptAlgoConfig)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addUserToHTPasswd - bcrypt', () => {
|
describe('addUserToHTPasswd - bcrypt', () => {
|
||||||
beforeAll(mockTimeAndRandomBytes);
|
beforeAll(mockTimeAndRandomBytes);
|
||||||
|
|
||||||
it('should add new htpasswd to the end', () => {
|
it('should add new htpasswd to the end', async () => {
|
||||||
const input = ['', 'username', 'password'];
|
const input = ['', 'username', 'password'];
|
||||||
expect(addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)).toMatchSnapshot();
|
expect(
|
||||||
|
await addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)
|
||||||
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add new htpasswd to the end in multiline input', () => {
|
it('should add new htpasswd to the end in multiline input', async () => {
|
||||||
const body = `test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
|
const body = `test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
|
||||||
test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z`;
|
test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z`;
|
||||||
const input = [body, 'username', 'password'];
|
const input = [body, 'username', 'password'];
|
||||||
expect(addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)).toMatchSnapshot();
|
expect(
|
||||||
|
await addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)
|
||||||
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error for incorrect username with space', () => {
|
it('should throw an error for incorrect username with space', async () => {
|
||||||
const [a, b, c] = ['', 'firstname lastname', 'password'];
|
const [a, b, c] = ['', 'firstname lastname', 'password'];
|
||||||
expect(() => addUserToHTPasswd(a, b, c, defaultHashConfig)).toThrowErrorMatchingSnapshot();
|
await expect(
|
||||||
|
addUserToHTPasswd(a, b, c, defaultHashConfig)
|
||||||
|
).rejects.toThrowErrorMatchingSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('lockAndRead', () => {
|
describe('lockAndRead', () => {
|
||||||
it('should call the readFile method', () => {
|
it('should call the readFile method', () => {
|
||||||
const cb = (): void => {};
|
const cb = (): void => {};
|
||||||
|
|
Loading…
Add table
Reference in a new issue