mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-30 22:34:10 -05:00
fix: unpublish and add or remove star colision (#1434)
* fix: unpublish and add or remove star colision The issue was the npm star use a similar payload, but we did not check properly the shape of the payload, this fix and allow unpublish correctly. Improve unit testing for publishing and unpublishing Add new code documentation for future changes. * chore: update secrets baseline * chore: add missing type this will requires update types in the future
This commit is contained in:
parent
8d51856dbf
commit
c264f944fb
22 changed files with 631 additions and 238 deletions
|
@ -3,7 +3,7 @@
|
|||
"files": null,
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2019-07-27T21:44:36Z",
|
||||
"generated_at": "2019-08-10T11:09:03Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"name": "AWSKeyDetector"
|
||||
|
@ -40,31 +40,26 @@
|
|||
"src/lib/auth-utils.ts": [
|
||||
{
|
||||
"hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8",
|
||||
"line_number": 10,
|
||||
"line_number": 12,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "ecb252044b5ea0f679ee78ec1a12904739e2904d",
|
||||
"line_number": 174,
|
||||
"line_number": 187,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "f35dd4c51c0a89bd055b5ad30c162c778981306d",
|
||||
"line_number": 179,
|
||||
"line_number": 192,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "45c43fe97e3a06ab078b0eeff6fbe622cc417a25",
|
||||
"line_number": 197,
|
||||
"line_number": 210,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
"src/lib/auth.ts": [
|
||||
{
|
||||
"hashed_secret": "3812d6abc055424d0556b35f48774c7b0044eac2",
|
||||
"line_number": 32,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "6981afa9890d125c05133d13053201f32292ec9f",
|
||||
"line_number": 38,
|
||||
|
@ -91,12 +86,12 @@
|
|||
"src/lib/constants.ts": [
|
||||
{
|
||||
"hashed_secret": "f34fbc9a9769ba9eff5aff3d008a6b49f85c08b1",
|
||||
"line_number": 14,
|
||||
"line_number": 15,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "b9343f1143ccb83555b450eb54dde96a05522ccc",
|
||||
"line_number": 115,
|
||||
"line_number": 116,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
@ -270,12 +265,12 @@
|
|||
"test/unit/modules/api/api.spec.ts": [
|
||||
{
|
||||
"hashed_secret": "97752a468368b0d6b192140d6a140c38fd0cbd8b",
|
||||
"line_number": 293,
|
||||
"line_number": 304,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "364bdf2ed77a8544d3b711a03b69eeadcc63c9d7",
|
||||
"line_number": 802,
|
||||
"line_number": 828,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
@ -304,12 +299,12 @@
|
|||
"test/unit/modules/auth/jwt.spec.ts": [
|
||||
{
|
||||
"hashed_secret": "364bdf2ed77a8544d3b711a03b69eeadcc63c9d7",
|
||||
"line_number": 121,
|
||||
"line_number": 118,
|
||||
"type": "Secret Keyword"
|
||||
},
|
||||
{
|
||||
"hashed_secret": "eaacdf2d9ed66df2601c8b51ab4084db14336d11",
|
||||
"line_number": 132,
|
||||
"line_number": 129,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -3,7 +3,7 @@ import Path from 'path';
|
|||
import mime from 'mime';
|
||||
|
||||
import { API_MESSAGE, HEADERS, DIST_TAGS, API_ERROR, HTTP_STATUS } from '../../../lib/constants';
|
||||
import { validateMetadata, isObject, ErrorCode } from '../../../lib/utils';
|
||||
import {validateMetadata, isObject, ErrorCode, hasDiffOneKey} from '../../../lib/utils';
|
||||
import { media, expectJson, allow } from '../../middleware';
|
||||
import { notify } from '../../../lib/notify';
|
||||
import star from './star';
|
||||
|
@ -12,14 +12,80 @@ import { Router } from 'express';
|
|||
import { Config, Callback, MergeTags, Version, Package } from '@verdaccio/types';
|
||||
import { IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler } from '../../../../types';
|
||||
import { logger } from '../../../lib/logger';
|
||||
import {isPublishablePackage} from "../../../lib/storage-utils";
|
||||
|
||||
export default function publish(router: Router, auth: IAuth, storage: IStorageHandler, config: Config) {
|
||||
const can = allow(auth);
|
||||
|
||||
// publishing a package
|
||||
router.put('/:package/:_rev?/:revision?', can('publish'), media(mime.getType('json')), expectJson, publishPackage(storage, config));
|
||||
/**
|
||||
* Publish a package / update package / un/start a package
|
||||
*
|
||||
* There are multiples scenarios here to considere:
|
||||
*
|
||||
* 1. Publish scenario
|
||||
*
|
||||
* Publish a package consist of at least 1 step (PUT) with a metadata payload.
|
||||
* When a package is published, an _attachment property is present that contains the data
|
||||
* of the tarball.
|
||||
*
|
||||
* Example flow of publish.
|
||||
*
|
||||
* npm http fetch PUT 201 http://localhost:4873/@scope%2ftest1 9627ms
|
||||
npm info lifecycle @scope/test1@1.0.1~publish: @scope/test1@1.0.1
|
||||
npm info lifecycle @scope/test1@1.0.1~postpublish: @scope/test1@1.0.1
|
||||
+ @scope/test1@1.0.1
|
||||
npm verb exit [ 0, true ]
|
||||
*
|
||||
*
|
||||
* 2. Unpublish scenario
|
||||
*
|
||||
* Unpublish consist in 3 steps.
|
||||
* 1. Try to fetch metadata -> if it fails, return 404
|
||||
* 2. Compute metadata locally (client side) and send a mutate payload excluding the version to be unpublished
|
||||
* eg: if metadata reflects 1.0.1, 1.0.2 and 1.0.3, the computed metadata won't include 1.0.3.
|
||||
* 3. Once the second step has been succesfully finished, delete the tarball.
|
||||
*
|
||||
* All these steps are consecutive and required, there is no transacions here, if step 3 fails, metadata might
|
||||
* get corrupted.
|
||||
*
|
||||
* Note the unpublish call will suffix in the url a /-rev/14-5d500cfce92f90fd revision number, this not
|
||||
* used internally.
|
||||
*
|
||||
*
|
||||
* Example flow of unpublish.
|
||||
*
|
||||
* npm http fetch GET 200 http://localhost:4873/@scope%2ftest1?write=true 1680ms
|
||||
npm http fetch PUT 201 http://localhost:4873/@scope%2ftest1/-rev/14-5d500cfce92f90fd 956606ms attempt #2
|
||||
npm http fetch GET 200 http://localhost:4873/@scope%2ftest1?write=true 1601ms
|
||||
npm http fetch DELETE 201 http://localhost:4873/@scope%2ftest1/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms
|
||||
*
|
||||
* 3. Star a package
|
||||
*
|
||||
* Permissions: start a package depends of the publish and unpublish permissions, there is no specific flag for star or un start.
|
||||
* The URL for star is similar to the unpublish (change package format)
|
||||
*
|
||||
* npm has no enpoint for star a package, rather mutate the metadata and acts as, the difference is the
|
||||
* users property which is part of the payload and the body only includes
|
||||
*
|
||||
* {
|
||||
"_id": pkgName,
|
||||
"_rev": "3-b0cdaefc9bdb77c8",
|
||||
"users": {
|
||||
[username]: boolean value (true, false)
|
||||
}
|
||||
}
|
||||
*
|
||||
*/
|
||||
router.put('/:package/:_rev?/:revision?', can('publish'), media(mime.getType('json')), expectJson, publishPackage(storage, config, auth));
|
||||
|
||||
// un-publishing an entire package
|
||||
/**
|
||||
* Un-publishing an entire package.
|
||||
*
|
||||
* This scenario happens when the first call detect there is only one version remaining
|
||||
* in the metadata, then the client decides to DELETE the resource
|
||||
* npm http fetch GET 304 http://localhost:4873/@scope%2ftest1?write=true 1076ms (from cache)
|
||||
npm http fetch DELETE 201 http://localhost:4873/@scope%2ftest1/-rev/18-d8ebe3020bd4ac9c 22ms
|
||||
*/
|
||||
router.delete('/:package/-rev/*', can('unpublish'), unPublishPackage(storage));
|
||||
|
||||
// removing a tarball
|
||||
|
@ -35,10 +101,13 @@ export default function publish(router: Router, auth: IAuth, storage: IStorageHa
|
|||
/**
|
||||
* Publish a package
|
||||
*/
|
||||
export function publishPackage(storage: IStorageHandler, config: Config) {
|
||||
export function publishPackage(storage: IStorageHandler, config: Config, auth: IAuth) {
|
||||
const starApi = star(storage);
|
||||
return function(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||
const packageName = req.params.package;
|
||||
|
||||
logger.debug({packageName} , `publishing or updating a new version for @{packageName}`);
|
||||
|
||||
/**
|
||||
* Write tarball of stream data from package clients.
|
||||
*/
|
||||
|
@ -72,9 +141,10 @@ export function publishPackage(storage: IStorageHandler, config: Config) {
|
|||
|
||||
const afterChange = function(error, okMessage, metadata) {
|
||||
const metadataCopy: Package = { ...metadata };
|
||||
|
||||
const { _attachments, versions } = metadataCopy;
|
||||
|
||||
// old npm behavior, if there is no attachments
|
||||
// if the is no attachments, it is change, it is a new package.
|
||||
if (_.isNil(_attachments)) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
|
@ -89,10 +159,14 @@ export function publishPackage(storage: IStorageHandler, config: Config) {
|
|||
// npm-registry-client 0.3+ embeds tarball into the json upload
|
||||
// https://github.com/isaacs/npm-registry-client/commit/e9fbeb8b67f249394f735c74ef11fe4720d46ca0
|
||||
// issue https://github.com/rlidwka/sinopia/issues/31, dealing with it here:
|
||||
if (isObject(_attachments) === false || Object.keys(_attachments).length !== 1 || isObject(versions) === false || Object.keys(versions).length !== 1) {
|
||||
const isInvalidBodyFormat = isObject(_attachments) === false || hasDiffOneKey(_attachments) ||
|
||||
isObject(versions) === false || hasDiffOneKey(versions);
|
||||
|
||||
if (isInvalidBodyFormat) {
|
||||
// npm is doing something strange again
|
||||
// if this happens in normal circumstances, report it as a bug
|
||||
return next(ErrorCode.getBadRequest('unsupported registry call'));
|
||||
logger.info({ packageName }, `wrong package format on publish a package @{packageName}`);
|
||||
return next(ErrorCode.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));
|
||||
}
|
||||
|
||||
if (error && error.status !== HTTP_STATUS.CONFLICT) {
|
||||
|
@ -100,7 +174,7 @@ export function publishPackage(storage: IStorageHandler, config: Config) {
|
|||
}
|
||||
|
||||
// at this point document is either created or existed before
|
||||
const firstAttachmentKey = Object.keys(_attachments)[0];
|
||||
const [firstAttachmentKey] = Object.keys(_attachments);
|
||||
|
||||
createTarball(Path.basename(firstAttachmentKey), _attachments[firstAttachmentKey], function(error) {
|
||||
if (error) {
|
||||
|
@ -134,23 +208,34 @@ export function publishPackage(storage: IStorageHandler, config: Config) {
|
|||
});
|
||||
};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(req.body, '_rev') && isObject(req.body.users)) {
|
||||
if (isPublishablePackage(req.body) === false && isObject(req.body.users)) {
|
||||
return starApi(req, res, next);
|
||||
}
|
||||
|
||||
try {
|
||||
const metadata = validateMetadata(req.body, packageName);
|
||||
if (req.params._rev) {
|
||||
logger.debug({packageName} , `updating a new version for @{packageName}`);
|
||||
// we check unpublish permissions, an update is basically remove versions
|
||||
const remote = req.remote_user;
|
||||
auth.allow_unpublish({packageName}, remote, (error, allowed) => {
|
||||
if (error) {
|
||||
logger.debug({packageName} , `not allowed to unpublish a version for @{packageName}`);
|
||||
return next(error);
|
||||
}
|
||||
|
||||
storage.changePackage(packageName, metadata, req.params.revision, function(error) {
|
||||
afterChange(error, API_MESSAGE.PKG_CHANGED, metadata);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
logger.debug({packageName} , `adding a new version for @{packageName}`);
|
||||
storage.addPackage(packageName, metadata, function(error) {
|
||||
afterChange(error, API_MESSAGE.PKG_CREATED, metadata);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({error}, 'error on publish, bad package data @{error}');
|
||||
logger.error({packageName}, 'error on publish, bad package data for @{packageName}');
|
||||
return next(ErrorCode.getBadData(API_ERROR.BAD_PACKAGE_DATA));
|
||||
}
|
||||
};
|
||||
|
@ -161,7 +246,10 @@ export function publishPackage(storage: IStorageHandler, config: Config) {
|
|||
*/
|
||||
export function unPublishPackage(storage: IStorageHandler) {
|
||||
return function(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||
storage.removePackage(req.params.package, function(err) {
|
||||
const packageName = req.params.package;
|
||||
|
||||
logger.debug({packageName} , `unpublishing @{packageName}`);
|
||||
storage.removePackage(packageName, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
@ -176,11 +264,17 @@ export function unPublishPackage(storage: IStorageHandler) {
|
|||
*/
|
||||
export function removeTarball(storage: IStorageHandler) {
|
||||
return function(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||
storage.removeTarball(req.params.package, req.params.filename, req.params.revision, function(err) {
|
||||
const packageName = req.params.package;
|
||||
const {filename, revision} = req.params;
|
||||
|
||||
logger.debug({packageName, filename, revision} , `removing a tarball for @{packageName}-@{tarballName}-@{revision}`);
|
||||
storage.removeTarball(packageName, filename, revision, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(HTTP_STATUS.CREATED);
|
||||
|
||||
logger.debug({packageName, filename, revision} , `success remove tarball for @{packageName}-@{tarballName}-@{revision}`);
|
||||
return next({ ok: API_MESSAGE.TARBALL_REMOVED });
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import { USERS, HTTP_STATUS } from '../../../lib/constants';
|
|||
import {Response} from 'express';
|
||||
import {$RequestExtend, $NextFunctionVer, IStorageHandler} from '../../../../types';
|
||||
import _ from 'lodash';
|
||||
import { logger } from '../../../lib/logger';
|
||||
|
||||
export default function(storage: IStorageHandler) {
|
||||
const validateInputs = (newUsers, localUsers, username, isStar) => {
|
||||
|
@ -21,6 +22,7 @@ export default function(storage: IStorageHandler) {
|
|||
|
||||
return (req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
|
||||
const name = req.params.package;
|
||||
logger.debug({name}, 'starring a package for @{name}');
|
||||
const afterChangePackage = function(err?: Error) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { validateName as utilValidateName, validatePackage as utilValidatePackage, getVersionFromTarball, isObject, ErrorCode } from '../lib/utils';
|
||||
import { API_ERROR, HEADER_TYPE, HEADERS, HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER } from '../lib/constants';
|
||||
import { stringToMD5 } from '../lib/crypto-utils';
|
||||
import { $ResponseExtend, $RequestExtend, $NextFunctionVer, IAuth } from '../../types';
|
||||
import { Config, Package } from '@verdaccio/types';
|
||||
import { Config, Package, RemoteUser } from '@verdaccio/types';
|
||||
import { logger } from '../lib/logger';
|
||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||
|
||||
|
@ -108,9 +103,10 @@ export function allow(auth: IAuth): Function {
|
|||
req.pause();
|
||||
const packageName = req.params.scope ? `@${req.params.scope}/${req.params.package}` : req.params.package;
|
||||
const packageVersion = req.params.filename ? getVersionFromTarball(req.params.filename) : undefined;
|
||||
const remote: RemoteUser = req.remote_user;
|
||||
logger.trace({ action, user: remote.name }, `[middleware/allow][@{action}] allow for @{user}`);
|
||||
|
||||
// $FlowFixMe
|
||||
auth['allow_' + action]({ packageName, packageVersion }, req.remote_user, function(error, allowed): void {
|
||||
auth['allow_' + action]({ packageName, packageVersion }, remote, function(error, allowed): void {
|
||||
req.resume();
|
||||
if (error) {
|
||||
next(error);
|
||||
|
|
|
@ -6,6 +6,8 @@ import { RemoteUser, Package, Callback, Config, Security, APITokenOptions, JWTOp
|
|||
import { CookieSessionToken, IAuthWebUI, AuthMiddlewarePayload, AuthTokenHeader, BasicPayload } from '../../types';
|
||||
import { aesDecrypt, verifyPayload } from './crypto-utils';
|
||||
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
export function validatePassword(password: string, minLength: number = DEFAULT_MIN_LIMIT_PASSWORD): boolean {
|
||||
return typeof password === 'string' && password.length >= minLength;
|
||||
}
|
||||
|
@ -40,10 +42,14 @@ export function createAnonymousRemoteUser(): RemoteUser {
|
|||
|
||||
export function allow_action(action: string): Function {
|
||||
return function(user: RemoteUser, pkg: Package, callback: Callback): void {
|
||||
logger.trace({remote: user.name}, `[auth/allow_action]: user: @{user.name}`);
|
||||
const { name, groups } = user;
|
||||
const hasPermission = pkg[action].some(group => name === group || groups.includes(group));
|
||||
const groupAccess = pkg[action];
|
||||
const hasPermission = groupAccess.some(group => name === group || groups.includes(group));
|
||||
logger.trace({pkgName: pkg.name, hasPermission, remote: user.name, groupAccess}, `[auth/allow_action]: hasPermission? @{hasPermission} for user: @{user}`);
|
||||
|
||||
if (hasPermission) {
|
||||
logger.trace({remote: user.name}, `auth/allow_action: access granted to: @{user}`);
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
|
@ -55,15 +61,22 @@ export function allow_action(action: string): Function {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function handleSpecialUnpublish(): any {
|
||||
return function(user: RemoteUser, pkg: Package, callback: Callback): void {
|
||||
const action = 'unpublish';
|
||||
const hasSupport: boolean = _.isNil(pkg[action]) === false ? pkg[action] : false;
|
||||
// verify whether the unpublish prop has been defined
|
||||
const isUnpublishMissing: boolean = _.isNil(pkg[action]);
|
||||
const hasGroups: boolean = isUnpublishMissing ? false : pkg[action].length > 0;
|
||||
logger.trace({user: user.name, name: pkg.name, hasGroups}, `fallback unpublish for @{name} has groups: @{hasGroups} for @{user}`);
|
||||
|
||||
if (hasSupport === false) {
|
||||
if (isUnpublishMissing || hasGroups === false) {
|
||||
return callback(null, undefined);
|
||||
}
|
||||
|
||||
logger.trace({user: user.name, name: pkg.name, action, hasGroups}, `allow_action for @{action} for @{name} has groups: @{hasGroups} for @{user}`);
|
||||
return allow_action(action)(user, pkg, callback);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -71,27 +71,32 @@ class Auth implements IAuth {
|
|||
}
|
||||
|
||||
for (const plugin of validPlugins) {
|
||||
this.logger.trace({ username }, 'updating password for @{username}');
|
||||
plugin.changePassword(
|
||||
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 },
|
||||
{username, err},
|
||||
`An error has been produced
|
||||
updating the password for @{username}. Error: @{err.message}`
|
||||
);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
this.logger.trace({ username }, 'updated password for @{username} was successful');
|
||||
this.logger.trace({username}, 'updated password for @{username} was successful');
|
||||
return cb(null, profile);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public authenticate(username: string, password: string, cb: Callback): void {
|
||||
const plugins = this.plugins.slice(0);
|
||||
|
@ -152,7 +157,7 @@ class Auth implements IAuth {
|
|||
// p.add_user() execution
|
||||
plugin[method](user, password, function(err, ok): void {
|
||||
if (err) {
|
||||
self.logger.trace({ user, err }, 'the user @{user} could not being added. Error: @{err}');
|
||||
self.logger.trace({ user, err: err.message }, 'the user @{user} could not being added. Error: @{err}');
|
||||
return cb(err);
|
||||
}
|
||||
if (ok) {
|
||||
|
@ -203,6 +208,7 @@ class Auth implements IAuth {
|
|||
|
||||
for (const plugin of this.plugins) {
|
||||
if (_.isNil(plugin) || _.isFunction(plugin.allow_unpublish) === false) {
|
||||
this.logger.trace({ packageName }, 'allow unpublish for @{packageName} plugin does not implement allow_unpublish');
|
||||
continue;
|
||||
} else {
|
||||
plugin.allow_unpublish!(
|
||||
|
|
|
@ -10,6 +10,7 @@ export const DEFAULT_DOMAIN = 'localhost';
|
|||
export const TIME_EXPIRATION_24H = '24h';
|
||||
export const TIME_EXPIRATION_7D = '7d';
|
||||
export const DIST_TAGS = 'dist-tags';
|
||||
export const LATEST = 'latest';
|
||||
export const USERS = 'users';
|
||||
export const DEFAULT_MIN_LIMIT_PASSWORD = 3;
|
||||
export const DEFAULT_USER = 'Anonymous';
|
||||
|
@ -128,6 +129,7 @@ export const API_ERROR = {
|
|||
MAX_USERS_REACHED: 'maximum amount of users reached',
|
||||
VERSION_NOT_EXIST: "this version doesn't exist",
|
||||
FILE_NOT_FOUND: 'File not found',
|
||||
UNSUPORTED_REGISTRY_CALL: 'unsupported registry call',
|
||||
BAD_STATUS_CODE: 'bad status code',
|
||||
PACKAGE_EXIST: 'this package is already present',
|
||||
BAD_AUTH_HEADER: 'bad authorization header',
|
||||
|
|
|
@ -58,6 +58,7 @@ class LocalStorage implements IStorage {
|
|||
*/
|
||||
public removePackage(name: string, callback: Callback): void {
|
||||
const storage: any = this._getLocalStorage(name);
|
||||
this.logger.debug({ name }, `[storage] removing package @{name}`);
|
||||
|
||||
if (_.isNil(storage)) {
|
||||
return callback(ErrorCode.getNotFound());
|
||||
|
@ -77,6 +78,8 @@ class LocalStorage implements IStorage {
|
|||
this.localData.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}`);
|
||||
|
||||
return callback(ErrorCode.getBadData(removeFailed.message));
|
||||
}
|
||||
|
||||
|
@ -309,9 +312,11 @@ class LocalStorage implements IStorage {
|
|||
*/
|
||||
public changePackage(name: string, incomingPkg: Package, revision: string | void, callback: Callback): void {
|
||||
if (!isObject(incomingPkg.versions) || !isObject(incomingPkg[DIST_TAGS])) {
|
||||
this.logger.debug({name}, `changePackage bad data for @{name}`);
|
||||
return callback(ErrorCode.getBadData());
|
||||
}
|
||||
|
||||
this.logger.debug({name}, `changePackage udapting package for @{name}`);
|
||||
this._updatePackage(
|
||||
name,
|
||||
(localData, cb): void => {
|
||||
|
@ -740,6 +745,7 @@ class LocalStorage implements IStorage {
|
|||
}
|
||||
|
||||
private _deleteAttachments(storage: any, attachments: string[], callback: Callback): void {
|
||||
this.logger.debug({l: attachments.length }, `[storage/_deleteAttachments] delete attachments total: @{l}`);
|
||||
const unlinkNext = function(cb): void {
|
||||
if (_.isEmpty(attachments)) {
|
||||
return cb();
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { ErrorCode, isObject, normalizeDistTags, semverSort } from './utils';
|
||||
import Search from './search';
|
||||
|
@ -249,3 +244,13 @@ export function prepareSearchPackage(data: Package, time: unknown): any {
|
|||
return pkg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the package metadta has enough data to be published
|
||||
* @param pkg metadata
|
||||
*/
|
||||
export function isPublishablePackage(pkg: Package): boolean {
|
||||
const keys: string[] = Object.keys(pkg);
|
||||
|
||||
return _.includes(keys, 'versions');
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ export function getLocalRegistryTarballUri(uri: string, pkgName: string, req: Re
|
|||
const protocol = getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol);
|
||||
const domainRegistry = combineBaseUrl(protocol, headers.host, urlPrefix);
|
||||
|
||||
return `${domainRegistry}/${pkgName.replace(/\//g, '%2f')}/-/${tarballName}`;
|
||||
return `${domainRegistry}/${encodeScopedUri(pkgName)}/-/${tarballName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -591,3 +591,11 @@ export function pad(str, max): string {
|
|||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function encodeScopedUri(packageName) {
|
||||
return packageName.replace(/\//g, '%2f');
|
||||
}
|
||||
|
||||
export function hasDiffOneKey(versions) {
|
||||
return Object.keys(versions).length !== 1;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import _ from 'lodash';
|
||||
import request from 'supertest';
|
||||
|
||||
import {HEADER_TYPE, HEADERS, HTTP_STATUS, TOKEN_BEARER} from '../../../src/lib/constants';
|
||||
import {buildToken} from '../../../src/lib/utils';
|
||||
import {buildToken, encodeScopedUri} from '../../../src/lib/utils';
|
||||
import { Package } from '@verdaccio/types';
|
||||
import {getTaggedVersionFromPackage} from "./expects";
|
||||
import {generateRandomHexString} from "../../../src/lib/crypto-utils";
|
||||
|
||||
// API Helpers
|
||||
|
||||
|
@ -13,14 +18,42 @@ import { Package } from '@verdaccio/types';
|
|||
export function putPackage(
|
||||
request: any,
|
||||
pkgName: string,
|
||||
publishMetadata: Package
|
||||
publishMetadata: Package,
|
||||
token?: string,
|
||||
httpStatus: number = HTTP_STATUS.CREATED): Promise<any[]> {
|
||||
return new Promise((resolve) => {
|
||||
let put = request.put(pkgName)
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.send(JSON.stringify(publishMetadata));
|
||||
|
||||
if (_.isEmpty(token) === false ) {
|
||||
expect(token).toBeDefined();
|
||||
put.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token as string))
|
||||
}
|
||||
|
||||
put.set('accept', 'gzip')
|
||||
.set('accept-encoding', HEADERS.JSON)
|
||||
.expect(HTTP_STATUS.CREATED)
|
||||
.end(function(err, res) {
|
||||
resolve([err, res]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function deletePackage(
|
||||
request: any,
|
||||
pkgName: string,
|
||||
token?: string
|
||||
): Promise<any[]> {
|
||||
return new Promise((resolve) => {
|
||||
request.put(pkgName)
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.send(JSON.stringify(publishMetadata))
|
||||
.set('accept', 'gzip')
|
||||
.set('accept-encoding', HEADERS.JSON)
|
||||
let del = request.put(`/${encodeScopedUri(pkgName)}/-rev/${generateRandomHexString(8)}`)
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);
|
||||
|
||||
if (_.isNil(token) === false ) {
|
||||
del.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token as string))
|
||||
}
|
||||
|
||||
del.set('accept-encoding', HEADERS.JSON)
|
||||
.expect(HTTP_STATUS.CREATED)
|
||||
.end(function(err, res) {
|
||||
resolve([err, res]);
|
||||
|
@ -30,13 +63,17 @@ export function putPackage(
|
|||
|
||||
export function getPackage(
|
||||
request: any,
|
||||
header: string,
|
||||
pkg: string,
|
||||
token: string,
|
||||
pkgName: string,
|
||||
statusCode: number = HTTP_STATUS.OK): Promise<any[]> {
|
||||
// $FlowFixMe
|
||||
return new Promise((resolve) => {
|
||||
request.get(`/${pkg}`)
|
||||
.set(HEADERS.AUTHORIZATION, header)
|
||||
let getRequest = request.get(`/${pkgName}`);
|
||||
|
||||
if (_.isNil(token) === false || _.isEmpty(token) === false) {
|
||||
getRequest.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token));
|
||||
}
|
||||
|
||||
getRequest
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.expect(statusCode)
|
||||
.end(function(err, res) {
|
||||
|
@ -116,3 +153,32 @@ export function postProfile(request: any, body: any, token: string, statusCode:
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPackageByVersionAndTag(app, encodedPkgName, pkgName, version, tag = 'latest') {
|
||||
// we retrieve the package to verify
|
||||
const [err, resp]= await getPackage(request(app), '', encodedPkgName);
|
||||
|
||||
expect(err).toBeNull();
|
||||
|
||||
// we check whether the latest version match with the previous published one
|
||||
return getTaggedVersionFromPackage(resp.body, pkgName, tag, version);
|
||||
}
|
||||
|
||||
export async function isExistPackage(app, packageName) {
|
||||
const [err]= await getPackage(request(app), '', encodeScopedUri(packageName), HTTP_STATUS.OK);
|
||||
|
||||
return _.isNull(err);
|
||||
}
|
||||
|
||||
export async function verifyPackageVersionDoesExist(app, packageName, version, token?: string) {
|
||||
const [, res]= await getPackage(request(app), token as string, encodeScopedUri(packageName), HTTP_STATUS.OK);
|
||||
|
||||
const { versions } = res.body;
|
||||
const versionsKeys = Object.keys(versions);
|
||||
|
||||
return versionsKeys.includes(version) === false;
|
||||
}
|
||||
|
||||
export function generateUnPublishURI(pkgName) {
|
||||
return `/${encodeScopedUri(pkgName)}/-rev/${generateRandomHexString(8)}`;
|
||||
}
|
||||
|
|
20
test/unit/__helper/expects.ts
Normal file
20
test/unit/__helper/expects.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {DIST_TAGS, LATEST} from "../../../src/lib/constants";
|
||||
|
||||
|
||||
/**
|
||||
* Verify whether the package tag match with the desired version.
|
||||
*/
|
||||
export function getTaggedVersionFromPackage(pkg, pkgName, tag: string = LATEST, version: string) {
|
||||
// extract the tagged version
|
||||
const taggedVersion = pkg[DIST_TAGS][tag];
|
||||
expect(taggedVersion).toBeDefined();
|
||||
expect(taggedVersion).toEqual(version);
|
||||
|
||||
// the version must exist
|
||||
const latestPkg = pkg.versions[taggedVersion];
|
||||
expect(latestPkg).toBeDefined();
|
||||
// the name must match
|
||||
expect(latestPkg.name).toEqual(pkgName);
|
||||
|
||||
return latestPkg;
|
||||
}
|
|
@ -1,17 +1,123 @@
|
|||
import { Package } from "@verdaccio/types";
|
||||
|
||||
export function generatePackageMetadata(pkgName: string): Package {
|
||||
export function generateAttachment(pkgName, version) {
|
||||
return {
|
||||
"content_type": "application\/octet-stream",
|
||||
"data": "H4sIAAAAAAAAE+2W32vbMBDH85y\/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo\/\/79KPeQsnIw5KUDX\/9IOvurLuz\/DHSjK\/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF\/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI\/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS\/pLQe+D+FIv\/agIWI6GX66kFuIhT+1gDjrp\/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0\/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi\/IHpU9fz3\/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6\/f88f\/Pu47zomiPk2Lv\/dOv8h+P\/34\/D\/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=",
|
||||
"length": 512
|
||||
}
|
||||
}
|
||||
|
||||
export function generateVersion(pkgName, version) {
|
||||
return {
|
||||
"name": pkgName,
|
||||
"version": version,
|
||||
"description": "some foo dependency",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "User NPM",
|
||||
"email": "user@domain.com"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"verdaccio": "^4.0.0"
|
||||
},
|
||||
"readme": "# test",
|
||||
"readmeFilename": "README.md",
|
||||
"_id": `${pkgName}@${version}`,
|
||||
"_npmVersion": "5.5.1",
|
||||
"_npmUser": {
|
||||
'name': 'foo',
|
||||
},
|
||||
"dist": {
|
||||
"integrity": "sha512-6gHiERpiDgtb3hjqpQH5\/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==",
|
||||
"shasum": "2c03764f651a9f016ca0b7620421457b619151b9", // pragma: allowlist secret
|
||||
"tarball": `http:\/\/localhost:5555\/${pkgName}\/-\/${pkgName}-${version}.tgz`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a metadata body including attachments.
|
||||
* If you intent to build a body for npm publish, please include only one version.
|
||||
* if you intent to to generate a complete metadata include multiple versions.
|
||||
*/
|
||||
export function generatePackageBody(pkgName: string, _versions: string[] = ['1.0.0']): Package {
|
||||
const latest: string = _versions[_versions.length - 1];
|
||||
const versions = _versions.reduce((cat, version) => {
|
||||
cat[version] = generateVersion(pkgName, version);
|
||||
return cat;
|
||||
}, {});
|
||||
|
||||
const attachtment = _versions.reduce((cat, version) => {
|
||||
cat[`${pkgName}-${version}.tgz`] = generateAttachment(pkgName, version);
|
||||
return cat;
|
||||
}, {});
|
||||
|
||||
// @ts-ignore
|
||||
return {
|
||||
"_id": pkgName,
|
||||
"name": pkgName,
|
||||
"readme": "# test",
|
||||
"dist-tags": {
|
||||
"latest": latest
|
||||
},
|
||||
"versions": versions,
|
||||
"_attachments": attachtment
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The metadata that comes from npm unpublish only contains the versions won't be removed and
|
||||
* also does not includes any _attachment.
|
||||
* @param pkgName
|
||||
* @param _versions
|
||||
*/
|
||||
export function generatePackageUnpublish(pkgName: string, _versions: string[] = ['1.0.0']): Package {
|
||||
const latest: string = _versions[_versions.length - 1];
|
||||
const versions = _versions.reduce((cat, version) => {
|
||||
cat[version] = generateVersion(pkgName, version);
|
||||
return cat;
|
||||
}, {});
|
||||
|
||||
// @ts-ignore
|
||||
return {
|
||||
"_id": pkgName,
|
||||
"name": pkgName,
|
||||
"readme": "# test",
|
||||
// users usually is present when run npm star [pkg]
|
||||
"users": {},
|
||||
"dist-tags": {
|
||||
"latest": latest
|
||||
},
|
||||
"versions": versions,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateStarMedatada(pkgName: string, users): any {
|
||||
return {
|
||||
"_id": pkgName,
|
||||
"_rev": "3-b0cdaefc9bdb77c8",
|
||||
"users": users
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePackageMetadata(pkgName: string, version: string = '1.0.0'): Package {
|
||||
// @ts-ignore
|
||||
return {
|
||||
"_id": pkgName,
|
||||
"name": pkgName,
|
||||
"dist-tags": {
|
||||
"latest": "1.0.0"
|
||||
"latest": version
|
||||
},
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
[version]: {
|
||||
"name": pkgName,
|
||||
"version": "1.0.0",
|
||||
"version": version,
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -30,7 +136,7 @@ export function generatePackageMetadata(pkgName: string): Package {
|
|||
},
|
||||
"readme": "# test",
|
||||
"readmeFilename": "README.md",
|
||||
"_id": `${pkgName}@1.0.0`,
|
||||
"_id": `${pkgName}@${version}`,
|
||||
"_npmVersion": "5.5.1",
|
||||
"_npmUser": {
|
||||
'name': 'foo',
|
||||
|
@ -38,13 +144,13 @@ export function generatePackageMetadata(pkgName: string): Package {
|
|||
"dist": {
|
||||
"integrity": "sha512-6gHiERpiDgtb3hjqpQH5\/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==",
|
||||
"shasum": "2c03764f651a9f016ca0b7620421457b619151b9", // pragma: allowlist secret
|
||||
"tarball": `http:\/\/localhost:5555\/${pkgName}\/-\/${pkgName}-1.0.0.tgz`
|
||||
"tarball": `http:\/\/localhost:5555\/${pkgName}\/-\/${pkgName}-${version}.tgz`
|
||||
}
|
||||
}
|
||||
},
|
||||
"readme": "# test",
|
||||
"_attachments": {
|
||||
[`${pkgName}-1.0.0.tgz`]: {
|
||||
[`${pkgName}-${version}.tgz`]: {
|
||||
"content_type": "application\/octet-stream",
|
||||
"data": "H4sIAAAAAAAAE+2W32vbMBDH85y\/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo\/\/79KPeQsnIw5KUDX\/9IOvurLuz\/DHSjK\/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF\/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI\/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS\/pLQe+D+FIv\/agIWI6GX66kFuIhT+1gDjrp\/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0\/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi\/IHpU9fz3\/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6\/f88f\/Pu47zomiPk2Lv\/dOv8h+P\/34\/D\/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=",
|
||||
"length": 512
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Publish endpoints - publish package should add a new package 1`] = `
|
||||
exports[`Publish endpoints - publish package should change the existing package 1`] = `[MockFunction]`;
|
||||
|
||||
exports[`Publish endpoints - publish package should publish a new a new package 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
|
@ -23,31 +25,7 @@ exports[`Publish endpoints - publish package should add a new package 1`] = `
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Publish endpoints - publish package should change the existing package 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"verdaccio",
|
||||
Object {
|
||||
"dist-tags": Object {},
|
||||
"name": "verdaccio",
|
||||
"time": Object {},
|
||||
"versions": Object {},
|
||||
},
|
||||
undefined,
|
||||
[Function],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Publish endpoints - publish package should star a package 1`] = `
|
||||
exports[`Publish endpoints - publish package test start should star a package 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
|
|
|
@ -5,19 +5,30 @@ import rimraf from 'rimraf';
|
|||
|
||||
import configDefault from '../../partials/config';
|
||||
import publishMetadata from '../../partials/publish-api';
|
||||
import starMetadata from '../../partials/star-api';
|
||||
import endPointAPI from '../../../../src/api';
|
||||
|
||||
import {HEADERS, API_ERROR, HTTP_STATUS, HEADER_TYPE, API_MESSAGE, TOKEN_BEARER} from '../../../../src/lib/constants';
|
||||
import {
|
||||
HEADERS,
|
||||
API_ERROR,
|
||||
HTTP_STATUS,
|
||||
HEADER_TYPE,
|
||||
API_MESSAGE,
|
||||
TOKEN_BEARER,
|
||||
} from '../../../../src/lib/constants';
|
||||
import {mockServer} from '../../__helper/mock';
|
||||
import {DOMAIN_SERVERS} from '../../../functional/config.functional';
|
||||
import {buildToken} from '../../../../src/lib/utils';
|
||||
import {getNewToken, putPackage} from '../../__helper/api';
|
||||
import { generatePackageMetadata } from '../../__helper/utils';
|
||||
import {buildToken, encodeScopedUri} from '../../../../src/lib/utils';
|
||||
import {
|
||||
getNewToken,
|
||||
putPackage,
|
||||
verifyPackageVersionDoesExist, generateUnPublishURI
|
||||
} from '../../__helper/api';
|
||||
import {generatePackageMetadata, generatePackageUnpublish, generateStarMedatada} from '../../__helper/utils';
|
||||
|
||||
require('../../../../src/lib/logger').setup([
|
||||
{ type: 'stdout', format: 'pretty', level: 'info' }
|
||||
{ type: 'stdout', format: 'pretty', level: 'warn' }
|
||||
]);
|
||||
|
||||
const credentials = { name: 'jota', password: 'secretPass' };
|
||||
|
||||
const putVersion = (app, name, publishMetadata) => {
|
||||
|
@ -625,60 +636,120 @@ describe('endpoint unit test', () => {
|
|||
});
|
||||
|
||||
describe('should test publish/unpublish api', () => {
|
||||
test('should publish a new package with no credentials', async (done) => {
|
||||
// const token = await putPackage('@scope%2fpk1-test');
|
||||
/**
|
||||
* It publish 2 versions and unpublish the latest one, then verifies
|
||||
* the version do not exist anymore in the body of the metadata.
|
||||
*/
|
||||
const runPublishUnPublishFlow = async (pkgName: string, done, token?: string) => {
|
||||
const version = '2.0.0';
|
||||
const pkg = generatePackageMetadata(pkgName, version);
|
||||
|
||||
request(app)
|
||||
.put('/@scope%2fpk1-test')
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.send(JSON.stringify(publishMetadata))
|
||||
.expect(HTTP_STATUS.CREATED)
|
||||
.end(function(err, res) {
|
||||
const [err] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`, pkg, token);
|
||||
if (err) {
|
||||
expect(err).toBeNull();
|
||||
return done(err);
|
||||
}
|
||||
expect(res.body.ok).toBeDefined();
|
||||
expect(res.body.success).toBeDefined();
|
||||
expect(res.body.success).toBeTruthy();
|
||||
expect(res.body.ok).toMatch(API_MESSAGE.PKG_CREATED);
|
||||
done();
|
||||
});
|
||||
|
||||
const newVersion = '2.0.1';
|
||||
const [newErr] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`,
|
||||
generatePackageMetadata(pkgName, newVersion), token);
|
||||
if (newErr) {
|
||||
expect(newErr).toBeNull();
|
||||
return done(newErr);
|
||||
}
|
||||
|
||||
const deletePayload = generatePackageUnpublish(pkgName, ['2.0.0']);
|
||||
const [err2, res2] = await putPackage(request(app), generateUnPublishURI(pkgName), deletePayload, token);
|
||||
|
||||
expect(err2).toBeNull();
|
||||
expect(res2.body.ok).toMatch(API_MESSAGE.PKG_CHANGED);
|
||||
|
||||
const existVersion = await verifyPackageVersionDoesExist(app, pkgName, newVersion, token);
|
||||
expect(existVersion).toBeTruthy();
|
||||
|
||||
return done();
|
||||
};
|
||||
|
||||
describe('un/publish scenarios with credentials', () => {
|
||||
test('should flow with no credentials', async (done) => {
|
||||
const pkgName = '@public-anyone-can-publish/pk1-test';
|
||||
runPublishUnPublishFlow(pkgName, done, undefined);
|
||||
});
|
||||
|
||||
test('should unpublish a new package with credentials', async (done) => {
|
||||
|
||||
test('should flow with credentials', async (done) => {
|
||||
const credentials = { name: 'jota_unpublish', password: 'secretPass' };
|
||||
const token = await getNewToken(request(app), credentials);
|
||||
//FUTURE: for some reason it does not remove the scope folder
|
||||
request(app)
|
||||
.del('/@scope%2fpk1-test/-rev/4-6abcdb4efd41a576')
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.expect(HTTP_STATUS.CREATED)
|
||||
.end(function(err, res) {
|
||||
if (err) {
|
||||
expect(err).toBeNull();
|
||||
return done(err);
|
||||
}
|
||||
expect(res.body.ok).toBeDefined();
|
||||
expect(res.body.ok).toMatch(API_MESSAGE.PKG_REMOVED);
|
||||
done();
|
||||
const pkgName = '@only-one-can-publish/pk1-test';
|
||||
|
||||
runPublishUnPublishFlow(pkgName, done, token);
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail due non-unpublish nobody can unpublish', async (done) => {
|
||||
describe('test error handling', () => {
|
||||
test('should fail if user is not allowed to unpublish', async (done) => {
|
||||
/**
|
||||
* Context:
|
||||
*
|
||||
* 'non-unpublish':
|
||||
access: $authenticated
|
||||
publish: jota_unpublish_fail
|
||||
# There is some conditions to keep on mind here
|
||||
# - If unpublish is empty, fallback with the publish value
|
||||
# - If the user has permissions to publish and this empty it will be allowed to unpublish
|
||||
# - If we want to forbid anyone to unpublish, just write here any unexisting user
|
||||
unpublish: none
|
||||
|
||||
The result of this test should fail and even if jota_unpublish_fail is allowed to publish.
|
||||
|
||||
*
|
||||
*/
|
||||
const credentials = { name: 'jota_unpublish_fail', password: 'secretPass' };
|
||||
const pkgName = 'non-unpublish';
|
||||
const newVersion = '1.0.0';
|
||||
const token = await getNewToken(request(app), credentials);
|
||||
request(app)
|
||||
.del('/non-unpublish/-rev/4-6abcdb4efd41a576')
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.end(function(err, res) {
|
||||
expect(err).toBeNull();
|
||||
expect(res.body.error).toBeDefined();
|
||||
expect(res.body.error).toMatch(/user jota_unpublish_fail is not allowed to unpublish package non-unpublish/);
|
||||
|
||||
const [newErr] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`,
|
||||
generatePackageMetadata(pkgName, newVersion), token);
|
||||
if (newErr) {
|
||||
expect(newErr).toBeNull();
|
||||
return done(newErr);
|
||||
}
|
||||
|
||||
const deletePayload = generatePackageUnpublish(pkgName, ['2.0.0']);
|
||||
const [err2, res2] = await putPackage(request(app), generateUnPublishURI(pkgName), deletePayload, token, HTTP_STATUS.FORBIDDEN);
|
||||
|
||||
expect(err2).not.toBeNull();
|
||||
expect(res2.body.error).toMatch(/user jota_unpublish_fail is not allowed to unpublish package non-unpublish/);
|
||||
done();
|
||||
});
|
||||
|
||||
test('should fail if publish prop is not defined', async (done) => {
|
||||
/**
|
||||
* Context:
|
||||
*
|
||||
* 'non-unpublish':
|
||||
access: $authenticated
|
||||
publish: jota_unpublish_fail
|
||||
# There is some conditions to keep on mind here
|
||||
# - If unpublish is empty, fallback with the publish value
|
||||
# - If the user has permissions to publish and this empty it will be allowed to unpublish
|
||||
# - If we want to forbid anyone to unpublish, just write here any unexisting user
|
||||
unpublish: none
|
||||
|
||||
The result of this test should fail and even if jota_unpublish_fail is allowed to publish.
|
||||
|
||||
*
|
||||
*/
|
||||
const credentials = { name: 'jota_only_unpublish_fail', password: 'secretPass' };
|
||||
const pkgName = 'only-unpublish';
|
||||
const newVersion = '1.0.0';
|
||||
const token = await getNewToken(request(app), credentials);
|
||||
|
||||
const [newErr, resp] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`,
|
||||
generatePackageMetadata(pkgName, newVersion), token);
|
||||
|
||||
expect(newErr).not.toBeNull();
|
||||
expect(resp.body.error).toMatch(/user jota_only_unpublish_fail is not allowed to publish package only-unpublish/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -754,21 +825,22 @@ describe('endpoint unit test', () => {
|
|||
|
||||
describe('should test star and stars api', () => {
|
||||
const pkgName = '@scope/starPackage';
|
||||
const credentials = { name: 'jota_star', password: 'secretPass' };
|
||||
let token = '';
|
||||
beforeAll(async (done) =>{
|
||||
await putPackage(request(app), `/${pkgName}`, generatePackageMetadata(pkgName));
|
||||
token = await getNewToken(request(app), credentials);
|
||||
await putPackage(request(app), `/${pkgName}`, generatePackageMetadata(pkgName), token);
|
||||
done();
|
||||
});
|
||||
|
||||
test('should star a package', (done) => {
|
||||
request(app)
|
||||
.put(`/${pkgName}`)
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.send(JSON.stringify({
|
||||
...starMetadata,
|
||||
users: {
|
||||
.send(JSON.stringify(generateStarMedatada(pkgName, {
|
||||
[credentials.name]: true
|
||||
}
|
||||
}))
|
||||
})))
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function(err, res) {
|
||||
if (err) {
|
||||
|
@ -785,7 +857,8 @@ describe('endpoint unit test', () => {
|
|||
request(app)
|
||||
.put(`/${pkgName}`)
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.send(JSON.stringify(starMetadata))
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.send(JSON.stringify(generateStarMedatada(pkgName, {})))
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function(err, res) {
|
||||
if (err) {
|
||||
|
@ -799,18 +872,11 @@ describe('endpoint unit test', () => {
|
|||
});
|
||||
|
||||
test('should retrieve stars list with credentials', async (done) => {
|
||||
const credentials = { name: 'star_user', password: 'secretPass' };
|
||||
const token = await getNewToken(request(app), credentials);
|
||||
request(app)
|
||||
.put(`/${pkgName}`)
|
||||
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
|
||||
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
|
||||
.send(JSON.stringify({
|
||||
...starMetadata,
|
||||
users: {
|
||||
[credentials.name]: true
|
||||
}
|
||||
}))
|
||||
.send(generateStarMedatada(pkgName, {[credentials.name]: true}))
|
||||
.expect(HTTP_STATUS.OK).end(function(err) {
|
||||
if (err) {
|
||||
expect(err).toBeNull();
|
||||
|
|
|
@ -244,7 +244,7 @@ describe('Publish endpoints - publish package', () => {
|
|||
expect(storage.changePackage).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should add a new package', () => {
|
||||
test('should publish a new a new package', () => {
|
||||
const storage = {
|
||||
addPackage: jest.fn(),
|
||||
};
|
||||
|
@ -266,6 +266,7 @@ describe('Publish endpoints - publish package', () => {
|
|||
expect(next).toHaveBeenCalledWith(new Error(API_ERROR.BAD_PACKAGE_DATA));
|
||||
});
|
||||
|
||||
describe('test start', () => {
|
||||
test('should star a package', () => {
|
||||
const storage = {
|
||||
changePackage: jest.fn(),
|
||||
|
@ -294,4 +295,5 @@ describe('Publish endpoints - publish package', () => {
|
|||
publishPackage(storage)(req, res, next);
|
||||
expect(storage.changePackage).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import request from 'supertest';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
|
||||
|
@ -8,19 +7,15 @@ import endPointAPI from '../../../../src/api';
|
|||
import {HEADERS, HTTP_STATUS, HEADER_TYPE, TOKEN_BEARER, TOKEN_BASIC, API_ERROR} from '../../../../src/lib/constants';
|
||||
import {mockServer} from '../../__helper/mock';
|
||||
import {DOMAIN_SERVERS} from '../../../functional/config.functional';
|
||||
import {buildToken, parseConfigFile} from '../../../../src/lib/utils';
|
||||
import {parseConfigurationFile} from '../../__helper';
|
||||
import {buildToken} from '../../../../src/lib/utils';
|
||||
import {addUser, getPackage, loginUserToken} from '../../__helper/api';
|
||||
import {setup} from '../../../../src/lib/logger';
|
||||
import configDefault from '../../partials/config';
|
||||
import {buildUserBuffer} from '../../../../src/lib/auth-utils';
|
||||
|
||||
setup([]);
|
||||
const credentials = { name: 'JotaJWT', password: 'secretPass' };
|
||||
|
||||
const parseConfigurationJWTFile = () => {
|
||||
return parseConfigurationFile(`api-jwt/jwt`);
|
||||
};
|
||||
|
||||
const FORBIDDEN_VUE = 'authorization required to access package vue';
|
||||
|
||||
describe('endpoint user auth JWT unit test', () => {
|
||||
|
@ -33,8 +28,7 @@ describe('endpoint user auth JWT unit test', () => {
|
|||
const store = path.join(__dirname, '../../partials/store/test-jwt-storage');
|
||||
const mockServerPort = 55546;
|
||||
rimraf(store, async () => {
|
||||
const confS = parseConfigFile(parseConfigurationJWTFile());
|
||||
const configForTest = _.assign({}, _.cloneDeep(confS), {
|
||||
const configForTest = configDefault({
|
||||
storage: store,
|
||||
uplinks: {
|
||||
npmjs: {
|
||||
|
@ -46,8 +40,11 @@ describe('endpoint user auth JWT unit test', () => {
|
|||
htpasswd: {
|
||||
file: './test-jwt-storage/.htpasswd_jwt_auth'
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
logs: [
|
||||
{ type: 'stdout', format: 'pretty', level: 'warn' }
|
||||
]
|
||||
}, 'api-jwt/jwt.yaml');
|
||||
|
||||
app = await endPointAPI(configForTest);
|
||||
mockRegistry = await mockServer(mockServerPort).init();
|
||||
|
@ -72,7 +69,7 @@ describe('endpoint user auth JWT unit test', () => {
|
|||
|
||||
// testing JWT auth headers with token
|
||||
// we need it here, because token is required
|
||||
const [err1, resp1] = await getPackage(request(app), buildToken(TOKEN_BEARER, token), 'vue');
|
||||
const [err1, resp1] = await getPackage(request(app), token, 'vue');
|
||||
expect(err1).toBeNull();
|
||||
expect(resp1.body).toBeDefined();
|
||||
expect(resp1.body.name).toMatch('vue');
|
||||
|
|
|
@ -14,6 +14,8 @@ jest.mock('../../../../src/lib/logger', () => ({
|
|||
logger: {
|
||||
child: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn()
|
||||
}
|
||||
|
|
|
@ -3,22 +3,51 @@ uplinks:
|
|||
npmjs:
|
||||
url: http://localhost:4873/
|
||||
packages:
|
||||
'@public-anyone-can-publish/*':
|
||||
access: $anonymous jota_unpublish
|
||||
publish: $anonymous jota_unpublish
|
||||
unpublish: $anonymous jota_unpublish
|
||||
'@scope/starPackage':
|
||||
access: $all
|
||||
publish: jota_star
|
||||
unpublish: jota_star
|
||||
'@only-one-can-publish/*':
|
||||
access: jota_unpublish
|
||||
publish: jota_unpublish
|
||||
unpublish: jota_unpublish
|
||||
'@jquery/*':
|
||||
access: $all
|
||||
publish: $all
|
||||
proxy: npmjs
|
||||
'@scope/*':
|
||||
access: test
|
||||
publish: dsadsa
|
||||
proxy: npmjs
|
||||
'@*/*':
|
||||
access: $all
|
||||
publish: $all
|
||||
unpublish: $authenticated
|
||||
proxy: npmjs
|
||||
'@jquery/*':
|
||||
access: $all
|
||||
publish: $all
|
||||
proxy: npmjs
|
||||
'auth-package':
|
||||
access: $authenticated
|
||||
publish: $authenticated
|
||||
'only-you-can-publish':
|
||||
access: $authenticated
|
||||
publish: you
|
||||
unpublish: you
|
||||
'non-unpublish':
|
||||
access: $authenticated
|
||||
publish: $authenticated
|
||||
# this is intended, empty block
|
||||
publish: jota_unpublish_fail
|
||||
# There is some conditions to keep on mind here
|
||||
# - If unpublish is empty, fallback with the publish value
|
||||
# - If the user has permissions to publish and this empty it will be allowed to unpublish
|
||||
# - If we want to forbid anyone to unpublish, just write here any unexisting user
|
||||
unpublish: some_unexisting_user_defined_here_might_be_a_hash
|
||||
'only-unpublish':
|
||||
access: $authenticated
|
||||
# comment out is intended, we want to test if publish prop is not defined
|
||||
# publish: jota_unpublish_fail
|
||||
#
|
||||
unpublish:
|
||||
'super-admin-can-unpublish':
|
||||
access: $authenticated
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
const json = {
|
||||
"_id": "@scope\/pk1-test",
|
||||
"_rev": "4-6abcdb4efd41a576",
|
||||
"users": {}
|
||||
}
|
||||
|
||||
module.exports = json;
|
|
@ -15,7 +15,13 @@ import {
|
|||
JWTSignOptions,
|
||||
PackageAccess,
|
||||
ILocalData,
|
||||
StringValue as verdaccio$StringValue, IReadTarball, Package, IPluginStorageFilter, Author} from '@verdaccio/types';
|
||||
StringValue as verdaccio$StringValue,
|
||||
IReadTarball,
|
||||
Package,
|
||||
IPluginStorageFilter,
|
||||
Author,
|
||||
AuthPluginPackage
|
||||
} from '@verdaccio/types';
|
||||
import lunrMutable from 'lunr-mutable-indexes';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
|
||||
|
@ -111,6 +117,7 @@ export interface IAuth extends IBasicAuth<Config>, IAuthMiddleware, IAuthWebUI {
|
|||
secret: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
plugins: any[];
|
||||
allow_unpublish(pkg: AuthPluginPackage, user: RemoteUser, callback: Callback): void;
|
||||
}
|
||||
|
||||
export interface IWebSearch {
|
||||
|
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in a new issue