0
Fork 0
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:
Juan Picado @jotadeveloper 2019-09-07 15:46:50 -07:00 committed by GitHub
parent 962d5d529a
commit dbf20175dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 708 additions and 193 deletions

View file

@ -1,3 +1,3 @@
{
"presets": [["@verdaccio", {"typescript": true}]]
"presets": [["@verdaccio"]]
}

View file

@ -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
View file

@ -0,0 +1 @@
always-auth = true

View file

@ -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"
}
],

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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,
})),
});
}
);
});
});
}

View 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());
}
});
}

View file

@ -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;
}

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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/")'
);
}

View file

@ -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 = () => {

View file

@ -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);
}
}

View file

@ -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 = {

View file

@ -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();

View file

@ -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;

View file

@ -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 };
});

View file

@ -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);

View file

@ -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;
}

View file

@ -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');
}

View file

@ -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,
{

View file

@ -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;

View file

@ -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)
});
});

View file

@ -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);

View 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);
}
});
});
});
});

View 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);
});
});
});

View file

@ -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()
}

View 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 }

View 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

View file

@ -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

Binary file not shown.