1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-13 03:35:52 +01:00

chore: fastify auth layer implementation

chore: fastify auth layer implementation
This commit is contained in:
Juan Picado 2022-10-31 21:27:33 +01:00
parent a206f476b0
commit 513407a0a4
18 changed files with 232 additions and 31 deletions

1
.gitignore vendored

@ -13,6 +13,7 @@ node_modules
### test ### test
test-storage* test-storage*
.verdaccio_test_env .verdaccio_test_env
packages/server/fastify/debug/storage
# docker examples # docker examples
docker-examples/v5/reverse_proxy/nginx/relative_path/storage/* docker-examples/v5/reverse_proxy/nginx/relative_path/storage/*

@ -7,3 +7,4 @@ export {
export { getLocalRegistryTarballUri } from './getLocalRegistryTarballUri'; export { getLocalRegistryTarballUri } from './getLocalRegistryTarballUri';
export { RequestOptions }; export { RequestOptions };
export { getVersionFromTarball } from './utils';

@ -0,0 +1,5 @@
export function getVersionFromTarball(name: string): string | void {
const groups = name.match(/.+-(\d.+)\.tgz/);
return groups !== null ? groups[1] : undefined;
}

@ -17,11 +17,12 @@ const debug = buildDebug('verdaccio:fastify:debug');
const configFile = path.join(__dirname, './fastify-conf.yaml'); const configFile = path.join(__dirname, './fastify-conf.yaml');
debug('configFile %s', configFile); debug('configFile %s', configFile);
const configParsed = parseConfigFile(configFile); const configParsed = parseConfigFile(configFile);
// @ts-ignore
setup(configParsed.log); setup(configParsed.log);
logger.info(`config location ${configFile}`); logger.info(`config location ${configFile}`);
debug('configParsed %s', configParsed); debug('configParsed %s', configParsed);
process.title = 'fastify-verdaccio'; process.title = 'fastify-verdaccio';
const ser = await server({ logger, config: configParsed }); const ser = await server(configParsed);
await ser.listen(4873); await ser.listen(4873);
logger.info('fastify running on port 4873'); logger.info('fastify running on port 4873');
} catch (err: any) { } catch (err: any) {

@ -3,6 +3,9 @@ import { FastifyInstance } from 'fastify';
import { MergeTags } from '@verdaccio/types'; import { MergeTags } from '@verdaccio/types';
import allow from '../plugins/allow';
import pkgMetadata from '../plugins/pkgMetadata';
const debug = buildDebug('verdaccio:fastify:dist-tags'); const debug = buildDebug('verdaccio:fastify:dist-tags');
interface ParamsInterface { interface ParamsInterface {
@ -10,6 +13,9 @@ interface ParamsInterface {
} }
async function distTagsRoute(fastify: FastifyInstance) { async function distTagsRoute(fastify: FastifyInstance) {
fastify.register(pkgMetadata);
fastify.register(allow, { type: 'access' });
fastify.get<{ Params: ParamsInterface }>( fastify.get<{ Params: ParamsInterface }>(
'/-/package/:packageName/dist-tags', '/-/package/:packageName/dist-tags',
async (request, reply) => { async (request, reply) => {

@ -3,10 +3,13 @@ import { FastifyInstance } from 'fastify';
import { stringUtils } from '@verdaccio/core'; import { stringUtils } from '@verdaccio/core';
import { Storage } from '@verdaccio/store'; import { Storage } from '@verdaccio/store';
import { Package, Version } from '@verdaccio/types'; import { Manifest, Version } from '@verdaccio/types';
import allow from '../plugins/allow';
import pkgMetadata from '../plugins/pkgMetadata';
const debug = buildDebug('verdaccio:fastify:api:sidebar'); const debug = buildDebug('verdaccio:fastify:api:sidebar');
export type $SidebarPackage = Package & { latest: Version }; export type $SidebarPackage = Manifest & { latest: Version };
interface ParamsInterface { interface ParamsInterface {
name: string; name: string;
@ -14,6 +17,9 @@ interface ParamsInterface {
} }
async function manifestRoute(fastify: FastifyInstance) { async function manifestRoute(fastify: FastifyInstance) {
fastify.register(pkgMetadata);
fastify.register(allow, { type: 'access' });
fastify.get<{ Params: ParamsInterface }>('/:name', async (request) => { fastify.get<{ Params: ParamsInterface }>('/:name', async (request) => {
const { name } = request.params; const { name } = request.params;
const storage = fastify.storage; const storage = fastify.storage;
@ -30,6 +36,8 @@ async function manifestRoute(fastify: FastifyInstance) {
protocol: request.protocol, protocol: request.protocol,
headers: request.headers as any, headers: request.headers as any,
host: request.hostname, host: request.hostname,
// @ts-ignore
username: request?.userRemote?.name,
}, },
abbreviated, abbreviated,
}); });
@ -41,7 +49,7 @@ async function manifestRoute(fastify: FastifyInstance) {
} }
fastify.get<{ Params: ParamsInterface; Querystring: QueryInterface }>( fastify.get<{ Params: ParamsInterface; Querystring: QueryInterface }>(
'/:packageName/:version', '/:name/:version',
async (request) => { async (request) => {
const { name, version } = request.params; const { name, version } = request.params;
const storage = fastify.storage; const storage = fastify.storage;

@ -1,12 +1,9 @@
/* eslint-disable no-console */
/* eslint-disable no-invalid-this */
import { FastifyInstance } from 'fastify'; import { FastifyInstance } from 'fastify';
import { logger } from '@verdaccio/logger'; import { logger } from '@verdaccio/logger';
async function pingRoute(fastify: FastifyInstance) { async function pingRoute(fastify: FastifyInstance) {
fastify.get('/-/ping', async () => { fastify.get('/-/ping', () => {
logger.http('ping'); logger.http('ping');
return {}; return {};
}); });

@ -18,7 +18,8 @@ async function searchRoute(fastify: FastifyInstance) {
// TODO: add validations for query, some parameters are mandatory // TODO: add validations for query, some parameters are mandatory
// TODO: review which query fields are mandatory // TODO: review which query fields are mandatory
const abort = new AbortController(); const abort = new AbortController();
request.socket.on('aborted', () => { // https://nodejs.org/dist/latest-v18.x/docs/api/http.html#event-close
request.socket.on('close', () => {
abort.abort(); abort.abort();
}); });
const { url, query } = request.query; const { url, query } = request.query;

@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import buildDebug from 'debug'; import buildDebug from 'debug';
import { FastifyInstance } from 'fastify'; import { FastifyInstance } from 'fastify';
import { HEADERS, HEADER_TYPE } from '@verdaccio/core'; import { HEADERS, HEADER_TYPE } from '@verdaccio/core';
import allow from '../plugins/allow';
import pkgMetadata from '../plugins/pkgMetadata';
const debug = buildDebug('verdaccio:fastify:tarball'); const debug = buildDebug('verdaccio:fastify:tarball');
interface ParamsInterface { interface ParamsInterface {
@ -12,6 +14,9 @@ interface ParamsInterface {
} }
async function tarballRoute(fastify: FastifyInstance) { async function tarballRoute(fastify: FastifyInstance) {
fastify.register(pkgMetadata);
fastify.register(allow, { type: 'access' });
fastify.get<{ Params: ParamsInterface }>('/:package/-/:filename', async (request, reply) => { fastify.get<{ Params: ParamsInterface }>('/:package/-/:filename', async (request, reply) => {
const { package: pkg, filename } = request.params; const { package: pkg, filename } = request.params;
debug('stream tarball for %s@%s', pkg, filename); debug('stream tarball for %s@%s', pkg, filename);
@ -25,10 +30,10 @@ async function tarballRoute(fastify: FastifyInstance) {
reply.header(HEADER_TYPE.CONTENT_LENGTH, size); reply.header(HEADER_TYPE.CONTENT_LENGTH, size);
}); });
// request.socket.on('abort', () => { // https://nodejs.org/dist/latest-v18.x/docs/api/http.html#event-close
// debug('request aborted for %o', request.url); request.socket.on('close', () => {
// abort.abort(); abort.abort();
// }); });
return stream; return stream;
}); });
@ -55,10 +60,10 @@ async function tarballRoute(fastify: FastifyInstance) {
reply.header(HEADER_TYPE.CONTENT_LENGTH, size); reply.header(HEADER_TYPE.CONTENT_LENGTH, size);
}); });
// request.socket.on('abort', () => { // https://nodejs.org/dist/latest-v18.x/docs/api/http.html#event-close
// debug('request aborted for %o', request.url); request.socket.on('close', () => {
// abort.abort(); abort.abort();
// }); });
reply.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM); reply.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);
return stream; return stream;

