Skip to content

Commit

Permalink
feat: impl registry token api (#1590)
Browse files Browse the repository at this point in the history
  • Loading branch information
killagu committed Sep 20, 2020
1 parent 90d39aa commit 45f2f8b
Show file tree
Hide file tree
Showing 21 changed files with 949 additions and 20 deletions.
5 changes: 3 additions & 2 deletions .travis.yml
Expand Up @@ -3,8 +3,9 @@ language: node_js
node_js:
- '8'
- '10'
addons:
- postgresql: '9.3'
services:
- mysql
- postgresql
script: 'make test-travis-all'
after_script:
- 'npm i codecov && codecov'
4 changes: 2 additions & 2 deletions Makefile
@@ -1,6 +1,6 @@
TESTS = $(shell ls -S `find test -type f -name "*.test.js" -print`)
REPORTER = spec
TIMEOUT = 60000
TIMEOUT = 600000
MOCHA_OPTS =
DB = sqlite

Expand Down Expand Up @@ -61,7 +61,7 @@ test-cov-mysql: init-mysql
@$(MAKE) test-cov DB=mysql

test-travis: init-database
@NODE_ENV=test DB=${DB} CNPM_SOURCE_NPM=https://registry.npmjs.com CNPM_SOURCE_NPM_ISCNPM=false \
@NODE_ENV=test DB=${DB} \
node \
node_modules/.bin/istanbul cover \
node_modules/.bin/_mocha \
Expand Down
53 changes: 53 additions & 0 deletions controllers/registry/token/create.js
@@ -0,0 +1,53 @@
'use strict';

var ipRegex = require('ip-regex');
var tokenService = require('../../../services/token');
var userService = require('../../../services/user');
var ipv4 = ipRegex.v4({ exact: true });

module.exports = function* createToken() {
var readonly = this.request.body.readonly;
if (typeof readonly !== 'undefined' && typeof readonly !== 'boolean') {
this.status = 400;
var error = '[bad_request] readonly ' + readonly + ' is not boolean';
this.body = {
error,
reason: error,
};
return;
}
var cidrWhitelist = this.request.body.cidr_whitelist;
if (typeof cidrWhitelist !== 'undefined') {
var isValidateWhiteList = Array.isArray(cidrWhitelist) && cidrWhitelist.every(function (cidr) {
return ipv4.test(cidr);
});
if (!isValidateWhiteList) {
this.status = 400;
var error = '[bad_request] cide white list ' + JSON.stringify(cidrWhitelist) + ' is not validate ip array';
this.body = {
error,
reason: error,
};
return;
}
}

var password = this.request.body.password;
var user = yield userService.auth(this.user.name, password);
if (!user) {
this.status = 401;
var error = '[unauthorized] incorrect or missing password.';
this.body = {
error,
reason: error,
};
return;
}

var token = yield tokenService.createToken(this.user.name, {
readonly: !!readonly,
cidrWhitelist: cidrWhitelist || [],
});
this.status = 201;
this.body = token;
};
8 changes: 8 additions & 0 deletions controllers/registry/token/del.js
@@ -0,0 +1,8 @@
'use strict';

var tokenService = require('../../../services/token');

module.exports = function* deleteToken() {
yield tokenService.deleteToken(this.user.name, this.params.UUID);
this.status = 204;
};
60 changes: 60 additions & 0 deletions controllers/registry/token/list.js
@@ -0,0 +1,60 @@
'use strict';

var tokenService = require('../../../services/token');

var DEFAULT_PER_PAGE = 10;
var MIN_PER_PAGE = 1;
var MAX_PER_PAGE = 9999;

module.exports = function* createToken() {
var perPage = typeof this.query.perPage === 'undefined' ? DEFAULT_PER_PAGE : parseInt(this.query.perPage);
if (Number.isNaN(perPage)) {
this.status = 400;
var error = 'perPage ' + this.query.perPage + ' is not a number';
this.body = {
error,
reason: error,
};
return;
}
if (perPage < MIN_PER_PAGE || perPage > MAX_PER_PAGE) {
this.status = 400;
var error = 'perPage ' + this.query.perPage + ' is out of boundary';
this.body = {
error,
reason: error,
};
return;
}

var page = typeof this.query.page === 'undefined' ? 0 : parseInt(this.query.page);
if (Number.isNaN(page)) {
this.status = 400;
var error = 'page ' + this.query.page + ' is not a number';
this.body = {
error,
reason: error,
};
return;
}
if (page < 0) {
this.status = 400;
var error = 'page ' + this.query.page + ' is invalidate';
this.body = {
error,
reason: error,
};
return;
}

var tokens = yield tokenService.listToken(this.user.name, {
page: page,
perPage: perPage,
});

this.status = 200;
this.body = {
objects: tokens,
urls: {},
};
};
13 changes: 13 additions & 0 deletions controllers/registry/user/add.js
Expand Up @@ -3,6 +3,7 @@
var ensurePasswordSalt = require('./common').ensurePasswordSalt;
var userService = require('../../../services/user');
var config = require('../../../config');
var tokenService = require('../../../services/token');

// npm 1.4.4
// add new user first
Expand Down Expand Up @@ -63,8 +64,13 @@ module.exports = function* addUser() {
return;
}
if (loginedUser) {
var token = yield tokenService.createToken(body.name, {
readonly: !!body.readonly,
cidrWhitelist: body.cidr_whitelist || [],
});
this.status = 201;
this.body = {
token: token.token,
ok: true,
id: 'org.couchdb.user:' + loginedUser.login,
rev: Date.now() + '-' + loginedUser.login
Expand Down Expand Up @@ -118,8 +124,15 @@ module.exports = function* addUser() {
// add new user
var result = yield userService.add(user);
this.etag = '"' + result.rev + '"';

var token = yield tokenService.createToken(body.name, {
readonly: !!body.readonly,
cidrWhitelist: body.cidr_whitelist || [],
});

this.status = 201;
this.body = {
token: token.token,
ok: true,
id: 'org.couchdb.user:' + name,
rev: result.rev
Expand Down
14 changes: 14 additions & 0 deletions docs/db.sql
Expand Up @@ -318,3 +318,17 @@ CREATE TABLE IF NOT EXISTS `dist_file` (
-- ALTER TABLE `dist_file`
-- CHANGE `name` `name` varchar(214) NOT NULL COMMENT 'file name',
-- CHANGE `parent` `parent` varchar(214) NOT NULL COMMENT 'parent dir' DEFAULT '/';

CREATE TABLE IF NOT EXISTS `token` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key',
`gmt_create` datetime NOT NULL COMMENT 'create time',
`gmt_modified` datetime NOT NULL COMMENT 'modified time',
`token` varchar(100) NOT NULL COMMENT 'token',
`user_id` varchar(100) NOT NULL COMMENT 'user name',
`readonly` tinyint NOT NULL DEFAULT 0 COMMENT 'readonly or not, 1: true, other: false',
`token_key` varchar(200) NOT NULL COMMENT 'token sha512 hash',
`cidr_whitelist` varchar(500) NOT NULL COMMENT 'ip list, ["127.0.0.1"]',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_token` (`token`),
KEY `idx_user` (`user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='token info';
84 changes: 82 additions & 2 deletions docs/registry-api.md
Expand Up @@ -50,14 +50,20 @@ Status: 4xx

## Authentication

There is only one way to authenticate through the API.
There are two ways to authenticate through the API.

## Basic Authentication

```bash
$ curl -u "username:password" https://registry.npmjs.org
```

## Bearer Authentication

```bash
$ curl -H "Authorization: Bearer ${UUId}" https://registry.npmjs.org
```

## Failed login limit

```bash
Expand Down Expand Up @@ -903,7 +909,8 @@ Status: 201 Created
{
"ok": true,
"id": "org.couchdb.user:fengmk2",
"rev": "32-984ee97e01aea166dcab6d1517c730e3"
"rev": "32-984ee97e01aea166dcab6d1517c730e3",
"token": "85d32fad-bd43-4dd7-9451-4f7d907313a2"
}
```
Expand Down Expand Up @@ -956,3 +963,76 @@ Status: 201 Created
```
## Search
## Token
- [Create token](/docs/registry-api.md#create-token)
- [List token](/docs/registry-api.md#list-token)
- [Delete token](/docs/registry-api.md#delete-token)
### Create token
* Authentication required.
```
POST /-/npm/v1/tokens
```
#### Input
```json
{
"password": "123",
"readonly": false,
"cidr_whitelist": [
"127.0.0.1"
]
}
```
#### Response 200
```json
HTTP/1.1 200 OK

{
"token": "85d32fad-bd43-4dd7-9451-4f7d907313a2",
"key": "d06309a210570ef71cd9c7bd4849e7e96eeaa841976e63326436f6fd320dc4bbd452710e4e0fedc2efc2ea4a793b7159e95e9596e85e00dee26adc3f8afbb97f",
"cidr_whitelist": [ "127.0.0.1" ],
"created": "2015-01-04T08:28:51.378Z",
"updated": "2015-01-04T08:28:51.378Z",
"readonly": false
}
```
### List token
* Authentication required.
```
GET /-/npm/v1/tokens
```
### Input
perPage=10&page=0
#### Response 200
```json
{
"objects": [{
"token": "85d32f...7313a2",
"key": "d06309a210570ef71cd9c7bd4849e7e96eeaa841976e63326436f6fd320dc4bbd452710e4e0fedc2efc2ea4a793b7159e95e9596e85e00dee26adc3f8afbb97f",
"cidr_whitelist": [ "127.0.0.1" ],
"created": "2015-01-04T08:28:51.378Z",
"updated": "2015-01-04T08:28:51.378Z",
"readonly": false
}]
}
```
### Delete token
* Authentication required.
```
GET /-/npm/v1/tokens/token/:UUID
```
#### Response 204
47 changes: 35 additions & 12 deletions middleware/auth.js
Expand Up @@ -2,7 +2,10 @@

var debug = require('debug')('cnpmjs.org:middleware:auth');
var UserService = require('../services/user');
var TokenService = require('../services/token');
var config = require('../config');
var BASIC_PREFIX = /basic /i;
var BEARER_PREFIX = /bearer /i;

/**
* Parse the request authorization
Expand All @@ -13,25 +16,21 @@ module.exports = function () {
return function* auth(next) {
this.user = {};

var authorization = (this.get('authorization') || '').split(' ')[1] || '';
authorization = authorization.trim();
var authorization = (this.get('authorization') || '').trim();
debug('%s %s with %j', this.method, this.url, authorization);
if (!authorization) {
return yield unauthorized.call(this, next);
}

authorization = Buffer.from(authorization, 'base64').toString();
var pos = authorization.indexOf(':');
if (pos === -1) {
return yield unauthorized.call(this, next);
}

var username = authorization.slice(0, pos);
var password = authorization.slice(pos + 1);

var row;
try {
row = yield UserService.auth(username, password);
if (BASIC_PREFIX.test(authorization)) {
row = yield basicAuth(authorization);
} else if (BEARER_PREFIX.test(authorization)) {
row = yield bearerAuth(authorization, this.method, this.ip);
} else {
return yield unauthorized.call(this, next);
}
} catch (err) {
// do not response error here
// many request do not need login
Expand All @@ -51,6 +50,30 @@ module.exports = function () {
};
};

function* basicAuth(authorization) {
authorization = authorization.split(' ')[1];
authorization = Buffer.from(authorization, 'base64').toString();

var pos = authorization.indexOf(':');
if (pos === -1) {
return null;
}

var username = authorization.slice(0, pos);
var password = authorization.slice(pos + 1);

return yield UserService.auth(username, password);
}

function* bearerAuth(authorization, method, ip) {
var token = authorization.split(' ')[1];
var isReadOperation = method === 'HEAD' || method === 'GET';
return yield TokenService.validateToken(token, {
isReadOperation: isReadOperation,
accessIp: ip,
});
}

function* unauthorized(next) {
if (!config.alwaysAuth || this.method !== 'GET') {
return yield next;
Expand Down

0 comments on commit 45f2f8b

Please sign in to comment.