1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-02-21 07:29:37 +01:00

Merge pull request #800 from verdaccio/refactor-config

refactor: config and utils
This commit is contained in:
Juan Picado @jotadeveloper 2018-07-16 11:48:55 +02:00 committed by GitHub
commit 45b21fa87d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1182 additions and 325 deletions

@ -31,6 +31,8 @@
},
"rules": {
"no-useless-escape": 2,
"react/no-deprecated": 1,
"react/jsx-no-target-blank": 1,
"handle-callback-err": 2,
"no-fallthrough": 2,
"no-new-require": 2,

@ -21,10 +21,9 @@ ENV NODE_ENV=production
RUN npm config set registry http://registry.npmjs.org/ && \
yarn global add -s flow-bin@0.69.0 && \
yarn install --production=false && \
yarn run lint && \
yarn run code:docker-build && \
yarn run build:webui && \
yarn run test:unit -- --silent true --coverage false --bail && \
yarn lint && \
yarn code:docker-build && \
yarn build:webui && \
yarn cache clean && \
yarn install --production=true --pure-lockfile

@ -1,5 +1,5 @@
// flow-typed signature: 6e1fc0a644aa956f79029fec0709e597
// flow-typed version: 07ebad4796/jest_v22.x.x/flow_>=v0.39.x
// flow-typed signature: 4cacceffd326bb118e4a3c1b4d629e98
// flow-typed version: e737b9832f/jest_v23.x.x/flow_>=v0.39.x
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
(...args: TArguments): TReturn,
@ -55,6 +55,11 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
mockImplementationOnce(
fn: (...args: TArguments) => TReturn
): JestMockFn<TArguments, TReturn>,
/**
* Accepts a string to use in test result output in place of "jest.fn()" to
* indicate which mock function is being referenced.
*/
mockName(name: string): JestMockFn<TArguments, TReturn>,
/**
* Just a simple sugar function for returning `this`
*/
@ -66,7 +71,23 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
/**
* Sugar for only returning a value once inside your mock
*/
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>,
/**
* Sugar for jest.fn().mockImplementation(() => Promise.resolve(value))
*/
mockResolvedValue(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>,
/**
* Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value))
*/
mockResolvedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>,
/**
* Sugar for jest.fn().mockImplementation(() => Promise.reject(value))
*/
mockRejectedValue(value: TReturn): JestMockFn<TArguments, Promise<any>>,
/**
* Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value))
*/
mockRejectedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<any>>
};
type JestAsymmetricEqualityType = {
@ -113,6 +134,12 @@ type JestPromiseType = {
resolves: JestExpectType
};
/**
* Jest allows functions and classes to be used as test names in test() and
* describe()
*/
type JestTestName = string | Function;
/**
* Plugin: jest-enzyme
*/
@ -120,14 +147,16 @@ type EnzymeMatchersType = {
toBeChecked(): void,
toBeDisabled(): void,
toBeEmpty(): void,
toBeEmptyRender(): void,
toBePresent(): void,
toContainReact(element: React$Element<any>): void,
toExist(): void,
toHaveClassName(className: string): void,
toHaveHTML(html: string): void,
toHaveProp(propKey: string, propValue?: any): void,
toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void),
toHaveRef(refName: string): void,
toHaveState(stateKey: string, stateValue?: any): void,
toHaveStyle(styleKey: string, styleValue?: any): void,
toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void),
toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void),
toHaveTagName(tagName: string): void,
toHaveText(text: string): void,
toIncludeText(text: string): void,
@ -136,8 +165,342 @@ type EnzymeMatchersType = {
toMatchSelector(selector: string): void
};
type JestExpectType = {
not: JestExpectType & EnzymeMatchersType,
// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers
type DomTestingLibraryType = {
toBeInTheDOM(): void,
toHaveTextContent(content: string): void,
toHaveAttribute(name: string, expectedValue?: string): void
};
// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers
type JestJQueryMatchersType = {
toExist(): void,
toHaveLength(len: number): void,
toHaveId(id: string): void,
toHaveClass(className: string): void,
toHaveTag(tag: string): void,
toHaveAttr(key: string, val?: any): void,
toHaveProp(key: string, val?: any): void,
toHaveText(text: string | RegExp): void,
toHaveData(key: string, val?: any): void,
toHaveValue(val: any): void,
toHaveCss(css: {[key: string]: any}): void,
toBeChecked(): void,
toBeDisabled(): void,
toBeEmpty(): void,
toBeHidden(): void,
toBeSelected(): void,
toBeVisible(): void,
toBeFocused(): void,
toBeInDom(): void,
toBeMatchedBy(sel: string): void,
toHaveDescendant(sel: string): void,
toHaveDescendantWithText(sel: string, text: string | RegExp): void
};
// Jest Extended Matchers: https://github.com/jest-community/jest-extended
type JestExtendedMatchersType = {
/**
* Note: Currently unimplemented
* Passing assertion
*
* @param {String} message
*/
// pass(message: string): void;
/**
* Note: Currently unimplemented
* Failing assertion
*
* @param {String} message
*/
// fail(message: string): void;
/**
* Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty.
*/
toBeEmpty(): void;
/**
* Use .toBeOneOf when checking if a value is a member of a given Array.
* @param {Array.<*>} members
*/
toBeOneOf(members: any[]): void;
/**
* Use `.toBeNil` when checking a value is `null` or `undefined`.
*/
toBeNil(): void;
/**
* Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`.
* @param {Function} predicate
*/
toSatisfy(predicate: (n: any) => boolean): void;
/**
* Use `.toBeArray` when checking if a value is an `Array`.
*/
toBeArray(): void;
/**
* Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x.
* @param {Number} x
*/
toBeArrayOfSize(x: number): void;
/**
* Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set.
* @param {Array.<*>} members
*/
toIncludeAllMembers(members: any[]): void;
/**
* Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set.
* @param {Array.<*>} members
*/
toIncludeAnyMembers(members: any[]): void;
/**
* Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array.
* @param {Function} predicate
*/
toSatisfyAll(predicate: (n: any) => boolean): void;
/**
* Use `.toBeBoolean` when checking if a value is a `Boolean`.
*/
toBeBoolean(): void;
/**
* Use `.toBeTrue` when checking a value is equal (===) to `true`.
*/
toBeTrue(): void;
/**
* Use `.toBeFalse` when checking a value is equal (===) to `false`.
*/
toBeFalse(): void;
/**
* Use .toBeDate when checking if a value is a Date.
*/
toBeDate(): void;
/**
* Use `.toBeFunction` when checking if a value is a `Function`.
*/
toBeFunction(): void;
/**
* Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`.
*
* Note: Required Jest version >22
* Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same
*
* @param {Mock} mock
*/
toHaveBeenCalledBefore(mock: JestMockFn<any, any>): void;
/**
* Use `.toBeNumber` when checking if a value is a `Number`.
*/
toBeNumber(): void;
/**
* Use `.toBeNaN` when checking a value is `NaN`.
*/
toBeNaN(): void;
/**
* Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`.
*/
toBeFinite(): void;
/**
* Use `.toBePositive` when checking if a value is a positive `Number`.
*/
toBePositive(): void;
/**
* Use `.toBeNegative` when checking if a value is a negative `Number`.
*/
toBeNegative(): void;
/**
* Use `.toBeEven` when checking if a value is an even `Number`.
*/
toBeEven(): void;
/**
* Use `.toBeOdd` when checking if a value is an odd `Number`.
*/
toBeOdd(): void;
/**
* Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive).
*
* @param {Number} start
* @param {Number} end
*/
toBeWithin(start: number, end: number): void;
/**
* Use `.toBeObject` when checking if a value is an `Object`.
*/
toBeObject(): void;
/**
* Use `.toContainKey` when checking if an object contains the provided key.
*
* @param {String} key
*/
toContainKey(key: string): void;
/**
* Use `.toContainKeys` when checking if an object has all of the provided keys.
*
* @param {Array.<String>} keys
*/
toContainKeys(keys: string[]): void;
/**
* Use `.toContainAllKeys` when checking if an object only contains all of the provided keys.
*
* @param {Array.<String>} keys
*/
toContainAllKeys(keys: string[]): void;
/**
* Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys.
*
* @param {Array.<String>} keys
*/
toContainAnyKeys(keys: string[]): void;
/**
* Use `.toContainValue` when checking if an object contains the provided value.
*
* @param {*} value
*/
toContainValue(value: any): void;
/**
* Use `.toContainValues` when checking if an object contains all of the provided values.
*
* @param {Array.<*>} values
*/
toContainValues(values: any[]): void;
/**
* Use `.toContainAllValues` when checking if an object only contains all of the provided values.
*
* @param {Array.<*>} values
*/
toContainAllValues(values: any[]): void;
/**
* Use `.toContainAnyValues` when checking if an object contains at least one of the provided values.
*
* @param {Array.<*>} values
*/
toContainAnyValues(values: any[]): void;
/**
* Use `.toContainEntry` when checking if an object contains the provided entry.
*
* @param {Array.<String, String>} entry
*/
toContainEntry(entry: [string, string]): void;
/**
* Use `.toContainEntries` when checking if an object contains all of the provided entries.
*
* @param {Array.<Array.<String, String>>} entries
*/
toContainEntries(entries: [string, string][]): void;
/**
* Use `.toContainAllEntries` when checking if an object only contains all of the provided entries.
*
* @param {Array.<Array.<String, String>>} entries
*/
toContainAllEntries(entries: [string, string][]): void;
/**
* Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries.
*
* @param {Array.<Array.<String, String>>} entries
*/
toContainAnyEntries(entries: [string, string][]): void;
/**
* Use `.toBeExtensible` when checking if an object is extensible.
*/
toBeExtensible(): void;
/**
* Use `.toBeFrozen` when checking if an object is frozen.
*/
toBeFrozen(): void;
/**
* Use `.toBeSealed` when checking if an object is sealed.
*/
toBeSealed(): void;
/**
* Use `.toBeString` when checking if a value is a `String`.
*/
toBeString(): void;
/**
* Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings.
*
* @param {String} string
*/
toEqualCaseInsensitive(string: string): void;
/**
* Use `.toStartWith` when checking if a `String` starts with a given `String` prefix.
*
* @param {String} prefix
*/
toStartWith(prefix: string): void;
/**
* Use `.toEndWith` when checking if a `String` ends with a given `String` suffix.
*
* @param {String} suffix
*/
toEndWith(suffix: string): void;
/**
* Use `.toInclude` when checking if a `String` includes the given `String` substring.
*
* @param {String} substring
*/
toInclude(substring: string): void;
/**
* Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times.
*
* @param {String} substring
* @param {Number} times
*/
toIncludeRepeated(substring: string, times: number): void;
/**
* Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings.
*
* @param {Array.<String>} substring
*/
toIncludeMultiple(substring: string[]): void;
};
interface JestExpectType {
not: JestExpectType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestExtendedMatchersType,
/**
* If you have a mock function, you can use .lastCalledWith to test what
* arguments it was last called with.
@ -148,10 +511,6 @@ type JestExpectType = {
* strict equality.
*/
toBe(value: any): void,
/**
* Use .toHaveBeenCalled to ensure that a mock function got called.
*/
toBeCalled(): void,
/**
* Use .toBeCalledWith to ensure that a mock function was called with
* specific arguments.
@ -227,21 +586,55 @@ type JestExpectType = {
* Use .toHaveBeenCalled to ensure that a mock function got called.
*/
toHaveBeenCalled(): void,
toBeCalled(): void;
/**
* Use .toHaveBeenCalledTimes to ensure that a mock function got called exact
* number of times.
*/
toHaveBeenCalledTimes(number: number): void,
toBeCalledTimes(number: number): void;
/**
*
*/
toHaveBeenNthCalledWith(nthCall: number, ...args: Array<any>): void;
nthCalledWith(nthCall: number, ...args: Array<any>): void;
/**
*
*/
toHaveReturned(): void;
toReturn(): void;
/**
*
*/
toHaveReturnedTimes(number: number): void;
toReturnTimes(number: number): void;
/**
*
*/
toHaveReturnedWith(value: any): void;
toReturnWith(value: any): void;
/**
*
*/
toHaveLastReturnedWith(value: any): void;
lastReturnedWith(value: any): void;
/**
*
*/
toHaveNthReturnedWith(nthCall: number, value: any): void;
nthReturnedWith(nthCall: number, value: any): void;
/**
* Use .toHaveBeenCalledWith to ensure that a mock function was called with
* specific arguments.
*/
toHaveBeenCalledWith(...args: Array<any>): void,
toBeCalledWith(...args: Array<any>): void,
/**
* Use .toHaveBeenLastCalledWith to ensure that a mock function was last called
* with specific arguments.
*/
toHaveBeenLastCalledWith(...args: Array<any>): void,
lastCalledWith(...args: Array<any>): void,
/**
* Check that an object has a .length property and it is set to a certain
* numeric value.
@ -260,9 +653,17 @@ type JestExpectType = {
*/
toMatchObject(object: Object | Array<Object>): void,
/**
* This ensures that a React component matches the most recent snapshot.
* Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object.
*/
toMatchSnapshot(name?: string): void,
toStrictEqual(value: any): void,
/**
* This ensures that an Object matches the most recent snapshot.
*/
toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void,
/**
* This ensures that an Object matches the most recent snapshot.
*/
toMatchSnapshot(name: string): void,
/**
* Use .toThrow to test that a function throws when it is called.
* If you want to test that a specific error gets thrown, you can provide an
@ -278,7 +679,7 @@ type JestExpectType = {
* matching the most recent snapshot when it is called.
*/
toThrowErrorMatchingSnapshot(): void
};
}
type JestObjectType = {
/**
@ -391,6 +792,13 @@ type JestObjectType = {
* Executes only the macro task queue (i.e. all tasks queued by setTimeout()
* or setInterval() and setImmediate()).
*/
advanceTimersByTime(msToRun: number): void,
/**
* Executes only the macro task queue (i.e. all tasks queued by setTimeout()
* or setInterval() and setImmediate()).
*
* Renamed to `advanceTimersByTime`.
*/
runTimersToTime(msToRun: number): void,
/**
* Executes only the macro-tasks that are currently pending (i.e., only the
@ -424,7 +832,7 @@ type JestObjectType = {
* Creates a mock function similar to jest.fn but also tracks calls to
* object[methodName].
*/
spyOn(object: Object, methodName: string): JestMockFn<any, any>,
spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn<any, any>,
/**
* Set the default timeout interval for tests and before/after hooks in milliseconds.
* Note: The default timeout interval is 5 seconds if this method is not called.
@ -462,17 +870,17 @@ declare var describe: {
/**
* Creates a block that groups together several related tests in one "test suite"
*/
(name: string, fn: () => void): void,
(name: JestTestName, fn: () => void): void,
/**
* Only run this describe block
*/
only(name: string, fn: () => void): void,
only(name: JestTestName, fn: () => void): void,
/**
* Skip running this describe block
*/
skip(name: string, fn: () => void): void
skip(name: JestTestName, fn: () => void): void
};
/** An individual test unit */
@ -480,54 +888,54 @@ declare var it: {
/**
* An individual test unit
*
* @param {string} Name of Test
* @param {JestTestName} Name of Test
* @param {Function} Test
* @param {number} Timeout for the test, in milliseconds.
*/
(
name: string,
name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
): void,
/**
* Only run this test
*
* @param {string} Name of Test
* @param {JestTestName} Name of Test
* @param {Function} Test
* @param {number} Timeout for the test, in milliseconds.
*/
only(
name: string,
name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
): void,
/**
* Skip running this test
*
* @param {string} Name of Test
* @param {JestTestName} Name of Test
* @param {Function} Test
* @param {number} Timeout for the test, in milliseconds.
*/
skip(
name: string,
name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
): void,
/**
* Run the test concurrently
*
* @param {string} Name of Test
* @param {JestTestName} Name of Test
* @param {Function} Test
* @param {number} Timeout for the test, in milliseconds.
*/
concurrent(
name: string,
name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
): void
};
declare function fit(
name: string,
name: JestTestName,
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
): void;
@ -542,23 +950,75 @@ declare var xit: typeof it;
/** A disabled individual test */
declare var xtest: typeof it;
type JestPrettyFormatColors = {
comment: { close: string, open: string },
content: { close: string, open: string },
prop: { close: string, open: string },
tag: { close: string, open: string },
value: { close: string, open: string },
};
type JestPrettyFormatIndent = string => string;
type JestPrettyFormatRefs = Array<any>;
type JestPrettyFormatPrint = any => string;
type JestPrettyFormatStringOrNull = string | null;
type JestPrettyFormatOptions = {|
callToJSON: boolean,
edgeSpacing: string,
escapeRegex: boolean,
highlight: boolean,
indent: number,
maxDepth: number,
min: boolean,
plugins: JestPrettyFormatPlugins,
printFunctionName: boolean,
spacing: string,
theme: {|
comment: string,
content: string,
prop: string,
tag: string,
value: string,
|},
|};
type JestPrettyFormatPlugin = {
print: (
val: any,
serialize: JestPrettyFormatPrint,
indent: JestPrettyFormatIndent,
opts: JestPrettyFormatOptions,
colors: JestPrettyFormatColors,
) => string,
test: any => boolean,
};
type JestPrettyFormatPlugins = Array<JestPrettyFormatPlugin>;
/** The expect function is used every time you want to test a value */
declare var expect: {
/** The object that you want to make assertions against */
(value: any): JestExpectType & JestPromiseType & EnzymeMatchersType,
(value: any): JestExpectType & JestPromiseType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestExtendedMatchersType,
/** Add additional Jasmine matchers to Jest's roster */
extend(matchers: { [name: string]: JestMatcher }): void,
/** Add a module that formats application-specific data structures. */
addSnapshotSerializer(serializer: (input: Object) => string): void,
addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void,
assertions(expectedAssertions: number): void,
hasAssertions(): void,
any(value: mixed): JestAsymmetricEqualityType,
anything(): void,
arrayContaining(value: Array<mixed>): void,
objectContaining(value: Object): void,
anything(): any,
arrayContaining(value: Array<mixed>): Array<mixed>,
objectContaining(value: Object): Object,
/** Matches any received string that contains the exact expected string. */
stringContaining(value: string): void,
stringMatching(value: string | RegExp): void
stringContaining(value: string): string,
stringMatching(value: string | RegExp): string,
not: {
arrayContaining: (value: $ReadOnlyArray<mixed>) => Array<mixed>,
objectContaining: (value: {}) => Object,
stringContaining: (value: string) => string,
stringMatching: (value: string | RegExp) => string,
},
};
// TODO handle return type
@ -575,14 +1035,14 @@ declare var jest: JestObjectType;
declare var jasmine: {
DEFAULT_TIMEOUT_INTERVAL: number,
any(value: mixed): JestAsymmetricEqualityType,
anything(): void,
arrayContaining(value: Array<mixed>): void,
anything(): any,
arrayContaining(value: Array<mixed>): Array<mixed>,
clock(): JestClockType,
createSpy(name: string): JestSpyType,
createSpyObj(
baseName: string,
methodNames: Array<string>
): { [methodName: string]: JestSpyType },
objectContaining(value: Object): void,
stringMatching(value: string): void
objectContaining(value: Object): Object,
stringMatching(value: string): string
};

