mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-16 21:56:25 -05:00
feat: npm token command support (#1427)
* feat: support for npm token This is an effor of: This commit intent to provide npm token support. https: //github.com/verdaccio/verdaccio/issues/541 https: //github.com/verdaccio/verdaccio/pull/1271 https: //github.com/verdaccio/local-storage/pull/168 Co-Authored-By: Manuel Spigolon <behemoth89@gmail.com> Co-Authored-By: Juan Gabriel Jiménez <juangabreil@gmail.com> * chore: update secrets baselines Co-Authored-By: Liran Tal <liran.tal@gmail.com> * chore: update lock file * chore: add logger mock methods * chore: update @verdaccio/types * refactor: unit test was flacky adapt the pkg access to the new configuration setup * refactor: add plugin methods validation * test: add test for aesEncrypt * chore: update local-storage dependency * chore: add support for experimetns token will be part of the experiment lists * chore: increase timeout * chore: increase timeout threshold * chore: update nock * chore: update dependencies * chore: update eslint config * chore: update dependencies * test: add unit test for npm token * chore: update readme
This commit is contained in:
parent
962d5d529a
commit
dbf20175dc
35 changed files with 708 additions and 193 deletions
2
.babelrc
2
.babelrc
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"presets": [["@verdaccio", {"typescript": true}]]
|
||||
"presets": [["@verdaccio"]]
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": ["warn"],
|
||||
"@typescript-eslint/ban-ts-ignore": ["warn"],
|
||||
"@typescript-eslint/ban-ts-ignore": 0,
|
||||
"@typescript-eslint/no-inferrable-types": ["warn"],
|
||||
"@typescript-eslint/no-empty-function": ["warn"],
|
||||
"@typescript-eslint/no-this-alias": ["warn"],
|
||||
|
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
always-auth = true
|
|
@ -3,7 +3,7 @@
|
|||
"files": null,
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2019-08-25T17:18:29Z",
|
||||
"generated_at": "2019-08-03T08:33:13Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"name": "AWSKeyDetector"
|
||||
|
@ -40,22 +40,22 @@
|
|||
"src/lib/auth-utils.ts": [
|
||||
{
|
||||
"hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8",
|
||||
"line_number": 12,
|
||||
"line_number": 10,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "ecb252044b5ea0f679ee78ec1a12904739e2904d",
|
||||
"line_number": 187,
|
||||
"line_number": 174,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "f35dd4c51c0a89bd055b5ad30c162c778981306d",
|
||||
"line_number": 192,
|
||||
"line_number": 179,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "45c43fe97e3a06ab078b0eeff6fbe622cc417a25",
|
||||
"line_number": 210,
|
||||
"line_number": 197,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
@ -86,12 +86,12 @@
|
|||
"src/lib/constants.ts": [
|
||||
{
|
||||
"hashed_secret": "f34fbc9a9769ba9eff5aff3d008a6b49f85c08b1",
|
||||
"line_number": 15,
|
||||
"line_number": 14,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "b9343f1143ccb83555b450eb54dde96a05522ccc",
|
||||
"line_number": 116,
|
||||
"line_number": 118,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
@ -265,12 +265,12 @@
|
|||
"test/unit/modules/api/api.spec.ts": [
|
||||
{
|
||||
"hashed_secret": "97752a468368b0d6b192140d6a140c38fd0cbd8b",
|
||||
"line_number": 304,
|
||||
"line_number": 293,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "364bdf2ed77a8544d3b711a03b69eeadcc63c9d7",
|
||||
"line_number": 828,
|
||||
"line_number": 802,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
@ -299,12 +299,12 @@
|
|||
"test/unit/modules/auth/jwt.spec.ts": [
|
||||
{
|
||||
"hashed_secret": "364bdf2ed77a8544d3b711a03b69eeadcc63c9d7",
|
||||
"line_number": 118,
|
||||
"line_number": 121,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "eaacdf2d9ed66df2601c8b51ab4084db14336d11",
|
||||
"line_number": 129,
|
||||
"line_number": 132,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -164,7 +164,7 @@ Verdaccio aims to support all features of a standard npm client that make sense
|
|||
- Registering new users (npm adduser {newuser}) - **supported**
|
||||
- Change password (npm profile set password) - **supported**
|
||||
- Transferring ownership (npm owner add {user} {pkg}) - not supported, *PR-welcome*
|
||||
- Token (npm token) - wip [#1271](https://github.com/verdaccio/verdaccio/pull/1271)
|
||||
- Token (npm token) - (more info [#1271](https://github.com/verdaccio/verdaccio/pull/1271)) - **supported**
|
||||
|
||||
### Miscellany
|
||||
|
||||
|
|
|
@ -80,3 +80,6 @@ logs:
|
|||
- { type: stdout, format: pretty, level: http }
|
||||
#- {type: file, path: verdaccio.log, level: info}
|
||||
|
||||
experiments:
|
||||
# support for npm token command
|
||||
token: false
|
||||
|
|
|
@ -79,3 +79,7 @@ middlewares:
|
|||
logs:
|
||||
- { type: stdout, format: pretty, level: http }
|
||||
#- {type: file, path: verdaccio.log, level: info}
|
||||
|
||||
experiments:
|
||||
# support for npm token command
|
||||
token: false
|
||||
|
|
41
package.json
41
package.json
|
@ -16,10 +16,10 @@
|
|||
"verdaccio": "./bin/verdaccio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@verdaccio/commons-api": "8.0.0",
|
||||
"@verdaccio/local-storage": "2.2.1",
|
||||
"@verdaccio/readme": "8.0.0",
|
||||
"@verdaccio/streams": "8.0.0",
|
||||
"@verdaccio/commons-api": "8.1.0",
|
||||
"@verdaccio/local-storage": "8.1.0",
|
||||
"@verdaccio/readme": "8.1.0",
|
||||
"@verdaccio/streams": "8.1.0",
|
||||
"@verdaccio/ui-theme": "0.3.0",
|
||||
"JSONStream": "1.3.5",
|
||||
"async": "3.1.0",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"dayjs": "1.8.15",
|
||||
"envinfo": "7.3.1",
|
||||
"express": "4.17.1",
|
||||
"handlebars": "4.1.2",
|
||||
"handlebars": "4.2.0",
|
||||
"http-errors": "1.7.3",
|
||||
"js-yaml": "3.13.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
|
@ -48,30 +48,30 @@
|
|||
"pkginfo": "0.4.1",
|
||||
"request": "2.87.0",
|
||||
"semver": "6.3.0",
|
||||
"verdaccio-audit": "8.0.0",
|
||||
"verdaccio-htpasswd": "8.0.0"
|
||||
"verdaccio-audit": "8.1.0",
|
||||
"verdaccio-htpasswd": "8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "8.1.0",
|
||||
"@commitlint/config-conventional": "8.1.0",
|
||||
"@octokit/rest": "16.28.7",
|
||||
"@octokit/rest": "16.28.9",
|
||||
"@types/async": "3.0.1",
|
||||
"@types/bunyan": "1.8.6",
|
||||
"@types/express": "4.17.1",
|
||||
"@types/http-errors": "1.6.2",
|
||||
"@types/jest": "24.0.18",
|
||||
"@types/lodash": "4.14.137",
|
||||
"@types/lodash": "4.14.138",
|
||||
"@types/mime": "2.0.1",
|
||||
"@types/minimatch": "3.0.3",
|
||||
"@types/node": "12.7.2",
|
||||
"@types/node": "12.7.4",
|
||||
"@types/request": "2.48.2",
|
||||
"@types/semver": "6.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "2.0.0",
|
||||
"@verdaccio/babel-preset": "0.2.1",
|
||||
"@verdaccio/eslint-config": "0.0.1",
|
||||
"@verdaccio/types": "5.2.2",
|
||||
"@types/semver": "6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "2.1.0",
|
||||
"@verdaccio/babel-preset": "8.1.0",
|
||||
"@verdaccio/eslint-config": "8.1.0",
|
||||
"@verdaccio/types": "8.1.0",
|
||||
"codecov": "3.5.0",
|
||||
"cross-env": "5.2.0",
|
||||
"cross-env": "5.2.1",
|
||||
"detect-secrets": "1.0.4",
|
||||
"eslint": "5.16.0",
|
||||
"get-stdin": "7.0.0",
|
||||
|
@ -80,14 +80,15 @@
|
|||
"jest": "24.9.0",
|
||||
"jest-environment-node": "24.9.0",
|
||||
"lint-staged": "8.2.1",
|
||||
"nock": "10.0.6",
|
||||
"nock": "11.3.3",
|
||||
"prettier": "1.18.2",
|
||||
"puppeteer": "1.8.0",
|
||||
"rimraf": "3.0.0",
|
||||
"standard-version": "7.0.0",
|
||||
"supertest": "4.0.2",
|
||||
"typescript": "3.5.3",
|
||||
"verdaccio-auth-memory": "8.0.0",
|
||||
"verdaccio-memory": "8.0.0"
|
||||
"typescript": "3.6.2",
|
||||
"verdaccio-auth-memory": "8.1.0",
|
||||
"verdaccio-memory": "8.1.0"
|
||||
},
|
||||
"keywords": [
|
||||
"private",
|
||||
|
|
|
@ -10,28 +10,25 @@ import { Package } from '@verdaccio/types';
|
|||
|
||||
type Packages = Package[];
|
||||
|
||||
export default function(route: Router, storage: IStorageHandler) {
|
||||
route.get(
|
||||
'/-/_view/starredByUser',
|
||||
(req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
|
||||
const remoteUsername = req.remote_user.name;
|
||||
export default function(route: Router, storage: IStorageHandler): void {
|
||||
route.get('/-/_view/starredByUser', (req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
|
||||
const remoteUsername = req.remote_user.name;
|
||||
|
||||
storage.getLocalDatabase((err, localPackages: Packages) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
storage.getLocalDatabase((err, localPackages: Packages) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const filteredPackages: Packages = localPackages.filter((localPackage: Package) =>
|
||||
localPackage[USERS] ? _.keys(localPackage[USERS]).indexOf(remoteUsername) >= 0 : false
|
||||
);
|
||||
const filteredPackages: Packages = localPackages.filter((localPackage: Package) =>
|
||||
localPackage[USERS] ? _.keys(localPackage[USERS]).indexOf(remoteUsername) >= 0 : false
|
||||
);
|
||||
|
||||
res.status(HTTP_STATUS.OK);
|
||||
next({
|
||||
rows: filteredPackages.map((filteredPackage: Package) => ({
|
||||
value: filteredPackage.name,
|
||||
})),
|
||||
});
|
||||
res.status(HTTP_STATUS.OK);
|
||||
next({
|
||||
rows: filteredPackages.map((filteredPackage: Package) => ({
|
||||
value: filteredPackage.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
125
src/api/endpoint/api/v1/token.ts
Normal file
125
src/api/endpoint/api/v1/token.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import _ from 'lodash';
|
||||
import { HTTP_STATUS, SUPPORT_ERRORS } from '../../../../lib/constants';
|
||||
import {ErrorCode, mask} from '../../../../lib/utils';
|
||||
import { getApiToken } from '../../../../lib/auth-utils';
|
||||
import { stringToMD5 } from '../../../../lib/crypto-utils';
|
||||
import { logger } from '../../../../lib/logger';
|
||||
|
||||
import { Response, Router } from 'express';
|
||||
import {$NextFunctionVer, $RequestExtend, IAuth, IStorageHandler} from '../../../../../types';
|
||||
import { Config, RemoteUser, Token } from '@verdaccio/types';
|
||||
|
||||
export type NormalizeToken = Token & {
|
||||
created: string;
|
||||
};
|
||||
|
||||
function normalizeToken(token: Token): NormalizeToken {
|
||||
return {
|
||||
...token,
|
||||
created: new Date(token.created).toISOString(),
|
||||
};
|
||||
};
|
||||
|
||||
// https://github.com/npm/npm-profile/blob/latest/lib/index.js
|
||||
export default function(route: Router, auth: IAuth, storage: IStorageHandler, config: Config) {
|
||||
route.get('/-/npm/v1/tokens', async function(req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||
const { name } = req.remote_user;
|
||||
|
||||
if (_.isNil(name) === false) {
|
||||
try {
|
||||
const tokens = await storage.readTokens({user: name});
|
||||
const totalTokens = tokens.length;
|
||||
logger.debug({totalTokens}, 'token list retrieved: @{totalTokens}');
|
||||
|
||||
res.status(HTTP_STATUS.OK);
|
||||
return next({
|
||||
objects: tokens.map(normalizeToken),
|
||||
urls: {
|
||||
next: '', // TODO: pagination?
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ error: error.msg }, 'token list has failed: @{error}');
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));
|
||||
}
|
||||
} else {
|
||||
return next(ErrorCode.getUnauthorized());
|
||||
}
|
||||
});
|
||||
|
||||
route.post('/-/npm/v1/tokens', function(req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||
const { password, readonly, cidr_whitelist } = req.body;
|
||||
const { name } = req.remote_user;
|
||||
|
||||
if (!_.isBoolean(readonly) || !_.isArray(cidr_whitelist)) {
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));
|
||||
}
|
||||
|
||||
auth.authenticate(name, password, async (err, user: RemoteUser) => {
|
||||
if (err) {
|
||||
const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;
|
||||
return next(ErrorCode.getCode(errorCode, err.message));
|
||||
} else {
|
||||
req.remote_user = user;
|
||||
|
||||
if (!_.isFunction(storage.saveToken)) {
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.NOT_IMPLEMENTED, SUPPORT_ERRORS.STORAGE_NOT_IMPLEMENT));
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await getApiToken(auth, config, user, password);
|
||||
const key = stringToMD5(token);
|
||||
// TODO: use a utility here
|
||||
const maskedToken = mask(token, 5);
|
||||
const created = new Date().getTime();
|
||||
|
||||
/**
|
||||
* cidr_whitelist: is not being used, we pass it through
|
||||
* token: we do not store the real token (it is generated once and retrieved to the user), just a mask of it.
|
||||
*/
|
||||
const saveToken: Token = {
|
||||
user: name,
|
||||
token: maskedToken,
|
||||
key,
|
||||
cidr: cidr_whitelist,
|
||||
readonly,
|
||||
created,
|
||||
};
|
||||
|
||||
await storage.saveToken(saveToken);
|
||||
logger.debug({ key, name }, 'token @{key} was created for user @{name}');
|
||||
return next(normalizeToken({
|
||||
token,
|
||||
user: name,
|
||||
key: saveToken.key,
|
||||
cidr: cidr_whitelist,
|
||||
readonly,
|
||||
created: saveToken.created,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error({ error: error.msg }, 'token creation has failed: @{error}');
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
route.delete('/-/npm/v1/tokens/token/:tokenKey', async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
|
||||
const { params: { tokenKey }} = req;
|
||||
const { name } = req.remote_user;
|
||||
|
||||
if (_.isNil(name) === false) {
|
||||
logger.debug({name}, '@{name} has requested remove a token');
|
||||
try {
|
||||
await storage.deleteToken(name, tokenKey);
|
||||
logger.info({ tokenKey, name }, 'token id @{tokenKey} was revoked for user @{name}');
|
||||
return next({});
|
||||
} catch(error) {
|
||||
logger.error({ error: error.msg }, 'token creation has failed: @{error}');
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));
|
||||
}
|
||||
} else {
|
||||
return next(ErrorCode.getUnauthorized());
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,10 +1,6 @@
|
|||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { IAuth, IStorageHandler } from '../../../types';
|
||||
import { Config } from '@verdaccio/types';
|
||||
import _ from 'lodash';
|
||||
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
|
@ -17,6 +13,7 @@ import search from './api/search';
|
|||
import pkg from './api/package';
|
||||
import stars from './api/stars';
|
||||
import profile from './api/v1/profile';
|
||||
import token from './api/v1/token';
|
||||
|
||||
const { match, validateName, validatePackage, encodeScopePackage, antiLoop } = require('../middleware');
|
||||
|
||||
|
@ -57,6 +54,8 @@ export default function(config: Config, auth: IAuth, storage: IStorageHandler) {
|
|||
publish(app, auth, storage, config);
|
||||
ping(app);
|
||||
stars(app, storage);
|
||||
|
||||
if (_.get(config, 'experiments.token') === true) {
|
||||
token(app, auth, storage, config);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
|
|
@ -49,20 +49,15 @@ function addUserAuthApi(route: Router, auth: IAuth, config: Config): void {
|
|||
const { name } = req.remote_user;
|
||||
|
||||
if (validatePassword(password.new) === false) {
|
||||
auth.changePassword(
|
||||
name as string,
|
||||
password.old,
|
||||
password.new,
|
||||
(err, isUpdated): void => {
|
||||
if (_.isNil(err) && isUpdated) {
|
||||
next({
|
||||
ok: true,
|
||||
});
|
||||
} else {
|
||||
return next(ErrorCode.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
auth.changePassword(name as string, password.old, password.new, (err, isUpdated): void => {
|
||||
if (_.isNil(err) && isUpdated) {
|
||||
next({
|
||||
ok: true,
|
||||
});
|
||||
} else {
|
||||
return next(ErrorCode.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.BAD_REQUEST, APP_ERROR.PASSWORD_VALIDATION));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||
|
||||
import { API_ERROR, SUPPORT_ERRORS, TOKEN_BASIC, TOKEN_BEARER } from './constants';
|
||||
import {API_ERROR, SUPPORT_ERRORS, TOKEN_BASIC, TOKEN_BEARER} from './constants';
|
||||
import loadPlugin from '../lib/plugin-loader';
|
||||
import { aesEncrypt, signPayload } from './crypto-utils';
|
||||
import {
|
||||
|
@ -66,36 +66,36 @@ class Auth implements IAuth {
|
|||
public changePassword(username: string, password: string, newPassword: string, cb: Callback): void {
|
||||
const validPlugins = _.filter(this.plugins, plugin => _.isFunction(plugin.changePassword));
|
||||
|
||||
if (_.isEmpty(validPlugins)) {
|
||||
return cb(ErrorCode.getInternalError(SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE));
|
||||
}
|
||||
if (_.isEmpty(validPlugins)) {
|
||||
return cb(ErrorCode.getInternalError(SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE));
|
||||
}
|
||||
|
||||
for (const plugin of validPlugins) {
|
||||
if (_.isNil(plugin) || _.isFunction(plugin.changePassword) === false) {
|
||||
this.logger.trace('auth plugin does not implement changePassword, trying next one');
|
||||
continue;
|
||||
} else {
|
||||
this.logger.trace({username}, 'updating password for @{username}');
|
||||
plugin.changePassword!(
|
||||
username,
|
||||
password,
|
||||
newPassword,
|
||||
(err, profile): void => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{username, err},
|
||||
`An error has been produced
|
||||
for (const plugin of validPlugins) {
|
||||
if (_.isNil(plugin) || _.isFunction(plugin.changePassword) === false) {
|
||||
this.logger.trace('auth plugin does not implement changePassword, trying next one');
|
||||
continue;
|
||||
} else {
|
||||
this.logger.trace({username}, 'updating password for @{username}');
|
||||
plugin.changePassword!(
|
||||
username,
|
||||
password,
|
||||
newPassword,
|
||||
(err, profile): void => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{username, err},
|
||||
`An error has been produced
|
||||
updating the password for @{username}. Error: @{err.message}`
|
||||
);
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.logger.trace({username}, 'updated password for @{username} was successful');
|
||||
return cb(null, profile);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
this.logger.trace({username}, 'updated password for @{username} was successful');
|
||||
return cb(null, profile);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public authenticate(username: string, password: string, cb: Callback): void {
|
||||
|
@ -223,6 +223,7 @@ class Auth implements IAuth {
|
|||
if (_.isNil(ok) === true) {
|
||||
this.logger.trace({ packageName }, 'we bypass unpublish for @{packageName}, publish will handle the access');
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
return this.allow_publish(...arguments);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import { assign, isObject, isFunction } from 'lodash';
|
||||
import URL from 'url';
|
||||
import fs from 'fs';
|
||||
|
@ -17,6 +13,16 @@ import { Application } from 'express';
|
|||
|
||||
const logger = require('./logger');
|
||||
|
||||
function displayExperimentsInfoBox(experiments) {
|
||||
const experimentList = Object.keys(experiments);
|
||||
if (experimentList.length >= 1) {
|
||||
logger.logger.warn('⚠️ experiments are enabled, we recommend do not use experiments in production, comment out this section to disable it');
|
||||
experimentList.forEach(experiment => {
|
||||
logger.logger.warn(` - support for ${experiment} ${experiments[experiment] ? 'is enabled' : ' is disabled'}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the server after configuration has been loaded.
|
||||
* @param {Object} config
|
||||
|
@ -30,6 +36,10 @@ function startVerdaccio(config: any, cliListen: string, configPath: string, pkgV
|
|||
throw new Error(API_ERROR.CONFIG_BAD_FORMAT);
|
||||
}
|
||||
|
||||
if ('experiments' in config) {
|
||||
displayExperimentsInfoBox(config.experiments);
|
||||
}
|
||||
|
||||
endPointAPI(config).then(
|
||||
(app): void => {
|
||||
const addresses = getListListenAddresses(cliListen, config.listen);
|
||||
|
|
|
@ -44,7 +44,9 @@ export function getListListenAddresses(argListen: string, configListen: any): an
|
|||
if (!parsedAddr) {
|
||||
logger.logger.warn(
|
||||
{ addr: addr },
|
||||
'invalid address - @{addr}, we expect a port (e.g. "4873"),' + ' host:port (e.g. "localhost:4873") or full url' + ' (e.g. "http://localhost:4873/")'
|
||||
'invalid address - @{addr}, we expect a port (e.g. "4873"),' +
|
||||
' host:port (e.g. "localhost:4873") or full url' +
|
||||
' (e.g. "http://localhost:4873/")'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,9 @@ function updateStorageLinks(configLocation, defaultConfig) {
|
|||
}
|
||||
|
||||
function getConfigPaths() {
|
||||
return [getXDGDirectory(), getWindowsDirectory(), getRelativeDefaultDirectory(), getOldDirectory()].filter(path => !!path);
|
||||
return [getXDGDirectory(), getWindowsDirectory(), getRelativeDefaultDirectory(), getOldDirectory()].filter(
|
||||
path => !!path
|
||||
);
|
||||
}
|
||||
|
||||
const getXDGDirectory = () => {
|
||||
|
|
|
@ -41,7 +41,9 @@ export function normalizeUserList(oldFormat: any, newFormat: any): any {
|
|||
// @ts-ignore
|
||||
result.push(arguments[i]);
|
||||
} else {
|
||||
throw ErrorCode.getInternalError('CONFIG: bad package acl (array or string expected): ' + JSON.stringify(arguments[i]));
|
||||
throw ErrorCode.getInternalError(
|
||||
'CONFIG: bad package acl (array or string expected): ' + JSON.stringify(arguments[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
return _.flatten(result);
|
||||
|
@ -64,7 +66,10 @@ export function uplinkSanityCheck(uplinks: UpLinksConfList, users: any = BLACKLI
|
|||
}
|
||||
|
||||
export function sanityCheckNames(item: string, users: any): any {
|
||||
assert(item !== 'all' && item !== 'owner' && item !== 'anonymous' && item !== 'undefined' && item !== 'none', 'CONFIG: reserved uplink name: ' + item);
|
||||
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;
|
||||
|
@ -117,7 +122,10 @@ export function normalisePackageAccess(packages: LegacyPackageList): LegacyPacka
|
|||
|
||||
for (const 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)`);
|
||||
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);
|
||||
|
@ -125,7 +133,9 @@ export function normalisePackageAccess(packages: LegacyPackageList): LegacyPacka
|
|||
normalizedPkgs[pkg].proxy = normalizeUserList(packages[pkg].proxy_access, packages[pkg].proxy);
|
||||
delete normalizedPkgs[pkg].proxy_access;
|
||||
// if unpublish is not defined, we set to false to fallback in publish access
|
||||
normalizedPkgs[pkg].unpublish = _.isUndefined(packages[pkg].unpublish) ? false : normalizeUserList([], packages[pkg].unpublish);
|
||||
normalizedPkgs[pkg].unpublish = _.isUndefined(packages[pkg].unpublish)
|
||||
? false
|
||||
: normalizeUserList([], packages[pkg].unpublish);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ export const HTTP_STATUS = {
|
|||
UNSUPPORTED_MEDIA: 415,
|
||||
BAD_DATA: 422,
|
||||
INTERNAL_ERROR: 500,
|
||||
NOT_IMPLEMENTED: 501,
|
||||
SERVICE_UNAVAILABLE: 503,
|
||||
LOOP_DETECTED: 508,
|
||||
};
|
||||
|
@ -105,6 +106,8 @@ export const API_MESSAGE = {
|
|||
export const SUPPORT_ERRORS = {
|
||||
PLUGIN_MISSING_INTERFACE: 'the plugin does not provide implementation of the requested feature',
|
||||
TFA_DISABLED: 'the two-factor authentication is not yet supported',
|
||||
STORAGE_NOT_IMPLEMENT: 'the storage does not support token saving',
|
||||
PARAMETERS_NOT_VALID: 'the parameters are not valid',
|
||||
};
|
||||
|
||||
export const API_ERROR = {
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { createDecipher, createCipher, createHash, pseudoRandomBytes } from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
|
@ -12,8 +7,9 @@ export const defaultAlgorithm = 'aes192';
|
|||
export const defaultTarballHashAlgorithm = 'sha1';
|
||||
|
||||
export function aesEncrypt(buf: Buffer, secret: string): Buffer {
|
||||
// deprecated
|
||||
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
|
||||
// https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options
|
||||
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
|
||||
const c = createCipher(defaultAlgorithm, secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
|
@ -22,8 +18,9 @@ export function aesEncrypt(buf: Buffer, secret: string): Buffer {
|
|||
|
||||
export function aesDecrypt(buf: Buffer, secret: string) {
|
||||
try {
|
||||
// deprecated
|
||||
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
|
||||
// https://nodejs.org/api/crypto.html#crypto_crypto_createdecipher_algorithm_password_options
|
||||
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
|
||||
const c = createDecipher(defaultAlgorithm, secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
|
|
|
@ -3,13 +3,13 @@ import UrlNode from 'url';
|
|||
import _ from 'lodash';
|
||||
import { ErrorCode, isObject, getLatestVersion, tagVersion, validateName } from './utils';
|
||||
import { generatePackageTemplate, normalizePackage, generateRevision, getLatestReadme, cleanUpReadme, normalizeContributors } from './storage-utils';
|
||||
import { API_ERROR, DIST_TAGS, HTTP_STATUS, STORAGE, USERS } from './constants';
|
||||
import {API_ERROR, DIST_TAGS, HTTP_STATUS, STORAGE, SUPPORT_ERRORS, USERS} from './constants';
|
||||
import { createTarballHash } from './crypto-utils';
|
||||
import { prepareSearchPackage } from './storage-utils';
|
||||
import loadPlugin from '../lib/plugin-loader';
|
||||
import LocalDatabase from '@verdaccio/local-storage';
|
||||
import { UploadTarball, ReadTarball } from '@verdaccio/streams';
|
||||
import { Package, Config, IUploadTarball, IReadTarball, MergeTags, Version, DistFile, Callback, Logger, ILocalData, IPackageStorage, Author } from '@verdaccio/types';
|
||||
import { Token, TokenFilter, Package, Config, IUploadTarball, IReadTarball, MergeTags, Version, DistFile, Callback, Logger, IPluginStorage, IPackageStorage, Author } from '@verdaccio/types';
|
||||
import { IStorage, StringValue } from '../../types';
|
||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||
|
||||
|
@ -18,13 +18,13 @@ import { VerdaccioError } from '@verdaccio/commons-api';
|
|||
*/
|
||||
class LocalStorage implements IStorage {
|
||||
public config: Config;
|
||||
public localData: ILocalData<Config>;
|
||||
public storagePlugin: IPluginStorage<Config>;
|
||||
public logger: Logger;
|
||||
|
||||
public constructor(config: Config, logger: Logger) {
|
||||
this.logger = logger.child({ sub: 'fs' });
|
||||
this.config = config;
|
||||
this.localData = this._loadStorage(config, logger);
|
||||
this.storagePlugin = this._loadStorage(config, logger);
|
||||
}
|
||||
|
||||
public addPackage(name: string, pkg: Package, callback: Callback): void {
|
||||
|
@ -75,7 +75,7 @@ class LocalStorage implements IStorage {
|
|||
|
||||
data = normalizePackage(data);
|
||||
|
||||
this.localData.remove(name, (removeFailed: Error): void => {
|
||||
this.storagePlugin.remove(name, (removeFailed: Error): void => {
|
||||
if (removeFailed) {
|
||||
// This will happen when database is locked
|
||||
this.logger.debug({ name }, `[storage/removePackage] the database is locked, removed has failed for @{name}`);
|
||||
|
@ -241,7 +241,7 @@ class LocalStorage implements IStorage {
|
|||
data.versions[version] = metadata;
|
||||
tagVersion(data, version, tag);
|
||||
|
||||
this.localData.add(name, (addFailed): void => {
|
||||
this.storagePlugin.add(name, (addFailed): void => {
|
||||
if (addFailed) {
|
||||
return cb(ErrorCode.getBadData(addFailed.message));
|
||||
}
|
||||
|
@ -618,7 +618,7 @@ class LocalStorage implements IStorage {
|
|||
* @return {Object}
|
||||
*/
|
||||
private _getLocalStorage(pkgName: string): IPackageStorage {
|
||||
return this.localData.getPackageStorage(pkgName);
|
||||
return this.storagePlugin.getPackageStorage(pkgName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -647,11 +647,11 @@ class LocalStorage implements IStorage {
|
|||
*/
|
||||
private _searchEachPackage(onPackage: Callback, onEnd: Callback): void {
|
||||
// save wait whether plugin still do not support search functionality
|
||||
if (_.isNil(this.localData.search)) {
|
||||
if (_.isNil(this.storagePlugin.search)) {
|
||||
this.logger.warn('plugin search not implemented yet');
|
||||
onEnd();
|
||||
} else {
|
||||
this.localData.search(onPackage, onEnd, validateName);
|
||||
this.storagePlugin.search(onPackage, onEnd, validateName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -787,35 +787,59 @@ class LocalStorage implements IStorage {
|
|||
}
|
||||
|
||||
public async getSecret(config: Config): Promise<void> {
|
||||
const secretKey = await this.localData.getSecret();
|
||||
const secretKey = await this.storagePlugin.getSecret();
|
||||
|
||||
return this.localData.setSecret(config.checkSecretKey(secretKey));
|
||||
return this.storagePlugin.setSecret(config.checkSecretKey(secretKey));
|
||||
}
|
||||
|
||||
private _loadStorage(config: Config, logger: Logger): ILocalData<Config> {
|
||||
private _loadStorage(config: Config, logger: Logger): IPluginStorage<Config> {
|
||||
const Storage = this._loadStorePlugin();
|
||||
|
||||
if (_.isNil(Storage)) {
|
||||
assert(this.config.storage, 'CONFIG: storage path not defined');
|
||||
return new LocalDatabase(this.config, logger);
|
||||
} else {
|
||||
return Storage as ILocalData<Config>;
|
||||
return Storage as IPluginStorage<Config>;
|
||||
}
|
||||
}
|
||||
|
||||
private _loadStorePlugin(): ILocalData<Config> | void {
|
||||
private _loadStorePlugin(): IPluginStorage<Config> | void {
|
||||
const plugin_params = {
|
||||
config: this.config,
|
||||
logger: this.logger,
|
||||
};
|
||||
|
||||
const plugins: ILocalData<Config>[] = loadPlugin<ILocalData<Config>>(this.config, this.config.store, plugin_params, (plugin): ILocalData<Config> => {
|
||||
const plugins: IPluginStorage<Config>[] = loadPlugin<IPluginStorage<Config>>(this.config, this.config.store, plugin_params, (plugin): IPluginStorage<Config> => {
|
||||
return plugin.getPackageStorage;
|
||||
});
|
||||
|
||||
|
||||
return _.head(plugins);
|
||||
}
|
||||
|
||||
public saveToken(token: Token): Promise<any> {
|
||||
if (_.isFunction(this.storagePlugin.saveToken) === false) {
|
||||
return Promise.reject(ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE));
|
||||
}
|
||||
|
||||
return this.storagePlugin.saveToken(token);
|
||||
}
|
||||
|
||||
public deleteToken(user: string, tokenKey: string): Promise<any> {
|
||||
if (_.isFunction(this.storagePlugin.deleteToken) === false) {
|
||||
return Promise.reject(ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE));
|
||||
}
|
||||
|
||||
return this.storagePlugin.deleteToken(user, tokenKey);
|
||||
}
|
||||
|
||||
public readTokens(filter: TokenFilter): Promise<Array<Token>> {
|
||||
if (_.isFunction(this.storagePlugin.readTokens) === false) {
|
||||
return Promise.reject(ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE));
|
||||
}
|
||||
|
||||
return this.storagePlugin.readTokens(filter);
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalStorage;
|
||||
|
|
|
@ -43,7 +43,7 @@ class Search implements IWebSearch {
|
|||
public query(query: string): any[] {
|
||||
const localStorage = this.storage.localStorage as IStorage;
|
||||
|
||||
return query === '*' ? localStorage.localData.get((items): any => {
|
||||
return query === '*' ? localStorage.storagePlugin.get((items): any => {
|
||||
items.map(function(pkg): any {
|
||||
return { ref: pkg, score: 1 };
|
||||
});
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import assert from 'assert';
|
||||
import async, { AsyncResultArrayCallback } from 'async';
|
||||
|
@ -20,7 +15,7 @@ import { IStorage, IProxy, IStorageHandler, ProxyList, StringValue, IGetPackageO
|
|||
import { IReadTarball, IUploadTarball, Versions, Package, Config, MergeTags, Version, DistFile, Callback, Logger } from '@verdaccio/types';
|
||||
import { hasProxyTo } from './config-utils';
|
||||
import { logger } from '../lib/logger';
|
||||
import { GenericBody } from '@verdaccio/types';
|
||||
import { GenericBody, TokenFilter, Token } from '@verdaccio/types';
|
||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||
|
||||
class Storage implements IStorageHandler {
|
||||
|
@ -67,6 +62,18 @@ class Storage implements IStorageHandler {
|
|||
return typeof this.config.publish !== 'undefined' && _.isBoolean(this.config.publish.allow_offline) && this.config.publish.allow_offline;
|
||||
}
|
||||
|
||||
public readTokens(filter: TokenFilter): Promise<Array<Token>> {
|
||||
return this.localStorage.readTokens(filter);
|
||||
}
|
||||
|
||||
public saveToken(token: Token): Promise<void> {
|
||||
return this.localStorage.saveToken(token);
|
||||
}
|
||||
|
||||
public deleteToken(user: string, tokenKey: string): Promise<any> {
|
||||
return this.localStorage.deleteToken(user, tokenKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new version of package {name} to a system
|
||||
Used storages: local (write)
|
||||
|
@ -385,7 +392,7 @@ class Storage implements IStorageHandler {
|
|||
*/
|
||||
public getLocalDatabase(callback: Callback): void {
|
||||
const self = this;
|
||||
this.localStorage.localData.get(
|
||||
this.localStorage.storagePlugin.get(
|
||||
(err, locals): void => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
|
|
|
@ -195,26 +195,31 @@ class ProxyStorage implements IProxy {
|
|||
}
|
||||
} : undefined;
|
||||
|
||||
const req = request(
|
||||
{
|
||||
url: uri,
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: json,
|
||||
// FIXME: ts complains ca cannot be undefined
|
||||
// @ts-ignore
|
||||
ca: this.ca,
|
||||
proxy: this.proxy,
|
||||
encoding: null,
|
||||
gzip: true,
|
||||
timeout: this.timeout,
|
||||
strictSSL: this.strict_ssl,
|
||||
},
|
||||
requestCallback
|
||||
);
|
||||
let requestOptions = {
|
||||
url: uri,
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: json,
|
||||
// FIXME: ts complains ca cannot be undefined
|
||||
proxy: this.proxy,
|
||||
encoding: null,
|
||||
gzip: true,
|
||||
timeout: this.timeout,
|
||||
strictSSL: this.strict_ssl,
|
||||
};
|
||||
|
||||
if (this.ca) {
|
||||
requestOptions = Object.assign({}, requestOptions, {
|
||||
ca: this.ca
|
||||
});
|
||||
}
|
||||
|
||||
const req = request(requestOptions, requestCallback);
|
||||
|
||||
let statusCalled = false;
|
||||
req.on('response', function(res): void {
|
||||
// FIXME: _verdaccio_aborted seems not used
|
||||
// @ts-ignore
|
||||
if (!req._verdaccio_aborted && !statusCalled) {
|
||||
statusCalled = true;
|
||||
self._statusCheck(true);
|
||||
|
@ -238,11 +243,14 @@ class ProxyStorage implements IProxy {
|
|||
}
|
||||
});
|
||||
req.on('error', function(_err): void {
|
||||
// FIXME: _verdaccio_aborted seems not used
|
||||
// @ts-ignore
|
||||
if (!req._verdaccio_aborted && !statusCalled) {
|
||||
statusCalled = true;
|
||||
self._statusCheck(false);
|
||||
}
|
||||
});
|
||||
// @ts-ignore
|
||||
return req;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,16 @@ import YAML from 'js-yaml';
|
|||
import URL from 'url';
|
||||
import sanitizyReadme from '@verdaccio/readme';
|
||||
|
||||
import { APP_ERROR, DEFAULT_PORT, DEFAULT_DOMAIN, DEFAULT_PROTOCOL, CHARACTER_ENCODING, HEADERS, DIST_TAGS, DEFAULT_USER } from './constants';
|
||||
import {
|
||||
APP_ERROR,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_DOMAIN,
|
||||
DEFAULT_PROTOCOL,
|
||||
CHARACTER_ENCODING,
|
||||
HEADERS,
|
||||
DIST_TAGS,
|
||||
DEFAULT_USER,
|
||||
} from './constants';
|
||||
import { generateGravatarUrl, GENERIC_AVATAR } from '../utils/user';
|
||||
|
||||
import { Package, Version, Author } from '@verdaccio/types';
|
||||
|
@ -170,7 +179,12 @@ export function convertDistRemoteToLocalTarballUrls(pkg: Package, req: Request,
|
|||
* @param {*} uri
|
||||
* @return {String} a parsed url
|
||||
*/
|
||||
export function getLocalRegistryTarballUri(uri: string, pkgName: string, req: Request, urlPrefix: string | void): string {
|
||||
export function getLocalRegistryTarballUri(
|
||||
uri: string,
|
||||
pkgName: string,
|
||||
req: Request,
|
||||
urlPrefix: string | void
|
||||
): string {
|
||||
const currentHost = req.headers.host;
|
||||
|
||||
if (!currentHost) {
|
||||
|
@ -442,17 +456,14 @@ export function addScope(scope: string, packageName: string): string {
|
|||
}
|
||||
|
||||
export function deleteProperties(propertiesToDelete: string[], objectItem: any): any {
|
||||
_.forEach(
|
||||
propertiesToDelete,
|
||||
(property): any => {
|
||||
delete objectItem[property];
|
||||
}
|
||||
);
|
||||
_.forEach(propertiesToDelete, (property): any => {
|
||||
delete objectItem[property];
|
||||
});
|
||||
|
||||
return objectItem;
|
||||
}
|
||||
|
||||
export function addGravatarSupport(pkgInfo: Package, online: boolean = true): AuthorAvatar {
|
||||
export function addGravatarSupport(pkgInfo: Package, online = true): AuthorAvatar {
|
||||
const pkgInfoCopy = { ...pkgInfo } as any;
|
||||
const author: any = _.get(pkgInfo, 'latest.author', null) as any;
|
||||
const contributors: AuthorAvatar[] = normalizeContributors(_.get(pkgInfo, 'latest.contributors', []));
|
||||
|
@ -493,12 +504,10 @@ export function addGravatarSupport(pkgInfo: Package, online: boolean = true): Au
|
|||
|
||||
// for maintainers
|
||||
if (_.isEmpty(maintainers) === false) {
|
||||
pkgInfoCopy.latest.maintainers = maintainers.map(
|
||||
(maintainer): void => {
|
||||
maintainer.avatar = generateGravatarUrl(maintainer.email, online);
|
||||
return maintainer;
|
||||
}
|
||||
);
|
||||
pkgInfoCopy.latest.maintainers = maintainers.map((maintainer): void => {
|
||||
maintainer.avatar = generateGravatarUrl(maintainer.email, online);
|
||||
return maintainer;
|
||||
});
|
||||
}
|
||||
|
||||
return pkgInfoCopy;
|
||||
|
@ -591,6 +600,16 @@ export function pad(str, max): string {
|
|||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* return a masquerade string with its first and last {charNum} and three dots in between.
|
||||
* @param {String} str
|
||||
* @param {Number} charNum
|
||||
* @returns {String}
|
||||
*/
|
||||
export function mask(str: string, charNum = 3) {
|
||||
return `${str.substr(0, charNum)}...${str.substr(-charNum)}`;
|
||||
}
|
||||
|
||||
export function encodeScopedUri(packageName) {
|
||||
return packageName.replace(/\//g, '%2f');
|
||||
}
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
"rules": {
|
||||
"valid-jsdoc": 0,
|
||||
"no-redeclare": 1,
|
||||
"jest/consistent-test-it": ["error", {"fn": "test"}],
|
||||
"jest/no-jasmine-globals": 2,
|
||||
"no-console": [
|
||||
2,
|
||||
{
|
||||
|
|
|
@ -9,13 +9,17 @@ function Plugin(config) {
|
|||
Plugin.prototype.register_middlewares = function (app, auth, storage) {
|
||||
|
||||
app.get('/test-uplink-timeout-*', function (req, res, next) {
|
||||
// https://github.com/nock/nock#readme
|
||||
nock('http://localhost:55552')
|
||||
.get(req.path)
|
||||
.socketDelay(31000).reply(200); // 31s is greater than the default 30s connection timeout
|
||||
// 31s is greater than the default 30s connection timeout
|
||||
.socketDelay(50000)
|
||||
// http-status 200 OK
|
||||
.reply(200);
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Plugin;
|
||||
|
|
|
@ -6,6 +6,10 @@ const PKG_MULTIPLE_UPLINKS = 'test-uplink-timeout-multiple';
|
|||
export default function (server, server2, server3) {
|
||||
|
||||
describe('uplink connection timeouts', () => {
|
||||
|
||||
//more info: https://github.com/verdaccio/verdaccio/pull/1331
|
||||
|
||||
jest.setTimeout(20000);
|
||||
beforeAll(async () => {
|
||||
await server2.addPackage(PKG_SINGLE_UPLINK).status(HTTP_STATUS.CREATED);
|
||||
await server2.addPackage(PKG_MULTIPLE_UPLINKS).status(HTTP_STATUS.CREATED);
|
||||
|
@ -17,7 +21,7 @@ export default function (server, server2, server3) {
|
|||
return server.getPackage(PKG_SINGLE_UPLINK).status(HTTP_STATUS.SERVICE_UNAVAILABLE);
|
||||
});
|
||||
|
||||
test('200 response even though one uplink timesout', () => {
|
||||
test('200 response even though one uplink timeout', () => {
|
||||
return server.getPackage(PKG_MULTIPLE_UPLINKS).status(HTTP_STATUS.OK)
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import request from 'supertest';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
|
||||
|
@ -8,7 +7,7 @@ import { setup } from '../../../../src/lib/logger';
|
|||
setup([]);
|
||||
|
||||
import { HEADERS, HTTP_STATUS } from '../../../../src/lib/constants';
|
||||
import configDefault from '../../partials/config/config_access';
|
||||
import configDefault from '../../partials/config';
|
||||
import endPointAPI from '../../../../src/api';
|
||||
import {mockServer} from '../../__helper/mock';
|
||||
import {DOMAIN_SERVERS} from '../../../functional/config.functional';
|
||||
|
@ -25,7 +24,7 @@ describe('api with no limited access configuration', () => {
|
|||
const mockServerPort = 55530;
|
||||
|
||||
rimraf(store, async () => {
|
||||
const configForTest = _.assign({}, _.cloneDeep(configDefault), {
|
||||
const configForTest = configDefault({
|
||||
auth: {
|
||||
htpasswd: {
|
||||
file: './access-storage/htpasswd-pkg-access'
|
||||
|
@ -33,11 +32,14 @@ describe('api with no limited access configuration', () => {
|
|||
},
|
||||
self_path: store,
|
||||
uplinks: {
|
||||
npmjs: {
|
||||
remote: {
|
||||
url: `http://${DOMAIN_SERVERS}:${mockServerPort}`
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
logs: [
|
||||
{ type: 'stdout', format: 'pretty', level: 'warn' }
|
||||
]
|
||||
}, 'pkg.access.spec.yaml');
|
||||
|
||||
app = await endPointAPI(configForTest);
|
||||
mockRegistry = await mockServer(mockServerPort).init();
|
||||
|
@ -59,13 +61,30 @@ describe('api with no limited access configuration', () => {
|
|||
|
||||
describe('test proxy packages partially restricted', () => {
|
||||
|
||||
test('should test fails on fetch endpoint /-/jquery', (done) => {
|
||||
|
||||
test('should test fails on fetch endpoint /-/not-found', (done) => {
|
||||
request(app)
|
||||
// @ts-ignore
|
||||
.get('/not-found-for-sure')
|
||||
.set(HEADERS.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.expect(HEADERS.CONTENT_TYPE, /json/)
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.end(function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should test fetch endpoint /-/jquery', (done) => {
|
||||
request(app)
|
||||
// @ts-ignore
|
||||
.get('/jquery')
|
||||
.set(HEADERS.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.expect(HEADERS.CONTENT_TYPE, /json/)
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
|
|
226
test/unit/modules/api/token.spec.ts
Normal file
226
test/unit/modules/api/token.spec.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
import request from 'supertest';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
import _ from 'lodash';
|
||||
|
||||
import configDefault from '../../partials/config';
|
||||
import endPointAPI from '../../../../src/api';
|
||||
|
||||
import {
|
||||
HEADERS,
|
||||
HTTP_STATUS,
|
||||
HEADER_TYPE, TOKEN_BEARER, API_ERROR, SUPPORT_ERRORS,
|
||||
} from '../../../../src/lib/constants';
|
||||
import {mockServer} from '../../__helper/mock';
|
||||
import {DOMAIN_SERVERS} from '../../../functional/config.functional';
|
||||
import { getNewToken } from '../../__helper/api';
|
||||
import {buildToken} from "../../../../src/lib/utils";
|
||||
|
||||
require('../../../../src/lib/logger').setup([
|
||||
{ type: 'stdout', format: 'pretty', level: 'trace' }
|
||||
]);
|
||||
|
||||
const credentials = { name: 'jota_token', password: 'secretPass' };
|
||||
|
||||
const generateTokenCLI = async (app, token, payload): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(app)
|
||||
.post('/-/npm/v1/tokens')
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.send(JSON.stringify(payload))
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.end(function(err, resp) {
|
||||
if (err) {
|
||||
return reject([err, resp]);
|
||||
}
|
||||
resolve([err, resp]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteTokenCLI = async (app, token, tokenToDelete): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(app)
|
||||
.delete(`/-/npm/v1/tokens/token/${tokenToDelete}`)
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.end(function(err, resp) {
|
||||
if (err) {
|
||||
return reject([err, resp]);
|
||||
}
|
||||
resolve([err, resp]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('endpoint unit test', () => {
|
||||
let app;
|
||||
let mockRegistry;
|
||||
let token;
|
||||
|
||||
beforeAll(function(done) {
|
||||
const store = path.join(__dirname, '../../partials/store/test-storage-token-spec');
|
||||
const mockServerPort = 55543;
|
||||
rimraf(store, async () => {
|
||||
const configForTest = configDefault({
|
||||
auth: {
|
||||
htpasswd: {
|
||||
file: './test-storage-token-spec/.htpasswd-token'
|
||||
}
|
||||
},
|
||||
storage: store,
|
||||
self_path: store,
|
||||
uplinks: {
|
||||
npmjs: {
|
||||
url: `http://${DOMAIN_SERVERS}:${mockServerPort}`
|
||||
}
|
||||
},
|
||||
logs: [
|
||||
{ type: 'stdout', format: 'pretty', level: 'trace' }
|
||||
]
|
||||
}, 'token.spec.yaml');
|
||||
|
||||
app = await endPointAPI(configForTest);
|
||||
mockRegistry = await mockServer(mockServerPort).init();
|
||||
token = await getNewToken(request(app), credentials);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(function(done) {
|
||||
mockRegistry[0].stop();
|
||||
done();
|
||||
});
|
||||
|
||||
describe('Registry Token Endpoints', () => {
|
||||
test('should list empty tokens', async (done) => {
|
||||
request(app)
|
||||
.get('/-/npm/v1/tokens')
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function(err, resp) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const { objects, urls} = resp.body;
|
||||
expect(objects).toHaveLength(0);
|
||||
expect(urls.next).toEqual('');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should generate one token', async (done) => {
|
||||
await generateTokenCLI(app, token, {
|
||||
password: credentials.password,
|
||||
readonly: false,
|
||||
cidr_whitelist: []
|
||||
});
|
||||
|
||||
request(app)
|
||||
.get('/-/npm/v1/tokens')
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function(err, resp) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const { objects, urls} = resp.body;
|
||||
|
||||
expect(objects).toHaveLength(1);
|
||||
const [tokenGenerated] = objects;
|
||||
expect(tokenGenerated.user).toEqual(credentials.name);
|
||||
expect(tokenGenerated.readonly).toBeFalsy();
|
||||
expect(tokenGenerated.token).toMatch(/.../);
|
||||
expect(_.isString(tokenGenerated.created)).toBeTruthy();
|
||||
|
||||
// we don't support pagination yet
|
||||
expect(urls.next).toEqual('');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should delete a token', async (done) => {
|
||||
const res = await generateTokenCLI(app, token, {
|
||||
password: credentials.password,
|
||||
readonly: false,
|
||||
cidr_whitelist: []
|
||||
});
|
||||
|
||||
const t = res[1].body.token;
|
||||
|
||||
await deleteTokenCLI(app, token, t);
|
||||
|
||||
request(app)
|
||||
.get('/-/npm/v1/tokens')
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function(err, resp) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
// FIXME: enable these checks
|
||||
// const { objects } = resp.body;
|
||||
// expect(objects).toHaveLength(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle errors', () => {
|
||||
test('should fail with wrong credentials', async (done) => {
|
||||
try {
|
||||
await generateTokenCLI(app, token, {
|
||||
password: 'wrongPassword',
|
||||
readonly: false,
|
||||
cidr_whitelist: []
|
||||
});
|
||||
done();
|
||||
} catch (e) {
|
||||
const [err, body] = e;
|
||||
expect(err).not.toBeNull();
|
||||
expect(body.error).toEqual(API_ERROR.BAD_USERNAME_PASSWORD);
|
||||
expect(body.status).toEqual(HTTP_STATUS.UNAUTHORIZED);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('should fail if readonly is missing', async (done) => {
|
||||
try {
|
||||
const res = await generateTokenCLI(app, token, {
|
||||
password: credentials.password,
|
||||
cidr_whitelist: []
|
||||
});
|
||||
|
||||
expect(res[0]).toBeNull();
|
||||
expect(res[1].body.error).toEqual(SUPPORT_ERRORS.PARAMETERS_NOT_VALID);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
|
||||
test('should fail if cidr_whitelist is missing', async (done) => {
|
||||
try {
|
||||
const res = await generateTokenCLI(app, token, {
|
||||
password: credentials.password,
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
expect(res[0]).toBeNull();
|
||||
expect(res[1].body.error).toEqual(SUPPORT_ERRORS.PARAMETERS_NOT_VALID);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
15
test/unit/modules/auth/crypto-utils.spec.ts
Normal file
15
test/unit/modules/auth/crypto-utils.spec.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {aesDecrypt, aesEncrypt} from "../../../../src/lib/crypto-utils";
|
||||
import {convertPayloadToBase64} from "../../../../src/lib/utils";
|
||||
|
||||
describe('test crypto utils', () => {
|
||||
describe('default encryption', () => {
|
||||
test('decrypt payload flow', () => {
|
||||
const payload = 'juan';
|
||||
const token = aesEncrypt(Buffer.from(payload), '12345').toString('base64');
|
||||
|
||||
const data = aesDecrypt(convertPayloadToBase64(token), '12345').toString('utf8');
|
||||
|
||||
expect(payload).toEqual(data);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,9 +13,9 @@ jest.mock('../../../../src/lib/logger', () => ({
|
|||
setup: jest.fn(),
|
||||
logger: {
|
||||
child: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn()
|
||||
}
|
||||
|
|
13
test/unit/partials/config/yaml/pkg.access.spec.yaml
Normal file
13
test/unit/partials/config/yaml/pkg.access.spec.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
storage: ./storage_default_storage
|
||||
uplinks:
|
||||
remote:
|
||||
url: http://localhost:4873/
|
||||
packages:
|
||||
'@*/*':
|
||||
access: $all
|
||||
proxy: remote
|
||||
'**':
|
||||
access: $all
|
||||
proxy: remote
|
||||
logs:
|
||||
- { type: stdout, format: pretty, level: trace }
|
22
test/unit/partials/config/yaml/token.spec.yaml
Normal file
22
test/unit/partials/config/yaml/token.spec.yaml
Normal file
|
@ -0,0 +1,22 @@
|
|||
storage: ./storage_default_storage
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: http://localhost:4873/
|
||||
security:
|
||||
api:
|
||||
jwt:
|
||||
sign:
|
||||
expiresIn: 5m
|
||||
notBefore: 0
|
||||
packages:
|
||||
'@token/*':
|
||||
access: $authenticated
|
||||
publish: $authenticated
|
||||
'only-you-can-publish':
|
||||
access: $authenticated
|
||||
publish: $authenticated
|
||||
logs:
|
||||
- { type: stdout, format: pretty, level: trace }
|
||||
experiments:
|
||||
## Enable token for testing
|
||||
token: true
|
|
@ -13,13 +13,16 @@ import {
|
|||
Logger,
|
||||
JWTSignOptions,
|
||||
PackageAccess,
|
||||
ILocalData,
|
||||
IPluginStorage,
|
||||
StringValue as verdaccio$StringValue,
|
||||
IReadTarball,
|
||||
Package,
|
||||
IPluginStorageFilter,
|
||||
Author,
|
||||
AuthPluginPackage
|
||||
AuthPluginPackage,
|
||||
Token,
|
||||
ITokenActions,
|
||||
TokenFilter
|
||||
} from '@verdaccio/types';
|
||||
import lunrMutable from 'lunr-mutable-indexes';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
|
@ -154,9 +157,9 @@ export interface IProxy {
|
|||
getRemoteMetadata(name: string, options: any, callback: Callback): void;
|
||||
}
|
||||
|
||||
export interface IStorage extends IBasicStorage<Config> {
|
||||
export interface IStorage extends IBasicStorage<Config>, ITokenActions {
|
||||
config: Config;
|
||||
localData: ILocalData<Config>;
|
||||
storagePlugin: IPluginStorage<Config>;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
|
@ -176,12 +179,15 @@ export interface ISyncUplinks {
|
|||
|
||||
export type IPluginFilters = IPluginStorageFilter<Config>[];
|
||||
|
||||
export interface IStorageHandler extends IStorageManager<Config> {
|
||||
export interface IStorageHandler extends IStorageManager<Config>, ITokenActions {
|
||||
config: Config;
|
||||
localStorage: IStorage | null;
|
||||
filters: IPluginFilters;
|
||||
uplinks: ProxyList;
|
||||
init(config: Config, filters: IPluginFilters): Promise<string>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Array<Token>>;
|
||||
_syncUplinksMetadata(name: string, packageInfo: Package, options: any, callback: Callback): void;
|
||||
_updateVersionsHiddenUpLink(versions: Versions, upLink: IProxy): void;
|
||||
}
|
||||
|
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in a new issue