mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-11-08 23:25:51 +01:00
refactor: utilities and auth
This commit is contained in:
parent
7739e6f4a2
commit
463888165d
@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@verdaccio/dev-types": "5.0.0-alpha.0",
|
"@verdaccio/dev-types": "5.0.0-alpha.0",
|
||||||
"@verdaccio/types": "^9.0.0"
|
"@verdaccio/types": "^9.3.0"
|
||||||
},
|
},
|
||||||
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
|
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import { loadPlugin } from '@verdaccio/loaders';
|
|||||||
import { aesEncrypt, signPayload } from '@verdaccio/utils';
|
import { aesEncrypt, signPayload } from '@verdaccio/utils';
|
||||||
import {
|
import {
|
||||||
getDefaultPlugins,
|
getDefaultPlugins,
|
||||||
getMiddlewareCredentials,
|
|
||||||
verifyJWTPayload,
|
verifyJWTPayload,
|
||||||
createAnonymousRemoteUser,
|
createAnonymousRemoteUser,
|
||||||
isAuthHeaderValid,
|
isAuthHeaderValid,
|
||||||
@ -23,6 +22,7 @@ import {
|
|||||||
import { getMatchedPackagesSpec } from '@verdaccio/utils';
|
import { getMatchedPackagesSpec } from '@verdaccio/utils';
|
||||||
import { Config, Logger, Callback, IPluginAuth, RemoteUser, JWTSignOptions, Security, AuthPluginPackage, AllowAccess, PackageAccess } from '@verdaccio/types';
|
import { Config, Logger, Callback, IPluginAuth, RemoteUser, JWTSignOptions, Security, AuthPluginPackage, AllowAccess, PackageAccess } from '@verdaccio/types';
|
||||||
import { $RequestExtend, $ResponseExtend, IAuth, AESPayload } from '@verdaccio/dev-types';
|
import { $RequestExtend, $ResponseExtend, IAuth, AESPayload } from '@verdaccio/dev-types';
|
||||||
|
import {getMiddlewareCredentials} from "./utils";
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const LoggerApi = require('@verdaccio/logger');
|
const LoggerApi = require('@verdaccio/logger');
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export { Auth } from './auth'
|
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,
|
buildUserBuffer,
|
||||||
getApiToken,
|
getApiToken,
|
||||||
getAuthenticatedMessage,
|
getAuthenticatedMessage,
|
||||||
getMiddlewareCredentials,
|
|
||||||
getSecurity,
|
getSecurity,
|
||||||
aesDecrypt, verifyPayload,
|
aesDecrypt,
|
||||||
buildToken, convertPayloadToBase64, parseConfigFile
|
verifyPayload,
|
||||||
|
buildToken,
|
||||||
|
convertPayloadToBase64,
|
||||||
|
parseConfigFile
|
||||||
} from '@verdaccio/utils';
|
} from '@verdaccio/utils';
|
||||||
|
|
||||||
|
import { getMiddlewareCredentials } from '../src'
|
||||||
|
|
||||||
import { IAuth } from '@verdaccio/dev-types';
|
import { IAuth } from '@verdaccio/dev-types';
|
||||||
import {Config, Security, RemoteUser} from '@verdaccio/types';
|
import {Config, Security, RemoteUser} from '@verdaccio/types';
|
||||||
import path from "path";
|
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 './constants';
|
||||||
|
export * from './helpers/pkg';
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@verdaccio/dev-commons": "5.0.0-alpha.0",
|
"@verdaccio/dev-commons": "5.0.0-alpha.0",
|
||||||
"@verdaccio/types": "^8.5.2"
|
"@verdaccio/types": "^9.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf ./build",
|
"clean": "rimraf ./build",
|
||||||
|
@ -7,7 +7,7 @@ import {parseConfigFile} from '@verdaccio/utils';
|
|||||||
/**
|
/**
|
||||||
* Override the default.yaml configuration file with any new config provided.
|
* 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) :
|
const locationFile = location ? path.join(location, configFile) :
|
||||||
path.join(__dirname, `./config/yaml/${configFile}`);
|
path.join(__dirname, `./config/yaml/${configFile}`);
|
||||||
const config = parseConfigFile(locationFile);
|
const config = parseConfigFile(locationFile);
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import _ from 'lodash';
|
|
||||||
import selfsigned from 'selfsigned';
|
import selfsigned from 'selfsigned';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
import { configExample } from '@verdaccio/mock';
|
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 {parseConfigFile} from '@verdaccio/utils';
|
||||||
|
|
||||||
import { getListListenAddresses } from '../src/cli-utils';
|
|
||||||
import { logger } from '@verdaccio/logger';
|
import { logger } from '@verdaccio/logger';
|
||||||
|
|
||||||
import { startVerdaccio } from '../src';
|
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": {
|
"devDependencies": {
|
||||||
"@verdaccio/dev-types": "5.0.0-alpha.0",
|
"@verdaccio/dev-types": "5.0.0-alpha.0",
|
||||||
"@verdaccio/types": "^8.5.2"
|
"@verdaccio/types": "^9.3.0"
|
||||||
},
|
},
|
||||||
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
|
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import _ from 'lodash';
|
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 { 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 { CookieSessionToken, IAuthWebUI, AuthTokenHeader, BasicPayload } from '@verdaccio/dev-types';
|
||||||
import { RemoteUser, Package, Callback, Config, Security, APITokenOptions, JWTOptions, IPluginAuth } from '@verdaccio/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 { convertPayloadToBase64, ErrorCode } from './utils';
|
||||||
import { aesDecrypt, verifyPayload } from './crypto-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;
|
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
|
* Create a RemoteUser object
|
||||||
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
||||||
*/
|
*/
|
||||||
export function createRemoteUser(name: string, pluginGroups: string[]): RemoteUser {
|
export function createRemoteUser(name: string, pluginGroups: string[]): RemoteUser {
|
||||||
const isGroupValid: boolean = Array.isArray(pluginGroups);
|
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 {
|
return {
|
||||||
name,
|
name,
|
||||||
@ -35,17 +51,26 @@ export function createRemoteUser(name: string, pluginGroups: string[]): RemoteUs
|
|||||||
export function createAnonymousRemoteUser(): RemoteUser {
|
export function createAnonymousRemoteUser(): RemoteUser {
|
||||||
return {
|
return {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
// groups without '$' are going to be deprecated eventually
|
groups: [...defaultNonLoggedUserRoles],
|
||||||
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
|
|
||||||
real_groups: [],
|
real_groups: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function allow_action(action: string): Function {
|
export type AllowActionCallbackResponse = boolean | undefined;
|
||||||
return function(user: RemoteUser, pkg: Package, callback: Callback): void {
|
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}`);
|
logger.trace({remote: user.name}, `[auth/allow_action]: user: @{user.name}`);
|
||||||
const { name, groups } = user;
|
const { name, groups } = user;
|
||||||
const groupAccess = pkg[action];
|
const groupAccess = pkg[action] as string[];
|
||||||
const hasPermission = groupAccess.some(group => name === group || groups.includes(group));
|
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}`);
|
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 {
|
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';
|
const action = 'unpublish';
|
||||||
// verify whether the unpublish prop has been defined
|
// verify whether the unpublish prop has been defined
|
||||||
const isUnpublishMissing: boolean = _.isNil(pkg[action]);
|
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}`);
|
logger.trace({user: user.name, name: pkg.name, hasGroups}, `fallback unpublish for @{name} has groups: @{hasGroups} for @{user}`);
|
||||||
|
|
||||||
if (isUnpublishMissing || hasGroups === false) {
|
if (isUnpublishMissing || hasGroups === false) {
|
||||||
@ -88,7 +113,7 @@ export function getDefaultPlugins(): IPluginAuth<Config> {
|
|||||||
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
|
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));
|
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 {
|
export function isAuthHeaderValid(authorization: string): boolean {
|
||||||
return authorization.split(' ').length === 2;
|
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}
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
export function stringToMD5(data: Buffer | string): 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 {
|
export function generateRandomHexString(length = 8): string {
|
||||||
return pseudoRandomBytes(length).toString('hex');
|
return pseudoRandomBytes(length).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function signPayload(
|
/**
|
||||||
payload: RemoteUser,
|
* Sign the payload and return JWT
|
||||||
secretOrPrivateKey: string,
|
* https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
|
||||||
options: JWTSignOptions
|
* @param payload
|
||||||
): Promise<string> {
|
* @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 new Promise(function(resolve, reject): Promise<string> {
|
||||||
return jwt.sign(
|
return jwt.sign(
|
||||||
payload,
|
payload,
|
||||||
secretOrPrivateKey,
|
secretOrPrivateKey,
|
||||||
{
|
{
|
||||||
|
// 1 === 1ms (one millisecond)
|
||||||
notBefore: '1', // Make sure the time will not rollback :)
|
notBefore: '1', // Make sure the time will not rollback :)
|
||||||
...options
|
...options,
|
||||||
},
|
},
|
||||||
(error, token) => (error ? reject(error) : resolve(token))
|
(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('test crypto utils', () => {
|
||||||
describe('default encryption', () => {
|
describe('default encryption', () => {
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in New Issue
Block a user