@ -52,11 +52,11 @@
"devDependencies": {
"@commitlint/cli": "6.1.3",
"@commitlint/config-conventional": "6.1.3",
"@verdaccio/types": "3.0.0",
"@verdaccio/types": "3.0.1",
"babel-cli": "6.26.0",
"babel-core": "6.26.0",
"babel-eslint": "8.2.2",
"babel-jest": "22.4.3",
"babel-jest": "23.2.0",
"babel-loader": "7.1.4",
"babel-plugin-flow-runtime": "0.17.0",
"babel-plugin-syntax-dynamic-import": "6.18.0",
@ -82,14 +82,14 @@
"element-theme-default": "1.4.13",
"enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1",
"eslint": "4.18.2",
"eslint": "5.0.1",
"eslint-config-google": "0.9.1",
"eslint-loader": "2.0.0",
"eslint-plugin-babel": "4.1.2",
"eslint-plugin-flowtype": "2.46.1",
"eslint-plugin-import": "2.9.0",
"eslint-plugin-jest": "21.14.0",
"eslint-plugin-react": "7.7.0",
"eslint-plugin-flowtype": "2.49.3",
"eslint-plugin-import": "2.13.0",
"eslint-plugin-jest": "21.17.0",
"eslint-plugin-react": "7.10.0",
"file-loader": "1.1.11",
"flow-bin": "0.69.0",
"flow-runtime": "0.17.0",
@ -99,10 +99,10 @@
"husky": "0.15.0-rc.8",
"identity-obj-proxy": "3.0.0",
"in-publish": "2.0.0",
"jest": "22.4.3",
"jest-environment-jsdom": "22.4.3",
"jest-environment-jsdom-global": "1.0.3",
"jest-environment-node": "22.4.3",
"jest": "23.2.0",
"jest-environment-jsdom": "23.2.0",
"jest-environment-jsdom-global": "1.1.0",
"jest-environment-node": "23.2.0",
"localstorage-memory": "1.0.2",
"mini-css-extract-plugin": "0.4.0",
"node-mocks-http": "1.6.7",
@ -154,10 +154,10 @@
"flow": "flow",
"pretest": "npm run code:build",
"test": "npm run test:unit",
"test:unit": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC jest --config ./jest.config.unit.js --maxWorkers 2",
"test:unit": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC jest --config ./jest.config.js --maxWorkers 2",
"test:functional": "cross-env NODE_ENV=testOldEnv jest --config ./test/jest.config.functional.js --testPathPattern ./test/functional/index*",
"test:e2e": "cross-env BABEL_ENV=testOldEnv jest --config ./test/jest.config.e2e.js",
"test:all": "npm run test && npm run test:functional && npm run test:e2e",
"test:all": "npm run build:webui && npm run test && npm run test:functional && npm run test:e2e",
"pre:ci": "npm run lint && npm run build:webui",
"commitmsg": "commitlint -e $GIT_PARAMS",
"coverage:publish": "codecov",

@ -15,13 +15,13 @@ import type {$ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler,
import type {Config as IConfig} from '@verdaccio/types';
import {ErrorCode} from '../lib/utils';
import {API_ERROR, HTTP_STATUS} from '../lib/constants';
import AppConfig from '../lib/config';
const LoggerApp = require('../lib/logger');
const Config = require('../lib/config');
const Middleware = require('./middleware');
const Cats = require('../lib/status-cats');
const defineAPI = function(config: Config, storage: IStorageHandler) {
const defineAPI = function(config: IConfig, storage: IStorageHandler) {
const auth: IAuth = new Auth(config);
const app: $Application = express();
// run in production mode by default, just in case
@ -103,7 +103,7 @@ const defineAPI = function(config: Config, storage: IStorageHandler) {
export default async function(configHash: any) {
LoggerApp.setup(configHash.logs);
const config: IConfig = new Config(configHash);
const config: IConfig = new AppConfig(configHash);
const storage: IStorageHandler = new Storage(config);
// waits until init calls have been intialized
await storage.init(config);

34
src/lib/auth-utils.js Normal file

@ -0,0 +1,34 @@
import {ErrorCode} from './utils';
import {API_ERROR} from './constants';
export function allow_action(action) {
return function(user, pkg, callback) {
const {name, groups} = user;
const hasPermission = pkg[action].some((group) => name === group || groups.includes(group));
if (hasPermission) {
return callback(null, true);
}
if (name) {
callback(ErrorCode.getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`));
} else {
callback(ErrorCode.getForbidden(`unregistered users are not allowed to ${action} package ${pkg.name}`));
}
};
}
export function getDefaultPlugins() {
return {
authenticate(user, password, cb) {
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
},
add_user(user, password, cb) {
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
},
allow_access: allow_action('access'),
allow_publish: allow_action('publish'),
};
}

@ -2,20 +2,20 @@
import _ from 'lodash';
import {loadPlugin} from '../lib/plugin-loader';
import {ErrorCode} from './utils';
import {buildBase64Buffer, ErrorCode} from './utils';
import {aesDecrypt, aesEncrypt, signPayload, verifyPayload} from './crypto-utils';
import type {Config, Logger, Callback} from '@verdaccio/types';
import type {$Response, NextFunction} from 'express';
import type {$RequestExtend, JWTPayload} from '../../types';
import {ROLES} from './constants';
import {API_ERROR, HTTP_STATUS, ROLES, TOKEN_BASIC, TOKEN_BEARER} from './constants';
import {getMatchedPackagesSpec} from './config-utils';
import type {IAuth} from '../../types';
import {getDefaultPlugins} from './auth-utils';
const LoggerApi = require('./logger');
/**
* Handles the authentification, load auth plugins.
*/
class Auth {
class Auth implements IAuth {
config: Config;
logger: Logger;
secret: string;
@ -31,48 +31,20 @@ class Auth {
}
_loadPlugin(config: Config) {
const plugin_params = {
const pluginOptions = {
config,
logger: this.logger,
};
return loadPlugin(config, config.auth, plugin_params, function(p) {
return p.authenticate || p.allow_access || p.allow_publish;
return loadPlugin(config, config.auth, pluginOptions, (plugin) => {
const {authenticate, allow_access, allow_publish} = plugin;
return authenticate || allow_access || allow_publish;
});
}
_applyDefaultPlugins() {
const allow_action = function(action) {
return function(user, pkg, cb) {
const ok = pkg[action].reduce(function(prev, curr) {
if (user.name === curr || user.groups.indexOf(curr) !== -1) return true;
return prev;
}, false);
if (ok) {
return cb(null, true);
}
if (user.name) {
cb(ErrorCode.getForbidden(`user ${user.name} is not allowed to ${action} package ${pkg.name}`));
} else {
cb(ErrorCode.getForbidden(`unregistered users are not allowed to ${action} package ${pkg.name}`));
}
};
};
this.plugins.push({
authenticate: function(user, password, cb) {
cb(ErrorCode.getForbidden('bad username/password, access denied'));
},
add_user: function(user, password, cb) {
return cb(ErrorCode.getConflict('bad username/password, access denied'));
},
allow_access: allow_action('access'),
allow_publish: allow_action('publish'),
});
this.plugins.push(getDefaultPlugins());
}
authenticate(user: string, password: string, cb: Callback) {
@ -80,7 +52,7 @@ class Auth {
(function next() {
const plugin = plugins.shift();
if (typeof(plugin.authenticate) !== 'function') {
if (_.isFunction(plugin.authenticate) === false) {
return next();
}
@ -98,12 +70,12 @@ class Auth {
// Info: Cannot use `== false to check falsey values`
if (!!groups && groups.length !== 0) {
// TODO: create a better understanding of expectations
if (typeof groups === 'string') {
if (_.isString(groups)) {
throw new TypeError('invalid type for function');
}
const isGroupValid: boolean = _.isArray(groups);
if (!isGroupValid) {
throw new TypeError('user groups is different than an array');
throw new TypeError(API_ERROR.BAD_FORMAT_USER_GROUP);
}
return cb(err, authenticatedUser(user, groups));
@ -115,19 +87,19 @@ class Auth {
add_user(user: string, password: string, cb: Callback) {
let self = this;
let plugins = this.plugins.slice(0)
let plugins = this.plugins.slice(0);
;(function next() {
let p = plugins.shift();
let n = 'adduser';
if (typeof(p[n]) !== 'function') {
n = 'add_user';
(function next() {
let plugin = plugins.shift();
let method = 'adduser';
if (_.isFunction(plugin[method]) === false) {
method = 'add_user';
}
if (typeof(p[n]) !== 'function') {
if (_.isFunction[method] === false) {
next();
} else {
// p.add_user() execution
p[n](user, password, function(err, ok) {
plugin[method](user, password, function(err, ok) {
if (err) {
return cb(err);
}
@ -146,7 +118,7 @@ class Auth {
allow_access(packageName: string, user: string, callback: Callback) {
let plugins = this.plugins.slice(0);
// $FlowFixMe
let pkg = Object.assign({name: packageName}, this.config.getMatchedPackagesSpec(packageName));
let pkg = Object.assign({name: packageName}, getMatchedPackagesSpec(packageName, this.config.packages));
(function next() {
const plugin = plugins.shift();
@ -175,7 +147,7 @@ class Auth {
allow_publish(packageName: string, user: string, callback: Callback) {
let plugins = this.plugins.slice(0);
// $FlowFixMe
let pkg = Object.assign({name: packageName}, this.config.getMatchedPackagesSpec(packageName));
let pkg = Object.assign({name: packageName}, getMatchedPackagesSpec(packageName, this.config.packages));
(function next() {
const plugin = plugins.shift();
@ -184,7 +156,7 @@ class Auth {
return next();
}
plugin.allow_publish(user, pkg, function(err, ok) {
plugin.allow_publish(user, pkg, (err, ok) => {
if (err) {
return callback(err);
}
@ -219,13 +191,13 @@ class Auth {
req.remote_user = buildAnonymousUser();
const authorization = req.headers.authorization;
if (authorization == null) {
if (_.isNil(authorization)) {
return next();
}
const parts = authorization.split(' ');
if (parts.length !== 2) {
return next( ErrorCode.getBadRequest('bad authorization header') );
return next( ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER) );
}
const credentials = this._parseCredentials(parts);
@ -257,12 +229,12 @@ class Auth {
_parseCredentials(parts: Array<string>) {
let credentials;
const scheme = parts[0];
if (scheme.toUpperCase() === 'BASIC') {
credentials = new Buffer(parts[1], 'base64').toString();
this.logger.info('basic authentication is deprecated, please use JWT instead');
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
credentials = buildBase64Buffer(parts[1]).toString();
this.logger.info(API_ERROR.DEPRECATED_BASIC_HEADER);
return credentials;
} else if (scheme.toUpperCase() === 'BEARER') {
const token = new Buffer(parts[1], 'base64');
} else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
const token = buildBase64Buffer(parts[1]);
credentials = aesDecrypt(token, this.secret).toString('utf8');
return credentials;
@ -276,17 +248,17 @@ class Auth {
*/
webUIJWTmiddleware() {
return (req: $RequestExtend, res: $Response, _next: NextFunction) => {
if (req.remote_user !== null && req.remote_user.name !== undefined) {
if (_.isNull(req.remote_user) === false && _.isNil(req.remote_user.name) === false) {
return _next();
}
req.pause();
const next = function(_err) {
const next = () => {
req.resume();
return _next();
};
const token = (req.headers.authorization || '').replace('Bearer ', '');
const token = (req.headers.authorization || '').replace(`${TOKEN_BEARER} `, '');
if (!token) {
return next();
}
@ -328,7 +300,7 @@ class Auth {
try {
decoded = verifyPayload(token, this.secret);
} catch (err) {
throw ErrorCode.getCode(401, err.message);
throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, err.message);
}
return decoded;
@ -350,7 +322,7 @@ function buildAnonymousUser() {
return {
name: undefined,
// groups without '$' are going to be deprecated eventually
groups: ['$all', '$anonymous', '@all', '@anonymous'],
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONUMOUS],
real_groups: [],
};
}
@ -361,7 +333,12 @@ function buildAnonymousUser() {
*/
function authenticatedUser(name: string, pluginGroups: Array<any>) {
const isGroupValid: boolean = _.isArray(pluginGroups);
const groups = (isGroupValid ? pluginGroups : []).concat([ROLES.$ALL, ROLES.$AUTH, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_AUTH, ROLES.ALL]);
const groups = (isGroupValid ? pluginGroups : []).concat([
ROLES.$ALL,
ROLES.$AUTH,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_AUTH,
ROLES.ALL]);
return {
name,

@ -64,9 +64,12 @@ export function getListListenAddresses(argListen: string, configListen: mixed) {
* @param {String} pkgVersion
* @param {String} pkgName
*/
function startVerdaccio(config: any, cliListen: string,
configPath: string, pkgVersion: string,
pkgName: string, callback: Callback) {
function startVerdaccio(config: any,
cliListen: string,
configPath: string,
pkgVersion: string,
pkgName: string,
callback: Callback) {
if (isObject(config) === false) {
throw new Error('config file must be an object');
}

128
src/lib/config-utils.js Normal file

@ -0,0 +1,128 @@
// @flow
import _ from 'lodash';
import assert from 'assert';
import minimatch from 'minimatch';
import {ErrorCode} from './utils';
import type {PackageList, UpLinksConfList} from '@verdaccio/types';
import type {MatchedPackage} from '../../types';
const BLACKLIST = {
all: true,
anonymous: true,
undefined: true,
owner: true,
none: true,
};
/**
* Normalise user list.
* @return {Array}
*/
export function normalizeUserlist(oldFormat: any, newFormat: any) {
const result = [];
/* eslint prefer-rest-params: "off" */
for (let i=0; i < arguments.length; i++) {
if (arguments[i] == null) {
continue;
}
// if it's a string, split it to array
if (_.isString(arguments[i])) {
result.push(arguments[i].split(/\s+/));
} else if (Array.isArray(arguments[i])) {
result.push(arguments[i]);
} else {
throw ErrorCode.getInternalError('CONFIG: bad package acl (array or string expected): ' + JSON.stringify(arguments[i]));
}
}
return _.flatten(result);
}
export function uplinkSanityCheck(uplinks: UpLinksConfList, users: any = BLACKLIST) {
const newUplinks = _.clone(uplinks);
let newUsers = _.clone(users);
for (let uplink in newUplinks) {
if (Object.prototype.hasOwnProperty.call(newUplinks, uplink)) {
if (_.isNil(newUplinks[uplink].cache)) {
newUplinks[uplink].cache = true;
}
newUsers = sanityCheckNames(uplink, newUsers);
}
}
return newUplinks;
}
export function sanityCheckNames(item: string, users: any) {
assert(item !== 'all' && item !== 'owner' && item !== 'anonymous' && item !== 'undefined' && item !== 'none', 'CONFIG: reserved uplink name: ' + item);
assert(!item.match(/\s/), 'CONFIG: invalid uplink name: ' + item);
assert(_.isNil(users[item]), 'CONFIG: duplicate uplink name: ' + item);
users[item] = true;
return users;
}
export function sanityCheckUplinksProps(configUpLinks: any) {
const uplinks = _.clone(configUpLinks);
for (let uplink in uplinks) {
if (Object.prototype.hasOwnProperty.call(uplinks, uplink)) {
assert(uplinks[uplink].url, 'CONFIG: no url for uplink: ' + uplink);
assert( _.isString(uplinks[uplink].url), 'CONFIG: wrong url format for uplink: ' + uplink);
uplinks[uplink].url = uplinks[uplink].url.replace(/\/$/, '');
}
}
return uplinks;
}
/**
* Check whether an uplink can proxy
*/
export function hasProxyTo(pkg: string, upLink: string, packages: PackageList): boolean {
const matchedPkg: MatchedPackage = (getMatchedPackagesSpec(pkg, packages): MatchedPackage);
const proxyList = typeof matchedPkg !== 'undefined' ? matchedPkg.proxy : [];
if (proxyList) {
return proxyList.some((curr) => upLink === curr);
}
return false;
}
export function getMatchedPackagesSpec(pkg: string, packages: PackageList): MatchedPackage {
for (let i in packages) {
// $FlowFixMe
if (minimatch.makeRe(i).exec(pkg)) {
return packages[i];
}
}
return;
}
export function normalisePackageAccess(packages: PackageList): PackageList {
const normalizedPkgs: PackageList = {...packages};
// add a default rule for all packages to make writing plugins easier
if (_.isNil(normalizedPkgs['**'])) {
normalizedPkgs['**'] = {};
}
for (let pkg in packages) {
if (Object.prototype.hasOwnProperty.call(packages, pkg)) {
assert(_.isObject(packages[pkg]) && _.isArray(packages[pkg]) === false,
`CONFIG: bad "'${pkg}'" package description (object expected)`);
normalizedPkgs[pkg].access = normalizeUserlist(packages[pkg].allow_access, packages[pkg].access);
delete normalizedPkgs[pkg].allow_access;
normalizedPkgs[pkg].publish = normalizeUserlist(packages[pkg].allow_publish, packages[pkg].publish);
delete normalizedPkgs[pkg].allow_publish;
normalizedPkgs[pkg].proxy = normalizeUserlist(packages[pkg].proxy_access, packages[pkg].proxy);
delete normalizedPkgs[pkg].proxy_access;
}
}
return normalizedPkgs;
}

@ -1,210 +1,107 @@
// @flow
import _ from 'lodash';
import assert from 'assert';
import {generateRandomHexString} from './crypto-utils';
import {
getMatchedPackagesSpec,
normalisePackageAccess,
sanityCheckUplinksProps,
uplinkSanityCheck} from './config-utils';
import {getUserAgent, isObject} from './utils';
import {APP_ERROR} from './constants';
const assert = require('assert');
const _ = require('lodash');
const Error = require('http-errors');
const minimatch = require('minimatch');
import type {
PackageList,
Config as AppConfig,
Logger,
} from '@verdaccio/types';
const Utils = require('./utils');
const pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-vars
const pkgVersion = module.exports.version;
const pkgName = module.exports.name;
const strategicConfigProps = ['users', 'uplinks', 'packages'];
import type {MatchedPackage, StartUpConfig} from '../../types';
const LoggerApi = require('./logger');
const strategicConfigProps = ['uplinks', 'packages'];
const allowedEnvConfig = ['http_proxy', 'https_proxy', 'no_proxy'];
/**
* [[a, [b, c]], d] -> [a, b, c, d]
* @param {*} array
* @return {Array}
*/
function flatten(array) {
let result = [];
for (let i=0; i < array.length; i++) {
if (Array.isArray(array[i])) {
/* eslint prefer-spread: "off" */
result.push.apply(result, flatten(array[i]));
} else {
result.push(array[i]);
}
}
return result;
}
function checkUserOrUplink(item, users) {
assert(item !== 'all' && item !== 'owner'
&& item !== 'anonymous' && item !== 'undefined' && item !== 'none', 'CONFIG: reserved user/uplink name: ' + item);
assert(!item.match(/\s/), 'CONFIG: invalid user name: ' + item);
assert(users[item] == null, 'CONFIG: duplicate user/uplink name: ' + item);
users[item] = true;
}
/**
* Normalise user list.
* @return {Array}
*/
function normalizeUserlist() {
let result = [];
/* eslint prefer-rest-params: "off" */
for (let i=0; i < arguments.length; i++) {
if (arguments[i] == null) {
continue;
}
// if it's a string, split it to array
if (typeof(arguments[i]) === 'string') {
result.push(arguments[i].split(/\s+/));
} else if (Array.isArray(arguments[i])) {
result.push(arguments[i]);
} else {
throw Error('CONFIG: bad package acl (array or string expected): ' + JSON.stringify(arguments[i]));
}
}
return flatten(result);
}
/**
* Coordinates the application configuration
*/
class Config {
/**
* @param {*} config config the content
*/
constructor(config) {
class Config implements AppConfig {
logger: Logger;
user_agent: string;
secret: string;
uplinks: any;
packages: PackageList;
users: any;
server_id: string;
self_path: string;
storage: string | void;
$key: any;
$value: any;
constructor(config: StartUpConfig) {
const self = this;
const users = {
all: true,
anonymous: true,
undefined: true,
owner: true,
none: true,
};
this.logger = LoggerApi.logger;
this.self_path = config.self_path;
this.storage = config.storage;
for (let configProp in config) {
if (self[configProp] == null) {
self[configProp] = config[configProp];
}
}
if (!self.user_agent) {
self.user_agent = `${pkgName}/${pkgVersion}`;
if (_.isNil(this.user_agent)) {
this.user_agent = getUserAgent();
}
// some weird shell scripts are valid yaml files parsed as string
assert.equal(typeof(config), 'object', 'CONFIG: it doesn\'t look like a valid config file');
assert(_.isObject(config), APP_ERROR.CONFIG_NOT_VALID);
// sanity check for strategic config properties
strategicConfigProps.forEach(function(x) {
if (self[x] == null) self[x] = {};
assert(Utils.isObject(self[x]), `CONFIG: bad "${x}" value (object expected)`);
if (self[x] == null) {
self[x] = {};
}
assert(isObject(self[x]), `CONFIG: bad "${x}" value (object expected)`);
});
// sanity check for users
for (let i in self.users) {
if (Object.prototype.hasOwnProperty.call(self.users, i)) {
checkUserOrUplink(i, users);
}
}
// sanity check for uplinks
/* eslint guard-for-in: 0 */
for (let i in self.uplinks) {
if (self.uplinks[i].cache == null) {
self.uplinks[i].cache = true;
}
if (Object.prototype.hasOwnProperty.call(self.uplinks, i)) {
checkUserOrUplink(i, users);
}
this.uplinks = sanityCheckUplinksProps(uplinkSanityCheck(this.uplinks));
if (_.isNil(this.users) === false) {
this.logger.warn(`[users]: property on configuration file
is not longer supported, property being ignored`);
}
for (let user in self.users) {
if (Object.prototype.hasOwnProperty.call(self.users, user)) {
assert(self.users[user].password, 'CONFIG: no password for user: ' + user);
assert(typeof(self.users[user].password) === 'string' &&
self.users[user].password.match(/^[a-f0-9]{40}$/)
, 'CONFIG: wrong password format for user: ' + user + ', sha1 expected');
}
}
for (let uplink in self.uplinks) {
if (Object.prototype.hasOwnProperty.call(self.uplinks, uplink)) {
assert(self.uplinks[uplink].url, 'CONFIG: no url for uplink: ' + uplink);
assert( typeof(self.uplinks[uplink].url) === 'string'
, 'CONFIG: wrong url format for uplink: ' + uplink);
self.uplinks[uplink].url = self.uplinks[uplink].url.replace(/\/$/, '');
}
}
// add a default rule for all packages to make writing plugins easier
if (self.packages['**'] == null) {
self.packages['**'] = {};
}
for (let pkg in self.packages) {
if (Object.prototype.hasOwnProperty.call(self.packages, pkg)) {
assert(
typeof(self.packages[pkg]) === 'object' &&
!Array.isArray(self.packages[pkg])
, 'CONFIG: bad "'+pkg+'" package description (object expected)');
self.packages[pkg].access = normalizeUserlist(self.packages[pkg].allow_access, self.packages[pkg].access);
delete self.packages[pkg].allow_access;
self.packages[pkg].publish = normalizeUserlist(self.packages[pkg].allow_publish, self.packages[pkg].publish);
delete self.packages[pkg].allow_publish;
self.packages[pkg].proxy = normalizeUserlist(self.packages[pkg].proxy_access, self.packages[pkg].proxy);
delete self.packages[pkg].proxy_access;
}
}
this.packages = normalisePackageAccess(self.packages);
// loading these from ENV if aren't in config
allowedEnvConfig.forEach((function(v) {
if (!(v in self)) {
self[v] = process.env[v] || process.env[v.toUpperCase()];
allowedEnvConfig.forEach((envConf) => {
if (!(envConf in self)) {
self[envConf] = process.env[envConf] || process.env[envConf.toUpperCase()];
}
}));
});
// unique identifier of self server (or a cluster), used to avoid loops
if (!self.server_id) {
self.server_id = generateRandomHexString(6);
if (!this.server_id) {
this.server_id = generateRandomHexString(6);
}
}
/**
* Check whether an uplink can proxy
* @param {String} pkg package anem
* @param {*} upLink
* @return {Boolean}
*/
hasProxyTo(pkg, upLink) {
return (this.getMatchedPackagesSpec(pkg).proxy || []).reduce(function(prev, curr) {
if (upLink === curr) {
return true;
}
return prev;
}, false);
}
/**
* Check for package spec
* @param {String} pkg package name
* @return {Object}
*/
getMatchedPackagesSpec(pkg) {
for (let i in this.packages) {
if (minimatch.makeRe(i).exec(pkg)) {
return this.packages[i];
}
}
return {};
getMatchedPackagesSpec(pkg: string): MatchedPackage {
return getMatchedPackagesSpec(pkg, this.packages);
}
/**
* Store or create whether recieve a secret key
* @param {String} secret
* @return {String}
*/
checkSecretKey(secret) {
if (_.isString(secret) && secret !== '') {
checkSecretKey(secret: string): string {
if (_.isString(secret) && _.isEmpty(secret) === false) {
this.secret = secret;
return secret;
}
@ -215,4 +112,4 @@ class Config {
}
}
module.exports = Config;
export default Config;

@ -29,10 +29,12 @@ export const DEFAULT_UPLINK = 'npmjs';
export const ROLES = {
$ALL: '$all',
ALL: 'all',
$AUTH: '$authenticated',
$ANONYMOUS: '$anonymous',
DEPRECATED_ALL: '@all',
DEPRECATED_AUTH: '@authenticated',
ALL: 'all',
DEPRECATED_ANONUMOUS: '@anonymous',
};
export const HTTP_STATUS = {
@ -65,21 +67,37 @@ export const API_MESSAGE = {
};
export const API_ERROR = {
BAD_USERNAME_PASSWORD: 'bad username/password, access denied {APP}',
NO_PACKAGE: 'no such package available',
NOT_ALLOWED: 'not allowed to access package',
INTERNAL_SERVER_ERROR: 'internal server error',
UNKNOWN_ERROR: 'unknown error',
NOT_PACKAGE_UPLINK: 'package does not exist on uplink',
UPLINK_OFFLINE_PUBLISH: 'one of the uplinks is down, refuse to publish',
UPLINK_OFFLINE: 'uplink is offline',
CONTENT_MISMATCH: 'content length mismatch',
NOT_FILE_UPLINK: 'file doesn\'t exist on uplink',
MAX_USERS_REACHED: 'maximum amount of users reached',
VERSION_NOT_EXIST: 'this version doesn\'t exist',
FILE_NOT_FOUND: 'File not found',
BAD_STATUS_CODE: 'bad status code',
PACKAGE_EXIST: 'this package is already present',
BAD_AUTH_HEADER: 'bad authorization header',
WEB_DISABLED: 'Web interface is disabled in the config file',
DEPRECATED_BASIC_HEADER: 'basic authentication is deprecated, please use JWT instead',
BAD_FORMAT_USER_GROUP: 'user groups is different than an array',
};
export const APP_ERROR = {
CONFIG_NOT_VALID: 'CONFIG: it does not look like a valid config file',
};
export const DEFAULT_NO_README = 'ERROR: No README data found!';
export const WEB_TITLE = 'Verdaccio';
export const PACKAGE_ACCESS = {
SCOPE: '@*/*',
ALL: '**',
};

@ -7,6 +7,7 @@ import {generateRandomHexString} from '../lib/crypto-utils';
import type {Package, Version} from '@verdaccio/types';
import type {IStorage} from '../../types';
import {API_ERROR, HTTP_STATUS} from './constants';
const pkgFileName = 'package.json';
const fileExist: string = 'EEXISTS';
@ -123,11 +124,11 @@ export function cleanUpLinksRef(keepUpLinkData: boolean, result: Package): Packa
export function checkPackageLocal(name: string, localStorage: IStorage): Promise<any> {
return new Promise((resolve, reject) => {
localStorage.getPackageMetadata(name, (err, results) => {
if (!_.isNil(err) && err.status !== 404) {
if (!_.isNil(err) && err.status !== HTTP_STATUS.NOT_FOUND) {
return reject(err);
}
if (results) {
return reject(ErrorCode.getConflict('this package is already present'));
return reject(ErrorCode.getConflict(API_ERROR.PACKAGE_EXIST));
}
return resolve();
});
@ -152,25 +153,25 @@ export function checkPackageRemote(name: string, isAllowPublishOffline: boolean,
// $FlowFixMe
syncMetadata(name, null, {}, (err, packageJsonLocal, upLinksErrors) => {
// something weird
if (err && err.status !== 404) {
if (err && err.status !== HTTP_STATUS.NOT_FOUND) {
return reject(err);
}
// checking package exist already
if (_.isNil(packageJsonLocal) === false) {
return reject(ErrorCode.getConflict('this package is already present'));
return reject(ErrorCode.getConflict(API_ERROR.PACKAGE_EXIST));
}
for (let errorItem = 0; errorItem < upLinksErrors.length; errorItem++) {
// checking error
// if uplink fails with a status other than 404, we report failure
if (_.isNil(upLinksErrors[errorItem][0]) === false) {
if (upLinksErrors[errorItem][0].status !== 404) {
if (upLinksErrors[errorItem][0].status !== HTTP_STATUS.NOT_FOUND) {
if (isAllowPublishOffline) {
return resolve();
}
return reject(ErrorCode.getServiceUnavailable('one of the uplinks is down, refuse to publish'));
return reject(ErrorCode.getServiceUnavailable(API_ERROR.UPLINK_OFFLINE_PUBLISH));
}
}
}

@ -26,6 +26,7 @@ Callback,
Logger,
} from '@verdaccio/types';
import type {IReadTarball, IUploadTarball} from '@verdaccio/streams';
import {hasProxyTo} from './config-utils';
const LoggerApi = require('../lib/logger');
@ -412,9 +413,9 @@ class Storage implements IStorageHandler {
packageInfo = generatePackageTemplate(name);
}
for (let up in this.uplinks) {
if (this.config.hasProxyTo(name, up)) {
upLinks.push(this.uplinks[up]);
for (let uplink in this.uplinks) {
if (hasProxyTo(name, uplink, this.config.packages)) {
upLinks.push(this.uplinks[uplink]);
}
}

@ -105,10 +105,10 @@ class ProxyStorage implements IProxy {
process.nextTick(function() {
if (cb) {
cb(ErrorCode.getInternalError('uplink is offline'));
cb(ErrorCode.getInternalError(API_ERROR.UPLINK_OFFLINE));
}
// $FlowFixMe
streamRead.emit('error', ErrorCode.getInternalError('uplink is offline'));
streamRead.emit('error', ErrorCode.getInternalError(API_ERROR.UPLINK_OFFLINE));
});
// $FlowFixMe
streamRead._read = function() {};

@ -17,9 +17,22 @@ import type {$Request} from 'express';
import type {StringValue} from '../../types';
const Logger = require('./logger');
const pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-vars
const pkgVersion = module.exports.version;
const pkgName = module.exports.name;
export const DIST_TAGS = 'dist-tags';
export function getUserAgent(): string {
assert(_.isString(pkgName));
assert(_.isString(pkgVersion));
return `${pkgName}/${pkgVersion}`;
}
export function buildBase64Buffer(payload: string): Buffer {
return new Buffer(payload, 'base64');
}
/**
* Validate a package.
* @return {Boolean} whether the package is valid or not

@ -1,9 +1,5 @@
storage: ./.verdaccio_test_env/test-storage
users:
test:
password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
uplinks:
npmjs:
url: https://registry.npmjs.org/

@ -4,17 +4,21 @@ import path from 'path';
import rimraf from 'rimraf';
import {HEADERS} from '../../../src/lib/constants';
import configDefault from '../partials/config/access';
import configDefault from '../partials/config/config_access';
import Config from '../../../src/lib/config';
import endPointAPI from '../../../src/api/index';
import {mockServer} from './mock';
import {DOMAIN_SERVERS} from '../../functional/config.functional';
require('../../../src/lib/logger').setup([]);
describe('api with no limited access configuration', () => {
let config;
let app;
let mockRegistry;
beforeAll(function(done) {
const mockServerPort = 55530;
const store = path.join(__dirname, './partials/store/access-storage');
rimraf(store, async () => {
const configForTest = _.clone(configDefault);
@ -24,8 +28,14 @@ describe('api with no limited access configuration', () => {
}
};
configForTest.self_path = store;
configForTest.uplinks = {
npmjs: {
url: `http://${DOMAIN_SERVERS}:${mockServerPort}`
}
};
config = new Config(configForTest);
app = await endPointAPI(config);
mockRegistry = await mockServer(mockServerPort).init();
done();
});
});
@ -34,9 +44,11 @@ describe('api with no limited access configuration', () => {
const store = path.join(__dirname, './partials/store/access-storage');
rimraf(store, (err) => {
if (err) {
mockRegistry[0].stop();
return done(err);
}
mockRegistry[0].stop();
return done();
});
});

@ -29,7 +29,7 @@ describe('endpoint unit test', () => {
const configForTest = _.clone(configDefault);
configForTest.auth = {
htpasswd: {
file: './test-storage/htpasswd-test'
file: './test-storage/.htpasswd'
}
};
configForTest.uplinks = {

@ -0,0 +1,226 @@
// @flow
import path from 'path';
import {spliceURL} from '../../../src/utils/string';
import {parseConfigFile} from '../../../src/lib/utils';
import {
getMatchedPackagesSpec,
hasProxyTo,
normalisePackageAccess, sanityCheckUplinksProps,
uplinkSanityCheck
} from '../../../src/lib/config-utils';
import {PACKAGE_ACCESS, ROLES} from '../../../src/lib/constants';
describe('Config Utilities', () => {
const parsePartial = (name) => {
return path.join(__dirname, `../partials/config/yaml/${name}.yaml`);
};
describe('uplinkSanityCheck', () => {
test('should test basic conversion', ()=> {
const uplinks = uplinkSanityCheck(parseConfigFile(parsePartial('uplink-basic')).uplinks);
expect(Object.keys(uplinks)).toContain('server1');
expect(Object.keys(uplinks)).toContain('server2');
});
test('should throw error on blacklisted uplink name', ()=> {
const {uplinks} = parseConfigFile(parsePartial('uplink-wrong'));
expect(() => {
uplinkSanityCheck(uplinks)
}).toThrow('CONFIG: reserved uplink name: anonymous');
});
});
describe('sanityCheckUplinksProps', () => {
test('should fails if url prop is missing', ()=> {
const {uplinks} = parseConfigFile(parsePartial('uplink-wrong'));
expect(() => {
sanityCheckUplinksProps(uplinks)
}).toThrow('CONFIG: no url for uplink: none-url');
});
test('should bypass an empty uplink list', ()=> {
expect(sanityCheckUplinksProps([])).toHaveLength(0);
});
});
describe('normalisePackageAccess', () => {
test('should test basic conversion', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-basic'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
const all = access[`${PACKAGE_ACCESS.ALL}`];
expect(scoped).toBeDefined();
expect(all).toBeDefined();
});
test('should test multi group', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-multi-group'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
const all = access[`${PACKAGE_ACCESS.ALL}`];
expect(scoped).toBeDefined();
expect(scoped.access).toContain('$all');
expect(scoped.publish).toHaveLength(2);
expect(scoped.publish).toContain('admin');
expect(scoped.publish).toContain('superadmin');
expect(all).toBeDefined();
expect(all.access).toHaveLength(3);
expect(all.access).toContain('$all');
expect(all.publish).toHaveLength(1);
expect(all.publish).toContain('admin');
});
test('should deprecated packages props', ()=> {
const {packages} = parseConfigFile(parsePartial('deprecated-pkgs-basic'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
const all = access[`${PACKAGE_ACCESS.ALL}`];
const react = access['react-*'];
expect(react).toBeDefined();
expect(react.access).toBeDefined();
// $FlowFixMe
expect(react.access[0]).toBe(ROLES.$ALL);
expect(react.publish).toBeDefined();
// $FlowFixMe);
expect(react.publish[0]).toBe('admin');
expect(react.proxy).toBeDefined();
// $FlowFixMe
expect(react.proxy[0]).toBe('uplink2');
expect(react.storage).toBeDefined();
expect(react.storage).toBe('react-storage');
expect(scoped).toBeDefined();
expect(scoped.storage).not.toBeDefined();
expect(all).toBeDefined();
expect(all.access).toBeDefined();
expect(all.storage).not.toBeDefined();
expect(all.publish).toBeDefined();
expect(all.proxy).toBeDefined();
expect(all.allow_access).toBeUndefined();
expect(all.allow_publish).toBeUndefined();
expect(all.proxy_access).toBeUndefined();
});
test('should check not default packages access', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-empty'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
expect(scoped).toBeUndefined();
// ** should be added by default
const all = access[`${PACKAGE_ACCESS.ALL}`];
expect(all).toBeDefined();
expect(all.access).toBeUndefined();
expect(all.publish).toBeUndefined();
});
});
describe('getMatchedPackagesSpec', () => {
test('should test basic config', () => {
const {packages} = parseConfigFile(parsePartial('pkgs-custom'));
// $FlowFixMe
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
// $FlowFixMe
expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google');
// $FlowFixMe
expect(getMatchedPackagesSpec('vue', packages).proxy).toMatch('npmjs');
// $FlowFixMe
expect(getMatchedPackagesSpec('@scope/vue', packages).proxy).toMatch('npmjs');
});
test('should test no ** wildcard on config', () => {
const {packages} = parseConfigFile(parsePartial('pkgs-nosuper-wildcard-custom'));
// $FlowFixMe
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
// $FlowFixMe
expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google');
// $FlowFixMe
expect(getMatchedPackagesSpec('@fake/angular', packages).proxy).toMatch('npmjs');
expect(getMatchedPackagesSpec('vue', packages)).toBeUndefined();
expect(getMatchedPackagesSpec('@scope/vue', packages)).toBeUndefined();
});
});
describe('hasProxyTo', () => {
test('should test basic config', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-basic')).packages);
// react
expect(hasProxyTo('react', 'facebook', packages)).toBeFalsy();
expect(hasProxyTo('react', 'google', packages)).toBeFalsy();
// vue
expect(hasProxyTo('vue', 'google', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'fake', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeTruthy();
// angular
expect(hasProxyTo('angular', 'google', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'facebook', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeTruthy();
});
test('should test resolve based on custom package access', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-custom')).packages);
// react
expect(hasProxyTo('react', 'facebook', packages)).toBeTruthy();
expect(hasProxyTo('react', 'google', packages)).toBeFalsy();
// vue
expect(hasProxyTo('vue', 'google', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'fake', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeTruthy();
// angular
expect(hasProxyTo('angular', 'google', packages)).toBeTruthy();
expect(hasProxyTo('angular', 'facebook', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
});
test('should not resolve any proxy', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-empty')).packages);
// react
expect(hasProxyTo('react', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('react', 'npmjs', packages)).toBeFalsy();
// vue
expect(hasProxyTo('vue', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeFalsy();
// angular
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
// private
expect(hasProxyTo('private', 'fake', packages)).toBeFalsy();
});
});
describe('spliceURL', () => {
test('should splice two strings and generate a url', () => {
const url: string = spliceURL('http://domain.com', '/-/static/logo.png');
expect(url).toMatch('http://domain.com/-/static/logo.png');
});
test('should splice a empty strings and generate a url', () => {
const url: string = spliceURL('', '/-/static/logo.png');
expect(url).toMatch('/-/static/logo.png');
});
});
});

@ -6,6 +6,7 @@ import {parseConfigFile} from '../../../src/lib/utils';
import {DEFAULT_REGISTRY, DEFAULT_UPLINK, ROLES, WEB_TITLE} from '../../../src/lib/constants';
const resolveConf = (conf) => path.join(__dirname, `../../../conf/${conf}.yaml`);
require('../../../src/lib/logger').setup([]);
const checkDefaultUplink = (config) => {
expect(_.isObject(config.uplinks[DEFAULT_UPLINK])).toBeTruthy();

@ -1,10 +1,10 @@
import assert from 'assert';
import Search from '../../../src/lib/search';
import Config from '../../../src/lib/config';
import Storage from '../../../src/lib/storage';
let config_hash = require('../partials/config/index');
let Config = require('../../../src/lib/config');
require('../../../src/lib/logger').setup([]);

@ -8,7 +8,7 @@ import {setup} from '../../../src/lib/logger';
import type {Config, UpLinkConf} from '@verdaccio/types';
import type {IProxy} from '../../../types/index';
import {API_ERROR} from "../../../src/lib/constants";
import {API_ERROR, HTTP_STATUS} from "../../../src/lib/constants";
import {mockServer} from './mock';
import {DOMAIN_SERVERS} from '../../functional/config.functional';
@ -102,8 +102,8 @@ describe('UpStorge', () => {
stream.on('error', function(err) {
expect(err).not.toBeNull();
expect(err.statusCode).toBe(404);
expect(err.message).toMatch(/file doesn't exist on uplink/);
expect(err.statusCode).toBe(HTTP_STATUS.NOT_FOUND);
expect(err.message).toMatch(API_ERROR.NOT_FILE_UPLINK);
done();
});
@ -141,9 +141,9 @@ describe('UpStorge', () => {
const streamThirdTry = proxy.fetchTarball(tarball);
streamThirdTry.on('error', function(err) {
expect(err).not.toBeNull();
expect(err.statusCode).toBe(500);
expect(err.statusCode).toBe(HTTP_STATUS.INTERNAL_ERROR);
expect(proxy.failed_requests).toBe(2);
expect(err.message).toMatch(/uplink is offline/);
expect(err.message).toMatch(API_ERROR.UPLINK_OFFLINE);
done();
});
});

@ -1,11 +1,10 @@
import path from 'path';
import {DEFAULT_REGISTRY} from '../../../../src/lib/constants';
const config = {
storage: path.join(__dirname, '../store/access-storage'),
uplinks: {
'npmjs': {
'url': DEFAULT_REGISTRY
'url': 'http://never_use:0000/'
}
},
packages: {

@ -0,0 +1,14 @@
packages:
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'react-*':
allow_access: $all
publish: admin
proxy_access: uplink2
storage: 'react-storage'
'**':
allow_access: $all
allow_publish: $authenticated
proxy_access: npmjs

@ -0,0 +1,9 @@
packages:
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs

@ -0,0 +1,17 @@
packages:
'react':
access: admin
publish: admin
proxy: facebook
'angular':
access: admin
publish: admin
proxy: google
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs

@ -0,0 +1,4 @@
packages:
'private':
access: admin
publish: admin

@ -0,0 +1,9 @@
packages:
'@*/*':
access: $all
publish: admin superadmin
proxy: npmjs
'**':
access: $all user1 user2
publish: admin
proxy: npmjs

@ -0,0 +1,13 @@
packages:
'react':
access: admin
publish: admin
proxy: facebook
'angular':
access: admin
publish: admin
proxy: google
'@fake/*':
access: $all
publish: $authenticated
proxy: npmjs

@ -0,0 +1,7 @@
uplinks:
server1:
url: http://localhost:55551/
maxage: 0
server2:
url: http://localhost:55551/
maxage: 0

@ -0,0 +1,9 @@
uplinks:
facebook:
url: http://localhost:55551/
maxage: 0
anonymous:
url: http://localhost:55551/
maxage: 0
none-url:
maxage: 0

@ -8,6 +8,7 @@ import type {
MergeTags,
Config,
Logger,
PackageAccess,
Package} from '@verdaccio/types';
import type {
IUploadTarball,
@ -95,6 +96,13 @@ export interface IStorageHandler {
_updateVersionsHiddenUpLink(versions: Versions, upLink: IProxy): void;
}
export type StartUpConfig = {
storage: string;
self_path: string;
}
export type MatchedPackage = PackageAccess | void;
export interface IStorage {
config: Config;
localData: ILocalData;

BIN
yarn.lock

Binary file not shown.