Skip to content

Commit

Permalink
feat: npm token command support (#1427)
Browse files Browse the repository at this point in the history
* feat: support for npm token

This is an effor of:

This commit intent to provide npm token support.

https: //github.com//issues/541
https: //github.com//pull/1271
https: //github.com/verdaccio/local-storage/pull/168
Co-Authored-By: Manuel Spigolon <behemoth89@gmail.com>
Co-Authored-By: Juan Gabriel Jiménez <juangabreil@gmail.com>

* chore: update secrets baselines

Co-Authored-By: Liran Tal <liran.tal@gmail.com>

* chore: update lock file

* chore: add logger mock methods

* chore: update @verdaccio/types

* refactor: unit test was flacky

adapt the pkg access to the new configuration setup

* refactor: add plugin methods validation

* test: add test for aesEncrypt

* chore: update local-storage dependency

* chore: add support for experimetns

token will be part of the experiment lists

* chore: increase timeout

* chore: increase timeout threshold

* chore: update nock

* chore: update dependencies

* chore: update eslint config

* chore: update dependencies

* test: add unit test for npm token

* chore: update readme
  • Loading branch information
juanpicado committed Sep 7, 2019
1 parent 962d5d5 commit dbf2017
Show file tree
Hide file tree
Showing 35 changed files with 1,557 additions and 1,025 deletions.
2 changes: 1 addition & 1 deletion .babelrc
@@ -1,3 +1,3 @@
{
"presets": [["@verdaccio", {"typescript": true}]]
"presets": [["@verdaccio"]]
}
2 changes: 1 addition & 1 deletion .eslintrc
Expand Up @@ -4,7 +4,7 @@
],
"rules": {
"@typescript-eslint/no-var-requires": ["warn"],
"@typescript-eslint/ban-ts-ignore": ["warn"],
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/no-inferrable-types": ["warn"],
"@typescript-eslint/no-empty-function": ["warn"],
"@typescript-eslint/no-this-alias": ["warn"],
Expand Down
1 change: 1 addition & 0 deletions .npmrc
@@ -0,0 +1 @@
always-auth = true
22 changes: 11 additions & 11 deletions .secrets-baseline
Expand Up @@ -3,7 +3,7 @@
"files": null,
"lines": null
},
"generated_at": "2019-08-25T17:18:29Z",
"generated_at": "2019-08-03T08:33:13Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -40,22 +40,22 @@
"src/lib/auth-utils.ts": [
{
"hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8",
"line_number": 12,
"line_number": 10,
"type": "Secret Keyword"
},
{
"hashed_secret": "ecb252044b5ea0f679ee78ec1a12904739e2904d",
"line_number": 187,
"line_number": 174,
"type": "Secret Keyword"
},
{
"hashed_secret": "f35dd4c51c0a89bd055b5ad30c162c778981306d",
"line_number": 192,
"line_number": 179,
"type": "Secret Keyword"
},
{
"hashed_secret": "45c43fe97e3a06ab078b0eeff6fbe622cc417a25",
"line_number": 210,
"line_number": 197,
"type": "Secret Keyword"
}
],
Expand Down Expand Up @@ -86,12 +86,12 @@
"src/lib/constants.ts": [
{
"hashed_secret": "f34fbc9a9769ba9eff5aff3d008a6b49f85c08b1",
"line_number": 15,
"line_number": 14,
"type": "Secret Keyword"
},
{
"hashed_secret": "b9343f1143ccb83555b450eb54dde96a05522ccc",
"line_number": 116,
"line_number": 118,
"type": "Secret Keyword"
}
],
Expand Down Expand Up @@ -265,12 +265,12 @@
"test/unit/modules/api/api.spec.ts": [
{
"hashed_secret": "97752a468368b0d6b192140d6a140c38fd0cbd8b",
"line_number": 304,
"line_number": 293,
"type": "Secret Keyword"
},
{
"hashed_secret": "364bdf2ed77a8544d3b711a03b69eeadcc63c9d7",
"line_number": 828,
"line_number": 802,
"type": "Secret Keyword"
}
],
Expand Down Expand Up @@ -299,12 +299,12 @@
"test/unit/modules/auth/jwt.spec.ts": [
{
"hashed_secret": "364bdf2ed77a8544d3b711a03b69eeadcc63c9d7",
"line_number": 118,
"line_number": 121,
"type": "Secret Keyword"
},
{
"hashed_secret": "eaacdf2d9ed66df2601c8b51ab4084db14336d11",
"line_number": 129,
"line_number": 132,
"type": "Secret Keyword"
}
],
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -164,7 +164,7 @@ Verdaccio aims to support all features of a standard npm client that make sense
- Registering new users (npm adduser {newuser}) - **supported**
- Change password (npm profile set password) - **supported**
- Transferring ownership (npm owner add {user} {pkg}) - not supported, *PR-welcome*
- Token (npm token) - wip [#1271](https://github.com/verdaccio/verdaccio/pull/1271)
- Token (npm token) - (more info [#1271](https://github.com/verdaccio/verdaccio/pull/1271)) - **supported**

### Miscellany

Expand Down
3 changes: 3 additions & 0 deletions conf/default.yaml
Expand Up @@ -80,3 +80,6 @@ logs:
- { type: stdout, format: pretty, level: http }
#- {type: file, path: verdaccio.log, level: info}

experiments:
# support for npm token command
token: false
4 changes: 4 additions & 0 deletions conf/docker.yaml
Expand Up @@ -79,3 +79,7 @@ middlewares:
logs:
- { type: stdout, format: pretty, level: http }
#- {type: file, path: verdaccio.log, level: info}

experiments:
# support for npm token command
token: false
41 changes: 21 additions & 20 deletions package.json
Expand Up @@ -16,10 +16,10 @@
"verdaccio": "./bin/verdaccio"
},
"dependencies": {
"@verdaccio/commons-api": "8.0.0",
"@verdaccio/local-storage": "2.2.1",
"@verdaccio/readme": "8.0.0",
"@verdaccio/streams": "8.0.0",
"@verdaccio/commons-api": "8.1.0",
"@verdaccio/local-storage": "8.1.0",
"@verdaccio/readme": "8.1.0",
"@verdaccio/streams": "8.1.0",
"@verdaccio/ui-theme": "0.3.0",
"JSONStream": "1.3.5",
"async": "3.1.0",
Expand All @@ -32,7 +32,7 @@
"dayjs": "1.8.15",
"envinfo": "7.3.1",
"express": "4.17.1",
"handlebars": "4.1.2",
"handlebars": "4.2.0",
"http-errors": "1.7.3",
"js-yaml": "3.13.1",
"jsonwebtoken": "8.5.1",
Expand All @@ -48,30 +48,30 @@
"pkginfo": "0.4.1",
"request": "2.87.0",
"semver": "6.3.0",
"verdaccio-audit": "8.0.0",
"verdaccio-htpasswd": "8.0.0"
"verdaccio-audit": "8.1.0",
"verdaccio-htpasswd": "8.1.0"
},
"devDependencies": {
"@commitlint/cli": "8.1.0",
"@commitlint/config-conventional": "8.1.0",
"@octokit/rest": "16.28.7",
"@octokit/rest": "16.28.9",
"@types/async": "3.0.1",
"@types/bunyan": "1.8.6",
"@types/express": "4.17.1",
"@types/http-errors": "1.6.2",
"@types/jest": "24.0.18",
"@types/lodash": "4.14.137",
"@types/lodash": "4.14.138",
"@types/mime": "2.0.1",
"@types/minimatch": "3.0.3",
"@types/node": "12.7.2",
"@types/node": "12.7.4",
"@types/request": "2.48.2",
"@types/semver": "6.0.1",
"@typescript-eslint/eslint-plugin": "2.0.0",
"@verdaccio/babel-preset": "0.2.1",
"@verdaccio/eslint-config": "0.0.1",
"@verdaccio/types": "5.2.2",
"@types/semver": "6.0.2",
"@typescript-eslint/eslint-plugin": "2.1.0",
"@verdaccio/babel-preset": "8.1.0",
"@verdaccio/eslint-config": "8.1.0",
"@verdaccio/types": "8.1.0",
"codecov": "3.5.0",
"cross-env": "5.2.0",
"cross-env": "5.2.1",
"detect-secrets": "1.0.4",
"eslint": "5.16.0",
"get-stdin": "7.0.0",
Expand All @@ -80,14 +80,15 @@
"jest": "24.9.0",
"jest-environment-node": "24.9.0",
"lint-staged": "8.2.1",
"nock": "10.0.6",
"nock": "11.3.3",
"prettier": "1.18.2",
"puppeteer": "1.8.0",
"rimraf": "3.0.0",
"standard-version": "7.0.0",
"supertest": "4.0.2",
"typescript": "3.5.3",
"verdaccio-auth-memory": "8.0.0",
"verdaccio-memory": "8.0.0"
"typescript": "3.6.2",
"verdaccio-auth-memory": "8.1.0",
"verdaccio-memory": "8.1.0"
},
"keywords": [
"private",
Expand Down
37 changes: 17 additions & 20 deletions src/api/endpoint/api/stars.ts
Expand Up @@ -10,28 +10,25 @@ import { Package } from '@verdaccio/types';

type Packages = Package[];

export default function(route: Router, storage: IStorageHandler) {
route.get(
'/-/_view/starredByUser',
(req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
const remoteUsername = req.remote_user.name;
export default function(route: Router, storage: IStorageHandler): void {
route.get('/-/_view/starredByUser', (req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
const remoteUsername = req.remote_user.name;

storage.getLocalDatabase((err, localPackages: Packages) => {
if (err) {
return next(err);
}
storage.getLocalDatabase((err, localPackages: Packages) => {
if (err) {
return next(err);
}

const filteredPackages: Packages = localPackages.filter((localPackage: Package) =>
localPackage[USERS] ? _.keys(localPackage[USERS]).indexOf(remoteUsername) >= 0 : false
);
const filteredPackages: Packages = localPackages.filter((localPackage: Package) =>
localPackage[USERS] ? _.keys(localPackage[USERS]).indexOf(remoteUsername) >= 0 : false
);

res.status(HTTP_STATUS.OK);
next({
rows: filteredPackages.map((filteredPackage: Package) => ({
value: filteredPackage.name,
})),
});
res.status(HTTP_STATUS.OK);
next({
rows: filteredPackages.map((filteredPackage: Package) => ({
value: filteredPackage.name,
})),
});
}
);
});
});
}
125 changes: 125 additions & 0 deletions src/api/endpoint/api/v1/token.ts
@@ -0,0 +1,125 @@
import _ from 'lodash';
import { HTTP_STATUS, SUPPORT_ERRORS } from '../../../../lib/constants';
import {ErrorCode, mask} from '../../../../lib/utils';
import { getApiToken } from '../../../../lib/auth-utils';
import { stringToMD5 } from '../../../../lib/crypto-utils';
import { logger } from '../../../../lib/logger';

import { Response, Router } from 'express';
import {$NextFunctionVer, $RequestExtend, IAuth, IStorageHandler} from '../../../../../types';
import { Config, RemoteUser, Token } from '@verdaccio/types';

export type NormalizeToken = Token & {
created: string;
};

function normalizeToken(token: Token): NormalizeToken {
return {
...token,
created: new Date(token.created).toISOString(),
};
};

// https://github.com/npm/npm-profile/blob/latest/lib/index.js
export default function(route: Router, auth: IAuth, storage: IStorageHandler, config: Config) {
route.get('/-/npm/v1/tokens', async function(req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { name } = req.remote_user;

if (_.isNil(name) === false) {
try {
const tokens = await storage.readTokens({user: name});
const totalTokens = tokens.length;
logger.debug({totalTokens}, 'token list retrieved: @{totalTokens}');

res.status(HTTP_STATUS.OK);
return next({
objects: tokens.map(normalizeToken),
urls: {
next: '', // TODO: pagination?
},
});
} catch (error) {
logger.error({ error: error.msg }, 'token list has failed: @{error}');
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));
}
} else {
return next(ErrorCode.getUnauthorized());
}
});

route.post('/-/npm/v1/tokens', function(req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { password, readonly, cidr_whitelist } = req.body;
const { name } = req.remote_user;

if (!_.isBoolean(readonly) || !_.isArray(cidr_whitelist)) {
return next(ErrorCode.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));
}

auth.authenticate(name, password, async (err, user: RemoteUser) => {
if (err) {
const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;
return next(ErrorCode.getCode(errorCode, err.message));
} else {
req.remote_user = user;

if (!_.isFunction(storage.saveToken)) {
return next(ErrorCode.getCode(HTTP_STATUS.NOT_IMPLEMENTED, SUPPORT_ERRORS.STORAGE_NOT_IMPLEMENT));
}

try {
const token = await getApiToken(auth, config, user, password);
const key = stringToMD5(token);
// TODO: use a utility here
const maskedToken = mask(token, 5);
const created = new Date().getTime();

/**
* cidr_whitelist: is not being used, we pass it through
* token: we do not store the real token (it is generated once and retrieved to the user), just a mask of it.
*/
const saveToken: Token = {
user: name,
token: maskedToken,
key,
cidr: cidr_whitelist,
readonly,
created,
};

await storage.saveToken(saveToken);
logger.debug({ key, name }, 'token @{key} was created for user @{name}');
return next(normalizeToken({
token,
user: name,
key: saveToken.key,
cidr: cidr_whitelist,
readonly,
created: saveToken.created,
}));
} catch (error) {
logger.error({ error: error.msg }, 'token creation has failed: @{error}');
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));
}
}
});
});

route.delete('/-/npm/v1/tokens/token/:tokenKey', async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
const { params: { tokenKey }} = req;
const { name } = req.remote_user;

if (_.isNil(name) === false) {
logger.debug({name}, '@{name} has requested remove a token');
try {
await storage.deleteToken(name, tokenKey);
logger.info({ tokenKey, name }, 'token id @{tokenKey} was revoked for user @{name}');
return next({});
} catch(error) {
logger.error({ error: error.msg }, 'token creation has failed: @{error}');
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));
}
} else {
return next(ErrorCode.getUnauthorized());
}
});
}

0 comments on commit dbf2017

Please sign in to comment.