@ -1,6 +1,3 @@
/* eslint-disable no-console */
/* eslint-disable no-invalid-this */
import buildDebug from 'debug'; import buildDebug from 'debug';
import { FastifyInstance } from 'fastify'; import { FastifyInstance } from 'fastify';
import _ from 'lodash'; import _ from 'lodash';
@ -39,6 +36,7 @@ async function userRoute(fastify: FastifyInstance) {
const { token } = request.params; const { token } = request.params;
const userRemote: RemoteUser = request.userRemote; const userRemote: RemoteUser = request.userRemote;
await fastify.auth.invalidateToken(token); await fastify.auth.invalidateToken(token);
// eslint-disable-next-line no-console
console.log('userRoute', userRemote); console.log('userRoute', userRemote);
reply.code(fastify.statusCode.OK); reply.code(fastify.statusCode.OK);
return { ok: fastify.apiMessage.LOGGED_OUT }; return { ok: fastify.apiMessage.LOGGED_OUT };

@ -0,0 +1,85 @@
import { FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';
import { Auth } from '@verdaccio/auth';
import { pluginUtils } from '@verdaccio/core';
import { RemoteUser } from '@verdaccio/types';
/**
* TODO: use @verdaccio/tarball
* return package version from tarball name
* @param {String} name
* @deprecated use @verdaccio/tarball
* @returns {String}
*/
export function getVersionFromTarball(name: string): string | void {
const groups = name.match(/.+-(\d.+)\.tgz/);
return groups !== null ? groups[1] : undefined;
}
export default fp(
async (fastify: FastifyInstance, opts: { type: string }) => {
// ensure user remote is populated on every request
fastify.addHook('preValidation', async function (request) {
const remote: RemoteUser = request.userRemote;
switch (opts.type) {
case 'access':
return new Promise((resolve, reject) => {
return fastify.auth.allow_access(request.pkgMetadata, remote, (err, allowed) => {
if (err) {
return reject(err);
}
if (allowed === true) {
return resolve();
}
return reject(fastify.errorUtils.getForbidden());
});
});
case 'publish':
return new Promise((resolve, reject) => {
return fastify.auth.allow_publish(request.pkgMetadata, remote, (err, allowed) => {
if (err) {
return reject(err);
}
if (allowed === true) {
return resolve();
}
return reject(fastify.errorUtils.getForbidden());
});
});
case 'unpublish':
return new Promise((resolve, reject) => {
return fastify.auth.allow_unpublish(request.pkgMetadata, remote, (err, allowed) => {
if (err) {
return reject(err);
}
if (allowed === true) {
return resolve();
}
return reject(fastify.errorUtils.getForbidden());
});
});
default:
}
});
},
{
fastify: '>=4.x',
}
);
declare module 'fastify' {
// @ts-ignore
interface FastifyInstance {
auth: Auth;
}
// @ts-ignore
interface FastifyRequest {
pkgMetadata: pluginUtils.AuthPluginPackage;
// TODO: scope not caugh yet.
pkgScope?: string;
}
}

@ -17,6 +17,7 @@ export default fp(
); );
declare module 'fastify' { declare module 'fastify' {
// @ts-ignore
interface FastifyInstance { interface FastifyInstance {
auth: Auth; auth: Auth;
} }

@ -16,6 +16,7 @@ export default fp(
); );
declare module 'fastify' { declare module 'fastify' {
// @ts-ignore
interface FastifyInstance { interface FastifyInstance {
configInstance: IConfig; configInstance: IConfig;
} }

@ -35,6 +35,7 @@ export default fp(
); );
declare module 'fastify' { declare module 'fastify' {
// @ts-ignore
interface FastifyInstance { interface FastifyInstance {
apiError: typeof API_ERROR; apiError: typeof API_ERROR;
apiMessage: typeof API_MESSAGE; apiMessage: typeof API_MESSAGE;

@ -0,0 +1,63 @@
import { FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';
import { Auth } from '@verdaccio/auth';
import { pluginUtils } from '@verdaccio/core';
import { Manifest } from '@verdaccio/types';
/**
* TODO: use @verdaccio/tarball
* return package version from tarball name
* @param {String} name
* @deprecated use @verdaccio/tarball
* @returns {String}
*/
export function getVersionFromTarball(name: string): string | void {
const groups = name.match(/.+-(\d.+)\.tgz/);
return groups !== null ? groups[1] : undefined;
}
export default fp(
async (fastify: FastifyInstance) => {
fastify.addHook<{
Params: { scope: string; name: string; filename?: string; version?: string; tag?: string };
Body: Manifest;
}>('onRequest', function (request, _reply, done) {
// TODO: scope is not implemented yet
const { scope, name, tag, filename } = request.params;
const packageName = typeof scope === 'string' ? `@${scope}/${name}` : name;
// version could be a param initially
let packageVersion: string | undefined = request.params.version;
// when request is tarball request version comes with the tarball
// eg: http://localhost:4873/@angular/cli/-/cli-8.3.5.tgz
if (filename && typeof packageVersion !== 'string') {
packageVersion = getVersionFromTarball(filename) || undefined;
}
const pkgMetadata: pluginUtils.AuthPluginPackage = {
packageName,
packageVersion,
tag,
};
request.pkgMetadata = pkgMetadata;
done();
});
},
{
fastify: '>=4.x',
}
);
declare module 'fastify' {
// @ts-ignore
interface FastifyInstance {
auth: Auth;
}
// @ts-ignore
interface FastifyRequest {
pkgMetadata: pluginUtils.AuthPluginPackage;
}
}

@ -18,6 +18,7 @@ export default fp(
); );
declare module 'fastify' { declare module 'fastify' {
// @ts-ignore
interface FastifyInstance { interface FastifyInstance {
storage: Storage; storage: Storage;
} }

@ -0,0 +1,27 @@
import { FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';
import { Auth } from '@verdaccio/auth';
import { createAnonymousRemoteUser } from '@verdaccio/config';
import { Config as IConfig } from '@verdaccio/types';
export default fp(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function (fastify: FastifyInstance, _opts: { config: IConfig; filters?: unknown }) {
// ensure user remote is populated on every request
fastify.addHook('onRequest', (request, _reply, done) => {
request.userRemote = createAnonymousRemoteUser();
done();
});
},
{
fastify: '>=4.x',
}
);
declare module 'fastify' {
// @ts-ignore
interface FastifyInstance {
auth: Auth;
}
}

@ -1,7 +1,7 @@
import buildDebug from 'debug'; import buildDebug from 'debug';
import fastify from 'fastify'; import fastify from 'fastify';
import { Config as AppConfig, createAnonymousRemoteUser } from '@verdaccio/config'; import { Config as AppConfig } from '@verdaccio/config';
import { logger } from '@verdaccio/logger'; import { logger } from '@verdaccio/logger';
import { ConfigYaml, Config as IConfig, RemoteUser } from '@verdaccio/types'; import { ConfigYaml, Config as IConfig, RemoteUser } from '@verdaccio/types';
@ -16,6 +16,7 @@ import authPlugin from './plugins/auth';
import configPlugin from './plugins/config'; import configPlugin from './plugins/config';
import coreUtils from './plugins/coreUtils'; import coreUtils from './plugins/coreUtils';
import storagePlugin from './plugins/storage'; import storagePlugin from './plugins/storage';
import userRemoteVerify from './plugins/userRemoteVerify';
import login from './routes/web/api/login'; import login from './routes/web/api/login';
import readme from './routes/web/api/readme'; import readme from './routes/web/api/readme';
import sidebar from './routes/web/api/sidebar'; import sidebar from './routes/web/api/sidebar';
@ -32,15 +33,11 @@ async function startServer(config: ConfigYaml): Promise<any> {
debug('start fastify server'); debug('start fastify server');
// TODO: custom logger type and logger accepted by fastify does not match // TODO: custom logger type and logger accepted by fastify does not match
const fastifyInstance = fastify({ logger: logger as any }); const fastifyInstance = fastify({ logger: logger as any });
fastifyInstance.addHook('onRequest', (request, reply, done) => {
request.userRemote = createAnonymousRemoteUser();
done();
});
fastifyInstance.register(coreUtils); fastifyInstance.register(coreUtils);
fastifyInstance.register(configPlugin, { config }); fastifyInstance.register(configPlugin, { config });
fastifyInstance.register(storagePlugin, { config: configInstance }); fastifyInstance.register(storagePlugin, { config: configInstance });
fastifyInstance.register(authPlugin, { config: configInstance }); fastifyInstance.register(authPlugin, { config: configInstance });
fastifyInstance.register(userRemoteVerify);
// api // api
fastifyInstance.register((instance, opts, done) => { fastifyInstance.register((instance, opts, done) => {
instance.register(ping); instance.register(ping);
@ -57,17 +54,19 @@ async function startServer(config: ConfigYaml): Promise<any> {
done(); done();
}); });
// web // debugging purpose
fastifyInstance.register((instance, opts, done) => { fastifyInstance.addHook('onRoute', (routeOptions) => {
instance.register(ping, { prefix: '/web' }); debug('route: prefix: %s url: %s', routeOptions.prefix, routeOptions.routePath);
done();
}); });
return fastifyInstance; return fastifyInstance;
} }
declare module 'fastify' { declare module 'fastify' {
// @ts-ignore
interface FastifyRequest { interface FastifyRequest {
userRemote: RemoteUser; userRemote: RemoteUser;
authenticationHeader?: string;
} }
} }