mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-03-11 02:15:57 -05:00
refactor: utilities and auth
This commit is contained in:
parent
7739e6f4a2
commit
463888165d
17 changed files with 412 additions and 120 deletions
|
@ -37,7 +37,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/dev-types": "5.0.0-alpha.0",
|
||||
"@verdaccio/types": "^9.0.0"
|
||||
"@verdaccio/types": "^9.3.0"
|
||||
},
|
||||
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { loadPlugin } from '@verdaccio/loaders';
|
|||
import { aesEncrypt, signPayload } from '@verdaccio/utils';
|
||||
import {
|
||||
getDefaultPlugins,
|
||||
getMiddlewareCredentials,
|
||||
verifyJWTPayload,
|
||||
createAnonymousRemoteUser,
|
||||
isAuthHeaderValid,
|
||||
|
@ -23,6 +22,7 @@ import {
|
|||
import { getMatchedPackagesSpec } from '@verdaccio/utils';
|
||||
import { Config, Logger, Callback, IPluginAuth, RemoteUser, JWTSignOptions, Security, AuthPluginPackage, AllowAccess, PackageAccess } from '@verdaccio/types';
|
||||
import { $RequestExtend, $ResponseExtend, IAuth, AESPayload } from '@verdaccio/dev-types';
|
||||
import {getMiddlewareCredentials} from "./utils";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const LoggerApi = require('@verdaccio/logger');
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { Auth } from './auth'
|
||||
export * from './utils';
|
||||
|
|
32
packages/auth/src/utils.ts
Normal file
32
packages/auth/src/utils.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {Security} from "@verdaccio/types";
|
||||
import {AuthMiddlewarePayload} from "@verdaccio/dev-types";
|
||||
import _ from "lodash";
|
||||
import {TOKEN_BEARER} from "@verdaccio/dev-commons";
|
||||
import {
|
||||
isAESLegacy,
|
||||
parseAESCredentials,
|
||||
parseAuthTokenHeader,
|
||||
parseBasicPayload,
|
||||
verifyJWTPayload
|
||||
} from "@verdaccio/utils";
|
||||
|
||||
export function getMiddlewareCredentials(security: Security, secret: string, authorizationHeader: string): AuthMiddlewarePayload {
|
||||
if (isAESLegacy(security)) {
|
||||
const credentials = parseAESCredentials(authorizationHeader, secret);
|
||||
if (!credentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedCredentials = parseBasicPayload(credentials);
|
||||
if (!parsedCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parsedCredentials;
|
||||
}
|
||||
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
|
||||
|
||||
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
|
||||
return verifyJWTPayload(token, secret);
|
||||
}
|
||||
}
|
|
@ -10,12 +10,16 @@ import {
|
|||
buildUserBuffer,
|
||||
getApiToken,
|
||||
getAuthenticatedMessage,
|
||||
getMiddlewareCredentials,
|
||||
getSecurity,
|
||||
aesDecrypt, verifyPayload,
|
||||
buildToken, convertPayloadToBase64, parseConfigFile
|
||||
aesDecrypt,
|
||||
verifyPayload,
|
||||
buildToken,
|
||||
convertPayloadToBase64,
|
||||
parseConfigFile
|
||||
} from '@verdaccio/utils';
|
||||
|
||||
import { getMiddlewareCredentials } from '../src'
|
||||
|
||||
import { IAuth } from '@verdaccio/dev-types';
|
||||
import {Config, Security, RemoteUser} from '@verdaccio/types';
|
||||
import path from "path";
|
||||
|
|
54
packages/commons/src/helpers/pkg.ts
Normal file
54
packages/commons/src/helpers/pkg.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Package } from "@verdaccio/types";
|
||||
|
||||
export function generatePackageMetadata(pkgName: string, version = '1.0.0'): Package {
|
||||
// @ts-ignore
|
||||
return {
|
||||
"_id": pkgName,
|
||||
"name": pkgName,
|
||||
"dist-tags": {
|
||||
"latest": version
|
||||
},
|
||||
"versions": {
|
||||
[version]: {
|
||||
"name": pkgName,
|
||||
"version": version,
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
],
|
||||
"author": {
|
||||
"name": "User NPM",
|
||||
"email": "user@domain.com"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"verdaccio": "^2.7.2"
|
||||
},
|
||||
"readme": "# test",
|
||||
"readmeFilename": "README.md",
|
||||
"_id": `${pkgName}@${version}`,
|
||||
"_npmVersion": "5.5.1",
|
||||
"_npmUser": {
|
||||
'name': 'foo',
|
||||
},
|
||||
"dist": {
|
||||
"integrity": "sha512-6gHiERpiDgtb3hjqpQH5\/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==",
|
||||
"shasum": "2c03764f651a9f016ca0b7620421457b619151b9", // pragma: allowlist secret
|
||||
"tarball": `http:\/\/localhost:5555\/${pkgName}\/-\/${pkgName}-${version}.tgz`
|
||||
}
|
||||
}
|
||||
},
|
||||
"readme": "# test",
|
||||
"_attachments": {
|
||||
[`${pkgName}-${version}.tgz`]: {
|
||||
"content_type": "application\/octet-stream",
|
||||
"data": "H4sIAAAAAAAAE+2W32vbMBDH85y\/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo\/\/79KPeQsnIw5KUDX\/9IOvurLuz\/DHSjK\/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF\/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI\/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS\/pLQe+D+FIv\/agIWI6GX66kFuIhT+1gDjrp\/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0\/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi\/IHpU9fz3\/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6\/f88f\/Pu47zomiPk2Lv\/dOv8h+P\/34\/D\/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=",
|
||||
"length": 512
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export * from './constants';
|
||||
export * from './helpers/pkg';
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/dev-commons": "5.0.0-alpha.0",
|
||||
"@verdaccio/types": "^8.5.2"
|
||||
"@verdaccio/types": "^9.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
|
|
|
@ -7,7 +7,7 @@ import {parseConfigFile} from '@verdaccio/utils';
|
|||
/**
|
||||
* Override the default.yaml configuration file with any new config provided.
|
||||
*/
|
||||
function configExample(externalConfig, configFile: string = 'default.yaml', location: string) {
|
||||
function configExample(externalConfig, configFile: string = 'default.yaml', location: string = '') {
|
||||
const locationFile = location ? path.join(location, configFile) :
|
||||
path.join(__dirname, `./config/yaml/${configFile}`);
|
||||
const config = parseConfigFile(locationFile);
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import selfsigned from 'selfsigned';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
|
||||
import { configExample } from '@verdaccio/mock';
|
||||
import {DEFAULT_DOMAIN, DEFAULT_PORT, DEFAULT_PROTOCOL} from '@verdaccio/dev-commons';
|
||||
import {DEFAULT_DOMAIN, DEFAULT_PROTOCOL} from '@verdaccio/dev-commons';
|
||||
import {parseConfigFile} from '@verdaccio/utils';
|
||||
|
||||
import { getListListenAddresses } from '../src/cli-utils';
|
||||
import { logger } from '@verdaccio/logger';
|
||||
|
||||
import { startVerdaccio } from '../src';
|
||||
|
@ -181,66 +179,4 @@ describe('startServer via API', () => {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getListListenAddresses test', () => {
|
||||
|
||||
test('should return no address if a single address is wrong', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses("wrong");
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return no address if a two address are wrong', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses(["wrong", "same-wrong"]);
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return a list of 1 address provided', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses(null, '1000');
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should return a list of 2 address provided', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses(null, ['1000', '2000']);
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(2);
|
||||
});
|
||||
|
||||
test(`should return by default ${DEFAULT_PORT}`, () => {
|
||||
// @ts-ignore
|
||||
const [addrs] = getListListenAddresses();
|
||||
|
||||
// @ts-ignore
|
||||
expect(addrs.proto).toBe(DEFAULT_PROTOCOL);
|
||||
// @ts-ignore
|
||||
expect(addrs.host).toBe(DEFAULT_DOMAIN);
|
||||
// @ts-ignore
|
||||
expect(addrs.port).toBe(DEFAULT_PORT);
|
||||
});
|
||||
|
||||
test('should return default proto, host and custom port', () => {
|
||||
const initPort = '1000';
|
||||
// @ts-ignore
|
||||
const [addrs] = getListListenAddresses(null, initPort);
|
||||
|
||||
// @ts-ignore
|
||||
expect(addrs.proto).toEqual(DEFAULT_PROTOCOL);
|
||||
// @ts-ignore
|
||||
expect(addrs.host).toEqual(DEFAULT_DOMAIN);
|
||||
// @ts-ignore
|
||||
expect(addrs.port).toEqual(initPort);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
76
packages/node-api/test/node-api.utils.spec.ts
Normal file
76
packages/node-api/test/node-api.utils.spec.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {getListListenAddresses} from "../src/cli-utils";
|
||||
import _ from "lodash";
|
||||
import {DEFAULT_DOMAIN, DEFAULT_PORT, DEFAULT_PROTOCOL} from "@verdaccio/dev-commons";
|
||||
|
||||
jest.mock('@verdaccio/logger', () => ({
|
||||
setup: jest.fn(),
|
||||
logger: {
|
||||
child: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
describe('getListListenAddresses test', () => {
|
||||
|
||||
test('should return no address if a single address is wrong', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses("wrong");
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return no address if a two address are wrong', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses(["wrong", "same-wrong"]);
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return a list of 1 address provided', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses(null, '1000');
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should return a list of 2 address provided', () => {
|
||||
// @ts-ignore
|
||||
const addrs = getListListenAddresses(null, ['1000', '2000']);
|
||||
|
||||
expect(_.isArray(addrs)).toBeTruthy();
|
||||
expect(addrs).toHaveLength(2);
|
||||
});
|
||||
|
||||
test(`should return by default ${DEFAULT_PORT}`, () => {
|
||||
// @ts-ignore
|
||||
const [addrs] = getListListenAddresses();
|
||||
|
||||
// @ts-ignore
|
||||
expect(addrs.proto).toBe(DEFAULT_PROTOCOL);
|
||||
// @ts-ignore
|
||||
expect(addrs.host).toBe(DEFAULT_DOMAIN);
|
||||
// @ts-ignore
|
||||
expect(addrs.port).toBe(DEFAULT_PORT);
|
||||
});
|
||||
|
||||
test('should return default proto, host and custom port', () => {
|
||||
const initPort = '1000';
|
||||
// @ts-ignore
|
||||
const [addrs] = getListListenAddresses(null, initPort);
|
||||
|
||||
// @ts-ignore
|
||||
expect(addrs.proto).toEqual(DEFAULT_PROTOCOL);
|
||||
// @ts-ignore
|
||||
expect(addrs.host).toEqual(DEFAULT_DOMAIN);
|
||||
// @ts-ignore
|
||||
expect(addrs.port).toEqual(initPort);
|
||||
});
|
||||
|
||||
});
|
|
@ -33,7 +33,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/dev-types": "5.0.0-alpha.0",
|
||||
"@verdaccio/types": "^8.5.2"
|
||||
"@verdaccio/types": "^9.3.0"
|
||||
},
|
||||
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { API_ERROR, HTTP_STATUS, ROLES, TIME_EXPIRATION_7D, TOKEN_BASIC, TOKEN_BEARER, DEFAULT_MIN_LIMIT_PASSWORD } from '@verdaccio/dev-commons';
|
||||
import { CookieSessionToken, IAuthWebUI, AuthMiddlewarePayload, AuthTokenHeader, BasicPayload } from '@verdaccio/dev-types';
|
||||
import { RemoteUser, Package, Callback, Config, Security, APITokenOptions, JWTOptions, IPluginAuth } from '@verdaccio/types';
|
||||
import { CookieSessionToken, IAuthWebUI, AuthTokenHeader, BasicPayload } from '@verdaccio/dev-types';
|
||||
import { RemoteUser, AllowAccess, PackageAccess, Callback, Config, Security, APITokenOptions, JWTOptions, IPluginAuth } from '@verdaccio/types';
|
||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||
|
||||
import { convertPayloadToBase64, ErrorCode } from './utils';
|
||||
import { aesDecrypt, verifyPayload } from './crypto-utils';
|
||||
|
@ -13,13 +14,28 @@ export function validatePassword(password: string, minLength: number = DEFAULT_M
|
|||
return typeof password === 'string' && password.length >= minLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* All logged users will have by default the group $all and $authenticate
|
||||
*/
|
||||
export const defaultLoggedUserRoles = [ROLES.$ALL, ROLES.$AUTH, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_AUTH, ROLES.ALL];
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const defaultNonLoggedUserRoles = [
|
||||
ROLES.$ALL,
|
||||
ROLES.$ANONYMOUS,
|
||||
// groups without '$' are going to be deprecated eventually
|
||||
ROLES.DEPRECATED_ALL,
|
||||
ROLES.DEPRECATED_ANONYMOUS
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a RemoteUser object
|
||||
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
||||
*/
|
||||
export function createRemoteUser(name: string, pluginGroups: string[]): RemoteUser {
|
||||
const isGroupValid: boolean = Array.isArray(pluginGroups);
|
||||
const groups = (isGroupValid ? pluginGroups : []).concat([ROLES.$ALL, ROLES.$AUTH, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_AUTH, ROLES.ALL]);
|
||||
const groups = (isGroupValid ? pluginGroups : []).concat([...defaultLoggedUserRoles]);
|
||||
|
||||
return {
|
||||
name,
|
||||
|
@ -35,17 +51,26 @@ export function createRemoteUser(name: string, pluginGroups: string[]): RemoteUs
|
|||
export function createAnonymousRemoteUser(): RemoteUser {
|
||||
return {
|
||||
name: undefined,
|
||||
// groups without '$' are going to be deprecated eventually
|
||||
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
|
||||
groups: [...defaultNonLoggedUserRoles],
|
||||
real_groups: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function allow_action(action: string): Function {
|
||||
return function(user: RemoteUser, pkg: Package, callback: Callback): void {
|
||||
export type AllowActionCallbackResponse = boolean | undefined;
|
||||
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): 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];
|
||||
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}`);
|
||||
|
||||
|
@ -66,11 +91,11 @@ export function allow_action(action: string): Function {
|
|||
*
|
||||
*/
|
||||
export function handleSpecialUnpublish(): any {
|
||||
return function(user: RemoteUser, pkg: Package, callback: Callback): void {
|
||||
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].length > 0;
|
||||
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) {
|
||||
|
@ -88,7 +113,7 @@ export function getDefaultPlugins(): IPluginAuth<Config> {
|
|||
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
},
|
||||
|
||||
add_user(user: string, password: string, cb: Callback): void {
|
||||
adduser(user: string, password: string, cb: Callback): void {
|
||||
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
|
||||
},
|
||||
|
||||
|
@ -227,24 +252,3 @@ export function verifyJWTPayload(token: string, secret: string): RemoteUser {
|
|||
export function isAuthHeaderValid(authorization: string): boolean {
|
||||
return authorization.split(' ').length === 2;
|
||||
}
|
||||
|
||||
export function getMiddlewareCredentials(security: Security, secret: string, authorizationHeader: string): AuthMiddlewarePayload {
|
||||
if (isAESLegacy(security)) {
|
||||
const credentials = parseAESCredentials(authorizationHeader, secret);
|
||||
if (!credentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedCredentials = parseBasicPayload(credentials);
|
||||
if (!parsedCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parsedCredentials;
|
||||
}
|
||||
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
|
||||
|
||||
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
|
||||
return verifyJWTPayload(token, secret);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,25 +42,31 @@ export function createTarballHash(): Hash {
|
|||
* @return {String}
|
||||
*/
|
||||
export function stringToMD5(data: Buffer | string): string {
|
||||
return createHash('md5').update(data).digest('hex');
|
||||
return createHash('md5')
|
||||
.update(data)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function generateRandomHexString(length = 8): string {
|
||||
return pseudoRandomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
export async function signPayload(
|
||||
payload: RemoteUser,
|
||||
secretOrPrivateKey: string,
|
||||
options: JWTSignOptions
|
||||
): Promise<string> {
|
||||
return new Promise(function (resolve, reject): Promise<string> {
|
||||
/**
|
||||
* 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
|
||||
...options,
|
||||
},
|
||||
(error, token) => (error ? reject(error) : resolve(token))
|
||||
);
|
||||
|
|
183
packages/utils/test/auth-utils.spec.ts
Normal file
183
packages/utils/test/auth-utils.spec.ts
Normal file
|
@ -0,0 +1,183 @@
|
|||
import {
|
||||
allow_action,
|
||||
createAnonymousRemoteUser,
|
||||
createRemoteUser,
|
||||
validatePassword,
|
||||
ActionsAllowed,
|
||||
AllowActionCallbackResponse,
|
||||
getDefaultPlugins,
|
||||
createSessionToken,
|
||||
getAuthenticatedMessage,
|
||||
verifyJWTPayload, defaultNonLoggedUserRoles, signPayload
|
||||
} from "../src";
|
||||
import { API_ERROR, ROLES } from "@verdaccio/dev-commons";
|
||||
import { VerdaccioError, getForbidden } from "@verdaccio/commons-api";
|
||||
import { Config, IPluginAuth } from '@verdaccio/types';
|
||||
jest.mock('@verdaccio/logger', () => ({
|
||||
logger: { trace: jest.fn() }
|
||||
}));
|
||||
|
||||
describe('Auth Utilities', () => {
|
||||
|
||||
describe('validatePassword', () => {
|
||||
test('should validate password according the length', () => {
|
||||
expect(validatePassword('12345', 1)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should fails on validate password according the length', () => {
|
||||
expect(validatePassword('12345', 10)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should fails on validate password according the length and default config', () => {
|
||||
expect(validatePassword('12')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should validate password according the length and default config', () => {
|
||||
expect(validatePassword('1235678910')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createRemoteUser and createAnonymousRemoteUser', () => {
|
||||
test('should create a remote user with default groups', () => {
|
||||
expect(createRemoteUser('12345', ['foo', 'bar'])).toEqual(
|
||||
{
|
||||
"groups": ["foo", "bar", ROLES.$ALL, ROLES.$AUTH, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_AUTH, ROLES.ALL],
|
||||
"name": "12345",
|
||||
"real_groups": ["foo", "bar"]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should create a anonymous remote user with default groups', () => {
|
||||
expect(createAnonymousRemoteUser()).toEqual(
|
||||
{
|
||||
"groups": [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
|
||||
"name": undefined,
|
||||
"real_groups": []
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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)(
|
||||
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)(
|
||||
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)(
|
||||
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)(
|
||||
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)(
|
||||
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');
|
||||
expect(createSessionToken().expires).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultPlugins', () => {
|
||||
test('authentication should fail by default (default)', () => {
|
||||
const plugin = getDefaultPlugins();
|
||||
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: IPluginAuth<Config> = getDefaultPlugins();
|
||||
// @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\'');
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyJWTPayload', () => {
|
||||
test('should fail on verify the token and return anonymous users', () => {
|
||||
expect(verifyJWTPayload('fakeToken', 'secret')).toEqual(createAnonymousRemoteUser());
|
||||
});
|
||||
|
||||
test('should fail on verify the token and return anonymous users', async () => {
|
||||
const remoteUser = createRemoteUser('foo', []);
|
||||
const token = await signPayload(remoteUser, '12345');
|
||||
const verifiedToken = verifyJWTPayload(token, '12345');
|
||||
expect(verifiedToken.groups).toEqual(remoteUser.groups);
|
||||
expect(verifiedToken.name).toEqual(remoteUser.name);
|
||||
});
|
||||
});
|
||||
|
||||
// verifyJWTPayload
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import {aesDecrypt, aesEncrypt, convertPayloadToBase64} from "@verdaccio/utils";
|
||||
import {aesDecrypt, aesEncrypt, convertPayloadToBase64} from '../src';
|
||||
|
||||
describe('test crypto utils', () => {
|
||||
describe('default encryption', () => {
|
|
@ -2745,16 +2745,11 @@
|
|||
resolved "https://registry.verdaccio.org/@verdaccio%2fstreams/-/streams-9.3.1.tgz#4e1524382e18d3121d60c32b7d5ee39e21381430"
|
||||
integrity sha512-AO0i8lsu3H1ss694Dtg9KbWpOlSRFNVeT5J2oscAAjQydXjOB63paxiOdUBTaavhT03T+i/AnSgWahsdSG1diA==
|
||||
|
||||
"@verdaccio/types@9.3.0", "@verdaccio/types@^9.0.0", "@verdaccio/types@^9.3.0":
|
||||
"@verdaccio/types@9.3.0", "@verdaccio/types@^9.3.0":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.verdaccio.org/@verdaccio%2ftypes/-/types-9.3.0.tgz#4062183e84bef3a56b4275d111181873f8a082d6"
|
||||
integrity sha512-TzBuWPKxhQILk3Tl8EGvAj6zinwBJw+bEhg5w7HYoE+FpEV6rJ1XW+GF/h/7mRBPhtKiPMMzclRRPysNsT/0ww==
|
||||
|
||||
"@verdaccio/types@^8.5.2":
|
||||
version "8.5.2"
|
||||
resolved "https://registry.verdaccio.org/@verdaccio%2ftypes/-/types-8.5.2.tgz#4e371aae10e8550b4a19b57f6ba1f1f185147669"
|
||||
integrity sha512-x/sacqVndl1dXVKPd7pomea6gs9BKC+i83LDn6MEAO+tuhqWRsKC3UwztLA1YSKjNF33//7JCYMDQk6uaJ9ipw==
|
||||
|
||||
"@verdaccio/ui-theme@^0.3.12", "@verdaccio/ui-theme@^0.3.13":
|
||||
version "0.3.13"
|
||||
resolved "https://registry.verdaccio.org/@verdaccio%2fui-theme/-/ui-theme-0.3.13.tgz#e6f06907b0940c47883f35861723012437b7b958"
|
||||
|
|
Loading…
Add table
Reference in a new issue