mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-03-18 02:22:46 -05:00
feat: upgrade load plugin and auth (#5130)
* feat: upgrade load plugin and auth * fix types * format * Update e2e-jest-workflow.yml * Update e2e-jest-workflow.yml * Update e2e-jest-workflow.yml * add filter plugin loader
This commit is contained in:
parent
f2cc71cd2c
commit
4c5c509566
20 changed files with 141 additions and 1642 deletions
2
.github/workflows/e2e-jest-workflow.yml
vendored
2
.github/workflows/e2e-jest-workflow.yml
vendored
|
@ -4,7 +4,7 @@ on:
|
|||
|
||||
concurrency:
|
||||
group: e2e-jest-${{ github.ref }}-6.x
|
||||
cancel-in-progress: true
|
||||
cancel-in-progress: true
|
||||
|
||||
name: 'E2E Jest with verdaccio'
|
||||
jobs:
|
||||
|
|
2
.pnp.cjs
generated
2
.pnp.cjs
generated
|
@ -73,6 +73,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@verdaccio/auth", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/config", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/core", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/loaders", "npm:8.0.0-next-8.4"],\
|
||||
["@verdaccio/local-storage-legacy", "npm:11.0.2"],\
|
||||
["@verdaccio/logger", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/middleware", "npm:8.0.0-next-8.10"],\
|
||||
|
@ -15333,6 +15334,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@verdaccio/auth", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/config", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/core", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/loaders", "npm:8.0.0-next-8.4"],\
|
||||
["@verdaccio/local-storage-legacy", "npm:11.0.2"],\
|
||||
["@verdaccio/logger", "npm:8.0.0-next-8.10"],\
|
||||
["@verdaccio/middleware", "npm:8.0.0-next-8.10"],\
|
||||
|
|
|
@ -43,7 +43,7 @@ module.exports = {
|
|||
global: {
|
||||
lines: 70,
|
||||
functions: 75,
|
||||
branches: 63,
|
||||
branches: 60,
|
||||
statements: 70,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"@verdaccio/auth": "8.0.0-next-8.10",
|
||||
"@verdaccio/config": "8.0.0-next-8.10",
|
||||
"@verdaccio/core": "8.0.0-next-8.10",
|
||||
"@verdaccio/loaders": "8.0.0-next-8.4",
|
||||
"@verdaccio/local-storage-legacy": "11.0.2",
|
||||
"@verdaccio/logger": "8.0.0-next-8.10",
|
||||
"@verdaccio/middleware": "8.0.0-next-8.10",
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
|
|||
);
|
||||
}
|
||||
|
||||
const restoredRemoteUser: RemoteUser = createRemoteUser(name, user.groups || []);
|
||||
const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups ?? []);
|
||||
const token = await getApiToken(auth, config, restoredRemoteUser, password);
|
||||
|
||||
res.status(HTTP_STATUS.CREATED);
|
||||
|
@ -82,7 +82,9 @@ export default function (route: Router, auth: Auth, config: Config): void {
|
|||
}
|
||||
|
||||
const token =
|
||||
name && password ? await getApiToken(auth, config, user, password) : undefined;
|
||||
name && password
|
||||
? await getApiToken(auth, config, user as RemoteUser, password)
|
||||
: undefined;
|
||||
|
||||
req.remote_user = user;
|
||||
res.status(HTTP_STATUS.CREATED);
|
||||
|
|
|
@ -66,7 +66,7 @@ export default function (router: Router, auth: Auth, storage: Storage, config: C
|
|||
return next(ErrorCode.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));
|
||||
}
|
||||
|
||||
auth.authenticate(name, password, async (err, user: RemoteUser) => {
|
||||
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));
|
||||
|
@ -81,7 +81,7 @@ export default function (router: Router, auth: Auth, storage: Storage, config: C
|
|||
}
|
||||
|
||||
try {
|
||||
const token = (await getApiToken(auth, config, user, password)) as string;
|
||||
const token = (await getApiToken(auth, config, user as RemoteUser, password)) as string;
|
||||
const key = stringToMD5(token as string);
|
||||
// TODO: use a utility here
|
||||
const maskedToken = mask(token as string, 5);
|
||||
|
|
|
@ -3,18 +3,18 @@ import cors from 'cors';
|
|||
import express, { Application } from 'express';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { Auth } from '@verdaccio/auth';
|
||||
import { getUserAgent } from '@verdaccio/config';
|
||||
import { pluginUtils } from '@verdaccio/core';
|
||||
import { PLUGIN_CATEGORY, pluginUtils } from '@verdaccio/core';
|
||||
import { asyncLoadPlugin } from '@verdaccio/loaders';
|
||||
import { errorReportingMiddleware, final, handleError } from '@verdaccio/middleware';
|
||||
import { log } from '@verdaccio/middleware';
|
||||
import { SearchMemoryIndexer } from '@verdaccio/search-indexer';
|
||||
import { Config as IConfig } from '@verdaccio/types';
|
||||
|
||||
import Auth from '../lib/auth';
|
||||
import AppConfig from '../lib/config';
|
||||
import { API_ERROR } from '../lib/constants';
|
||||
import { logger, setup } from '../lib/logger';
|
||||
import loadPlugin from '../lib/plugin-loader';
|
||||
import Storage from '../lib/storage';
|
||||
import { ErrorCode } from '../lib/utils';
|
||||
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||
|
@ -25,24 +25,9 @@ import webMiddleware from './web';
|
|||
|
||||
const { version } = require('../../package.json');
|
||||
|
||||
export function loadTheme(config) {
|
||||
if (_.isNil(config.theme) === false) {
|
||||
return _.head(
|
||||
loadPlugin(
|
||||
config,
|
||||
config.theme,
|
||||
{},
|
||||
function (plugin) {
|
||||
return plugin.staticPath && plugin.manifest && plugin.manifestFiles;
|
||||
},
|
||||
'verdaccio-theme'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const defineAPI = async function (config: IConfig, storage: Storage): Promise<express.Application> {
|
||||
const auth = new Auth(config);
|
||||
const auth = new Auth(config, logger);
|
||||
await auth.init();
|
||||
const app: Application = express();
|
||||
SearchMemoryIndexer.configureStorage(storage);
|
||||
await SearchMemoryIndexer.init(logger);
|
||||
|
@ -85,14 +70,17 @@ const defineAPI = async function (config: IConfig, storage: Storage): Promise<ex
|
|||
logger: logger,
|
||||
};
|
||||
|
||||
const plugins: pluginUtils.Auth<IConfig>[] = loadPlugin(
|
||||
config,
|
||||
const plugins: pluginUtils.ExpressMiddleware<IConfig, {}, Auth>[] = await asyncLoadPlugin(
|
||||
config.middlewares,
|
||||
plugin_params,
|
||||
function (plugin: pluginUtils.ManifestFilter<IConfig>) {
|
||||
// @ts-ignore
|
||||
return plugin.register_middlewares;
|
||||
}
|
||||
{
|
||||
config,
|
||||
logger,
|
||||
},
|
||||
function (plugin) {
|
||||
return typeof plugin.register_middlewares !== 'undefined';
|
||||
},
|
||||
config?.serverSettings?.pluginPrefix ?? 'verdaccio',
|
||||
PLUGIN_CATEGORY.MIDDLEWARE
|
||||
);
|
||||
|
||||
plugins.forEach((plugin: any) => {
|
||||
|
@ -108,7 +96,8 @@ const defineAPI = async function (config: IConfig, storage: Storage): Promise<ex
|
|||
res.locals.app_version = version ?? '';
|
||||
next();
|
||||
});
|
||||
app.use(webMiddleware(config, auth, storage));
|
||||
const middleware = await webMiddleware(config, auth, storage, logger);
|
||||
app.use(middleware);
|
||||
} else {
|
||||
app.get('/', function (_, __, next: $NextFunctionVer) {
|
||||
next(ErrorCode.getNotFound(API_ERROR.WEB_DISABLED));
|
||||
|
@ -127,20 +116,8 @@ const defineAPI = async function (config: IConfig, storage: Storage): Promise<ex
|
|||
export default (async function (configHash: any) {
|
||||
setup(configHash.logs);
|
||||
const config: IConfig = new AppConfig(_.cloneDeep(configHash));
|
||||
// register middleware plugins
|
||||
const plugin_params = {
|
||||
config: config,
|
||||
logger: logger,
|
||||
};
|
||||
const filters = loadPlugin(
|
||||
config,
|
||||
config.filters || {},
|
||||
plugin_params,
|
||||
// @ts-ignore
|
||||
(plugin: pluginUtils.ManifestFilter<IConfig>) => plugin.filter_metadata
|
||||
);
|
||||
const storage = new Storage(config);
|
||||
// waits until init calls have been initialized
|
||||
await storage.init(config, filters);
|
||||
await storage.init(config, []);
|
||||
return await defineAPI(config, storage);
|
||||
});
|
||||
|
|
|
@ -49,7 +49,7 @@ function addPackageWebApi(storage: Storage, auth: Auth, config: Config): Router
|
|||
if (err) {
|
||||
resolve(false);
|
||||
}
|
||||
resolve(allowed);
|
||||
resolve(allowed as boolean);
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
|
|
|
@ -21,16 +21,16 @@ function addUserAuthApi(auth: Auth, config: Config): Router {
|
|||
function (req: Request, res: Response, next: $NextFunctionVer): void {
|
||||
const { username, password } = req.body;
|
||||
|
||||
auth.authenticate(username, password, async (err, user: RemoteUser): Promise<void> => {
|
||||
auth.authenticate(username, password, async (err, user?: RemoteUser): Promise<void> => {
|
||||
if (err) {
|
||||
const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;
|
||||
next(ErrorCode.getCode(errorCode, err.message));
|
||||
} else {
|
||||
req.remote_user = user;
|
||||
req.remote_user = user as RemoteUser;
|
||||
const jWTSignOptions: JWTSignOptions = getSecurity(config).web.sign;
|
||||
res.set(HEADERS.CACHE_CONTROL, 'no-cache, no-store');
|
||||
next({
|
||||
token: await auth.jwtEncrypt(user, jWTSignOptions),
|
||||
token: await auth.jwtEncrypt(user as RemoteUser, jWTSignOptions),
|
||||
username: req.remote_user.name,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import buildDebug from 'debug';
|
||||
import express, { RequestHandler, Router } from 'express';
|
||||
import express, { Router } from 'express';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { PLUGIN_CATEGORY } from '@verdaccio/core';
|
||||
import { asyncLoadPlugin } from '@verdaccio/loaders';
|
||||
import { logger } from '@verdaccio/logger';
|
||||
import {
|
||||
renderWebMiddleware,
|
||||
setSecurityWebHeaders,
|
||||
|
@ -9,25 +12,35 @@ import {
|
|||
validatePackage,
|
||||
} from '@verdaccio/middleware';
|
||||
|
||||
import loadPlugin from '../../lib/plugin-loader';
|
||||
import webEndpointsApi from './api';
|
||||
|
||||
const debug = buildDebug('verdaccio:web');
|
||||
export const PLUGIN_UI_PREFIX = 'verdaccio-theme';
|
||||
export const DEFAULT_PLUGIN_UI_THEME = '@verdaccio/ui-theme';
|
||||
|
||||
export function loadTheme(config) {
|
||||
export async function loadTheme(config: any) {
|
||||
if (_.isNil(config.theme) === false) {
|
||||
debug('loading custom ui theme');
|
||||
return _.head(
|
||||
loadPlugin(
|
||||
config,
|
||||
config.theme,
|
||||
{},
|
||||
function (plugin) {
|
||||
return plugin.staticPath && plugin.manifest && plugin.manifestFiles;
|
||||
},
|
||||
'verdaccio-theme'
|
||||
)
|
||||
const plugin = await asyncLoadPlugin(
|
||||
config.theme,
|
||||
{ config, logger },
|
||||
// TODO: add types { staticPath: string; manifest: unknown; manifestFiles: unknown }
|
||||
function (plugin: any) {
|
||||
/**
|
||||
*
|
||||
- `staticPath`: is the same data returned in Verdaccio 5.
|
||||
- `manifest`: A webpack manifest object.
|
||||
- `manifestFiles`: A object with one property `js` and the array (order matters) of the manifest id to be loaded in the template dynamically.
|
||||
*/
|
||||
return plugin.staticPath && plugin.manifest && plugin.manifestFiles;
|
||||
},
|
||||
config?.serverSettings?.pluginPrefix ?? PLUGIN_UI_PREFIX,
|
||||
PLUGIN_CATEGORY.THEME
|
||||
);
|
||||
if (plugin.length > 1) {
|
||||
logger.warn('multiple ui themes are not supported; only the first plugin is used');
|
||||
}
|
||||
|
||||
return _.head(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,8 +60,15 @@ export function localWebEndpointsApi(auth, storage, config): Router {
|
|||
return route;
|
||||
}
|
||||
|
||||
export default (config, auth, storage) => {
|
||||
const pluginOptions = loadTheme(config) || require('@verdaccio/ui-theme')();
|
||||
export default async (config, auth, storage, logger) => {
|
||||
let pluginOptions = await loadTheme(config);
|
||||
if (!pluginOptions) {
|
||||
pluginOptions = require(DEFAULT_PLUGIN_UI_THEME)(config.web);
|
||||
logger.info(
|
||||
{ name: DEFAULT_PLUGIN_UI_THEME, pluginCategory: PLUGIN_CATEGORY.THEME },
|
||||
'plugin @{name} successfully loaded (@{pluginCategory})'
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line new-cap
|
||||
const router = Router();
|
||||
// @ts-ignore
|
||||
|
|
546
src/lib/auth.ts
546
src/lib/auth.ts
|
@ -1,547 +1,3 @@
|
|||
import buildDebug from 'debug';
|
||||
import { NextFunction } from 'express';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
getMiddlewareCredentials,
|
||||
isAESLegacy,
|
||||
isAuthHeaderValid,
|
||||
parseAuthTokenHeader,
|
||||
verifyJWTPayload,
|
||||
} from '@verdaccio/auth';
|
||||
import { createAnonymousRemoteUser, createRemoteUser } from '@verdaccio/config';
|
||||
import { VerdaccioError, pluginUtils } from '@verdaccio/core';
|
||||
import {
|
||||
aesEncrypt,
|
||||
aesEncryptDeprecated,
|
||||
parseBasicPayload,
|
||||
signPayload,
|
||||
utils as signatureUtils,
|
||||
} from '@verdaccio/signature';
|
||||
import {
|
||||
AllowAccess,
|
||||
Callback,
|
||||
Config,
|
||||
JWTSignOptions,
|
||||
Logger,
|
||||
PackageAccess,
|
||||
RemoteUser,
|
||||
Security,
|
||||
} from '@verdaccio/types';
|
||||
import { getMatchedPackagesSpec } from '@verdaccio/utils';
|
||||
|
||||
import loadPlugin from '../lib/plugin-loader';
|
||||
import { $RequestExtend, $ResponseExtend, AESPayload } from '../types';
|
||||
import { getDefaultPlugins, getSecurity } from './auth-utils';
|
||||
import { API_ERROR, SUPPORT_ERRORS, TOKEN_BASIC, TOKEN_BEARER } from './constants';
|
||||
import { logger } from './logger';
|
||||
import { ErrorCode, convertPayloadToBase64 } from './utils';
|
||||
|
||||
const debug = buildDebug('verdaccio:auth');
|
||||
|
||||
class Auth {
|
||||
public config: Config;
|
||||
public logger: Logger;
|
||||
public secret: string;
|
||||
public plugins: pluginUtils.Auth<Config>[];
|
||||
|
||||
public constructor(config: Config) {
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
this.secret = config.secret;
|
||||
this.plugins = this._loadPlugin(config);
|
||||
this._applyDefaultPlugins();
|
||||
}
|
||||
|
||||
private _loadPlugin(config: Config): pluginUtils.Auth<Config>[] {
|
||||
const pluginOptions = {
|
||||
config,
|
||||
logger: this.logger,
|
||||
};
|
||||
|
||||
let authConf = { ...config.auth };
|
||||
if (authConf?.htpasswd) {
|
||||
// special case for htpasswd plugin, the v6 version uses bcrypt by default
|
||||
// 6.x enforces crypt to avoid breaking changes, but is highly recommended using
|
||||
// bcrypt instead.
|
||||
if (!authConf.htpasswd.algorithm) {
|
||||
authConf.htpasswd.algorithm = 'crypt';
|
||||
this.logger.info(
|
||||
// eslint-disable-next-line max-len
|
||||
'the "crypt" algorithm is deprecated consider switch to "bcrypt" in the configuration file. Read the documentation for additional details'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return loadPlugin<pluginUtils.Auth<Config>>(
|
||||
config,
|
||||
authConf,
|
||||
pluginOptions,
|
||||
(plugin: pluginUtils.Auth<Config>): boolean => {
|
||||
const { authenticate, allow_access, allow_publish } = plugin;
|
||||
// @ts-ignore
|
||||
return authenticate || allow_access || allow_publish;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _applyDefaultPlugins(): void {
|
||||
this.plugins.push(getDefaultPlugins(this.logger));
|
||||
}
|
||||
|
||||
public changePassword(
|
||||
username: string,
|
||||
password: string, // pragma: allowlist secret
|
||||
newPassword: string, // pragma: allowlist secret
|
||||
cb: Callback
|
||||
): void {
|
||||
const validPlugins = _.filter(this.plugins, (plugin) => _.isFunction(plugin.changePassword));
|
||||
|
||||
if (_.isEmpty(validPlugins)) {
|
||||
return cb(ErrorCode.getInternalError(SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE));
|
||||
}
|
||||
|
||||
for (const plugin of validPlugins) {
|
||||
if (_.isNil(plugin) || _.isFunction(plugin.changePassword) === false) {
|
||||
debug('auth plugin does not implement changePassword, trying next one');
|
||||
continue;
|
||||
} else {
|
||||
debug('updating password for %o', 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);
|
||||
}
|
||||
this.logger.info({ 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);
|
||||
const self = this;
|
||||
(function next(): void {
|
||||
const plugin = plugins.shift() as pluginUtils.Auth<Config>;
|
||||
if (_.isFunction(plugin.authenticate) === false) {
|
||||
return next();
|
||||
}
|
||||
debug('authenticating %o', username);
|
||||
plugin.authenticate(username, password, function (err, groups): void {
|
||||
if (err) {
|
||||
self.logger.error(
|
||||
{ username, err },
|
||||
'authenticating for user @{username} failed. Error: @{err.message}'
|
||||
);
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// Expect: SKIP if groups is falsey and not an array
|
||||
// with at least one item (truthy length)
|
||||
// Expect: CONTINUE otherwise (will error if groups is not
|
||||
// an array, but this is current behavior)
|
||||
// Caveat: STRING (if valid) will pass successfully
|
||||
// bug give unexpected results
|
||||
// Info: Cannot use `== false to check falsey values`
|
||||
if (!!groups && groups.length !== 0) {
|
||||
// TODO: create a better understanding of expectations
|
||||
if (_.isString(groups)) {
|
||||
throw new TypeError('plugin group error: invalid type for function');
|
||||
}
|
||||
const isGroupValid: boolean = _.isArray(groups);
|
||||
if (!isGroupValid) {
|
||||
throw new TypeError(API_ERROR.BAD_FORMAT_USER_GROUP);
|
||||
}
|
||||
debug('authentication for user %o was successfully. Groups: %o', username, groups);
|
||||
return cb(err, createRemoteUser(username, groups));
|
||||
}
|
||||
next();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
public add_user(user: string, password: string, cb: Callback): void {
|
||||
const self = this;
|
||||
const plugins = this.plugins.slice(0);
|
||||
debug('add user %o', user);
|
||||
(function next(): void {
|
||||
const plugin = plugins.shift() as pluginUtils.Auth<Config>;
|
||||
let method = 'adduser';
|
||||
if (_.isFunction(plugin[method]) === false) {
|
||||
method = 'add_user';
|
||||
self.logger.warn(
|
||||
'the plugin method add_user in the auth plugin is deprecated and will be removed in next major release, notify to the plugin author'
|
||||
);
|
||||
}
|
||||
|
||||
if (_.isFunction(plugin[method]) === false) {
|
||||
next();
|
||||
} else {
|
||||
// p.add_user() execution
|
||||
plugin[method](user, password, function (err, ok): void {
|
||||
if (err) {
|
||||
self.logger.error(
|
||||
{ user, err: err.message },
|
||||
'the user @{user} could not being added. Error: @{err}'
|
||||
);
|
||||
return cb(err);
|
||||
}
|
||||
if (ok) {
|
||||
self.logger.info({ user }, 'the user @{user} has been added');
|
||||
return self.authenticate(user, password, cb);
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow user to access a package.
|
||||
*/
|
||||
public allow_access(
|
||||
{ packageName, packageVersion }: pluginUtils.AuthPluginPackage,
|
||||
user: RemoteUser,
|
||||
callback: Callback
|
||||
): void {
|
||||
const plugins = this.plugins.slice(0);
|
||||
const self = this;
|
||||
const pkgAllowAcces: AllowAccess = { name: packageName, version: packageVersion };
|
||||
const pkg = Object.assign(
|
||||
{},
|
||||
pkgAllowAcces,
|
||||
getMatchedPackagesSpec(packageName, this.config.packages)
|
||||
) as AllowAccess & PackageAccess;
|
||||
debug('allow access for %o', packageName);
|
||||
|
||||
(function next(): void {
|
||||
const plugin: pluginUtils.Auth<unknown> = plugins.shift() as pluginUtils.Auth<Config>;
|
||||
|
||||
if (_.isNil(plugin) || _.isFunction(plugin.allow_access) === false) {
|
||||
return next();
|
||||
}
|
||||
|
||||
plugin.allow_access!(user, pkg, function (err: VerdaccioError | null, ok?: boolean): void {
|
||||
if (err) {
|
||||
self.logger.error(
|
||||
{ packageName, err },
|
||||
'forbidden access for @{packageName}. Error: @{err.message}'
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
self.logger.info({ packageName }, 'allowed access for @{packageName}');
|
||||
return callback(null, ok);
|
||||
}
|
||||
|
||||
next(); // cb(null, false) causes next plugin to roll
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
public allow_unpublish(
|
||||
{ packageName, packageVersion }: pluginUtils.AuthPluginPackage,
|
||||
user: RemoteUser,
|
||||
callback: Callback
|
||||
): void {
|
||||
const pkg = Object.assign(
|
||||
{ name: packageName, version: packageVersion },
|
||||
getMatchedPackagesSpec(packageName, this.config.packages)
|
||||
);
|
||||
debug('allow unpublish for %o', packageName);
|
||||
for (const plugin of this.plugins) {
|
||||
if (_.isNil(plugin) || _.isFunction(plugin.allow_unpublish) === false) {
|
||||
debug('allow unpublish for %o plugin does not implement allow_unpublish', packageName);
|
||||
continue;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
plugin.allow_unpublish!(user, pkg, (err, ok: boolean): void => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{ packageName, user: user?.name },
|
||||
'@{user} forbidden publish for @{packageName}, it will fallback on unpublish permissions'
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (_.isNil(ok) === true) {
|
||||
debug('we bypass unpublish for %o, publish will handle the access', packageName);
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
return this.allow_publish(...arguments);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
this.logger.info(
|
||||
{ packageName, user: user?.name },
|
||||
'@{user} allowed unpublish for @{packageName}'
|
||||
);
|
||||
return callback(null, ok);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow user to publish a package.
|
||||
*/
|
||||
public allow_publish(
|
||||
{ packageName, packageVersion }: pluginUtils.AuthPluginPackage,
|
||||
user: RemoteUser,
|
||||
callback: Callback
|
||||
): void {
|
||||
const plugins = this.plugins.slice(0);
|
||||
const self = this;
|
||||
const pkg = Object.assign(
|
||||
{ name: packageName, version: packageVersion },
|
||||
getMatchedPackagesSpec(packageName, this.config.packages)
|
||||
);
|
||||
debug('allow publish for %o init | plugins: %o', packageName, plugins?.length);
|
||||
(function next(): void {
|
||||
const plugin = plugins.shift();
|
||||
|
||||
if (_.isNil(plugin) || _.isFunction(plugin.allow_publish) === false) {
|
||||
debug('allow publish for %o plugin does not implement allow_publish', packageName);
|
||||
return next();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
plugin.allow_publish(user, pkg, (err: any, ok: boolean): void => {
|
||||
if (_.isNil(err) === false && _.isError(err)) {
|
||||
self.logger.error(
|
||||
{ packageName, user: user?.name },
|
||||
'@{user} is forbidden publish for @{packageName}'
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
self.logger.info(
|
||||
{ packageName, user: user?.name },
|
||||
'@{user} is allowed publish for @{packageName}'
|
||||
);
|
||||
return callback(null, ok);
|
||||
}
|
||||
debug('allow publish skip validation for %o', packageName);
|
||||
next(); // cb(null, false) causes next plugin to roll
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
public apiJWTmiddleware() {
|
||||
const plugins = this.plugins.slice(0);
|
||||
const helpers = { createAnonymousRemoteUser, createRemoteUser };
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.apiJWTmiddleware) {
|
||||
return plugin.apiJWTmiddleware(helpers);
|
||||
}
|
||||
}
|
||||
|
||||
return (req: $RequestExtend, res: $ResponseExtend, _next: NextFunction): void => {
|
||||
req.pause();
|
||||
|
||||
const next = function (err: any | void): void {
|
||||
req.resume();
|
||||
// uncomment this to reject users with bad auth headers
|
||||
// return _next.apply(null, arguments)
|
||||
// swallow error, user remains unauthorized
|
||||
// set remoteUserError to indicate that user was attempting authentication
|
||||
if (err) {
|
||||
req.remote_user.error = err.message;
|
||||
}
|
||||
return _next();
|
||||
};
|
||||
|
||||
if (this._isRemoteUserValid(req.remote_user)) {
|
||||
// @ts-ignore
|
||||
return next();
|
||||
}
|
||||
|
||||
// in case auth header does not exist we return anonymous function
|
||||
req.remote_user = createAnonymousRemoteUser();
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (_.isNil(authorization)) {
|
||||
// @ts-ignore
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!isAuthHeaderValid(authorization)) {
|
||||
debug('api middleware auth heather is not valid');
|
||||
return next(ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER));
|
||||
}
|
||||
|
||||
const security: Security = getSecurity(this.config);
|
||||
const { secret } = this.config;
|
||||
|
||||
if (isAESLegacy(security)) {
|
||||
debug('api middleware using legacy auth token');
|
||||
this._handleAESMiddleware(req, security, secret, authorization, next);
|
||||
} else {
|
||||
debug('api middleware using JWT auth token');
|
||||
this._handleJWTAPIMiddleware(req, security, secret, authorization, next);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleJWTAPIMiddleware(
|
||||
req: $RequestExtend,
|
||||
security: Security,
|
||||
secret: string,
|
||||
authorization: string,
|
||||
next: Function
|
||||
): void {
|
||||
const { scheme, token } = parseAuthTokenHeader(authorization);
|
||||
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
|
||||
// this should happen when client tries to login with an existing user
|
||||
const credentials = convertPayloadToBase64(token).toString();
|
||||
const { user, password } = parseBasicPayload(credentials) as AESPayload;
|
||||
this.authenticate(user, password, (err, user): void => {
|
||||
if (!err) {
|
||||
req.remote_user = user;
|
||||
next();
|
||||
} else {
|
||||
req.remote_user = createAnonymousRemoteUser();
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// jwt handler
|
||||
const credentials: any = getMiddlewareCredentials(security, secret, authorization);
|
||||
if (credentials) {
|
||||
// if the signature is valid we rely on it
|
||||
req.remote_user = credentials;
|
||||
next();
|
||||
} else {
|
||||
// with JWT throw 401
|
||||
next(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAESMiddleware(
|
||||
req: $RequestExtend,
|
||||
security: Security,
|
||||
secret: string,
|
||||
authorization: string,
|
||||
next: Function
|
||||
): void {
|
||||
const credentials: any = getMiddlewareCredentials(security, secret, authorization);
|
||||
if (credentials) {
|
||||
const { user, password } = credentials;
|
||||
this.authenticate(user, password, (err, user): void => {
|
||||
if (!err) {
|
||||
req.remote_user = user;
|
||||
next();
|
||||
} else {
|
||||
req.remote_user = createAnonymousRemoteUser();
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// we force npm client to ask again with basic authentication
|
||||
return next(ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER));
|
||||
}
|
||||
}
|
||||
|
||||
private _isRemoteUserValid(remote_user: RemoteUser): boolean {
|
||||
return _.isUndefined(remote_user) === false && _.isUndefined(remote_user.name) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT middleware for WebUI
|
||||
*/
|
||||
public webUIJWTmiddleware(): Function {
|
||||
return (req: $RequestExtend, res: $ResponseExtend, _next: NextFunction): void => {
|
||||
if (this._isRemoteUserValid(req.remote_user)) {
|
||||
return _next();
|
||||
}
|
||||
|
||||
req.pause();
|
||||
const next = (err: any | void): void => {
|
||||
req.resume();
|
||||
if (err) {
|
||||
// req.remote_user.error = err.message;
|
||||
res.status(err.statusCode).send(err.message);
|
||||
}
|
||||
|
||||
return _next();
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (_.isNil(authorization)) {
|
||||
// @ts-ignore
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!isAuthHeaderValid(authorization)) {
|
||||
return next(ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER));
|
||||
}
|
||||
|
||||
const token = (authorization || '').replace(`${TOKEN_BEARER} `, '');
|
||||
if (!token) {
|
||||
// @ts-ignore
|
||||
return next();
|
||||
}
|
||||
|
||||
let credentials;
|
||||
try {
|
||||
credentials = verifyJWTPayload(token, this.config.secret);
|
||||
} catch (err) {
|
||||
// FIXME: intended behaviour, do we want it?
|
||||
}
|
||||
|
||||
if (this._isRemoteUserValid(credentials)) {
|
||||
const { name, groups } = credentials;
|
||||
req.remote_user = createRemoteUser(name, groups);
|
||||
} else {
|
||||
req.remote_user = createAnonymousRemoteUser();
|
||||
}
|
||||
// @ts-ignore
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
public async jwtEncrypt(user: RemoteUser, signOptions: JWTSignOptions): Promise<string> {
|
||||
const { real_groups, name, groups } = user;
|
||||
const realGroupsValidated = _.isNil(real_groups) ? [] : real_groups;
|
||||
const groupedGroups = _.isNil(groups)
|
||||
? real_groups
|
||||
: Array.from(new Set([...groups.concat(realGroupsValidated)]));
|
||||
const payload: RemoteUser = {
|
||||
real_groups: realGroupsValidated,
|
||||
name,
|
||||
groups: groupedGroups,
|
||||
};
|
||||
|
||||
// TODO: fix on update signature package
|
||||
const token: string = await signPayload(payload, this.secret, signOptions as any);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a string.
|
||||
*/
|
||||
public aesEncrypt(value: string): string | void {
|
||||
if (this.secret.length === signatureUtils.TOKEN_VALID_LENGTH) {
|
||||
debug('signing with enhanced aes legacy');
|
||||
const token = aesEncrypt(value, this.secret);
|
||||
return token;
|
||||
} else {
|
||||
debug('signing with enhanced aes deprecated legacy');
|
||||
// deprecated aes (legacy) signature, only must be used for legacy version
|
||||
const token = aesEncryptDeprecated(Buffer.from(value), this.secret).toString('base64');
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { Auth } from '@verdaccio/auth';
|
||||
|
||||
export default Auth;
|
||||
|
|
|
@ -3,7 +3,6 @@ import builDebug from 'debug';
|
|||
import _ from 'lodash';
|
||||
import UrlNode from 'url';
|
||||
|
||||
import LocalDatabase from '@verdaccio/local-storage-legacy';
|
||||
import { ReadTarball, UploadTarball } from '@verdaccio/streams';
|
||||
import {
|
||||
Author,
|
||||
|
@ -28,7 +27,6 @@ import {
|
|||
} from '@verdaccio/utils';
|
||||
|
||||
import { StoragePluginLegacy } from '../../types/custom';
|
||||
import loadPlugin from '../lib/plugin-loader';
|
||||
import { StringValue } from '../types';
|
||||
import { API_ERROR, DIST_TAGS, HTTP_STATUS, STORAGE, SUPPORT_ERRORS, USERS } from './constants';
|
||||
import {
|
||||
|
@ -42,7 +40,7 @@ import { prepareSearchPackage } from './storage-utils';
|
|||
import { ErrorCode, isObject, tagVersion } from './utils';
|
||||
|
||||
const debug = builDebug('verdaccio:local-storage');
|
||||
type StoragePlugin = StoragePluginLegacy<Config> | any;
|
||||
export type StoragePlugin = StoragePluginLegacy<Config> | any;
|
||||
/**
|
||||
* Implements Storage interface (same for storage.js, local-storage.js, up-storage.js).
|
||||
*/
|
||||
|
@ -51,10 +49,10 @@ class LocalStorage {
|
|||
public storagePlugin: StoragePlugin;
|
||||
public logger: Logger;
|
||||
|
||||
public constructor(config: Config, logger: Logger) {
|
||||
public constructor(config: Config, logger: Logger, localStorage: StoragePlugin) {
|
||||
this.logger = logger;
|
||||
this.config = config;
|
||||
this.storagePlugin = this._loadStorage(config, logger);
|
||||
this.storagePlugin = localStorage;
|
||||
}
|
||||
|
||||
public addPackage(name: string, pkg: Package, callback: Callback): void {
|
||||
|
@ -871,35 +869,6 @@ class LocalStorage {
|
|||
return this.storagePlugin.setSecret(config.checkSecretKey(secretKey));
|
||||
}
|
||||
|
||||
private _loadStorage(config: Config, logger: Logger): StoragePlugin {
|
||||
const Storage = this._loadStorePlugin();
|
||||
|
||||
if (_.isNil(Storage)) {
|
||||
assert(this.config.storage, 'CONFIG: storage default path not defined');
|
||||
return new LocalDatabase(this.config, logger);
|
||||
}
|
||||
return Storage as StoragePlugin;
|
||||
}
|
||||
|
||||
private _loadStorePlugin(): StoragePlugin | void {
|
||||
const plugin_params = {
|
||||
config: this.config,
|
||||
logger: this.logger,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const plugins: StoragePlugin[] = loadPlugin<StoragePlugin>(
|
||||
this.config,
|
||||
this.config.store,
|
||||
plugin_params,
|
||||
(plugin): StoragePlugin => {
|
||||
return plugin.getPackageStorage;
|
||||
}
|
||||
);
|
||||
|
||||
return _.head(plugins);
|
||||
}
|
||||
|
||||
public saveToken(token: Token): Promise<any> {
|
||||
if (_.isFunction(this.storagePlugin.saveToken) === false) {
|
||||
return Promise.reject(
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
import buildDebug from 'debug';
|
||||
import _ from 'lodash';
|
||||
import Path from 'path';
|
||||
|
||||
import { pluginUtils } from '@verdaccio/core';
|
||||
import { Config } from '@verdaccio/types';
|
||||
|
||||
import { MODULE_NOT_FOUND } from './constants';
|
||||
import { logger } from './logger';
|
||||
|
||||
const debug = buildDebug('verdaccio:plugin:loader');
|
||||
|
||||
/**
|
||||
* Requires a module.
|
||||
* @param {*} path the module's path
|
||||
* @return {Object}
|
||||
*/
|
||||
function tryLoad(path: string): any {
|
||||
try {
|
||||
debug('loading plugin %s', path);
|
||||
return require(path);
|
||||
} catch (err: any) {
|
||||
if (err.code === MODULE_NOT_FOUND) {
|
||||
debug('plugin %s not found', path);
|
||||
return null;
|
||||
}
|
||||
logger.error({ err: err.msg }, 'error loading plugin @{err}');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeConfig(appConfig, pluginConfig): Config {
|
||||
return _.merge(appConfig, pluginConfig);
|
||||
}
|
||||
|
||||
function isValid(plugin): boolean {
|
||||
return _.isFunction(plugin) || _.isFunction(plugin.default);
|
||||
}
|
||||
|
||||
function isES6(plugin): boolean {
|
||||
return Object.keys(plugin).includes('default');
|
||||
}
|
||||
|
||||
// export type PluginGeneric<R, T extends IPlugin<R> = ;
|
||||
|
||||
/**
|
||||
* Load a plugin following the rules
|
||||
* - First try to load from the internal directory plugins (which will disappear soon or later).
|
||||
* - If the package is scoped eg: @scope/foo, try to load as a package
|
||||
* - A second attempt from the external plugin directory
|
||||
* - A third attempt from node_modules, in case to have multiple match as for instance verdaccio-ldap
|
||||
* and sinopia-ldap. All verdaccio prefix will have preferences.
|
||||
* @param {*} config a reference of the configuration settings
|
||||
* @param {*} pluginConfigs
|
||||
* @param {*} params a set of params to initialize the plugin
|
||||
* @param {*} sanityCheck callback that check the shape that should fulfill the plugin
|
||||
* @return {Array} list of plugins
|
||||
*/
|
||||
export default function loadPlugin<T extends pluginUtils.Plugin<T>>(
|
||||
config: Config,
|
||||
pluginConfigs: any = {},
|
||||
params: any,
|
||||
sanityCheck: any,
|
||||
prefix: string = 'verdaccio'
|
||||
): any[] {
|
||||
return Object.keys(pluginConfigs).map((pluginId: string): pluginUtils.Plugin<T> => {
|
||||
let plugin;
|
||||
const isScoped: boolean = pluginId.startsWith('@') && pluginId.includes('/');
|
||||
debug('isScoped %s', isScoped);
|
||||
if (isScoped) {
|
||||
plugin = tryLoad(pluginId);
|
||||
}
|
||||
|
||||
const localPlugin = Path.resolve(__dirname + '/../plugins', pluginId);
|
||||
// try local plugins first
|
||||
plugin = tryLoad(localPlugin);
|
||||
|
||||
// try the external plugin directory
|
||||
if (plugin === null && config.plugins) {
|
||||
const pluginDir = config.plugins;
|
||||
const externalFilePlugin = Path.resolve(pluginDir, pluginId);
|
||||
plugin = tryLoad(externalFilePlugin);
|
||||
|
||||
// npm package
|
||||
if (plugin === null && pluginId.match(/^[^\.\/]/)) {
|
||||
plugin = tryLoad(Path.resolve(pluginDir, `${prefix}-${pluginId}`));
|
||||
// compatibility for old sinopia plugins
|
||||
if (!plugin) {
|
||||
plugin = tryLoad(Path.resolve(pluginDir, `sinopia-${pluginId}`));
|
||||
if (plugin) {
|
||||
logger.warn(
|
||||
{ name: pluginId },
|
||||
`plugin names that start with sinopia-* will be removed in the future, please rename package to verdaccio-*`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// npm package
|
||||
if (plugin === null && pluginId.match(/^[^\.\/]/)) {
|
||||
plugin = tryLoad(`${prefix}-${pluginId}`);
|
||||
// compatibility for old sinopia plugins
|
||||
if (!plugin) {
|
||||
plugin = tryLoad(`sinopia-${pluginId}`);
|
||||
}
|
||||
if (plugin) {
|
||||
debug('plugin %s is an npm package', pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin === null) {
|
||||
plugin = tryLoad(pluginId);
|
||||
}
|
||||
|
||||
// relative to config path
|
||||
if (plugin === null && pluginId.match(/^\.\.?($|\/)/)) {
|
||||
// compatible with 6.x
|
||||
plugin = tryLoad(Path.resolve(Path.dirname(config.self_path ?? config.configPath), pluginId));
|
||||
}
|
||||
|
||||
if (plugin === null) {
|
||||
if (isScoped) {
|
||||
logger.error({ content: pluginId }, 'plugin not found. try npm install @{content}');
|
||||
} else {
|
||||
logger.error(
|
||||
{ content: pluginId, prefix },
|
||||
'plugin not found. try npm install @{prefix}-@{content}'
|
||||
);
|
||||
}
|
||||
const msg = isScoped
|
||||
? `
|
||||
${pluginId} plugin not found. try "npm install ${pluginId}"`
|
||||
: `
|
||||
${prefix}-${pluginId} plugin not found. try "npm install ${prefix}-${pluginId}"`;
|
||||
throw Error(msg);
|
||||
}
|
||||
|
||||
if (!isValid(plugin)) {
|
||||
logger.error(
|
||||
{ content: pluginId },
|
||||
'@{prefix}-@{content} plugin does not have the right code structure'
|
||||
);
|
||||
throw Error(`"${pluginId}" plugin does not have the right code structure`);
|
||||
}
|
||||
|
||||
/* eslint new-cap:off */
|
||||
try {
|
||||
if (isES6(plugin)) {
|
||||
debug('plugin is ES6');
|
||||
plugin = new plugin.default(mergeConfig(config, pluginConfigs[pluginId]), params);
|
||||
} else {
|
||||
debug('plugin is commonJS');
|
||||
plugin = plugin(pluginConfigs[pluginId], params);
|
||||
}
|
||||
} catch (error: any) {
|
||||
plugin = null;
|
||||
logger.error({ error, pluginId }, 'error loading a plugin @{pluginId}: @{error}');
|
||||
}
|
||||
/* eslint new-cap:off */
|
||||
|
||||
if (plugin === null || !sanityCheck(plugin)) {
|
||||
if (isScoped) {
|
||||
logger.error({ content: pluginId }, "@{content} doesn't look like a valid plugin");
|
||||
} else {
|
||||
logger.error(
|
||||
{ content: pluginId, prefix },
|
||||
"@{prefix}-@{content} doesn't look like a valid plugin"
|
||||
);
|
||||
}
|
||||
throw Error(`sanity check has failed, "${pluginId}" is not a valid plugin`);
|
||||
}
|
||||
|
||||
if (isScoped) {
|
||||
logger.info({ content: pluginId }, 'plugin successfully loaded: @{content}');
|
||||
} else {
|
||||
logger.info(
|
||||
{ content: pluginId, prefix },
|
||||
'plugin successfully loaded: @{prefix}-@{content}'
|
||||
);
|
||||
}
|
||||
return plugin;
|
||||
});
|
||||
}
|
|
@ -4,7 +4,9 @@ import buildDebug from 'debug';
|
|||
import _ from 'lodash';
|
||||
import Stream from 'stream';
|
||||
|
||||
import { validatioUtils } from '@verdaccio/core';
|
||||
import { PLUGIN_CATEGORY, pluginUtils, validatioUtils } from '@verdaccio/core';
|
||||
import { asyncLoadPlugin } from '@verdaccio/loaders';
|
||||
import LocalDatabasePlugin from '@verdaccio/local-storage-legacy';
|
||||
import { SearchMemoryIndexer } from '@verdaccio/search-indexer';
|
||||
import { ReadTarball } from '@verdaccio/streams';
|
||||
import {
|
||||
|
@ -20,11 +22,12 @@ import {
|
|||
} from '@verdaccio/types';
|
||||
import { GenericBody, Token, TokenFilter } from '@verdaccio/types';
|
||||
|
||||
import { StoragePluginLegacy } from '../../types/custom';
|
||||
import { logger } from '../lib/logger';
|
||||
import { IPluginFilters, ISyncUplinks, StringValue } from '../types';
|
||||
import { hasProxyTo } from './config-utils';
|
||||
import { API_ERROR, DIST_TAGS, HTTP_STATUS } from './constants';
|
||||
import LocalStorage from './local-storage';
|
||||
import LocalStorage, { StoragePlugin } from './local-storage';
|
||||
import { mergeVersions } from './metadata-utils';
|
||||
import {
|
||||
checkPackageLocal,
|
||||
|
@ -60,12 +63,69 @@ class Storage {
|
|||
public async init(config: Config, filters: IPluginFilters = []): Promise<void> {
|
||||
if (this.localStorage === null) {
|
||||
this.filters = filters;
|
||||
this.localStorage = new LocalStorage(this.config, logger);
|
||||
const storageInstance = await this.loadStorage(config, this.logger);
|
||||
this.localStorage = new LocalStorage(this.config, logger, storageInstance);
|
||||
await this.localStorage.getSecret(config);
|
||||
debug('initialization completed');
|
||||
} else {
|
||||
debug('storage has been already initialized');
|
||||
}
|
||||
|
||||
if (!this.filters) {
|
||||
this.filters = await asyncLoadPlugin<pluginUtils.ManifestFilter<unknown>>(
|
||||
this.config.filters,
|
||||
{
|
||||
config: this.config,
|
||||
logger: this.logger,
|
||||
},
|
||||
(plugin: pluginUtils.ManifestFilter<Config>) => {
|
||||
return typeof plugin.filter_metadata !== 'undefined';
|
||||
},
|
||||
this.config?.serverSettings?.pluginPrefix,
|
||||
PLUGIN_CATEGORY.FILTER
|
||||
);
|
||||
debug('filters available %o', this.filters.length);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadStorage(config: Config, logger: Logger): Promise<StoragePlugin> {
|
||||
const Storage = await this.loadStorePlugin();
|
||||
if (_.isNil(Storage)) {
|
||||
assert(this.config.storage, 'CONFIG: storage path not defined');
|
||||
debug('no custom storage found, loading default storage @verdaccio/local-storage');
|
||||
const localStorage = new LocalDatabasePlugin(config, logger);
|
||||
logger.info(
|
||||
{ name: '@verdaccio/local-storage', pluginCategory: PLUGIN_CATEGORY.STORAGE },
|
||||
'plugin @{name} successfully loaded (@{pluginCategory})'
|
||||
);
|
||||
return localStorage;
|
||||
}
|
||||
return Storage as StoragePlugin;
|
||||
}
|
||||
|
||||
private async loadStorePlugin(): Promise<StoragePluginLegacy<Config> | undefined> {
|
||||
const plugins: StoragePluginLegacy<Config>[] = await asyncLoadPlugin<
|
||||
pluginUtils.Storage<unknown>
|
||||
>(
|
||||
this.config.store,
|
||||
{
|
||||
config: this.config,
|
||||
logger: this.logger,
|
||||
},
|
||||
(plugin) => {
|
||||
return typeof plugin.getPackageStorage !== 'undefined';
|
||||
},
|
||||
this.config?.serverSettings?.pluginPrefix,
|
||||
PLUGIN_CATEGORY.STORAGE
|
||||
);
|
||||
|
||||
if (plugins.length > 1) {
|
||||
this.logger.warn(
|
||||
'more than one storage plugins has been detected, multiple storage are not supported, one will be selected automatically'
|
||||
);
|
||||
}
|
||||
|
||||
return _.head(plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,11 +3,11 @@ import express, { Application } from 'express';
|
|||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { Auth } from '@verdaccio/auth';
|
||||
import { errorUtils } from '@verdaccio/core';
|
||||
import { errorReportingMiddleware, final, handleError } from '@verdaccio/middleware';
|
||||
import { generateRandomHexString } from '@verdaccio/utils';
|
||||
|
||||
import Auth from '../../src/lib/auth';
|
||||
import Config from '../../src/lib/config';
|
||||
import { logger } from '../../src/lib/logger';
|
||||
|
||||
|
@ -29,7 +29,8 @@ export async function initializeServer(
|
|||
debug('storage: %s', config.storage);
|
||||
const storage = new Storage(config);
|
||||
await storage.init(config, []);
|
||||
const auth: Auth = new Auth(config);
|
||||
const auth: Auth = new Auth(config, logger);
|
||||
await auth.init();
|
||||
// FUTURE: in v6 auth.init() is being called
|
||||
// TODO: this might not be need it, used in apiEndpoints
|
||||
app.use(express.json({ strict: false, limit: '100mb' }));
|
||||
|
|
|
@ -16,6 +16,7 @@ jest.mock('../../../../src/lib/logger', () => ({
|
|||
logger: {
|
||||
child: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { errorUtils } from '@verdaccio/core';
|
||||
import { Config } from '@verdaccio/types';
|
||||
|
||||
import Auth from '../../../../src/lib/auth';
|
||||
import AppConfig from '../../../../src/lib/config';
|
||||
import { ROLES } from '../../../../src/lib/constants';
|
||||
import { setup } from '../../../../src/lib/logger';
|
||||
import { IAuth } from '../../../types';
|
||||
import { authPluginFailureConf, authPluginPassThrougConf, authProfileConf } from './helper/plugin';
|
||||
|
||||
setup([]);
|
||||
|
||||
describe('AuthTest', () => {
|
||||
test('should be defined', () => {
|
||||
const config: Config = new AppConfig(_.cloneDeep(authProfileConf));
|
||||
const auth: IAuth = new Auth(config);
|
||||
|
||||
expect(auth).toBeDefined();
|
||||
});
|
||||
|
||||
describe('test authenticate method', () => {
|
||||
describe('test authenticate states', () => {
|
||||
test('should be a success login', () => {
|
||||
const config: Config = new AppConfig(_.cloneDeep(authProfileConf));
|
||||
const auth: IAuth = new Auth(config);
|
||||
|
||||
expect(auth).toBeDefined();
|
||||
|
||||
const callback = jest.fn();
|
||||
const groups = ['test'];
|
||||
|
||||
auth.authenticate('foo', 'bar', callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback).toHaveBeenCalledWith(null, {
|
||||
groups: [
|
||||
'test',
|
||||
ROLES.$ALL,
|
||||
ROLES.$AUTH,
|
||||
ROLES.DEPRECATED_ALL,
|
||||
ROLES.DEPRECATED_AUTH,
|
||||
ROLES.ALL,
|
||||
],
|
||||
name: 'foo',
|
||||
real_groups: groups,
|
||||
});
|
||||
});
|
||||
|
||||
test('should be a fail on login', () => {
|
||||
const config: Config = new AppConfig(_.cloneDeep(authPluginFailureConf));
|
||||
const auth: IAuth = new Auth(config);
|
||||
|
||||
expect(auth).toBeDefined();
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
auth.authenticate('foo', 'bar', callback);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback).toHaveBeenCalledWith(errorUtils.getInternalError());
|
||||
});
|
||||
});
|
||||
|
||||
// plugins are free to send whatever they want, so, we need to test some scenarios
|
||||
// that might make break the request
|
||||
// the @ts-ignore below are intended
|
||||
describe('test authenticate out of control inputs from plugins', () => {
|
||||
test('should skip falsy values', () => {
|
||||
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
|
||||
const auth: IAuth = new Auth(config);
|
||||
|
||||
expect(auth).toBeDefined();
|
||||
|
||||
const callback = jest.fn();
|
||||
let index = 0;
|
||||
|
||||
// as defined by https://developer.mozilla.org/en-US/docs/Glossary/Falsy
|
||||
for (const value of [false, 0, '', null, undefined, NaN]) {
|
||||
// @ts-ignore
|
||||
auth.authenticate(null, value, callback);
|
||||
const call = callback.mock.calls[index++];
|
||||
expect(call[0]).toBeDefined();
|
||||
expect(call[1]).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('should error truthy non-array', () => {
|
||||
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
|
||||
const auth: IAuth = new Auth(config);
|
||||
|
||||
expect(auth).toBeDefined();
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
for (const value of [true, 1, 'test', {}]) {
|
||||
expect(function () {
|
||||
// @ts-ignore
|
||||
auth.authenticate(null, value, callback);
|
||||
}).toThrow(TypeError);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should skip empty array', () => {
|
||||
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
|
||||
const auth: IAuth = new Auth(config);
|
||||
|
||||
expect(auth).toBeDefined();
|
||||
|
||||
const callback = jest.fn();
|
||||
const value = [];
|
||||
|
||||
// @ts-ignore
|
||||
auth.authenticate(null, value, callback);
|
||||
expect(callback.mock.calls).toHaveLength(1);
|
||||
expect(callback.mock.calls[0][0]).toBeDefined();
|
||||
expect(callback.mock.calls[0][1]).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should accept valid array', () => {
|
||||
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
|
||||
const auth: IAuth = new Auth(config);
|
||||
|
||||
expect(auth).toBeDefined();
|
||||
|
||||
const callback = jest.fn();
|
||||
let index = 0;
|
||||
|
||||
for (const value of [[''], ['1'], ['0'], ['000']]) {
|
||||
// @ts-ignore
|
||||
auth.authenticate(null, value, callback);
|
||||
const call = callback.mock.calls[index++];
|
||||
expect(call[0]).toBeNull();
|
||||
expect(call[1].real_groups).toBe(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,113 +0,0 @@
|
|||
import path from 'path';
|
||||
|
||||
import { setup } from '../../../../src/lib/logger';
|
||||
import loadPlugin from '../../../../src/lib/plugin-loader';
|
||||
|
||||
setup([]);
|
||||
|
||||
describe('plugin loader', () => {
|
||||
const relativePath = path.join(__dirname, './partials/test-plugin-storage');
|
||||
const buildConf = (name) => {
|
||||
return {
|
||||
self_path: path.join(__dirname, './'),
|
||||
max_users: 0,
|
||||
auth: {
|
||||
[`${relativePath}/${name}`]: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('auth plugins', () => {
|
||||
test('testing auth valid plugin loader', () => {
|
||||
const _config = buildConf('verdaccio-plugin');
|
||||
// @ts-ignore
|
||||
const plugins = loadPlugin(_config, _config.auth, {}, function (plugin) {
|
||||
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
|
||||
});
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('fails on load scoped auth missing package', () => {
|
||||
const _config = buildConf('@scope/package');
|
||||
try {
|
||||
// @ts-ignore
|
||||
loadPlugin(_config, { '@scope/package': {} }, {}, undefined);
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
`@scope/package plugin not found. try \"npm install @scope/package\"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// This package is locally installed, just a dummy scoped auth plugin
|
||||
// TODO: move this package to the public registry
|
||||
test('should load @verdaccio-scope/verdaccio-auth-foo scoped package', () => {
|
||||
const _config = buildConf('@verdaccio-scope/verdaccio-auth-foo');
|
||||
// @ts-ignore
|
||||
const plugins = loadPlugin(
|
||||
_config,
|
||||
{ '@verdaccio-scope/verdaccio-auth-foo': {} },
|
||||
{},
|
||||
function (plugin) {
|
||||
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
|
||||
}
|
||||
);
|
||||
expect(plugins).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('testing storage valid plugin loader', () => {
|
||||
const _config = buildConf('verdaccio-es6-plugin');
|
||||
// @ts-ignore
|
||||
const plugins = loadPlugin(_config, _config.auth, {}, function (p) {
|
||||
return p.getPackageStorage;
|
||||
});
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('testing auth plugin invalid plugin', () => {
|
||||
const _config = buildConf('invalid-plugin');
|
||||
try {
|
||||
// @ts-ignore
|
||||
loadPlugin(_config, _config.auth, {}, function (p) {
|
||||
return p.authenticate || p.allow_access || p.allow_publish;
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual(
|
||||
`"${relativePath}/invalid-plugin" plugin does not have the right code structure`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('testing auth plugin invalid plugin sanityCheck', () => {
|
||||
const _config = buildConf('invalid-plugin-sanity');
|
||||
try {
|
||||
// @ts-ignore
|
||||
loadPlugin(_config, _config.auth, {}, function (plugin) {
|
||||
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`sanity check has failed, "${relativePath}/invalid-plugin-sanity" is not a valid plugin`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('testing auth plugin no plugins', () => {
|
||||
const _config = buildConf('invalid-package');
|
||||
try {
|
||||
// @ts-ignore
|
||||
loadPlugin(_config, _config.auth, {}, function (plugin) {
|
||||
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('plugin not found');
|
||||
expect(e.message).toMatch('/partials/test-plugin-storage/invalid-package');
|
||||
}
|
||||
});
|
||||
|
||||
test.todo('test middleware plugins');
|
||||
test.todo('test storage plugins');
|
||||
});
|
||||
});
|
|
@ -1,554 +0,0 @@
|
|||
import path from 'path';
|
||||
import rimRaf from 'rimraf';
|
||||
|
||||
import { Config, MergeTags, Package } from '@verdaccio/types';
|
||||
|
||||
import AppConfig from '../../../../src/lib/config';
|
||||
import { API_ERROR, DIST_TAGS, HTTP_STATUS } from '../../../../src/lib/constants';
|
||||
import LocalStorage from '../../../../src/lib/local-storage';
|
||||
import { logger, setup } from '../../../../src/lib/logger';
|
||||
import { generatePackageTemplate } from '../../../../src/lib/storage-utils';
|
||||
import { readFile } from '../../../functional/lib/test.utils';
|
||||
import { generateNewVersion } from '../../../lib/utils-test';
|
||||
// @ts-ignore
|
||||
import configExample from '../../partials/config';
|
||||
|
||||
const readMetadata = (fileName = 'metadata') =>
|
||||
readFile(`../../unit/partials/${fileName}`).toString();
|
||||
|
||||
setup([]);
|
||||
|
||||
describe('LocalStorage', () => {
|
||||
let storage: LocalStorage;
|
||||
const pkgName = 'npm_test';
|
||||
const pkgNameScoped = `@scope/${pkgName}-scope`;
|
||||
const tarballName = `${pkgName}-add-tarball-1.0.4.tgz`;
|
||||
const tarballName2 = `${pkgName}-add-tarball-1.0.5.tgz`;
|
||||
|
||||
const getStorage = (LocalStorageClass = LocalStorage) => {
|
||||
const config: Config = new AppConfig(
|
||||
configExample({
|
||||
self_path: path.join('../partials/store'),
|
||||
})
|
||||
);
|
||||
|
||||
return new LocalStorageClass(config, logger);
|
||||
};
|
||||
|
||||
const getPackageMetadataFromStore = (pkgName: string): Promise<Package> => {
|
||||
return new Promise((resolve) => {
|
||||
storage.getPackageMetadata(pkgName, (err, data) => {
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addNewVersion = (pkgName: string, version: string) => {
|
||||
return new Promise((resolve) => {
|
||||
storage.addVersion(
|
||||
pkgName,
|
||||
version,
|
||||
generateNewVersion(pkgName, version),
|
||||
'',
|
||||
(err, data) => {
|
||||
resolve(data);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
const addTarballToStore = (pkgName: string, tarballName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tarballData = JSON.parse(readMetadata('addTarball').toString());
|
||||
const stream = storage.addTarball(pkgName, tarballName);
|
||||
|
||||
stream.on('error', (err) => {
|
||||
expect(err).toBeNull();
|
||||
reject();
|
||||
});
|
||||
stream.on('success', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
stream.end(Buffer.from(tarballData.data, 'base64'));
|
||||
stream.done();
|
||||
});
|
||||
};
|
||||
|
||||
const addPackageToStore = (pkgName, metadata) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
const pkgStoragePath = storage._getLocalStorage(pkgName);
|
||||
rimRaf(pkgStoragePath.path, (err) => {
|
||||
expect(err).toBeNull();
|
||||
storage.addPackage(pkgName, metadata, async (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
storage = getStorage();
|
||||
});
|
||||
|
||||
test('should be defined', () => {
|
||||
expect(storage).toBeDefined();
|
||||
});
|
||||
|
||||
describe('LocalStorage::preparePackage', () => {
|
||||
test('should add a package', (done) => {
|
||||
const metadata = JSON.parse(readMetadata().toString());
|
||||
// @ts-ignore
|
||||
const pkgStoragePath = storage._getLocalStorage(pkgName);
|
||||
rimRaf(pkgStoragePath.path, (err) => {
|
||||
expect(err).toBeNull();
|
||||
storage.addPackage(pkgName, metadata, (err, data) => {
|
||||
expect(data.version).toMatch(/1.0.0/);
|
||||
expect(data.dist.tarball).toMatch(/npm_test-1.0.0.tgz/);
|
||||
expect(data.name).toEqual(pkgName);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should add a @scope package', (done) => {
|
||||
const metadata = JSON.parse(readMetadata());
|
||||
// @ts-ignore
|
||||
const pkgStoragePath = storage._getLocalStorage(pkgNameScoped);
|
||||
|
||||
rimRaf(pkgStoragePath.path, (err) => {
|
||||
expect(err).toBeNull();
|
||||
storage.addPackage(pkgNameScoped, metadata, (err, data) => {
|
||||
expect(data.version).toMatch(/1.0.0/);
|
||||
expect(data.dist.tarball).toMatch(/npm_test-1.0.0.tgz/);
|
||||
expect(data.name).toEqual(pkgName);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails on add a package', (done) => {
|
||||
const metadata = JSON.parse(readMetadata());
|
||||
|
||||
storage.addPackage(pkgName, metadata, (err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.CONFLICT);
|
||||
expect(err.message).toMatch(API_ERROR.PACKAGE_EXIST);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::mergeTags', () => {
|
||||
test('should mergeTags', async () => {
|
||||
const pkgName = 'merge-tags-test-1';
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
await addNewVersion(pkgName, '1.0.0');
|
||||
await addNewVersion(pkgName, '2.0.0');
|
||||
await addNewVersion(pkgName, '3.0.0');
|
||||
const tags: MergeTags = {
|
||||
beta: '3.0.0',
|
||||
latest: '2.0.0',
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
storage.mergeTags(pkgName, tags, async (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBeUndefined();
|
||||
const metadata: Package = await getPackageMetadataFromStore(pkgName);
|
||||
expect(metadata[DIST_TAGS]).toBeDefined();
|
||||
expect(metadata[DIST_TAGS]['beta']).toBeDefined();
|
||||
expect(metadata[DIST_TAGS]['beta']).toBe('3.0.0');
|
||||
expect(metadata[DIST_TAGS]['latest']).toBe('2.0.0');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails mergeTags version not found', async () => {
|
||||
const pkgName = 'merge-tags-test-1';
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
// const tarballName: string = `${pkgName}-${version}.tgz`;
|
||||
await addNewVersion(pkgName, '1.0.0');
|
||||
await addNewVersion(pkgName, '2.0.0');
|
||||
await addNewVersion(pkgName, '3.0.0');
|
||||
const tags: MergeTags = {
|
||||
beta: '9999.0.0',
|
||||
};
|
||||
|
||||
return new Promise((done) => {
|
||||
storage.mergeTags(pkgName, tags, async (err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.message).toMatch(API_ERROR.VERSION_NOT_EXIST);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails on mergeTags', async () => {
|
||||
const tags: MergeTags = {
|
||||
beta: '3.0.0',
|
||||
latest: '2.0.0',
|
||||
};
|
||||
return new Promise((done) => {
|
||||
storage.mergeTags('not-found', tags, async (err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.message).toMatch(API_ERROR.NO_PACKAGE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::addVersion', () => {
|
||||
test('should add new version without tag', async () => {
|
||||
const pkgName = 'add-version-test-1';
|
||||
const version = '1.0.1';
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
const tarballName = `${pkgName}-${version}.tgz`;
|
||||
await addNewVersion(pkgName, '9.0.0');
|
||||
await addTarballToStore(pkgName, `${pkgName}-9.0.0.tgz`);
|
||||
await addTarballToStore(pkgName, tarballName);
|
||||
|
||||
return new Promise((done) => {
|
||||
storage.addVersion(
|
||||
pkgName,
|
||||
version,
|
||||
generateNewVersion(pkgName, version),
|
||||
'',
|
||||
(err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBeUndefined();
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails on add a duplicated version without tag', async () => {
|
||||
const pkgName = 'add-version-test-2';
|
||||
const version = '1.0.1';
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
await addNewVersion(pkgName, version);
|
||||
|
||||
return new Promise((done) => {
|
||||
storage.addVersion(pkgName, version, generateNewVersion(pkgName, version), '', (err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.CONFLICT);
|
||||
expect(err.message).toMatch(API_ERROR.PACKAGE_EXIST);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails add new version wrong shasum', async () => {
|
||||
const pkgName = 'add-version-test-4';
|
||||
const version = '4.0.0';
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
const tarballName = `${pkgName}-${version}.tgz`;
|
||||
await addTarballToStore(pkgName, tarballName);
|
||||
return new Promise((done) => {
|
||||
storage.addVersion(
|
||||
pkgName,
|
||||
version,
|
||||
generateNewVersion(pkgName, version, 'fake'),
|
||||
'',
|
||||
(err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(err.message).toMatch(/shasum error/);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should add new second version without tag', async () => {
|
||||
const pkgName = 'add-version-test-3';
|
||||
const version = '1.0.2';
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
await addNewVersion(pkgName, '1.0.1');
|
||||
await addNewVersion(pkgName, '1.0.3');
|
||||
return new Promise((done) => {
|
||||
storage.addVersion(
|
||||
pkgName,
|
||||
version,
|
||||
generateNewVersion(pkgName, version),
|
||||
'beta',
|
||||
(err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBeUndefined();
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::updateVersions', () => {
|
||||
const metadata = JSON.parse(readMetadata('metadata-update-versions-tags'));
|
||||
const pkgName = 'add-update-versions-test-1';
|
||||
const version = '1.0.2';
|
||||
let _storage;
|
||||
beforeEach((done) => {
|
||||
class MockLocalStorage extends LocalStorage {}
|
||||
// @ts-ignore
|
||||
MockLocalStorage.prototype._writePackage = jest.fn(LocalStorage.prototype._writePackage);
|
||||
_storage = getStorage(MockLocalStorage);
|
||||
rimRaf(path.join(configExample().storage, pkgName), async () => {
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
await addNewVersion(pkgName, '1.0.1');
|
||||
await addNewVersion(pkgName, version);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should update versions from external source', async () => {
|
||||
return new Promise((done) => {
|
||||
_storage.updateVersions(pkgName, metadata, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(_storage._writePackage).toHaveBeenCalledTimes(1);
|
||||
expect(data.versions['1.0.1']).toBeDefined();
|
||||
expect(data.versions[version]).toBeDefined();
|
||||
expect(data.versions['1.0.4']).toBeDefined();
|
||||
expect(data[DIST_TAGS]['latest']).toBeDefined();
|
||||
expect(data[DIST_TAGS]['latest']).toBe('1.0.1');
|
||||
expect(data[DIST_TAGS]['beta']).toBeDefined();
|
||||
expect(data[DIST_TAGS]['beta']).toBe('1.0.2');
|
||||
expect(data[DIST_TAGS]['next']).toBeDefined();
|
||||
expect(data[DIST_TAGS]['next']).toBe('1.0.4');
|
||||
expect(data['_rev'] === metadata['_rev']).toBeFalsy();
|
||||
expect(data.readme).toBe('readme 1.0.4');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should not update if the metadata match', (done) => {
|
||||
_storage.updateVersions(pkgName, metadata, (e) => {
|
||||
expect(e).toBeNull();
|
||||
_storage.updateVersions(pkgName, metadata, (err) => {
|
||||
expect(err).toBeNull();
|
||||
expect(_storage._writePackage).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::changePackage', () => {
|
||||
const pkgName = 'change-package';
|
||||
|
||||
test('should unpublish a version', async () => {
|
||||
await addPackageToStore(pkgName, generatePackageTemplate(pkgName));
|
||||
await addNewVersion(pkgName, '1.0.1');
|
||||
await addNewVersion(pkgName, '1.0.2');
|
||||
await addNewVersion(pkgName, '1.0.3');
|
||||
const metadata = JSON.parse(readMetadata('changePackage/metadata-change'));
|
||||
const rev: string = metadata['_rev'];
|
||||
|
||||
return new Promise((done) => {
|
||||
storage.changePackage(pkgName, metadata, rev, (err) => {
|
||||
expect(err).toBeUndefined();
|
||||
storage.getPackageMetadata(pkgName, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data.versions['1.0.1']).toBeDefined();
|
||||
expect(data.versions['1.0.2']).toBeUndefined();
|
||||
expect(data.versions['1.0.3']).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::tarball operations', () => {
|
||||
describe('LocalStorage::addTarball', () => {
|
||||
test('should add a new tarball', (done) => {
|
||||
const tarballData = JSON.parse(readMetadata('addTarball'));
|
||||
const stream = storage.addTarball(pkgName, tarballName);
|
||||
|
||||
stream.on('error', (err) => {
|
||||
expect(err).toBeNull();
|
||||
done();
|
||||
});
|
||||
stream.on('success', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
stream.end(Buffer.from(tarballData.data, 'base64'));
|
||||
stream.done();
|
||||
});
|
||||
|
||||
test('should add a new second tarball', (done) => {
|
||||
const tarballData = JSON.parse(readMetadata('addTarball'));
|
||||
const stream = storage.addTarball(pkgName, tarballName2);
|
||||
stream.on('error', (err) => {
|
||||
expect(err).toBeNull();
|
||||
done();
|
||||
});
|
||||
stream.on('success', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
stream.end(Buffer.from(tarballData.data, 'base64'));
|
||||
stream.done();
|
||||
});
|
||||
|
||||
test('should fails on add a duplicated new tarball', (done) => {
|
||||
const tarballData = JSON.parse(readMetadata('addTarball'));
|
||||
const stream = storage.addTarball(pkgName, tarballName);
|
||||
stream.on('error', (err: any) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.CONFLICT);
|
||||
expect(err.message).toMatch(/this package is already present/);
|
||||
done();
|
||||
});
|
||||
stream.end(Buffer.from(tarballData.data, 'base64'));
|
||||
stream.done();
|
||||
});
|
||||
|
||||
test('should fails on add a new tarball on missing package', (done) => {
|
||||
const tarballData = JSON.parse(readMetadata('addTarball'));
|
||||
const stream = storage.addTarball('unexsiting-package', tarballName);
|
||||
stream.on('error', (err: any) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.message).toMatch(/no such package available/);
|
||||
done();
|
||||
});
|
||||
|
||||
stream.on('success', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
stream.end(Buffer.from(tarballData.data, 'base64'));
|
||||
stream.done();
|
||||
});
|
||||
|
||||
test('should fails on use invalid package name on add a new tarball', (done) => {
|
||||
const stream = storage.addTarball(pkgName, `${pkgName}-fails-add-tarball-1.0.4.tgz`);
|
||||
stream.on('error', function (err: any) {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.BAD_DATA);
|
||||
expect(err.message).toMatch(/refusing to accept zero-length file/);
|
||||
done();
|
||||
});
|
||||
|
||||
stream.done();
|
||||
});
|
||||
|
||||
test('should fails on abort on add a new tarball', (done) => {
|
||||
const stream = storage.addTarball('__proto__', `${pkgName}-fails-add-tarball-1.0.4.tgz`);
|
||||
stream.abort();
|
||||
stream.on('error', function (err: any) {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.FORBIDDEN);
|
||||
expect(err.message).toMatch(/can't use this filename/);
|
||||
done();
|
||||
});
|
||||
|
||||
stream.done();
|
||||
});
|
||||
});
|
||||
describe('LocalStorage::removeTarball', () => {
|
||||
test('should remove a tarball', (done) => {
|
||||
storage.removeTarball(pkgName, tarballName2, 'rev', (err, pkg) => {
|
||||
expect(err).toBeNull();
|
||||
expect(pkg).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should remove a tarball that does not exist', (done) => {
|
||||
storage.removeTarball(pkgName, tarballName2, 'rev', (err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.message).toMatch(/no such file available/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::getTarball', () => {
|
||||
test('should get a existing tarball', (done) => {
|
||||
const stream = storage.getTarball(pkgName, tarballName);
|
||||
stream.on('content-length', function (contentLength) {
|
||||
expect(contentLength).toBe(279);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails on get a tarball that does not exist', (done) => {
|
||||
const stream = storage.getTarball('fake', tarballName);
|
||||
stream.on('error', function (err: any) {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.message).toMatch(/no such file available/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::search', () => {
|
||||
test('should find a tarball', (done) => {
|
||||
// @ts-ignore
|
||||
const stream = storage.search('99999');
|
||||
|
||||
stream.on('data', function each(pkg) {
|
||||
expect(pkg.name).toEqual(pkgName);
|
||||
});
|
||||
|
||||
stream.on('error', function (err) {
|
||||
expect(err).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalStorage::removePackage', () => {
|
||||
test.skip('should remove completely package', (done) => {
|
||||
storage.removePackage(pkgName, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should remove completely @scoped package', (done) => {
|
||||
storage.removePackage(pkgNameScoped, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails with package not found', (done) => {
|
||||
const pkgName = 'npm_test_fake';
|
||||
storage.removePackage(pkgName, (err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.message).toMatch(/no such package available/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should fails with @scoped package not found', (done) => {
|
||||
storage.removePackage(pkgNameScoped, (err) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(err.message).toMatch(API_ERROR.NO_PACKAGE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11916,6 +11916,7 @@ __metadata:
|
|||
"@verdaccio/auth": 8.0.0-next-8.10
|
||||
"@verdaccio/config": 8.0.0-next-8.10
|
||||
"@verdaccio/core": 8.0.0-next-8.10
|
||||
"@verdaccio/loaders": 8.0.0-next-8.4
|
||||
"@verdaccio/local-storage-legacy": 11.0.2
|
||||
"@verdaccio/logger": 8.0.0-next-8.10
|
||||
"@verdaccio/middleware": 8.0.0-next-8.10
|
||||
|
|
Loading…
Add table
Reference in a new issue