mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-01-06 22:40:26 -05:00
refactor: auth-utils (#1951)
* chore: refactor auth utils * chore: relocate crypto utils
This commit is contained in:
parent
5f3072a819
commit
fbd761c8ee
12 changed files with 307 additions and 310 deletions
|
@ -29,6 +29,7 @@
|
|||
"@verdaccio/logger": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/utils": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/auth": "workspace:5.0.0-alpha.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"debug": "^4.1.1",
|
||||
"express": "4.17.1",
|
||||
"lodash": "4.17.15"
|
||||
|
|
|
@ -10,17 +10,6 @@ import {
|
|||
} from '@verdaccio/commons-api';
|
||||
import { API_ERROR, SUPPORT_ERRORS, TOKEN_BASIC, TOKEN_BEARER } from '@verdaccio/dev-commons';
|
||||
import { loadPlugin } from '@verdaccio/loaders';
|
||||
import {
|
||||
aesEncrypt,
|
||||
signPayload,
|
||||
isNil,
|
||||
isFunction,
|
||||
getMatchedPackagesSpec,
|
||||
getDefaultPlugins,
|
||||
createAnonymousRemoteUser,
|
||||
convertPayloadToBase64,
|
||||
createRemoteUser,
|
||||
} from '@verdaccio/utils';
|
||||
|
||||
import {
|
||||
Config,
|
||||
|
@ -35,9 +24,20 @@ import {
|
|||
AllowAccess,
|
||||
PackageAccess,
|
||||
} from '@verdaccio/types';
|
||||
|
||||
import {
|
||||
isNil,
|
||||
isFunction,
|
||||
getMatchedPackagesSpec,
|
||||
createAnonymousRemoteUser,
|
||||
convertPayloadToBase64,
|
||||
createRemoteUser,
|
||||
} from '@verdaccio/utils';
|
||||
|
||||
import {
|
||||
getMiddlewareCredentials,
|
||||
getSecurity,
|
||||
getDefaultPlugins,
|
||||
verifyJWTPayload,
|
||||
parseBasicPayload,
|
||||
parseAuthTokenHeader,
|
||||
|
@ -45,6 +45,8 @@ import {
|
|||
isAESLegacy,
|
||||
} from './utils';
|
||||
|
||||
import { aesEncrypt, signPayload } from './crypto-utils';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const LoggerApi = require('@verdaccio/logger');
|
||||
|
||||
|
|
60
packages/auth/src/crypto-utils.ts
Normal file
60
packages/auth/src/crypto-utils.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { createDecipher, createCipher } from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { JWTSignOptions, RemoteUser } from '@verdaccio/types';
|
||||
|
||||
export const defaultAlgorithm = 'aes192';
|
||||
|
||||
export function aesEncrypt(buf: Buffer, secret: string): Buffer {
|
||||
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
|
||||
// https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options
|
||||
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
|
||||
const c = createCipher(defaultAlgorithm, secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
return Buffer.concat([b1, b2]);
|
||||
}
|
||||
|
||||
export function aesDecrypt(buf: Buffer, secret: string): Buffer {
|
||||
try {
|
||||
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
|
||||
// https://nodejs.org/api/crypto.html#crypto_crypto_createdecipher_algorithm_password_options
|
||||
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
|
||||
const c = createDecipher(defaultAlgorithm, secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
return Buffer.concat([b1, b2]);
|
||||
} catch (_) {
|
||||
return new Buffer(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the payload and return JWT
|
||||
* https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
|
||||
* @param payload
|
||||
* @param secretOrPrivateKey
|
||||
* @param options
|
||||
*/
|
||||
export async function signPayload(
|
||||
payload: RemoteUser,
|
||||
secretOrPrivateKey: string,
|
||||
options: JWTSignOptions = {}
|
||||
): Promise<string> {
|
||||
return new Promise(function (resolve, reject): Promise<string> {
|
||||
return jwt.sign(
|
||||
payload,
|
||||
secretOrPrivateKey,
|
||||
{
|
||||
// 1 === 1ms (one millisecond)
|
||||
notBefore: '1', // Make sure the time will not rollback :)
|
||||
...options,
|
||||
},
|
||||
(error, token) => (error ? reject(error) : resolve(token))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyPayload(token: string, secretOrPrivateKey: string): RemoteUser {
|
||||
return jwt.verify(token, secretOrPrivateKey);
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export { Auth, IAuth, IAuthWebUI } from './auth';
|
||||
export * from './utils';
|
||||
export * from './crypto-utils';
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import _ from 'lodash';
|
||||
import { Config, RemoteUser, Security } from '@verdaccio/types';
|
||||
import { HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER } from '@verdaccio/dev-commons';
|
||||
import { Callback, Config, IPluginAuth, RemoteUser, Security } from '@verdaccio/types';
|
||||
import { HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER, API_ERROR } from '@verdaccio/dev-commons';
|
||||
import { getForbidden, getUnauthorized, getConflict, getCode } from '@verdaccio/commons-api';
|
||||
import {
|
||||
aesDecrypt,
|
||||
AllowAction,
|
||||
AllowActionCallback,
|
||||
AuthPackageAllow,
|
||||
buildUserBuffer,
|
||||
convertPayloadToBase64,
|
||||
createAnonymousRemoteUser,
|
||||
defaultSecurity,
|
||||
ErrorCode,
|
||||
verifyPayload,
|
||||
} from '@verdaccio/utils';
|
||||
|
||||
import { IAuthWebUI, AESPayload } from './auth';
|
||||
import { aesDecrypt, verifyPayload } from './crypto-utils';
|
||||
|
||||
export type BasicPayload = AESPayload | void;
|
||||
export type AuthMiddlewarePayload = RemoteUser | BasicPayload;
|
||||
|
@ -127,7 +129,7 @@ export function verifyJWTPayload(token: string, secret: string): RemoteUser {
|
|||
// we return an anonymous user to force log in.
|
||||
return createAnonymousRemoteUser();
|
||||
}
|
||||
throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, error.message);
|
||||
throw getCode(HTTP_STATUS.UNAUTHORIZED, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,3 +148,78 @@ export function parseBasicPayload(credentials: string): BasicPayload {
|
|||
|
||||
return { user, password };
|
||||
}
|
||||
|
||||
export function getDefaultPlugins(logger: any): IPluginAuth<Config> {
|
||||
return {
|
||||
authenticate(user: string, password: string, cb: Callback): void {
|
||||
cb(getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
},
|
||||
|
||||
adduser(user: string, password: string, cb: Callback): void {
|
||||
return cb(getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
},
|
||||
|
||||
// FIXME: allow_action and allow_publish should be in the @verdaccio/types
|
||||
// @ts-ignore
|
||||
allow_access: allow_action('access', logger),
|
||||
// @ts-ignore
|
||||
allow_publish: allow_action('publish', logger),
|
||||
allow_unpublish: handleSpecialUnpublish(logger),
|
||||
};
|
||||
}
|
||||
|
||||
export type ActionsAllowed = 'publish' | 'unpublish' | 'access';
|
||||
|
||||
export function allow_action(action: ActionsAllowed, logger): AllowAction {
|
||||
return function allowActionCallback(
|
||||
user: RemoteUser,
|
||||
pkg: AuthPackageAllow,
|
||||
callback: AllowActionCallback
|
||||
): void {
|
||||
logger.trace({ remote: user.name }, `[auth/allow_action]: user: @{user.name}`);
|
||||
const { name, groups } = user;
|
||||
const groupAccess = pkg[action] as string[];
|
||||
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);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
callback(getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`));
|
||||
} else {
|
||||
callback(getUnauthorized(`authorization required to ${action} package ${pkg.name}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function handleSpecialUnpublish(logger): any {
|
||||
return function (user: RemoteUser, pkg: AuthPackageAllow, callback: AllowActionCallback): void {
|
||||
const action = 'unpublish';
|
||||
// verify whether the unpublish prop has been defined
|
||||
const isUnpublishMissing: boolean = _.isNil(pkg[action]);
|
||||
const hasGroups: boolean = isUnpublishMissing ? false : (pkg[action] as string[]).length > 0;
|
||||
logger.trace(
|
||||
{ user: user.name, name: pkg.name, hasGroups },
|
||||
`fallback unpublish for @{name} has groups: @{hasGroups} for @{user}`
|
||||
);
|
||||
|
||||
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, logger)(user, pkg, callback);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import { CHARACTER_ENCODING, TOKEN_BEARER } from '@verdaccio/dev-commons';
|
||||
import { CHARACTER_ENCODING, TOKEN_BEARER, ROLES, API_ERROR } from '@verdaccio/dev-commons';
|
||||
|
||||
import { configExample } from '@verdaccio/mock';
|
||||
import { Config as AppConfig } from '@verdaccio/config';
|
||||
|
@ -9,19 +9,30 @@ import { setup } from '@verdaccio/logger';
|
|||
import {
|
||||
buildUserBuffer,
|
||||
getAuthenticatedMessage,
|
||||
aesDecrypt,
|
||||
verifyPayload,
|
||||
buildToken,
|
||||
convertPayloadToBase64,
|
||||
parseConfigFile,
|
||||
createAnonymousRemoteUser,
|
||||
createRemoteUser,
|
||||
signPayload,
|
||||
AllowActionCallbackResponse,
|
||||
} from '@verdaccio/utils';
|
||||
|
||||
import { Config, Security, RemoteUser } from '@verdaccio/types';
|
||||
import { Auth, IAuth } from '../src';
|
||||
import { getMiddlewareCredentials, getApiToken, verifyJWTPayload, getSecurity } from '../src';
|
||||
import { VerdaccioError, getForbidden } from '@verdaccio/commons-api';
|
||||
import {
|
||||
IAuth,
|
||||
Auth,
|
||||
ActionsAllowed,
|
||||
allow_action,
|
||||
getDefaultPlugins,
|
||||
getMiddlewareCredentials,
|
||||
getApiToken,
|
||||
verifyJWTPayload,
|
||||
getSecurity,
|
||||
aesDecrypt,
|
||||
verifyPayload,
|
||||
signPayload,
|
||||
} from '../src';
|
||||
|
||||
setup([]);
|
||||
|
||||
|
@ -86,6 +97,7 @@ describe('Auth utilities', () => {
|
|||
|
||||
const verifyAES = (token: string, user: string, password: string, secret: string) => {
|
||||
const payload = aesDecrypt(convertPayloadToBase64(token), secret).toString(
|
||||
// @ts-ignore
|
||||
CHARACTER_ENCODING.UTF8
|
||||
);
|
||||
const content = payload.split(':');
|
||||
|
@ -94,6 +106,120 @@ describe('Auth utilities', () => {
|
|||
expect(content[0]).toBe(password);
|
||||
};
|
||||
|
||||
describe('getDefaultPlugins', () => {
|
||||
test('authentication should fail by default (default)', () => {
|
||||
const plugin = getDefaultPlugins({ trace: jest.fn() });
|
||||
plugin.authenticate('foo', 'bar', (error: any) => {
|
||||
expect(error).toEqual(getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
});
|
||||
});
|
||||
|
||||
test('add user should fail by default (default)', () => {
|
||||
const plugin = getDefaultPlugins({ trace: jest.fn() });
|
||||
// @ts-ignore
|
||||
plugin.adduser('foo', 'bar', (error: any) => {
|
||||
expect(error).toEqual(getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('allow_action', () => {
|
||||
describe('access/publish/unpublish and anonymous', () => {
|
||||
const packageAccess = {
|
||||
name: 'foo',
|
||||
version: undefined,
|
||||
access: ['foo'],
|
||||
unpublish: false,
|
||||
};
|
||||
|
||||
// const type = 'access';
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should restrict %s to anonymous users',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createAnonymousRemoteUser(),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: ['foo'],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(allowed).toBeUndefined();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should allow %s to anonymous users',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createAnonymousRemoteUser(),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: [ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).toBeNull();
|
||||
expect(allowed).toBe(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should allow %s only if user is anonymous if the logged user has groups',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createRemoteUser('juan', ['maintainer', 'admin']),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: [ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(allowed).toBeUndefined();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should allow %s only if user is anonymous match any other groups',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createRemoteUser('juan', ['maintainer', 'admin']),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: ['admin', 'some-other-group', ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).toBeNull();
|
||||
expect(allowed).toBe(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should not allow %s anonymous if other groups are defined and does not match',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createRemoteUser('juan', ['maintainer', 'admin']),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: ['bla-bla-group', 'some-other-group', ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(allowed).toBeUndefined();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApiToken test', () => {
|
||||
test('should sign token with aes and security missing', async () => {
|
||||
const token = await signCredentials(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { aesDecrypt, aesEncrypt, convertPayloadToBase64 } from '../src';
|
||||
import { convertPayloadToBase64 } from '@verdaccio/utils';
|
||||
import { aesDecrypt, aesEncrypt } from '../src/crypto-utils';
|
||||
|
||||
describe('test crypto utils', () => {
|
||||
describe('default encryption', () => {
|
|
@ -19,7 +19,6 @@
|
|||
"@verdaccio/dev-commons": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/readme": "workspace:*",
|
||||
"js-yaml": "3.13.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"minimatch": "3.0.4",
|
||||
"semver": "7.3.2"
|
||||
},
|
||||
|
|
|
@ -1,26 +1,14 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
API_ERROR,
|
||||
ROLES,
|
||||
TIME_EXPIRATION_7D,
|
||||
DEFAULT_MIN_LIMIT_PASSWORD,
|
||||
} from '@verdaccio/dev-commons';
|
||||
import { ROLES, TIME_EXPIRATION_7D, DEFAULT_MIN_LIMIT_PASSWORD } from '@verdaccio/dev-commons';
|
||||
import {
|
||||
RemoteUser,
|
||||
AllowAccess,
|
||||
PackageAccess,
|
||||
Callback,
|
||||
Config,
|
||||
Security,
|
||||
APITokenOptions,
|
||||
JWTOptions,
|
||||
IPluginAuth,
|
||||
} from '@verdaccio/types';
|
||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||
|
||||
import { ErrorCode } from './utils';
|
||||
|
||||
export interface CookieSessionToken {
|
||||
expires: Date;
|
||||
}
|
||||
|
@ -85,95 +73,18 @@ export type AllowActionCallback = (
|
|||
error: VerdaccioError | null,
|
||||
allowed?: AllowActionCallbackResponse
|
||||
) => void;
|
||||
|
||||
export type AllowAction = (
|
||||
user: RemoteUser,
|
||||
pkg: AuthPackageAllow,
|
||||
callback: AllowActionCallback
|
||||
) => void;
|
||||
|
||||
export interface AuthPackageAllow extends PackageAccess, AllowAccess {
|
||||
// TODO: this should be on @verdaccio/types
|
||||
unpublish: boolean | string[];
|
||||
}
|
||||
|
||||
export type ActionsAllowed = 'publish' | 'unpublish' | 'access';
|
||||
|
||||
export function allow_action(action: ActionsAllowed, logger): AllowAction {
|
||||
return function allowActionCallback(
|
||||
user: RemoteUser,
|
||||
pkg: AuthPackageAllow,
|
||||
callback: AllowActionCallback
|
||||
): void {
|
||||
logger.trace({ remote: user.name }, `[auth/allow_action]: user: @{user.name}`);
|
||||
const { name, groups } = user;
|
||||
const groupAccess = pkg[action] as string[];
|
||||
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);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
callback(
|
||||
ErrorCode.getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`)
|
||||
);
|
||||
} else {
|
||||
callback(
|
||||
ErrorCode.getUnauthorized(`authorization required to ${action} package ${pkg.name}`)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function handleSpecialUnpublish(logger): any {
|
||||
return function (user: RemoteUser, pkg: AuthPackageAllow, callback: AllowActionCallback): void {
|
||||
const action = 'unpublish';
|
||||
// verify whether the unpublish prop has been defined
|
||||
const isUnpublishMissing: boolean = _.isNil(pkg[action]);
|
||||
const hasGroups: boolean = isUnpublishMissing ? false : (pkg[action] as string[]).length > 0;
|
||||
logger.trace(
|
||||
{ user: user.name, name: pkg.name, hasGroups },
|
||||
`fallback unpublish for @{name} has groups: @{hasGroups} for @{user}`
|
||||
);
|
||||
|
||||
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, logger)(user, pkg, callback);
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultPlugins(logger: any): IPluginAuth<Config> {
|
||||
return {
|
||||
authenticate(user: string, password: string, cb: Callback): void {
|
||||
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
},
|
||||
|
||||
adduser(user: string, password: string, cb: Callback): void {
|
||||
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
},
|
||||
|
||||
// FIXME: allow_action and allow_publish should be in the @verdaccio/types
|
||||
// @ts-ignore
|
||||
allow_access: allow_action('access', logger),
|
||||
// @ts-ignore
|
||||
allow_publish: allow_action('publish', logger),
|
||||
allow_unpublish: handleSpecialUnpublish(logger),
|
||||
};
|
||||
}
|
||||
|
||||
export function createSessionToken(): CookieSessionToken {
|
||||
const tenHoursTime = 10 * 60 * 60 * 1000;
|
||||
|
||||
|
|
|
@ -1,35 +1,8 @@
|
|||
import { createDecipher, createCipher, createHash, pseudoRandomBytes, Hash } from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { createHash, pseudoRandomBytes, Hash } from 'crypto';
|
||||
|
||||
import { JWTSignOptions, RemoteUser } from '@verdaccio/types';
|
||||
|
||||
export const defaultAlgorithm = 'aes192';
|
||||
export const defaultTarballHashAlgorithm = 'sha1';
|
||||
|
||||
export function aesEncrypt(buf: Buffer, secret: string): Buffer {
|
||||
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
|
||||
// https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options
|
||||
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
|
||||
const c = createCipher(defaultAlgorithm, secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
return Buffer.concat([b1, b2]);
|
||||
}
|
||||
|
||||
export function aesDecrypt(buf: Buffer, secret: string): Buffer {
|
||||
try {
|
||||
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
|
||||
// https://nodejs.org/api/crypto.html#crypto_crypto_createdecipher_algorithm_password_options
|
||||
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
|
||||
const c = createDecipher(defaultAlgorithm, secret);
|
||||
const b1 = c.update(buf);
|
||||
const b2 = c.final();
|
||||
return Buffer.concat([b1, b2]);
|
||||
} catch (_) {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
}
|
||||
|
||||
// podria moverse a storage donde se usa
|
||||
export function createTarballHash(): Hash {
|
||||
return createHash(defaultTarballHashAlgorithm);
|
||||
}
|
||||
|
@ -41,40 +14,12 @@ export function createTarballHash(): Hash {
|
|||
* @param {Object} data
|
||||
* @return {String}
|
||||
*/
|
||||
// se usa en api, middleware, web
|
||||
export function stringToMD5(data: Buffer | string): string {
|
||||
return createHash('md5').update(data).digest('hex');
|
||||
}
|
||||
|
||||
// se usa en config
|
||||
export function generateRandomHexString(length = 8): string {
|
||||
return pseudoRandomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the payload and return JWT
|
||||
* https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
|
||||
* @param payload
|
||||
* @param secretOrPrivateKey
|
||||
* @param options
|
||||
*/
|
||||
export async function signPayload(
|
||||
payload: RemoteUser,
|
||||
secretOrPrivateKey: string,
|
||||
options: JWTSignOptions = {}
|
||||
): Promise<string> {
|
||||
return new Promise(function (resolve, reject): Promise<string> {
|
||||
return jwt.sign(
|
||||
payload,
|
||||
secretOrPrivateKey,
|
||||
{
|
||||
// 1 === 1ms (one millisecond)
|
||||
notBefore: '1', // Make sure the time will not rollback :)
|
||||
...options,
|
||||
},
|
||||
(error, token) => (error ? reject(error) : resolve(token))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyPayload(token: string, secretOrPrivateKey: string): RemoteUser {
|
||||
return jwt.verify(token, secretOrPrivateKey);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { API_ERROR, ROLES } from '@verdaccio/dev-commons';
|
||||
import { VerdaccioError, getForbidden } from '@verdaccio/commons-api';
|
||||
import {
|
||||
allow_action,
|
||||
createAnonymousRemoteUser,
|
||||
createRemoteUser,
|
||||
validatePassword,
|
||||
ActionsAllowed,
|
||||
AllowActionCallbackResponse,
|
||||
getDefaultPlugins,
|
||||
createSessionToken,
|
||||
getAuthenticatedMessage,
|
||||
} from '../src';
|
||||
|
||||
jest.mock('@verdaccio/logger', () => ({
|
||||
logger: { trace: jest.fn() },
|
||||
}));
|
||||
|
@ -60,102 +57,6 @@ describe('Auth Utilities', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('allow_action', () => {
|
||||
describe('access/publish/unpublish and anonymous', () => {
|
||||
const packageAccess = {
|
||||
name: 'foo',
|
||||
version: undefined,
|
||||
access: ['foo'],
|
||||
unpublish: false,
|
||||
};
|
||||
|
||||
// const type = 'access';
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should restrict %s to anonymous users',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createAnonymousRemoteUser(),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: ['foo'],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(allowed).toBeUndefined();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should allow %s to anonymous users',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createAnonymousRemoteUser(),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: [ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).toBeNull();
|
||||
expect(allowed).toBe(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should allow %s only if user is anonymous if the logged user has groups',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createRemoteUser('juan', ['maintainer', 'admin']),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: [ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(allowed).toBeUndefined();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should allow %s only if user is anonymous match any other groups',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createRemoteUser('juan', ['maintainer', 'admin']),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: ['admin', 'some-other-group', ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).toBeNull();
|
||||
expect(allowed).toBe(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['access', 'publish', 'unpublish'])(
|
||||
'should not allow %s anonymous if other groups are defined and does not match',
|
||||
(type) => {
|
||||
allow_action(type as ActionsAllowed, { trace: jest.fn() })(
|
||||
createRemoteUser('juan', ['maintainer', 'admin']),
|
||||
{
|
||||
...packageAccess,
|
||||
[type]: ['bla-bla-group', 'some-other-group', ROLES.$ANONYMOUS],
|
||||
},
|
||||
(error: VerdaccioError | null, allowed: AllowActionCallbackResponse) => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(allowed).toBeUndefined();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('createSessionToken', () => {
|
||||
test('should generate session token', () => {
|
||||
expect(createSessionToken()).toHaveProperty('expires');
|
||||
|
@ -163,23 +64,6 @@ describe('Auth Utilities', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getDefaultPlugins', () => {
|
||||
test('authentication should fail by default (default)', () => {
|
||||
const plugin = getDefaultPlugins({ trace: jest.fn() });
|
||||
plugin.authenticate('foo', 'bar', (error: any) => {
|
||||
expect(error).toEqual(getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
});
|
||||
});
|
||||
|
||||
test('add user should fail by default (default)', () => {
|
||||
const plugin = getDefaultPlugins({ trace: jest.fn() });
|
||||
// @ts-ignore
|
||||
plugin.adduser('foo', 'bar', (error: any) => {
|
||||
expect(error).toEqual(getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAuthenticatedMessage', () => {
|
||||
test('should generate user message token', () => {
|
||||
expect(getAuthenticatedMessage('foo')).toEqual("you are authenticated as 'foo'");
|
||||
|
|
|
@ -224,6 +224,7 @@ importers:
|
|||
'@verdaccio/utils': 'link:../utils'
|
||||
debug: 4.1.1
|
||||
express: 4.17.1
|
||||
jsonwebtoken: 8.5.1
|
||||
lodash: 4.17.15
|
||||
devDependencies:
|
||||
'@verdaccio/config': 'link:../config'
|
||||
|
@ -241,6 +242,7 @@ importers:
|
|||
'@verdaccio/utils': 'workspace:5.0.0-alpha.0'
|
||||
debug: ^4.1.1
|
||||
express: 4.17.1
|
||||
jsonwebtoken: 8.5.1
|
||||
lodash: 4.17.15
|
||||
packages/cli:
|
||||
dependencies:
|
||||
|
@ -622,7 +624,6 @@ importers:
|
|||
'@verdaccio/dev-commons': 'link:../commons'
|
||||
'@verdaccio/readme': 'link:../core/readme'
|
||||
js-yaml: 3.13.1
|
||||
jsonwebtoken: 8.5.1
|
||||
minimatch: 3.0.4
|
||||
semver: 7.3.2
|
||||
devDependencies:
|
||||
|
@ -636,7 +637,6 @@ importers:
|
|||
'@verdaccio/logger': 'workspace:5.0.0-alpha.0'
|
||||
'@verdaccio/readme': 'workspace:*'
|
||||
js-yaml: 3.13.1
|
||||
jsonwebtoken: 8.5.1
|
||||
lodash: ^4.17.20
|
||||
minimatch: 3.0.4
|
||||
semver: 7.3.2
|
||||
|
@ -940,7 +940,7 @@ packages:
|
|||
/@babel/helper-compilation-targets/7.10.4:
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.11.0
|
||||
browserslist: 4.14.0
|
||||
browserslist: 4.14.4
|
||||
invariant: 2.2.4
|
||||
levenary: 1.1.1
|
||||
semver: 5.7.1
|
||||
|
@ -993,7 +993,7 @@ packages:
|
|||
dependencies:
|
||||
'@babel/helper-annotate-as-pure': 7.10.4
|
||||
'@babel/helper-regex': 7.10.5
|
||||
regexpu-core: 4.7.0
|
||||
regexpu-core: 4.7.1
|
||||
dev: false
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
|
@ -1339,8 +1339,8 @@ packages:
|
|||
dependencies:
|
||||
'@babel/core': 7.10.5
|
||||
'@babel/helper-plugin-utils': 7.10.4
|
||||
'@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.10.5
|
||||
'@babel/plugin-transform-parameters': 7.10.5_@babel+core@7.10.5
|
||||
'@babel/plugin-syntax-object-rest-spread': 7.8.3
|
||||
'@babel/plugin-transform-parameters': 7.10.5
|
||||
dev: false
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
@ -1732,6 +1732,7 @@ packages:
|
|||
dependencies:
|
||||
'@babel/core': 7.10.5
|
||||
'@babel/helper-plugin-utils': 7.10.4
|
||||
dev: true
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
resolution:
|
||||
|
@ -2251,16 +2252,6 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
resolution:
|
||||
integrity: sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==
|
||||
/@babel/plugin-transform-parameters/7.10.5_@babel+core@7.10.5:
|
||||
dependencies:
|
||||
'@babel/core': 7.10.5
|
||||
'@babel/helper-get-function-arity': 7.10.4
|
||||
'@babel/helper-plugin-utils': 7.10.4
|
||||
dev: false
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
resolution:
|
||||
integrity: sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==
|
||||
/@babel/plugin-transform-parameters/7.10.5_@babel+core@7.11.6:
|
||||
dependencies:
|
||||
'@babel/core': 7.11.6
|
||||
|
@ -18839,7 +18830,6 @@ packages:
|
|||
regjsparser: 0.6.4
|
||||
unicode-match-property-ecmascript: 1.0.4
|
||||
unicode-match-property-value-ecmascript: 1.2.0
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=4'
|
||||
resolution:
|
||||
|
|
Loading…
Reference in a new issue